/**
 * AST + CST→AST visitor — Task H.1.3 (chevrotain v11 visitor).
 *
 * Lowers the CST from parser.ts (H.1.2) into a 6-kind discriminated union
 * (literal | name | path | call | unary | binary). Switches over
 * `ASTNode['kind']` MUST end with `assertNever(node)` so a new kind breaks
 * typecheck at every callsite. Nodes carry `offset: number` from the
 * originating token's `startOffset`; binary nodes inherit the LHS offset so
 * left-folded chains point at the leftmost operand. orExpr/andExpr fold
 * left-to-right (`((a&&b)&&c)`). Strings strip outer quotes then process
 * `\"` `\\` `\n` `\t` `\r` (unknown escapes pass through). `validateVisitor()`
 * is REQUIRED — without it, visitor-method typos silently return undefined.
 */
import { parserInstance } from './parser.js';
/** Throw on unhandled discriminator — call from any switch over `ASTNode['kind']`. */
export function assertNever(x) {
    throw new Error(`Unhandled AST kind: ${JSON.stringify(x)}`);
}
const BaseVisitor = parserInstance.getBaseCstVisitorConstructor();
class CstToAstVisitor extends BaseVisitor {
    constructor() {
        super();
        this.validateVisitor();
    }
    // Typed wrappers around chevrotain's `any`-typed `visit()` — confine the
    // unsafe surface to two callees. `no-unsafe-return` disabled at each
    // return; per-method return annotation provides downstream type narrowing.
    visitNode(child) {
        // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- chevrotain visit() returns any.
        return this.visit(child);
    }
    visitSeg(child) {
        // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- chevrotain visit() returns any.
        return this.visit(child);
    }
    expression(ctx) {
        return this.visitNode(ctx.orExpr);
    }
    orExpr(ctx) {
        return this.foldBinary(ctx, '||');
    }
    andExpr(ctx) {
        return this.foldBinary(ctx, '&&');
    }
    // Shared left-fold for orExpr/andExpr (both have lhs + MANY rhs).
    foldBinary(ctx, op) {
        let acc = this.visitNode(ctx.lhs);
        const rhs = ctx.rhs;
        if (rhs) {
            for (const r of rhs) {
                acc = { kind: 'binary', op, lhs: acc, rhs: this.visitNode(r), offset: acc.offset };
            }
        }
        return acc;
    }
    notExpr(ctx) {
        const operand = this.visitNode(ctx.compareExpr);
        const bang = ctx.Bang?.[0];
        return bang ? { kind: 'unary', op: '!', operand, offset: bang.startOffset } : operand;
    }
    compareExpr(ctx) {
        const lhs = this.visitNode(ctx.lhs);
        const r = ctx.rhs;
        if (!r)
            return lhs;
        const op = pickCompareOp(ctx);
        return { kind: 'binary', op, lhs, rhs: this.visitNode(r), offset: lhs.offset };
    }
    primary(ctx) {
        const sub = (ctx.expression ?? ctx.literal ?? ctx.callOrPath);
        if (!sub)
            throw new Error('primary: no alternative matched (parser invariant violated)');
        return this.visitNode(sub);
    }
    callOrPath(ctx) {
        const id = ctx.Identifier[0];
        const offset = id.startOffset;
        if (ctx.LParen) {
            const args = (ctx.arg ?? []).map((a) => this.visitNode(a));
            return { kind: 'call', name: id.image, args, offset };
        }
        const segs = ctx.pathSegment;
        const name = { kind: 'name', id: id.image, offset };
        if (!segs?.length)
            return name;
        return { kind: 'path', target: name, segments: segs.map((s) => this.visitSeg(s)), offset };
    }
    pathSegment(ctx) {
        if (ctx.Dot)
            return { kind: 'prop', name: ctx.Identifier[0].image };
        const num = ctx.NumberLit?.[0];
        if (num)
            return { kind: 'index', value: Number(num.image) };
        const str = ctx.StringLit?.[0];
        if (str)
            return { kind: 'index', value: parseStringLit(str.image) };
        throw new Error('pathSegment: no alternative matched (parser invariant violated)');
    }
    literal(ctx) {
        if (ctx.StringLit)
            return litNode(ctx.StringLit[0], parseStringLit);
        if (ctx.NumberLit)
            return litNode(ctx.NumberLit[0], Number);
        if (ctx.True)
            return litNode(ctx.True[0], () => true);
        if (ctx.False)
            return litNode(ctx.False[0], () => false);
        if (ctx.Null)
            return litNode(ctx.Null[0], () => null);
        throw new Error('literal: no alternative matched (parser invariant violated)');
    }
}
function litNode(t, project) {
    return { kind: 'literal', value: project(t.image), offset: t.startOffset };
}
// Pick the one compare op token populated under compareExpr's children.
// Exactly one of the six is present when ctx.rhs is set (parser invariant).
function pickCompareOp(ctx) {
    if (ctx.EqEq)
        return '==';
    if (ctx.NotEq)
        return '!=';
    if (ctx.Lte)
        return '<=';
    if (ctx.Gte)
        return '>=';
    if (ctx.Lt)
        return '<';
    if (ctx.Gt)
        return '>';
    throw new Error('compareExpr: rhs present but no operator token (parser invariant violated)');
}
// Strip outer quotes + process JSON-ish escapes (\" \\ \n \t \r). Unknown
// escapes pass through as the escaped character.
const ESC = { n: '\n', t: '\t', r: '\r', '"': '"', '\\': '\\' };
function parseStringLit(raw) {
    const inner = raw.slice(1, -1);
    let out = '';
    for (let i = 0; i < inner.length; i++) {
        const c = inner[i];
        if (c !== '\\' || i === inner.length - 1) {
            out += c;
            continue;
        }
        const next = inner[++i];
        out += ESC[next] ?? next;
    }
    return out;
}
const visitorInstance = new CstToAstVisitor();
/** Lower a parser CST into an AST. Throws if the CST shape is malformed. */
export function cstToAst(cst) {
    // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- chevrotain visit() returns any.
    return visitorInstance.visit(cst);
}
//# sourceMappingURL=ast.js.map