Skip to main content

native/oxc_ex_nif/src/codegen.rs

use std::cell::Cell;

use oxc_allocator::{Allocator, Box as OxcBox, Vec as OxcVec};
use oxc_ast::{ast::*, AstBuilder, NONE};
use oxc_codegen::{Codegen, CodegenReturn};
use oxc_span::{SourceType, SPAN};
use oxc_str::Str as OxcStr;
use oxc_syntax::{
    node::NodeId,
    number::{BigintBase, NumberBase},
    operator::{
        AssignmentOperator, BinaryOperator, LogicalOperator, UnaryOperator, UpdateOperator,
    },
};
use rustler::{Encoder, Env, NifResult, Term};

use crate::atoms;
use crate::error::error_to_term;

mod a {
    rustler::atoms! {
        r#type = "type",
        block, body, expression, argument, arguments, left, right, operator,
        test, consequent, alternate, init, update, object, callee,
        optional, computed, name, value, raw, cooked, tail,
        declarations, kind, id, params, rest, generator,
        async_field = "async", await_field = "await",
        source, specifiers, imported, exported, local, declaration,
        properties, elements, key, shorthand, method, quasis, expressions,
        tag, quasi, prefix, delegate, label, cases, handler, param,
        finalizer, discriminant, super_class = "superClass",
        meta, property, regex, pattern, flags, bigint,
        static_field = "static",

        // node types
        program, expression_statement, block_statement, return_statement,
        variable_declaration, function_declaration, class_declaration,
        if_statement, for_statement, for_in_statement, for_of_statement,
        while_statement, do_while_statement, switch_statement, try_statement,
        throw_statement, break_statement, continue_statement,
        labeled_statement, empty_statement, debugger_statement, with_statement,
        import_declaration, export_named_declaration,
        export_default_declaration, export_all_declaration,

        identifier, identifier_reference, literal,
        numeric_literal, string_literal, boolean_literal, null_literal,
        big_int_literal, reg_exp_literal,
        binary_expression, logical_expression, unary_expression,
        update_expression, assignment_expression, conditional_expression,
        call_expression, new_expression,
        member_expression, static_member_expression, computed_member_expression,
        chain_expression, object_expression, array_expression,
        arrow_function_expression, function_expression, class_expression,
        template_literal, tagged_template_expression,
        sequence_expression, this_expression,
        super_expr = "super",
        await_expression, yield_expression, import_expression,
        meta_property, parenthesized_expression,
        spread_element, rest_element,
        object_pattern, array_pattern, assignment_pattern,
        import_specifier, import_default_specifier, import_namespace_specifier,
        export_specifier,
        method_definition, property_definition, static_block,
    }
}

type R<T> = Result<T, String>;

fn err<T>(msg: impl Into<String>) -> R<T> {
    Err(msg.into())
}

include!("generated_term_helpers.rs");
include!("generated_ast_decoders.rs");

fn nid() -> Cell<NodeId> {
    Cell::new(NodeId::DUMMY)
}

fn oxc_s<'a>(b: AstBuilder<'a>, s: &str) -> OxcStr<'a> {
    b.str(s)
}

fn ident_name<'a>(b: AstBuilder<'a>, s: &str) -> IdentifierName<'a> {
    b.identifier_name(SPAN, b.ident(s))
}

fn str_lit<'a>(b: AstBuilder<'a>, s: &str) -> StringLiteral<'a> {
    b.string_literal(SPAN, b.str(s), None)
}

fn opt_binding_id<'a>(b: AstBuilder<'a>, term: Term) -> Option<BindingIdentifier<'a>> {
    opt(term, a::id()).map(|t| {
        let n = str_val(t, a::name());
        b.binding_identifier(SPAN, b.ident(&n))
    })
}

fn static_member<'a>(
    b: AstBuilder<'a>,
    object: Expression<'a>,
    prop: &str,
    optional: bool,
) -> Expression<'a> {
    Expression::StaticMemberExpression(b.alloc(b.static_member_expression(
        SPAN,
        object,
        ident_name(b, prop),
        optional,
    )))
}

fn computed_member<'a>(
    b: AstBuilder<'a>,
    object: Expression<'a>,
    prop: Expression<'a>,
    optional: bool,
) -> Expression<'a> {
    Expression::ComputedMemberExpression(
        b.alloc(b.computed_member_expression(SPAN, object, prop, optional)),
    )
}

// ── NIF entry point ──

#[rustler::nif(schedule = "DirtyCpu")]
pub fn codegen<'a>(env: Env<'a>, ast: Term<'a>) -> NifResult<Term<'a>> {
    let allocator = Allocator::default();
    let b = AstBuilder::new(&allocator);

    let input = decode_program_input(ast)?;

    match build_program(b, input.body) {
        Ok(program) => {
            let CodegenReturn { code, .. } = Codegen::new().build(&program);
            Ok((atoms::ok(), code).encode(env))
        }
        Err(msg) => error_to_term(env, &[msg]),
    }
}

// ── Program ──

fn build_program<'a>(b: AstBuilder<'a>, body_terms: Vec<Term>) -> R<Program<'a>> {
    let body = build_stmts(b, body_terms)?;
    Ok(b.program(SPAN, SourceType::mjs(), "", b.vec(), None, b.vec(), body))
}

// ── Statements ──

