diff --git a/src/ast-builder.js b/src/ast-builder.js index d65dd98..a09d48a 100644 --- a/src/ast-builder.js +++ b/src/ast-builder.js @@ -9,12 +9,13 @@ function buildBinaryExpression(ctx, visitor) { // Collect all possible operator tokens from the context. const operators = []; if (ctx.EqEq) operators.push(...ctx.EqEq); + if (ctx.Gt) operators.push(...ctx.Gt); + if (ctx.Lt) operators.push(...ctx.Lt); if (ctx.Plus) operators.push(...ctx.Plus); if (ctx.Minus) operators.push(...ctx.Minus); if (ctx.Star) operators.push(...ctx.Star); if (ctx.Slash) operators.push(...ctx.Slash); - - // Sort operators by their position in the source text to handle mixed operators correctly. + operators.sort((a, b) => a.startOffset - b.startOffset); ctx.right.forEach((rhs, i) => { @@ -30,22 +31,57 @@ function buildBinaryExpression(ctx, visitor) { return left; } +// Build member chain using token ordering to preserve dots/brackets sequence. +function buildMemberChain(ctx, base, visitor, idStartIndex = 0) { + let obj = base; + + const steps = []; + if (ctx.Dot) steps.push(...ctx.Dot.map(tok => ({ kind: 'dot', tok }))); + if (ctx.LBracket) steps.push(...ctx.LBracket.map(tok => ({ kind: 'brack', tok }))); + steps.sort((a, b) => a.tok.startOffset - b.tok.startOffset); + + let idIdx = idStartIndex; + let exprIdx = 0; + + steps.forEach(step => { + if (step.kind === 'dot') { + const propTok = ctx.Identifier[idIdx++]; + obj = { + type: 'MemberExpression', + computed: false, + object: obj, + property: { type: 'Identifier', name: propTok.image } + }; + } else { + const propExpr = visitor.visit(ctx.expression[exprIdx++]); + obj = { + type: 'MemberExpression', + computed: true, + object: obj, + property: propExpr + }; + } + }); + + return obj; +} + class AstBuilder extends BaseHiVisitor { constructor() { super(); this.validateVisitor(); } - + program(ctx) { return { type: 'Program', body: this.visit(ctx.statements) }; } statements(ctx) { return ctx.statement?.map(stmt => this.visit(stmt)) || []; } - + statement(ctx) { if (ctx.declaration) return this.visit(ctx.declaration); if (ctx.assignment) return this.visit(ctx.assignment); if (ctx.returnStatement) return this.visit(ctx.returnStatement); if (ctx.expressionStatement) return this.visit(ctx.expressionStatement); } - + returnStatement(ctx) { return { type: 'ReturnStatement', argument: ctx.expression ? this.visit(ctx.expression) : null }; } @@ -60,14 +96,23 @@ class AstBuilder extends BaseHiVisitor { }; } + // assignment: assignable '=' expression assignment(ctx) { return { type: 'AssignmentExpression', - left: { type: 'Identifier', name: ctx.Identifier[0].image }, + left: this.visit(ctx.left), right: this.visit(ctx.expression), }; } - + + // assignable: Identifier (.Identifier | [expr])* + assignable(ctx) { + const base = { type: 'Identifier', name: ctx.Identifier[0].image }; + // ctx.Identifier contains base + dot-props + if (!ctx.Dot && !ctx.LBracket) return base; + return buildMemberChain(ctx, base, this, 1); + } + expression(ctx) { return this.visit(ctx.conditionalExpression); } conditionalExpression(ctx) { @@ -79,8 +124,9 @@ class AstBuilder extends BaseHiVisitor { alternate: ctx.alternate ? this.visit(ctx.alternate) : { type: 'NullLiteral' }, }; } - + equalityExpression(ctx) { return buildBinaryExpression(ctx, this); } + relationalExpression(ctx) { return buildBinaryExpression(ctx, this); } additiveExpression(ctx) { return buildBinaryExpression(ctx, this); } multiplicativeExpression(ctx) { return buildBinaryExpression(ctx, this); } @@ -99,60 +145,82 @@ class AstBuilder extends BaseHiVisitor { } argumentList(ctx) { return ctx.expression.map(expr => this.visit(expr)); } - + memberExpression(ctx) { - let obj = this.visit(ctx.object); - ctx.Identifier?.forEach(prop => { - obj = { type: 'MemberExpression', computed: false, object: obj, property: { type: 'Identifier', name: prop.image } }; - }); - ctx.expression?.forEach(prop => { - obj = { type: 'MemberExpression', computed: true, object: obj, property: this.visit(prop) }; - }); - return obj; + const base = this.visit(ctx.object); + if (!ctx.Dot && !ctx.LBracket) return base; + + // ctx.Identifier are only the dot properties (no base here) + return buildMemberChain(ctx, base, this, 0); } primary(ctx) { if (ctx.literal) return this.visit(ctx.literal); if (ctx.Identifier) return { type: 'Identifier', name: ctx.Identifier[0].image }; if (ctx.At) return { type: 'ThisExpression' }; - if (ctx.block) return this.visit(ctx.block); if (ctx.arrowExpression) return this.visit(ctx.arrowExpression); + if (ctx.functionExpression) return this.visit(ctx.functionExpression); + if (ctx.objectLiteral) return this.visit(ctx.objectLiteral); + if (ctx.codeBlock) return this.visit(ctx.codeBlock); if (ctx.arrayLiteral) return this.visit(ctx.arrayLiteral); - if (ctx.expression) return this.visit(ctx.expression); + if (ctx.parenthesizedExpression) return this.visit(ctx.parenthesizedExpression); } - + literal(ctx) { if (ctx.Number) { const image = ctx.Number[0].image; if (image === '!0') return { type: 'BooleanLiteral', value: true }; return { type: 'NumericLiteral', value: Number(image) }; } - if (ctx.String) return { type: 'StringLiteral', value: ctx.String[0].image.slice(1,-1) }; + if (ctx.String) return { type: 'StringLiteral', value: ctx.String[0].image.slice(1, -1) }; if (ctx.Null) return { type: 'NullLiteral' }; } - block(ctx) { - const params = ctx.parameterList ? this.visit(ctx.parameterList) : []; + // codeBlock -> Block node (for implicit return/IIFE in conditionals) + codeBlock(ctx) { const body = this.visit(ctx.statements); - if (ctx.parameterList) { // If there's a param list, it's a function - return { type: 'FunctionExpression', params, body: { type: 'BlockStatement', body } }; - } return { type: 'Block', body }; } + // functionExpression -> FunctionExpression node + functionExpression(ctx) { + const params = this.visit(ctx.parameterList); + const body = this.visit(ctx.codeBlock); + return { type: 'FunctionExpression', params, body: { type: 'BlockStatement', body: body.body } }; + } + + // arrowExpression(params => body) arrowExpression(ctx) { const params = this.visit(ctx.parameterList); - const body = ctx.block ? this.visit(ctx.block) : this.visit(ctx.expression); + const body = ctx.codeBlock ? this.visit(ctx.codeBlock) : this.visit(ctx.expression); return { type: 'ArrowFunctionExpression', params, body }; } - + + // parenthesizedExpression simply returns the inner expression + parenthesizedExpression(ctx) { return this.visit(ctx.expression); } + parameterList(ctx) { return ctx.Identifier?.map(id => ({ type: 'Identifier', name: id.image })) || []; } - + arrayLiteral(ctx) { return { type: 'ArrayLiteral', elements: ctx.expression?.map(e => this.visit(e)) || [] }; } + + objectLiteral(ctx) { + const props = []; + const ids = ctx.Identifier || []; + const exprs = ctx.expression || []; + for (let i = 0; i < ids.length; i++) { + props.push({ + type: 'Property', + key: ids[i].image, + value: this.visit(exprs[i]) + }); + } + return { type: 'ObjectLiteral', properties: props }; + } } export const astBuilder = new AstBuilder(); +