"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MacroExpander = void 0;
/**
 * Hygienic macro expansion for `macro rules!` definitions.
 */
class MacroExpander {
    expand(program) {
        const errors = [];
        const env = { macros: new Map(), hygieneCounter: 0 };
        const withoutDefs = this.collectAndStrip(program.statements, env, errors);
        const expandedStmts = [];
        for (const stmt of withoutDefs) {
            const result = this.expandStatement(stmt, env, errors);
            if (Array.isArray(result)) {
                expandedStmts.push(...result);
            }
            else if (result) {
                expandedStmts.push(result);
            }
        }
        return {
            program: {
                type: "Program",
                statements: expandedStmts,
                tokenLiteral: () => expandedStmts.length > 0 ? expandedStmts[0].tokenLiteral() : "",
            },
            errors,
        };
    }
    collectAndStrip(statements, env, errors) {
        const out = [];
        for (const stmt of statements) {
            if (stmt.type === "MacroDefinition") {
                const def = stmt;
                if (env.macros.has(def.name.value)) {
                    errors.push(`duplicate macro definition \`${def.name.value}\` at line ${def.token.line}, column ${def.token.column}`);
                }
                env.macros.set(def.name.value, def);
                continue;
            }
            out.push(stmt);
        }
        return out;
    }
    expandStatement(stmt, env, errors) {
        switch (stmt.type) {
            case "LetStatement": {
                const ls = stmt;
                const next = {
                    ...ls,
                    value: ls.value ? this.expandExpression(ls.value, env, errors) : null,
                };
                return next;
            }
            case "ReturnStatement": {
                const rs = stmt;
                const next = {
                    ...rs,
                    returnValue: rs.returnValue
                        ? this.expandExpression(rs.returnValue, env, errors)
                        : null,
                };
                return next;
            }
            case "ExpressionStatement": {
                const es = stmt;
                if (!es.expression) {
                    return es;
                }
                const expanded = this.expandExpression(es.expression, env, errors);
                if (expanded.type === "BlockStatement") {
                    return expanded.statements;
                }
                const next = { ...es, expression: expanded };
                return next;
            }
            case "BlockStatement": {
                const block = stmt;
                const stmts = [];
                for (const s of block.statements) {
                    const r = this.expandStatement(s, env, errors);
                    if (Array.isArray(r)) {
                        stmts.push(...r);
                    }
                    else if (r) {
                        stmts.push(r);
                    }
                }
                const next = { ...block, statements: stmts };
                return next;
            }
            default:
                return stmt;
        }
    }
    expandExpression(expr, env, errors) {
        switch (expr.type) {
            case "MacroInvocation":
                return this.expandInvocation(expr, env, errors);
            case "CallExpression": {
                const c = expr;
                const next = {
                    ...c,
                    function: this.expandExpression(c.function, env, errors),
                    arguments: c.arguments.map((a) => this.expandExpression(a, env, errors)),
                };
                return next;
            }
            case "PrefixExpression": {
                const p = expr;
                const next = {
                    ...p,
                    right: p.right ? this.expandExpression(p.right, env, errors) : null,
                };
                return next;
            }
            case "InfixExpression": {
                const i = expr;
                const next = {
                    ...i,
                    left: this.expandExpression(i.left, env, errors),
                    right: i.right ? this.expandExpression(i.right, env, errors) : null,
                };
                return next;
            }
            case "IfExpression": {
                const iff = expr;
                const next = {
                    ...iff,
                    condition: iff.condition
                        ? this.expandExpression(iff.condition, env, errors)
                        : null,
                    consequence: this.expandStatement(iff.consequence, env, errors),
                    alternative: iff.alternative
                        ? this.expandStatement(iff.alternative, env, errors)
                        : null,
                };
                return next;
            }
            case "FunctionLiteral": {
                const fn = expr;
                const next = {
                    ...fn,
                    body: this.expandStatement(fn.body, env, errors),
                };
                return next;
            }
            case "MatchExpression": {
                const m = expr;
                const next = {
                    ...m,
                    scrutinee: m.scrutinee
                        ? this.expandExpression(m.scrutinee, env, errors)
                        : null,
                    arms: m.arms.map((arm) => ({
                        ...arm,
                        body: this.expandExpression(arm.body, env, errors),
                    })),
                };
                return next;
            }
            case "AsyncExpression": {
                const a = expr;
                if (a.body.type === "BlockStatement") {
                    const next = {
                        ...a,
                        body: this.expandStatement(a.body, env, errors),
                    };
                    return next;
                }
                const next = {
                    ...a,
                    body: this.expandExpression(a.body, env, errors),
                };
                return next;
            }
            case "AwaitExpression": {
                const a = expr;
                const next = {
                    ...a,
                    argument: a.argument
                        ? this.expandExpression(a.argument, env, errors)
                        : null,
                };
                return next;
            }
            case "SpawnExpression": {
                const s = expr;
                const next = {
                    ...s,
                    argument: s.argument
                        ? this.expandExpression(s.argument, env, errors)
                        : null,
                };
                return next;
            }
            case "ChanExpression": {
                const c = expr;
                const next = {
                    ...c,
                    capacity: c.capacity
                        ? this.expandExpression(c.capacity, env, errors)
                        : null,
                };
                return next;
            }
            case "RefExpression": {
                const r = expr;
                const next = {
                    ...r,
                    value: r.value ? this.expandExpression(r.value, env, errors) : null,
                };
                return next;
            }
            case "MoveExpression": {
                const m = expr;
                const next = {
                    ...m,
                    value: m.value ? this.expandExpression(m.value, env, errors) : null,
                };
                return next;
            }
            case "ConstructorExpression": {
                const c = expr;
                const next = {
                    ...c,
                    arguments: c.arguments.map((a) => this.expandExpression(a, env, errors)),
                };
                return next;
            }
            default:
                return expr;
        }
    }
    expandInvocation(inv, env, errors) {
        const def = env.macros.get(inv.name.value);
        if (!def) {
            errors.push(`unknown macro \`${inv.name.value}\` at line ${inv.token.line}, column ${inv.token.column}`);
            return inv;
        }
        for (const rule of def.rules) {
            const captures = this.matchRule(rule, inv.arguments);
            if (captures) {
                const hygieneId = ++env.hygieneCounter;
                const substituted = this.substitute(rule.body, captures, hygieneId, new Set(this.introducedBindings(rule.body)));
                return this.expandExpression(substituted, env, errors);
            }
        }
        errors.push(`no matching macro rule for \`${inv.name.value}!\` at line ${inv.token.line}, column ${inv.token.column}`);
        return inv;
    }
    matchRule(rule, args) {
        const captures = new Map();
        const metaFragments = rule.pattern.filter((f) => f.kind === "expr" || f.kind === "ident" || f.kind === "tt");
        if (metaFragments.length === 0) {
            return args.length === 0 ? captures : null;
        }
        if (metaFragments.length !== args.length) {
            return null;
        }
        for (let i = 0; i < metaFragments.length; i++) {
            const frag = metaFragments[i];
            const arg = args[i];
            if (!frag.name) {
                return null;
            }
            if (frag.kind === "ident" && arg.type !== "Identifier") {
                return null;
            }
            captures.set(frag.name, arg);
        }
        return captures;
    }
    introducedBindings(expr) {
        const names = [];
        const visitStmt = (s) => {
            if (s.type === "LetStatement") {
                names.push(s.name.value);
            }
            else if (s.type === "BlockStatement") {
                for (const st of s.statements) {
                    visitStmt(st);
                }
            }
            else if (s.type === "ExpressionStatement") {
                const e = s.expression;
                if (e) {
                    visitExpr(e);
                }
            }
        };
        const visitExpr = (e) => {
            if (e.type === "IfExpression") {
                const iff = e;
                visitStmt(iff.consequence);
                if (iff.alternative) {
                    visitStmt(iff.alternative);
                }
            }
            else if (e.type === "FunctionLiteral") {
                visitStmt(e.body);
            }
            else if (e.type === "AsyncExpression") {
                const a = e;
                if (a.body.type === "BlockStatement") {
                    visitStmt(a.body);
                }
            }
        };
        visitExpr(expr);
        return names;
    }
    substitute(expr, captures, hygieneId, introduced) {
        if (expr.type === "Identifier") {
            const id = expr;
            const captured = captures.get(id.value) ??
                (id.value.startsWith("$")
                    ? captures.get(id.value.slice(1))
                    : undefined) ??
                captures.get(`$${id.value}`);
            if (captured) {
                return captured;
            }
            if (introduced.has(id.value)) {
                return this.gensymIdent(id, hygieneId);
            }
            return expr;
        }
        switch (expr.type) {
            case "PrefixExpression": {
                const p = expr;
                const next = {
                    ...p,
                    right: p.right
                        ? this.substitute(p.right, captures, hygieneId, introduced)
                        : null,
                };
                return next;
            }
            case "InfixExpression": {
                const i = expr;
                const next = {
                    ...i,
                    left: this.substitute(i.left, captures, hygieneId, introduced),
                    right: i.right
                        ? this.substitute(i.right, captures, hygieneId, introduced)
                        : null,
                };
                return next;
            }
            case "IfExpression": {
                const iff = expr;
                const next = {
                    ...iff,
                    condition: iff.condition
                        ? this.substitute(iff.condition, captures, hygieneId, introduced)
                        : null,
                    consequence: this.substituteBlock(iff.consequence, captures, hygieneId, introduced),
                    alternative: iff.alternative
                        ? this.substituteBlock(iff.alternative, captures, hygieneId, introduced)
                        : null,
                };
                return next;
            }
            case "CallExpression": {
                const c = expr;
                const next = {
                    ...c,
                    function: this.substitute(c.function, captures, hygieneId, introduced),
                    arguments: c.arguments.map((a) => this.substitute(a, captures, hygieneId, introduced)),
                };
                return next;
            }
            case "FunctionLiteral": {
                const fn = expr;
                const next = {
                    ...fn,
                    body: this.substituteBlock(fn.body, captures, hygieneId, introduced),
                };
                return next;
            }
            case "MatchExpression": {
                const m = expr;
                const next = {
                    ...m,
                    scrutinee: m.scrutinee
                        ? this.substitute(m.scrutinee, captures, hygieneId, introduced)
                        : null,
                    arms: m.arms.map((arm) => ({
                        ...arm,
                        body: this.substitute(arm.body, captures, hygieneId, introduced),
                    })),
                };
                return next;
            }
            case "ConstructorExpression": {
                const c = expr;
                const next = {
                    ...c,
                    arguments: c.arguments.map((a) => this.substitute(a, captures, hygieneId, introduced)),
                };
                return next;
            }
            case "BlockStatement":
                return this.substituteBlock(expr, captures, hygieneId, introduced);
            default:
                return expr;
        }
    }
    substituteBlock(block, captures, hygieneId, introduced) {
        const statements = block.statements.map((stmt) => {
            if (stmt.type === "LetStatement") {
                const ls = stmt;
                const name = introduced.has(ls.name.value)
                    ? this.gensymIdent(ls.name, hygieneId)
                    : ls.name;
                const next = {
                    ...ls,
                    name,
                    value: ls.value
                        ? this.substitute(ls.value, captures, hygieneId, introduced)
                        : null,
                };
                return next;
            }
            if (stmt.type === "ReturnStatement") {
                const rs = stmt;
                const next = {
                    ...rs,
                    returnValue: rs.returnValue
                        ? this.substitute(rs.returnValue, captures, hygieneId, introduced)
                        : null,
                };
                return next;
            }
            if (stmt.type === "ExpressionStatement") {
                const es = stmt;
                const next = {
                    ...es,
                    expression: es.expression
                        ? this.substitute(es.expression, captures, hygieneId, introduced)
                        : null,
                };
                return next;
            }
            if (stmt.type === "BlockStatement") {
                return this.substituteBlock(stmt, captures, hygieneId, introduced);
            }
            return stmt;
        });
        return { ...block, statements };
    }
    gensymIdent(id, hygieneId) {
        const token = {
            ...id.token,
            literal: `__nx${hygieneId}_${id.value}`,
        };
        return {
            ...id,
            token,
            value: `__nx${hygieneId}_${id.value}`,
            tokenLiteral: () => token.literal,
        };
    }
}
exports.MacroExpander = MacroExpander;
//# sourceMappingURL=macro.js.map