fn build_stmt<'a>(b: AstBuilder<'a>, term: Term) -> R<Statement<'a>> {
    let ty = type_atom(term).ok_or_else(|| "Missing :type on statement".to_string())?;

    if ty == a::expression_statement() {
        return Ok(b.statement_expression(
            SPAN,
            build_expr(b, get(term, a::expression()).ok_or("Missing :expression")?)?,
        ));
    }
    if ty == a::block_statement() {
        return Ok(b.statement_block(SPAN, build_stmts(b, list_val(term, a::body()))?));
    }
    if ty == a::return_statement() {
        return Ok(b.statement_return(SPAN, opt_expr(b, term, a::argument())?));
    }
    if ty == a::throw_statement() {
        return Ok(b.statement_throw(
            SPAN,
            build_expr(b, get(term, a::argument()).ok_or("Missing :argument")?)?,
        ));
    }
    if ty == a::empty_statement() {
        return Ok(b.statement_empty(SPAN));
    }
    if ty == a::debugger_statement() {
        return Ok(b.statement_debugger(SPAN));
    }

    if ty == a::variable_declaration() {
        return Ok(Statement::from(build_var_decl(b, term)?));
    }
    if ty == a::function_declaration() {
        return Ok(Statement::from(build_fn_decl(b, term)?));
    }
    if ty == a::class_declaration() {
        return Ok(Statement::from(build_class_decl(b, term)?));
    }

    if ty == a::if_statement() {
        let input = decode_if_statement_input(term)?;
        let test = build_expr(b, input.test)?;
        let cons = build_stmt(b, input.consequent)?;
        let alt = match input.alternate {
            Some(t) if !is_nil(t) => Some(build_stmt(b, t)?),
            _ => None,
        };
        return Ok(b.statement_if(SPAN, test, cons, alt));
    }
    if ty == a::for_statement() {
        let init = match opt(term, a::init()) {
            Some(t) if type_eq(t, a::variable_declaration()) => Some(
                ForStatementInit::VariableDeclaration(build_var_decl_boxed(b, t)?),
            ),
            Some(t) => Some(ForStatementInit::from(build_expr(b, t)?)),
            None => None,
        };
        let test = opt_expr(b, term, a::test())?;
        let update = opt_expr(b, term, a::update())?;
        let body = build_stmt(b, get(term, a::body()).ok_or("Missing :body")?)?;
        return Ok(b.statement_for(SPAN, init, test, update, body));
    }
    if ty == a::for_in_statement() {
        let left = build_for_left(b, get(term, a::left()).ok_or("Missing :left")?)?;
        let right = build_expr(b, get(term, a::right()).ok_or("Missing :right")?)?;
        let body = build_stmt(b, get(term, a::body()).ok_or("Missing :body")?)?;
        return Ok(b.statement_for_in(SPAN, left, right, body));
    }
    if ty == a::for_of_statement() {
        let aw = bool_val(term, a::await_field());
        let left = build_for_left(b, get(term, a::left()).ok_or("Missing :left")?)?;
        let right = build_expr(b, get(term, a::right()).ok_or("Missing :right")?)?;
        let body = build_stmt(b, get(term, a::body()).ok_or("Missing :body")?)?;
        return Ok(b.statement_for_of(SPAN, aw, left, right, body));
    }
    if ty == a::while_statement() {
        let test = build_expr(b, get(term, a::test()).ok_or("Missing :test")?)?;
        let body = build_stmt(b, get(term, a::body()).ok_or("Missing :body")?)?;
        return Ok(b.statement_while(SPAN, test, body));
    }
    if ty == a::do_while_statement() {
        let body = build_stmt(b, get(term, a::body()).ok_or("Missing :body")?)?;
        let test = build_expr(b, get(term, a::test()).ok_or("Missing :test")?)?;
        return Ok(b.statement_do_while(SPAN, body, test));
    }
    if ty == a::switch_statement() {
        let disc = build_expr(
            b,
            get(term, a::discriminant()).ok_or("Missing :discriminant")?,
        )?;
        let cases_list = list_val(term, a::cases());
        let mut cases = b.vec_with_capacity(cases_list.len());
        for c in &cases_list {
            let test = opt_expr(b, *c, a::test())?;
            let cons = build_stmts(b, list_val(*c, a::consequent()))?;
            cases.push(b.switch_case(SPAN, test, cons));
        }
        return Ok(b.statement_switch(SPAN, disc, cases));
    }
    if ty == a::try_statement() {
        let block_term = opt(term, a::block())
            .or_else(|| opt(term, a::body()))
            .ok_or("Missing try body")?;
        let block = build_block(b, block_term)?;
        let handler = match opt(term, a::handler()) {
            Some(h) => {
                let param = match opt(h, a::param()) {
                    Some(p) => Some(CatchParameter {
                        node_id: nid(),
                        span: SPAN,
                        pattern: build_binding_pat(b, p)?,
                        type_annotation: None,
                    }),
                    None => None,
                };
                let hbody = build_block(b, get(h, a::body()).ok_or("Missing catch body")?)?;
                Some(b.catch_clause(SPAN, param, hbody))
            }
            None => None,
        };
        let finalizer = match opt(term, a::finalizer()) {
            Some(f) => Some(build_block(b, f)?),
            None => None,
        };
        return Ok(b.statement_try(SPAN, block, handler, finalizer));
    }
    if ty == a::break_statement() {
        return Ok(b.statement_break(SPAN, opt_label(b, term)));
    }
    if ty == a::continue_statement() {
        return Ok(b.statement_continue(SPAN, opt_label(b, term)));
    }
    if ty == a::labeled_statement() {
        let lt = get(term, a::label()).ok_or("Missing :label")?;
        let label = LabelIdentifier {
            node_id: nid(),
            span: SPAN,
            name: b.ident(&str_val(lt, a::name())),
        };
        let body = build_stmt(b, get(term, a::body()).ok_or("Missing :body")?)?;
        return Ok(b.statement_labeled(SPAN, label, body));
    }
    if ty == a::with_statement() {
        let obj = build_expr(b, get(term, a::object()).ok_or("Missing :object")?)?;
        let body = build_stmt(b, get(term, a::body()).ok_or("Missing :body")?)?;
        return Ok(b.statement_with(SPAN, obj, body));
    }
    if ty == a::import_declaration() {
        return Ok(Statement::from(build_import(b, term)?));
    }
    if ty == a::export_named_declaration() {
        return Ok(Statement::from(build_export_named(b, term)?));
    }
    if ty == a::export_default_declaration() {
        return Ok(Statement::from(build_export_default(b, term)?));
    }
    if ty == a::export_all_declaration() {
        return Ok(Statement::from(build_export_all(b, term)?));
    }

    err(format!("Unsupported statement: {}", type_str(term)))
}

fn build_stmts<'a>(b: AstBuilder<'a>, list: Vec<Term>) -> R<OxcVec<'a, Statement<'a>>> {
    let mut out = b.vec_with_capacity(list.len());
    for t in &list {
        out.push(build_stmt(b, *t)?);
    }
    Ok(out)
}

fn build_block<'a>(b: AstBuilder<'a>, term: Term) -> R<BlockStatement<'a>> {
    Ok(BlockStatement {
        node_id: nid(),
        span: SPAN,
        body: build_stmts(b, list_val(term, a::body()))?,
        scope_id: Default::default(),
    })
}

// ── Expressions ──

