Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 91 additions & 53 deletions packages/compiler/src/expressions/compile-expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import {
type Expression,
type BinaryOperator,
type TemplatePart,
MemberExpression,
AtIdentifier,
Expand All @@ -31,7 +32,7 @@ import {
decomposeAtMemberExpression,
escapeStringValue,
} from '@agentscript/language';
import type { Range, SyntaxNode } from '@agentscript/types';
import type { Range } from '@agentscript/types';
import type { CompilerContext } from '../compiler-context.js';
import { extractNumberValue } from '../ast-helpers.js';

Expand Down Expand Up @@ -297,17 +298,21 @@ function compileMemberExpression(
}

default: {
const obj = compileExprNode(expr.object, ctx, opts);
const obj = compileAccessTarget(expr.object, ctx, opts);
return `${obj}.${property}`;
}
}
}

const obj = compileExprNode(expr.object, ctx, opts);
// Convert .length to len() function call for runtime compatibility
if (expr.property === 'length') {
return `len(${obj})`;
return `len(${compileExprNode(expr.object, ctx, opts)})`;
}
// A number before `.` would lex as a float literal (`1.real`), so wrap it.
const obj =
expr.object instanceof NumberLiteral
? `(${compileExprNode(expr.object, ctx, opts)})`
: compileAccessTarget(expr.object, ctx, opts);
return `${obj}.${expr.property}`;
}

Expand Down Expand Up @@ -437,7 +442,7 @@ function compileSubscriptExpression(
return `system.uploaded_files[${compileIndex()}]`;
}

const obj = compileExprNode(expr.object, ctx, opts);
const obj = compileAccessTarget(expr.object, ctx, opts);
return `${obj}[${compileIndex()}]`;
}

Expand All @@ -458,42 +463,75 @@ function compileSlice(
return `${start}:${stop}`;
}