fn build_expr<'a>(b: AstBuilder<'a>, term: Term) -> R<Expression<'a>> {
    let ty = type_atom(term).ok_or_else(|| "Missing :type on expression".to_string())?;

    if ty == a::identifier() || ty == a::identifier_reference() {
        let n = str_val(term, a::name());
        return Ok(b.expression_identifier(SPAN, b.ident(&n)));
    }
    if ty == a::literal() {
        return build_generic_lit(b, term);
    }
    if ty == a::numeric_literal() {
        return Ok(b.expression_numeric_literal(
            SPAN,
            f64_val(term, a::value()),
            None,
            NumberBase::Decimal,
        ));
    }
    if ty == a::string_literal() {
        let s = str_val(term, a::value());
        return Ok(b.expression_string_literal(SPAN, oxc_s(b, &s), None));
    }
    if ty == a::boolean_literal() {
        return Ok(b.expression_boolean_literal(SPAN, bool_val(term, a::value())));
    }
    if ty == a::null_literal() {
        return Ok(b.expression_null_literal(SPAN));
    }
    if ty == a::big_int_literal() {
        let v = str_val(term, a::value());
        return Ok(b.expression_big_int_literal(SPAN, oxc_s(b, &v), None, BigintBase::Decimal));
    }
    if ty == a::reg_exp_literal() {
        return build_regexp(b, term);
    }

    if ty == a::binary_expression() {
        let l = build_expr(b, get(term, a::left()).ok_or("Missing :left")?)?;
        let op = parse_bin_op(&str_val(term, a::operator()))?;
        let r = build_expr(b, get(term, a::right()).ok_or("Missing :right")?)?;
        return Ok(b.expression_binary(SPAN, l, op, r));
    }
    if ty == a::logical_expression() {
        let l = build_expr(b, get(term, a::left()).ok_or("Missing :left")?)?;
        let op = parse_log_op(&str_val(term, a::operator()))?;
        let r = build_expr(b, get(term, a::right()).ok_or("Missing :right")?)?;
        return Ok(b.expression_logical(SPAN, l, op, r));
    }
    if ty == a::unary_expression() {
        let op = parse_unary_op(&str_val(term, a::operator()))?;
        return Ok(b.expression_unary(
            SPAN,
            op,
            build_expr(b, get(term, a::argument()).ok_or("Missing :argument")?)?,
        ));
    }
    if ty == a::update_expression() {
        let op = parse_update_op(&str_val(term, a::operator()))?;
        let prefix = bool_val(term, a::prefix());
        let arg = build_simple_target(b, get(term, a::argument()).ok_or("Missing :argument")?)?;
        return Ok(b.expression_update(SPAN, op, prefix, arg));
    }
    if ty == a::assignment_expression() {
        let op = parse_assign_op(&str_val(term, a::operator()))?;
        let l = build_assign_target(b, get(term, a::left()).ok_or("Missing :left")?)?;
        let r = build_expr(b, get(term, a::right()).ok_or("Missing :right")?)?;
        return Ok(b.expression_assignment(SPAN, op, l, r));
    }
    if ty == a::conditional_expression() {
        let test = build_expr(b, get(term, a::test()).ok_or("Missing :test")?)?;
        let cons = build_expr(b, get(term, a::consequent()).ok_or("Missing :consequent")?)?;
        let alt = build_expr(b, get(term, a::alternate()).ok_or("Missing :alternate")?)?;
        return Ok(b.expression_conditional(SPAN, test, cons, alt));
    }
    if ty == a::call_expression() {
        let callee = build_expr(b, get(term, a::callee()).ok_or("Missing :callee")?)?;
        let args = build_args(b, list_val(term, a::arguments()))?;
        return Ok(b.expression_call(SPAN, callee, NONE, args, bool_val(term, a::optional())));
    }
    if ty == a::new_expression() {
        let callee = build_expr(b, get(term, a::callee()).ok_or("Missing :callee")?)?;
        let args = build_args(b, list_val(term, a::arguments()))?;
        return Ok(b.expression_new(SPAN, callee, NONE, args));
    }
    if ty == a::member_expression() || ty == a::static_member_expression() {
        return build_member(b, term);
    }
    if ty == a::computed_member_expression() {
        let obj = build_expr(b, get(term, a::object()).ok_or("Missing :object")?)?;
        let prop = build_expr(
            b,
            get(term, a::expression())
                .or_else(|| get(term, a::property()))
                .ok_or("Missing prop")?,
        )?;
        return Ok(computed_member(b, obj, prop, bool_val(term, a::optional())));
    }
    if ty == a::chain_expression() {
        let inner = get(term, a::expression()).ok_or("Missing chain :expression")?;
        return build_chain(b, inner);
    }
    if ty == a::object_expression() {
        return Ok(b.expression_object(SPAN, build_obj_props(b, list_val(term, a::properties()))?));
    }
    if ty == a::array_expression() {
        let elems_list = list_val(term, a::elements());
        let mut elems = b.vec_with_capacity(elems_list.len());
        for e in &elems_list {
            if is_nil(*e) {
                elems.push(ArrayExpressionElement::Elision(b.alloc(Elision {
                    node_id: nid(),
                    span: SPAN,
                })));
            } else if type_eq(*e, a::spread_element()) {
                let arg = build_expr(b, get(*e, a::argument()).ok_or("Missing spread :argument")?)?;
                elems.push(ArrayExpressionElement::SpreadElement(
                    b.alloc(b.spread_element(SPAN, arg)),
                ));
            } else {
                elems.push(ArrayExpressionElement::from(build_expr(b, *e)?));
            }
        }
        return Ok(b.expression_array(SPAN, elems));
    }
    if ty == a::arrow_function_expression() {
        let is_async = bool_val(term, a::async_field());
        let is_expr = bool_val(term, a::expression());
        let params = build_params(b, term)?;
        let body_term = get(term, a::body()).ok_or("Missing arrow body")?;
        let body = if is_expr && !type_eq(body_term, a::block_statement()) {
            let expr = build_expr(b, body_term)?;
            b.function_body(SPAN, b.vec(), b.vec1(b.statement_expression(SPAN, expr)))
        } else {
            build_fn_body(b, body_term)?
        };
        return Ok(b.expression_arrow_function(SPAN, is_expr, is_async, NONE, params, NONE, body));
    }
    if ty == a::function_expression() {
        let id = opt_binding_id(b, term);
        let params = build_params(b, term)?;
        let body = build_fn_body(b, get(term, a::body()).ok_or("Missing fn body")?)?;
        return Ok(b.expression_function(
            SPAN,
            FunctionType::FunctionExpression,
            id,
            bool_val(term, a::generator()),
            bool_val(term, a::async_field()),
            false,
            NONE,
            NONE,
            params,
            NONE,
            Some(b.alloc(body)),
        ));
    }
    if ty == a::class_expression() {
        let id = opt_binding_id(b, term);
        let sc = opt_expr(b, term, a::super_class())?;
        let body = build_class_body(b, get(term, a::body()).ok_or("Missing class body")?)?;
        return Ok(b.expression_class(
            SPAN,
            ClassType::ClassExpression,
            b.vec(),
            id,
            NONE,
            sc,
            NONE,
            b.vec(),
            body,
            false,
            false,
        ));
    }
    if ty == a::template_literal() {
        return build_template(b, term);
    }
    if ty == a::tagged_template_expression() {
        let tag = build_expr(b, get(term, a::tag()).ok_or("Missing :tag")?)?;
        let qt = get(term, a::quasi()).ok_or("Missing :quasi")?;
        let quasis = build_quasis(b, list_val(qt, a::quasis()))?;
        let exprs = build_exprs(b, list_val(qt, a::expressions()))?;
        return Ok(b.expression_tagged_template(
            SPAN,
            tag,
            NONE,
            b.template_literal(SPAN, quasis, exprs),
        ));
    }
    if ty == a::sequence_expression() {
        return Ok(b.expression_sequence(SPAN, build_exprs(b, list_val(term, a::expressions()))?));
    }
    if ty == a::this_expression() {
        return Ok(b.expression_this(SPAN));
    }
    if ty == a::super_expr() {
        return Ok(Expression::Super(b.alloc(Super {
            node_id: nid(),
            span: SPAN,
        })));
    }
    if ty == a::await_expression() {
        return Ok(b.expression_await(
            SPAN,
            build_expr(b, get(term, a::argument()).ok_or("Missing :argument")?)?,
        ));
    }
    if ty == a::yield_expression() {
        return Ok(b.expression_yield(
            SPAN,
            bool_val(term, a::delegate()),
            opt_expr(b, term, a::argument())?,
        ));
    }
    if ty == a::import_expression() {
        return Ok(b.expression_import(
            SPAN,
            build_expr(b, get(term, a::source()).ok_or("Missing :source")?)?,
            None,
            None,
        ));
    }
    if ty == a::meta_property() {
        let m = ident_name(b, &str_val(get(term, a::meta()).unwrap_or(term), a::name()));
        let p = ident_name(
            b,
            &str_val(get(term, a::property()).unwrap_or(term), a::name()),
        );
        return Ok(b.expression_meta_property(SPAN, m, p));
    }
    if ty == a::parenthesized_expression() {
        return Ok(b.expression_parenthesized(
            SPAN,
            build_expr(b, get(term, a::expression()).ok_or("Missing :expression")?)?,
        ));
    }

    err(format!("Unsupported expression: {}", type_str(term)))
}

fn opt_expr<'a>(b: AstBuilder<'a>, term: Term, key: rustler::Atom) -> R<Option<Expression<'a>>> {
    match opt(term, key) {
        Some(t) => Ok(Some(build_expr(b, t)?)),
        None => Ok(None),
    }
}

fn build_exprs<'a>(b: AstBuilder<'a>, list: Vec<Term>) -> R<OxcVec<'a, Expression<'a>>> {
    let mut out = b.vec_with_capacity(list.len());
    for t in &list {
        out.push(build_expr(b, *t)?);
    }
    Ok(out)
}

// ── Literals ──

fn build_generic_lit<'a>(b: AstBuilder<'a>, term: Term) -> R<Expression<'a>> {
    if let Some(rx) = opt(term, a::regex()) {
        return build_regexp_from(b, rx);
    }
    if opt(term, a::bigint()).is_some() {
        let v = str_val(term, a::bigint());
        return Ok(b.expression_big_int_literal(SPAN, oxc_s(b, &v), None, BigintBase::Decimal));
    }
    match get(term, a::value()) {
        None => Ok(b.expression_null_literal(SPAN)),
        Some(t) if is_nil(t) => Ok(b.expression_null_literal(SPAN)),
        Some(t) => {
            if let Ok(v) = t.decode::<bool>() {
                return Ok(b.expression_boolean_literal(SPAN, v));
            }
            if let Ok(v) = t.decode::<f64>() {
                return Ok(b.expression_numeric_literal(SPAN, v, None, NumberBase::Decimal));
            }
            if let Ok(v) = t.decode::<i64>() {
                return Ok(b.expression_numeric_literal(SPAN, v as f64, None, NumberBase::Decimal));
            }
            if let Ok(v) = t.decode::<String>() {
                return Ok(b.expression_string_literal(SPAN, oxc_s(b, &v), None));
            }
            Ok(b.expression_null_literal(SPAN))
        }
    }
}

fn build_regexp<'a>(b: AstBuilder<'a>, term: Term) -> R<Expression<'a>> {
    build_regexp_from(b, get(term, a::regex()).unwrap_or(term))
}

fn build_regexp_from<'a>(b: AstBuilder<'a>, rx: Term) -> R<Expression<'a>> {
    let pat = str_val(rx, a::pattern());
    let fl = str_val(rx, a::flags());
    Ok(Expression::RegExpLiteral(b.alloc(RegExpLiteral {
        node_id: nid(),
        span: SPAN,
        raw: None,
        regex: RegExp {
            pattern: RegExpPattern {
                text: oxc_s(b, &pat),
                pattern: None,
            },
            flags: parse_regex_flags(&fl),
        },
    })))
}

// ── Member / Chain ──

fn build_member<'a>(b: AstBuilder<'a>, term: Term) -> R<Expression<'a>> {
    let obj = build_expr(b, get(term, a::object()).ok_or("Missing :object")?)?;
    let optional = bool_val(term, a::optional());
    if bool_val(term, a::computed()) {
        let prop = build_expr(b, get(term, a::property()).ok_or("Missing :property")?)?;
        Ok(computed_member(b, obj, prop, optional))
    } else {
        let pn = str_val(
            get(term, a::property()).ok_or("Missing :property")?,
            a::name(),
        );
        Ok(static_member(b, obj, &pn, optional))
    }
}

fn build_chain<'a>(b: AstBuilder<'a>, inner: Term) -> R<Expression<'a>> {
    let inner_ty = type_atom(inner).ok_or("Missing chain inner type")?;
    let elem = if inner_ty == a::call_expression() {
        let callee = build_expr(b, get(inner, a::callee()).ok_or("Missing :callee")?)?;
        let args = build_args(b, list_val(inner, a::arguments()))?;
        ChainElement::CallExpression(b.alloc(CallExpression {
            node_id: nid(),
            span: SPAN,
            callee,
            type_arguments: None,
            arguments: args,
            optional: bool_val(inner, a::optional()),
            pure: false,
        }))
    } else {
        let obj = build_expr(b, get(inner, a::object()).ok_or("Missing :object")?)?;
        let optional = bool_val(inner, a::optional());
        if bool_val(inner, a::computed()) {
            let prop = build_expr(b, get(inner, a::property()).ok_or("Missing :property")?)?;
            ChainElement::ComputedMemberExpression(
                b.alloc(b.computed_member_expression(SPAN, obj, prop, optional)),
            )
        } else {
            let pn = str_val(
                get(inner, a::property()).ok_or("Missing :property")?,
                a::name(),
            );
            ChainElement::StaticMemberExpression(b.alloc(b.static_member_expression(
                SPAN,
                obj,
                ident_name(b, &pn),
                optional,
            )))
        }
    };
    Ok(b.expression_chain(SPAN, elem))
}

// ── Template literals ──

fn build_template<'a>(b: AstBuilder<'a>, term: Term) -> R<Expression<'a>> {
    let quasis = build_quasis(b, list_val(term, a::quasis()))?;
    let exprs = build_exprs(b, list_val(term, a::expressions()))?;
    Ok(b.expression_template_literal(SPAN, quasis, exprs))
}

fn build_quasis<'a>(b: AstBuilder<'a>, list: Vec<Term>) -> R<OxcVec<'a, TemplateElement<'a>>> {
    let mut out = b.vec_with_capacity(list.len());
    for q in &list {
        let vt = get(*q, a::value()).unwrap_or(*q);
        let raw = str_val(vt, a::raw());
        let cooked = opt(vt, a::cooked()).and_then(|t| t.decode::<String>().ok());
        let tail = bool_val(*q, a::tail());
        out.push(b.template_element(
            SPAN,
            TemplateElementValue {
                raw: oxc_s(b, &raw),
                cooked: cooked.as_deref().map(|s| oxc_s(b, s)),
            },
            tail,
            false,
        ));
    }
    Ok(out)
}

// ── Declarations ──