function getParenthesizedCst(expr: Expression): SyntaxNode | undefined {
let node = expr.__cst?.node;
let parenthesized: SyntaxNode | undefined;
// The dialect unwraps grouping and expression/atom nodes from the AST.
// Retain the outermost grouping node for comparison source-text boundaries.
while (node) {
if (node.type === 'parenthesized_expression') parenthesized = node;
const parent = node.parent;
if (
parent?.type !== 'parenthesized_expression' &&
parent?.type !== 'expression' &&
parent?.type !== 'atom'
) {
break;
}
node = parent;
/**
* Operator binding strength, following Python's precedence because compiled
* expressions are evaluated as Python. Higher binds tighter; everything that
* is not an operator expression (literals, references, calls, subscripts,
* member accesses) is primary.
*/
const PRECEDENCE = {
ternary: 1,
or: 2,
and: 3,
not: 4,
comparison: 5,
additive: 6,
multiplicative: 7,
unary: 8,
primary: 9,
} as const;

function binaryPrecedence(operator: BinaryOperator): number {
switch (operator) {
case 'or':
return PRECEDENCE.or;
case 'and':
return PRECEDENCE.and;
case '+':
case '-':
return PRECEDENCE.additive;
case '*':
case '/':
return PRECEDENCE.multiplicative;
}
}

function precedenceOf(expr: Expression): number {
if (expr instanceof TernaryExpression) return PRECEDENCE.ternary;
if (expr instanceof ComparisonExpression) return PRECEDENCE.comparison;
if (expr instanceof UnaryExpression) {
return expr.operator === 'not' ? PRECEDENCE.not : PRECEDENCE.unary;
}
return parenthesized;
if (expr instanceof BinaryExpression) return binaryPrecedence(expr.operator);
// A negative literal (only constructible in code) prints with a leading `-`.
if (expr instanceof NumberLiteral && expr.value < 0) return PRECEDENCE.unary;
return PRECEDENCE.primary;
}

/**
* Compile an operand, adding parentheses when it binds more loosely than
* `minPrecedence`. Grouping comes from the shape of the tree, not from the
* source text, so the output is correct for hand-built ASTs too.
*/
function compileOperand(
expr: Expression,
minPrecedence: number,
ctx: CompilerContext,
opts: CompileExpressionOptions
): string {
const compiled = compileExprNode(expr, ctx, opts);
return getParenthesizedCst(expr) ? `(${compiled})` : compiled;
return precedenceOf(expr) < minPrecedence ? `(${compiled})` : compiled;
}

function compileBinaryExpression(
expr: BinaryExpression,
ctx: CompilerContext,
opts: CompileExpressionOptions
): string {
const left = compileOperand(expr.left, ctx, opts);
const right = compileOperand(expr.right, ctx, opts);
// Left-associative: an equal-precedence right operand needs parentheses.
const precedence = binaryPrecedence(expr.operator);
const left = compileOperand(expr.left, precedence, ctx, opts);
const right = compileOperand(expr.right, precedence + 1, ctx, opts);
return `${left} ${expr.operator} ${right}`;
}

Expand All @@ -502,10 +540,11 @@ function compileUnaryExpression(
ctx: CompilerContext,
opts: CompileExpressionOptions
): string {
const operand = compileOperand(expr.operand, ctx, opts);
if (expr.operator === 'not') {
return `not ${operand}`;
return `not ${compileOperand(expr.operand, PRECEDENCE.not, ctx, opts)}`;
}
// Wrap a nested sign too, so `-(-1)` never prints as `--1`.
const operand = compileOperand(expr.operand, PRECEDENCE.primary, ctx, opts);
return `${expr.operator}${operand}`;
}

Expand All @@ -514,28 +553,11 @@ function compileComparisonExpression(
ctx: CompilerContext,
opts: CompileExpressionOptions
): string {
const left = compileOperand(expr.left, ctx, opts);
const right = compileOperand(expr.right, ctx, opts);

// Preserve original whitespace around the operator from the source text
const cstText = expr.__cst?.node?.text;
if (cstText) {
const leftCst = (getParenthesizedCst(expr.left) ?? expr.left.__cst?.node)
?.text;
const rightCst = (getParenthesizedCst(expr.right) ?? expr.right.__cst?.node)
?.text;
if (leftCst && rightCst) {
// Extract the operator with surrounding whitespace from the CST
const leftStart = cstText.indexOf(leftCst);
const leftEnd = leftStart + leftCst.length;
const rightStart = cstText.lastIndexOf(rightCst);
if (leftStart >= 0 && rightStart > leftEnd) {
const operatorWithSpace = cstText.slice(leftEnd, rightStart);
return `${left}${operatorWithSpace}${right}`;
}
}
}

// Python chains comparisons (`a < b < c` is `a < b and b < c`), so a
// comparison operand of a comparison is always parenthesized.
const minPrecedence = PRECEDENCE.comparison + 1;
const left = compileOperand(expr.left, minPrecedence, ctx, opts);
const right = compileOperand(expr.right, minPrecedence, ctx, opts);
return `${left} ${expr.operator} ${right}`;
}

Expand All @@ -544,18 +566,34 @@ function compileTernaryExpression(
ctx: CompilerContext,
opts: CompileExpressionOptions
): string {
const consequence = compileExprNode(expr.consequence, ctx, opts);
const condition = compileExprNode(expr.condition, ctx, opts);
const alternative = compileExprNode(expr.alternative, ctx, opts);
// Right-associative: only the alternative may be an unparenthesized ternary.
const operandMin = PRECEDENCE.ternary + 1;
const consequence = compileOperand(expr.consequence, operandMin, ctx, opts);
const condition = compileOperand(expr.condition, operandMin, ctx, opts);
const alternative = compileOperand(
expr.alternative,
PRECEDENCE.ternary,
ctx,
opts
);
return `${consequence} if ${condition} else ${alternative}`;
}

/** The target of `.x`, `[i]` or `(...)` must be primary. */
function compileAccessTarget(
expr: Expression,
ctx: CompilerContext,
opts: CompileExpressionOptions
): string {
return compileOperand(expr, PRECEDENCE.primary, ctx, opts);
}

function compileCallExpression(
expr: CallExpression,
ctx: CompilerContext,
opts: CompileExpressionOptions
): string {
const func = compileExprNode(expr.func, ctx, opts);
const func = compileAccessTarget(expr.func, ctx, opts);
const args = expr.args
.map((a: Expression) => compileExprNode(a, ctx, opts))
.join(', ');
Expand Down
4 changes: 2 additions & 2 deletions packages/compiler/src/nodes/compile-collect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,8 @@ export function buildPriorGuardByTarget(
/**
* Right-associate a list of operands under a binary operator, parenthesizing
* the tail so that 3+ operand expressions read `A OP (B OP (C OP D))`. The
* runtime evaluator requires explicit parens for 3+ operand expressions (the
* compiler's expression emitter never auto-parenthesizes), and this matches the
* runtime evaluator requires explicit parens for 3+ operand expressions (these
* strings are built here, not by the expression emitter), and this matches the
* only existing 3-operand `enabled` condition in the golden corpus
* (`pronto_customer_support`: `A and (B and C)`). For 1 operand the operand is
* returned as-is; for 2 it is `A OP B` (no parens needed).
Expand Down
Loading
Loading