fn build_var_decl<'a>(b: AstBuilder<'a>, term: Term) -> R<Declaration<'a>> {
    let kind = match str_val(term, a::kind()).as_str() {
        "let" => VariableDeclarationKind::Let,
        "var" => VariableDeclarationKind::Var,
        "using" => VariableDeclarationKind::Using,
        "await_using" | "await using" => VariableDeclarationKind::AwaitUsing,
        _ => VariableDeclarationKind::Const,
    };
    let dl = list_val(term, a::declarations());
    let mut decls = b.vec_with_capacity(dl.len());
    for d in &dl {
        let id = build_binding_pat(b, get(*d, a::id()).ok_or("Missing declarator :id")?)?;
        let init = opt_expr(b, *d, a::init())?;
        decls.push(b.variable_declarator(SPAN, kind, id, NONE, init, false));
    }
    Ok(b.declaration_variable(SPAN, kind, decls, false))
}

fn build_var_decl_boxed<'a>(
    b: AstBuilder<'a>,
    term: Term,
) -> R<OxcBox<'a, VariableDeclaration<'a>>> {
    let kind = match str_val(term, a::kind()).as_str() {
        "let" => VariableDeclarationKind::Let,
        "var" => VariableDeclarationKind::Var,
        "using" => VariableDeclarationKind::Using,
        "await_using" | "await using" => VariableDeclarationKind::AwaitUsing,
        _ => VariableDeclarationKind::Const,
    };
    let dl = list_val(term, a::declarations());
    let mut decls = b.vec_with_capacity(dl.len());
    for d in &dl {
        let id = build_binding_pat(b, get(*d, a::id()).ok_or("Missing declarator :id")?)?;
        let init = opt_expr(b, *d, a::init())?;
        decls.push(b.variable_declarator(SPAN, kind, id, NONE, init, false));
    }
    Ok(b.alloc(b.variable_declaration(SPAN, kind, decls, false)))
}

fn build_fn_decl<'a>(b: AstBuilder<'a>, term: Term) -> R<Declaration<'a>> {
    let id = opt_binding_id(b, term);
    let params = build_params(b, term)?;
    let body = build_fn_body(b, get(term, a::body()).ok_or("Missing fn body")?)?;
    Ok(b.declaration_function(
        SPAN,
        FunctionType::FunctionDeclaration,
        id,
        bool_val(term, a::generator()),
        bool_val(term, a::async_field()),
        false,
        NONE,
        NONE,
        params,
        NONE,
        Some(b.alloc(body)),
    ))
}

fn build_class_decl<'a>(b: AstBuilder<'a>, term: Term) -> R<Declaration<'a>> {
    let id = opt_binding_id(b, term);
    let sc = opt_expr(b, term, a::super_class())?;
    let body = build_class_body(b, get(term, a::body()).ok_or("Missing class body")?)?;
    Ok(b.declaration_class(
        SPAN,
        ClassType::ClassDeclaration,
        b.vec(),
        id,
        NONE,
        sc,
        NONE,
        b.vec(),
        body,
        false,
        false,
    ))
}

// ── Class body ──

fn build_class_body<'a>(b: AstBuilder<'a>, term: Term) -> R<ClassBody<'a>> {
    let bl = list_val(term, a::body());
    let mut elems = b.vec_with_capacity(bl.len());
    for e in &bl {
        let ty = type_atom(*e).ok_or("Missing class element type")?;
        if ty == a::method_definition() {
            let kind_s = str_val(*e, a::kind());
            let kind = match kind_s.as_str() {
                "constructor" => MethodDefinitionKind::Constructor,
                "get" => MethodDefinitionKind::Get,
                "set" => MethodDefinitionKind::Set,
                _ => MethodDefinitionKind::Method,
            };
            let key = build_prop_key(b, get(*e, a::key()).ok_or("Missing method key")?)?;
            let is_static = bool_val(*e, a::static_field());
            let is_computed = bool_val(*e, a::computed());
            let vt = get(*e, a::value()).ok_or("Missing method value")?;
            let params = build_params(b, vt)?;
            let body = build_fn_body(b, get(vt, a::body()).ok_or("Missing method body")?)?;
            let func = Function {
                node_id: nid(),
                span: SPAN,
                r#type: FunctionType::FunctionExpression,
                id: None,
                generator: bool_val(vt, a::generator()),
                r#async: bool_val(vt, a::async_field()),
                declare: false,
                type_parameters: None,
                this_param: None,
                params: b.alloc(params),
                return_type: None,
                body: Some(b.alloc(body)),
                scope_id: Default::default(),
                pure: false,
                pife: false,
            };
            elems.push(ClassElement::MethodDefinition(b.alloc(
                b.method_definition(
                    SPAN,
                    MethodDefinitionType::MethodDefinition,
                    b.vec(),
                    key,
                    func,
                    kind,
                    is_computed,
                    is_static,
                    false,
                    false,
                    None,
                ),
            )));
        } else if ty == a::property_definition() {
            let key = build_prop_key(b, get(*e, a::key()).ok_or("Missing property key")?)?;
            let val = opt_expr(b, *e, a::value())?;
            elems.push(ClassElement::PropertyDefinition(b.alloc(
                b.property_definition(
                    SPAN,
                    PropertyDefinitionType::PropertyDefinition,
                    b.vec(),
                    key,
                    NONE,
                    val,
                    bool_val(*e, a::computed()),
                    bool_val(*e, a::static_field()),
                    false,
                    false,
                    false,
                    false,
                    false,
                    None,
                ),
            )));
        } else if ty == a::static_block() {
            let body = build_stmts(b, list_val(*e, a::body()))?;
            elems.push(ClassElement::StaticBlock(b.alloc(StaticBlock {
                node_id: nid(),
                span: SPAN,
                body,
                scope_id: Default::default(),
            })));
        } else {
            return err(format!("Unsupported class element: {}", type_str(*e)));
        }
    }
    Ok(b.class_body(SPAN, elems))
}

// ── Module declarations ──

fn build_import<'a>(b: AstBuilder<'a>, term: Term) -> R<ModuleDeclaration<'a>> {
    let src = str_val(
        get(term, a::source()).ok_or("Missing import :source")?,
        a::value(),
    );
    let sl = str_lit(b, &src);
    let specs_list = list_val(term, a::specifiers());
    let specifiers = if specs_list.is_empty() {
        if get(term, a::specifiers()).is_none_or(|t| is_nil(t)) {
            None
        } else {
            Some(b.vec())
        }
    } else {
        let mut specs = b.vec_with_capacity(specs_list.len());
        for s in &specs_list {
            let ty = type_atom(*s).ok_or("Missing specifier type")?;
            if ty == a::import_specifier() {
                let imp_name = str_val(get(*s, a::imported()).unwrap_or(*s), a::name());
                let loc_name = str_val(get(*s, a::local()).unwrap_or(*s), a::name());
                let imported = ModuleExportName::IdentifierName(ident_name(b, &imp_name));
                let local = b.binding_identifier(SPAN, b.ident(&loc_name));
                specs.push(ImportDeclarationSpecifier::ImportSpecifier(b.alloc(
                    b.import_specifier(SPAN, imported, local, ImportOrExportKind::Value),
                )));
            } else if ty == a::import_default_specifier() {
                let loc_name = str_val(get(*s, a::local()).unwrap_or(*s), a::name());
                specs.push(ImportDeclarationSpecifier::ImportDefaultSpecifier(b.alloc(
                    b.import_default_specifier(
                        SPAN,
                        b.binding_identifier(SPAN, b.ident(&loc_name)),
                    ),
                )));
            } else if ty == a::import_namespace_specifier() {
                let loc_name = str_val(get(*s, a::local()).unwrap_or(*s), a::name());
                specs.push(ImportDeclarationSpecifier::ImportNamespaceSpecifier(
                    b.alloc(b.import_namespace_specifier(
                        SPAN,
                        b.binding_identifier(SPAN, b.ident(&loc_name)),
                    )),
                ));
            } else {
                return err(format!("Unsupported import specifier: {}", type_str(*s)));
            }
        }
        Some(specs)
    };
    Ok(ModuleDeclaration::ImportDeclaration(b.alloc(
        b.import_declaration(SPAN, specifiers, sl, None, NONE, ImportOrExportKind::Value),
    )))
}

fn build_export_named<'a>(b: AstBuilder<'a>, term: Term) -> R<ModuleDeclaration<'a>> {
    let declaration = match opt(term, a::declaration()) {
        Some(t) => {
            let ty = type_atom(t).ok_or("Missing export decl type")?;
            Some(if ty == a::variable_declaration() {
                build_var_decl(b, t)?
            } else if ty == a::function_declaration() {
                build_fn_decl(b, t)?
            } else if ty == a::class_declaration() {
                build_class_decl(b, t)?
            } else {
                return err(format!("Unsupported export declaration: {}", type_str(t)));
            })
        }
        None => None,
    };
    let sl = list_val(term, a::specifiers());
    let mut specifiers = b.vec_with_capacity(sl.len());
    for s in &sl {
        let loc = str_val(get(*s, a::local()).unwrap_or(*s), a::name());
        let exp = str_val(get(*s, a::exported()).unwrap_or(*s), a::name());
        specifiers.push(b.export_specifier(
            SPAN,
            ModuleExportName::IdentifierName(ident_name(b, &loc)),
            ModuleExportName::IdentifierName(ident_name(b, &exp)),
            ImportOrExportKind::Value,
        ));
    }
    let source = opt(term, a::source()).map(|t| str_lit(b, &str_val(t, a::value())));
    Ok(ModuleDeclaration::ExportNamedDeclaration(b.alloc(
        b.export_named_declaration(
            SPAN,
            declaration,
            specifiers,
            source,
            ImportOrExportKind::Value,
            NONE,
        ),
    )))
}

fn build_export_default<'a>(b: AstBuilder<'a>, term: Term) -> R<ModuleDeclaration<'a>> {
    let dt = get(term, a::declaration()).ok_or("Missing default export declaration")?;
    let ty = type_atom(dt).ok_or("Missing declaration type")?;
    let kind = if ty == a::function_declaration() {
        let id = opt_binding_id(b, dt);
        let params = build_params(b, dt)?;
        let body = build_fn_body(b, get(dt, a::body()).ok_or("Missing fn body")?)?;
        let func = Function {
            node_id: nid(),
            span: SPAN,
            r#type: FunctionType::FunctionDeclaration,
            id,
            generator: bool_val(dt, a::generator()),
            r#async: bool_val(dt, a::async_field()),
            declare: false,
            type_parameters: None,
            this_param: None,
            params: b.alloc(params),
            return_type: None,
            body: Some(b.alloc(body)),
            scope_id: Default::default(),
            pure: false,
            pife: false,
        };
        ExportDefaultDeclarationKind::FunctionDeclaration(b.alloc(func))
    } else if ty == a::class_declaration() {
        let id = opt_binding_id(b, dt);
        let sc = opt_expr(b, dt, a::super_class())?;
        let body = build_class_body(b, get(dt, a::body()).ok_or("Missing class body")?)?;
        let class = Class {
            node_id: nid(),
            span: SPAN,
            r#type: ClassType::ClassDeclaration,
            decorators: b.vec(),
            id,
            type_parameters: None,
            super_class: sc,
            super_type_arguments: None,
            implements: b.vec(),
            body: b.alloc(body),
            r#abstract: false,
            declare: false,
            scope_id: Default::default(),
        };
        ExportDefaultDeclarationKind::ClassDeclaration(b.alloc(class))
    } else {
        ExportDefaultDeclarationKind::from(build_expr(b, dt)?)
    };
    Ok(ModuleDeclaration::ExportDefaultDeclaration(
        b.alloc(b.export_default_declaration(SPAN, kind)),
    ))
}

fn build_export_all<'a>(b: AstBuilder<'a>, term: Term) -> R<ModuleDeclaration<'a>> {
    let src = str_val(
        get(term, a::source()).ok_or("Missing export :source")?,
        a::value(),
    );
    let exported = opt(term, a::exported())
        .map(|t| ModuleExportName::IdentifierName(ident_name(b, &str_val(t, a::name()))));
    Ok(ModuleDeclaration::ExportAllDeclaration(b.alloc(
        b.export_all_declaration(
            SPAN,
            exported,
            str_lit(b, &src),
            NONE,
            ImportOrExportKind::Value,
        ),
    )))
}

// ── Patterns ──

fn build_binding_pat<'a>(b: AstBuilder<'a>, term: Term) -> R<BindingPattern<'a>> {
    let ty = type_atom(term).ok_or("Missing pattern type")?;
    if ty == a::identifier() {
        let n = str_val(term, a::name());
        return Ok(b.binding_pattern_binding_identifier(SPAN, b.ident(&n)));
    }
    if ty == a::object_pattern() {
        let pl = list_val(term, a::properties());
        let mut props = b.vec_with_capacity(pl.len());
        let mut rest = None;
        for p in &pl {
            if type_eq(*p, a::rest_element()) {
                let arg =
                    build_binding_pat(b, get(*p, a::argument()).ok_or("Missing rest :argument")?)?;
                rest = Some(b.alloc(BindingRestElement {
                    node_id: nid(),
                    span: SPAN,
                    argument: arg,
                }));
            } else {
                let key = build_prop_key(b, get(*p, a::key()).ok_or("Missing property :key")?)?;
                let val =
                    build_binding_pat(b, get(*p, a::value()).ok_or("Missing property :value")?)?;
                props.push(BindingProperty {
                    node_id: nid(),
                    span: SPAN,
                    key,
                    value: val,
                    shorthand: bool_val(*p, a::shorthand()),
                    computed: bool_val(*p, a::computed()),
                });
            }
        }
        return Ok(b.binding_pattern_object_pattern(SPAN, props, rest));
    }
    if ty == a::array_pattern() {
        let el = list_val(term, a::elements());
        let mut elems = b.vec_with_capacity(el.len());
        let mut rest = None;
        for e in &el {
            if is_nil(*e) {
                elems.push(None);
            } else if type_eq(*e, a::rest_element()) {
                let arg =
                    build_binding_pat(b, get(*e, a::argument()).ok_or("Missing rest :argument")?)?;
                rest = Some(b.alloc(BindingRestElement {
                    node_id: nid(),
                    span: SPAN,
                    argument: arg,
                }));
            } else {
                elems.push(Some(build_binding_pat(b, *e)?));
            }
        }
        return Ok(b.binding_pattern_array_pattern(SPAN, elems, rest));
    }
    if ty == a::assignment_pattern() {
        let left = build_binding_pat(b, get(term, a::left()).ok_or("Missing :left")?)?;
        let right = build_expr(b, get(term, a::right()).ok_or("Missing :right")?)?;
        return Ok(b.binding_pattern_assignment_pattern(SPAN, left, right));
    }
    err(format!("Unsupported binding pattern: {}", type_str(term)))
}

fn build_assign_target<'a>(b: AstBuilder<'a>, term: Term) -> R<AssignmentTarget<'a>> {
    let ty = type_atom(term).ok_or("Missing target type")?;
    if ty == a::identifier() || ty == a::identifier_reference() {
        let n = str_val(term, a::name());
        return Ok(AssignmentTarget::AssignmentTargetIdentifier(b.alloc(
            IdentifierReference {
                node_id: nid(),
                span: SPAN,
                name: b.ident(&n),
                reference_id: Default::default(),
            },
        )));
    }
    if ty == a::member_expression()
        || ty == a::static_member_expression()
        || ty == a::computed_member_expression()
    {
        let obj = build_expr(b, get(term, a::object()).ok_or("Missing :object")?)?;
        let optional = bool_val(term, a::optional());
        if bool_val(term, a::computed()) || ty == a::computed_member_expression() {
            let prop = build_expr(
                b,
                get(term, a::property())
                    .or_else(|| get(term, a::expression()))
                    .ok_or("Missing prop")?,
            )?;
            return Ok(AssignmentTarget::ComputedMemberExpression(
                b.alloc(b.computed_member_expression(SPAN, obj, prop, optional)),
            ));
        }
        let pn = str_val(
            get(term, a::property()).ok_or("Missing :property")?,
            a::name(),
        );
        return Ok(AssignmentTarget::StaticMemberExpression(b.alloc(
            b.static_member_expression(SPAN, obj, ident_name(b, &pn), optional),
        )));
    }
    err(format!("Unsupported assignment target: {}", type_str(term)))
}

fn build_simple_target<'a>(b: AstBuilder<'a>, term: Term) -> R<SimpleAssignmentTarget<'a>> {
    let ty = type_atom(term).ok_or("Missing target type")?;
    if ty == a::identifier() || ty == a::identifier_reference() {
        let n = str_val(term, a::name());
        return Ok(SimpleAssignmentTarget::AssignmentTargetIdentifier(b.alloc(
            IdentifierReference {
                node_id: nid(),
                span: SPAN,
                name: b.ident(&n),
                reference_id: Default::default(),
            },
        )));
    }
    if ty == a::member_expression()
        || ty == a::static_member_expression()
        || ty == a::computed_member_expression()
    {
        let obj = build_expr(b, get(term, a::object()).ok_or("Missing :object")?)?;
        let optional = bool_val(term, a::optional());
        if bool_val(term, a::computed()) || ty == a::computed_member_expression() {
            let prop = build_expr(
                b,
                get(term, a::property())
                    .or_else(|| get(term, a::expression()))
                    .ok_or("Missing prop")?,
            )?;
            return Ok(SimpleAssignmentTarget::ComputedMemberExpression(
                b.alloc(b.computed_member_expression(SPAN, obj, prop, optional)),
            ));
        }
        let pn = str_val(
            get(term, a::property()).ok_or("Missing :property")?,
            a::name(),
        );
        return Ok(SimpleAssignmentTarget::StaticMemberExpression(b.alloc(
            b.static_member_expression(SPAN, obj, ident_name(b, &pn), optional),
        )));
    }
    err(format!("Unsupported simple target: {}", type_str(term)))
}

// ── Helpers ──

fn build_params<'a>(b: AstBuilder<'a>, term: Term) -> R<FormalParameters<'a>> {
    let pl = list_val(term, a::params());
    let mut items = b.vec_with_capacity(pl.len());
    let mut rest = None;
    for p in &pl {
        if type_eq(*p, a::rest_element()) {
            let arg =
                build_binding_pat(b, get(*p, a::argument()).ok_or("Missing rest :argument")?)?;
            let rest_elem = BindingRestElement {
                node_id: nid(),
                span: SPAN,
                argument: arg,
            };
            rest = Some(b.alloc(FormalParameterRest {
                node_id: nid(),
                span: SPAN,
                decorators: b.vec(),
                rest: rest_elem,
                type_annotation: None,
            }));
        } else {
            let pat = build_binding_pat(b, *p)?;
            items.push(FormalParameter {
                node_id: nid(),
                span: SPAN,
                decorators: b.vec(),
                pattern: pat,
                type_annotation: None,
                initializer: None,
                optional: false,
                accessibility: None,
                readonly: false,
                r#override: false,
            });
        }
    }
    Ok(b.formal_parameters(SPAN, FormalParameterKind::FormalParameter, items, rest))
}

fn build_fn_body<'a>(b: AstBuilder<'a>, term: Term) -> R<FunctionBody<'a>> {
    Ok(b.function_body(SPAN, b.vec(), build_stmts(b, list_val(term, a::body()))?))
}

fn build_for_left<'a>(b: AstBuilder<'a>, term: Term) -> R<ForStatementLeft<'a>> {
    if type_eq(term, a::variable_declaration()) {
        Ok(ForStatementLeft::VariableDeclaration(build_var_decl_boxed(
            b, term,
        )?))
    } else {
        Ok(ForStatementLeft::from(build_assign_target(b, term)?))
    }
}

fn opt_label<'a>(b: AstBuilder<'a>, term: Term) -> Option<LabelIdentifier<'a>> {
    opt(term, a::label()).map(|t| {
        let n = str_val(t, a::name());
        LabelIdentifier {
            node_id: nid(),
            span: SPAN,
            name: b.ident(&n),
        }
    })
}

fn build_prop_key<'a>(b: AstBuilder<'a>, term: Term) -> R<PropertyKey<'a>> {
    let ty = type_atom(term).ok_or("Missing key type")?;
    if ty == a::identifier() {
        return Ok(PropertyKey::StaticIdentifier(
            b.alloc(ident_name(b, &str_val(term, a::name()))),
        ));
    }
    if ty == a::literal() || ty == a::string_literal() {
        let vt = get(term, a::value());
        if let Some(t) = vt {
            if let Ok(s) = t.decode::<String>() {
                return Ok(PropertyKey::StringLiteral(b.alloc(str_lit(b, &s))));
            }
            if let Ok(n) = t.decode::<f64>() {
                return Ok(PropertyKey::NumericLiteral(b.alloc(NumericLiteral {
                    node_id: nid(),
                    span: SPAN,
                    value: n,
                    raw: None,
                    base: NumberBase::Decimal,
                })));
            }
            if let Ok(n) = t.decode::<i64>() {
                return Ok(PropertyKey::NumericLiteral(b.alloc(NumericLiteral {
                    node_id: nid(),
                    span: SPAN,
                    value: n as f64,
                    raw: None,
                    base: NumberBase::Decimal,
                })));
            }
        }
    }
    if ty == a::numeric_literal() {
        return Ok(PropertyKey::NumericLiteral(b.alloc(NumericLiteral {
            node_id: nid(),
            span: SPAN,
            value: f64_val(term, a::value()),
            raw: None,
            base: NumberBase::Decimal,
        })));
    }
    Ok(PropertyKey::from(build_expr(b, term)?))
}

fn build_obj_props<'a>(
    b: AstBuilder<'a>,
    list: Vec<Term>,
) -> R<OxcVec<'a, ObjectPropertyKind<'a>>> {
    let mut out = b.vec_with_capacity(list.len());
    for p in &list {
        if type_eq(*p, a::spread_element()) {
            let arg = build_expr(b, get(*p, a::argument()).ok_or("Missing spread :argument")?)?;
            out.push(ObjectPropertyKind::SpreadProperty(
                b.alloc(b.spread_element(SPAN, arg)),
            ));
        } else {
            let key = build_prop_key(b, get(*p, a::key()).ok_or("Missing :key")?)?;
            let val = build_expr(b, get(*p, a::value()).ok_or("Missing :value")?)?;
            let kind_s = str_val(*p, a::kind());
            let kind = match kind_s.as_str() {
                "get" => PropertyKind::Get,
                "set" => PropertyKind::Set,
                _ => PropertyKind::Init,
            };
            out.push(ObjectPropertyKind::ObjectProperty(b.alloc(
                b.object_property(
                    SPAN,
                    kind,
                    key,
                    val,
                    bool_val(*p, a::method()),
                    bool_val(*p, a::shorthand()),
                    bool_val(*p, a::computed()),
                ),
            )));
        }
    }
    Ok(out)
}

fn build_args<'a>(b: AstBuilder<'a>, list: Vec<Term>) -> R<OxcVec<'a, Argument<'a>>> {
    let mut out = b.vec_with_capacity(list.len());
    for a_term in &list {
        if type_eq(*a_term, a::spread_element()) {
            let arg = build_expr(
                b,
                get(*a_term, a::argument()).ok_or("Missing spread :argument")?,
            )?;
            out.push(Argument::SpreadElement(
                b.alloc(b.spread_element(SPAN, arg)),
            ));
        } else {
            out.push(Argument::from(build_expr(b, *a_term)?));
        }
    }
    Ok(out)
}

// ── Operators ──

fn parse_bin_op(op: &str) -> R<BinaryOperator> {
    Ok(match op {
        "==" => BinaryOperator::Equality,
        "!=" => BinaryOperator::Inequality,
        "===" => BinaryOperator::StrictEquality,
        "!==" => BinaryOperator::StrictInequality,
        "<" => BinaryOperator::LessThan,
        "<=" => BinaryOperator::LessEqualThan,
        ">" => BinaryOperator::GreaterThan,
        ">=" => BinaryOperator::GreaterEqualThan,
        "+" => BinaryOperator::Addition,
        "-" => BinaryOperator::Subtraction,
        "*" => BinaryOperator::Multiplication,
        "/" => BinaryOperator::Division,
        "%" => BinaryOperator::Remainder,
        "**" => BinaryOperator::Exponential,
        "<<" => BinaryOperator::ShiftLeft,
        ">>" => BinaryOperator::ShiftRight,
        ">>>" => BinaryOperator::ShiftRightZeroFill,
        "|" => BinaryOperator::BitwiseOR,
        "^" => BinaryOperator::BitwiseXOR,
        "&" => BinaryOperator::BitwiseAnd,
        "in" => BinaryOperator::In,
        "instanceof" => BinaryOperator::Instanceof,
        _ => return err(format!("Unknown binary operator: {op}")),
    })
}

fn parse_log_op(op: &str) -> R<LogicalOperator> {
    Ok(match op {
        "||" => LogicalOperator::Or,
        "&&" => LogicalOperator::And,
        "??" => LogicalOperator::Coalesce,
        _ => return err(format!("Unknown logical operator: {op}")),
    })
}

fn parse_unary_op(op: &str) -> R<UnaryOperator> {
    Ok(match op {
        "+" => UnaryOperator::UnaryPlus,
        "-" => UnaryOperator::UnaryNegation,
        "!" => UnaryOperator::LogicalNot,
        "~" => UnaryOperator::BitwiseNot,
        "typeof" => UnaryOperator::Typeof,
        "void" => UnaryOperator::Void,
        "delete" => UnaryOperator::Delete,
        _ => return err(format!("Unknown unary operator: {op}")),
    })
}

fn parse_update_op(op: &str) -> R<UpdateOperator> {
    Ok(match op {
        "++" => UpdateOperator::Increment,
        "--" => UpdateOperator::Decrement,
        _ => return err(format!("Unknown update operator: {op}")),
    })
}

fn parse_assign_op(op: &str) -> R<AssignmentOperator> {
    Ok(match op {
        "=" => AssignmentOperator::Assign,
        "+=" => AssignmentOperator::Addition,
        "-=" => AssignmentOperator::Subtraction,
        "*=" => AssignmentOperator::Multiplication,
        "/=" => AssignmentOperator::Division,
        "%=" => AssignmentOperator::Remainder,
        "**=" => AssignmentOperator::Exponential,
        "<<=" => AssignmentOperator::ShiftLeft,
        ">>=" => AssignmentOperator::ShiftRight,
        ">>>=" => AssignmentOperator::ShiftRightZeroFill,
        "|=" => AssignmentOperator::BitwiseOR,
        "^=" => AssignmentOperator::BitwiseXOR,
        "&=" => AssignmentOperator::BitwiseAnd,
        "||=" => AssignmentOperator::LogicalOr,
        "&&=" => AssignmentOperator::LogicalAnd,
        "??=" => AssignmentOperator::LogicalNullish,
        _ => return err(format!("Unknown assignment operator: {op}")),
    })
}

fn parse_regex_flags(flags: &str) -> RegExpFlags {
    let mut r = RegExpFlags::empty();
    for ch in flags.chars() {
        r |= match ch {
            'g' => RegExpFlags::G,
            'i' => RegExpFlags::I,
            'm' => RegExpFlags::M,
            's' => RegExpFlags::S,
            'u' => RegExpFlags::U,
            'y' => RegExpFlags::Y,
            'd' => RegExpFlags::D,
            'v' => RegExpFlags::V,
            _ => RegExpFlags::empty(),
        };
    }
    r
}