Skip to content

feat(engine): AST interpreter with arithmetic ops and LRU cache#10

Open
albertgwo wants to merge 2 commits intomainfrom
feat/engine-pr0b-ast-arithmetic-lru
Open

feat(engine): AST interpreter with arithmetic ops and LRU cache#10
albertgwo wants to merge 2 commits intomainfrom
feat/engine-pr0b-ast-arithmetic-lru

Conversation

@albertgwo
Copy link
Copy Markdown
Contributor

@albertgwo albertgwo commented Mar 19, 2026

User description

Summary

  • Add AST-based expression interpreter (browser-safe replacement for node:vm)
  • Support arithmetic operators (+, -, *, /, %, **), comparisons, logical ops
  • Add LRU parse cache for repeated expression evaluation
  • Add expression validator for safe-list enforcement

Dependency

Base: main (independent of PR 0a)

PR 2 of 16 in the execution engine implementation.

Test plan

  • Arithmetic operations produce correct results
  • AST interpreter handles nested expressions and dot-path access
  • LRU cache evicts correctly at capacity
  • Validator rejects unsafe function calls and property access

Generated description

Below is a concise technical summary of the changes proposed in this PR:
Enable safer flowprint expression execution by adding a browser-safe AST interpreter that uses the allowlist and re-exports interpretExpression, InterpreterError, and InterpreterContext so the engine can evaluate arithmetic/logical expressions without node:vm. Introduce a shared LRU-backed parse cache exposed through parseExpression/clearParseCache so repeated parses reuse results for faster validation flows.

TopicDetails
Parse caching Introduce an LRU cache implementation, wire it into parseExpression with caching/clearing helpers, verify cache semantics via dedicated tests, and expand parser suites to cover arithmetic parsing plus cache behavior.
Modified files (5)
  • packages/engine/src/__tests__/expressions/cache.test.ts
  • packages/engine/src/__tests__/expressions/parse-cache.test.ts
  • packages/engine/src/__tests__/expressions/parser.test.ts
  • packages/engine/src/expressions/cache.ts
  • packages/engine/src/expressions/parser.ts
Latest Contributors(1)
UserCommitDate
albertgwo@gmail.comfeat-engine-add-expres...January 31, 2026
AST Interpreter Implement an allowlisted AST interpreter that evaluates literals, member/member-call accesses, arithmetic/comparison/logical/unary/conditional expressions, manages safe Math calls, and exposes the interpreter plus error/context types through the expression and engine entry points while covering behavior with interpreter and parser-security suites.
Modified files (6)
  • packages/engine/src/__tests__/expressions/interpreter.test.ts
  • packages/engine/src/__tests__/expressions/parser-security.test.ts
  • packages/engine/src/expressions/allowlist.ts
  • packages/engine/src/expressions/index.ts
  • packages/engine/src/expressions/interpreter.ts
  • packages/engine/src/index.ts
Latest Contributors(1)
UserCommitDate
albertgwo@gmail.comtest-add-targeted-test...February 07, 2026
This pull request is reviewed by Baz. Review like a pro on (Baz).

…e cache

Add +, -, *, /, % to the expression allowlist so arithmetic expressions
are accepted by the parser. Introduce a browser-safe AST interpreter
that walks parsed acorn ASTs without node:vm, supporting all allowlisted
operators, methods, and Math members. Replace unbounded parse cache with
a 1000-entry LRU cache to bound memory usage.
…cache

Arithmetic parser tests: +, -, *, /, %, precedence (2+3*4=14), mixed
arithmetic+comparison. Interpreter tests: arithmetic evaluation, comparison,
logical, unary, ternary, template literals, method calls, Math functions,
node results, error handling. LRU cache tests: store/retrieve, eviction at
capacity, get promotes recency, capacity=1 edge case, capacity=1000.
Parse cache integration tests: cached result identity, clear, isolation.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying flowprint with  Cloudflare Pages  Cloudflare Pages

Latest commit: f7b9e89
Status: ✅  Deploy successful!
Preview URL: https://abf6e643.flowprint.pages.dev
Branch Preview URL: https://feat-engine-pr0b-ast-arithme.flowprint.pages.dev

View logs

Comment on lines +1 to +12
/**
* Browser-safe AST interpreter for flowprint expressions.
*
* Walks the acorn AST and evaluates expressions without `node:vm`.
* Only supports the allowlisted AST types and operators.
*/
import * as acorn from 'acorn'
import {
ALLOWED_BINARY_OPS,
ALLOWED_LOGICAL_OPS,
ALLOWED_UNARY_OPS,
ALLOWED_METHODS,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

packages/engine includes package-level changes but lacks a .changeset/*.md entry — should we add the required changeset per CLAUDE.md?

Finding type: AI Coding Guidelines | Severity: 🟢 Low


Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents:

In packages/engine/src/expressions/interpreter.ts around lines 1 to 15, a new
interpreter API and exports were introduced but no .changeset entry was added. Create a
new .changeset/<short-descriptive-name>.md file that names the affected package
(packages/engine), describes the change (adds a browser-safe flowprint interpreter API
and new exports), and specifies the release type (patch/minor/major — choose minor if
this is a new public API, otherwise patch). Include a one-sentence summary and any
migration notes (e.g., new exported InterpreterError and interpretExpression), save the
file, and ensure it follows the repo's CLAUDE.md changeset format so CI recognizes the
package-level change.

Heads up!

Your free trial ends tomorrow.
To keep getting your PRs reviewed by Baz, update your team's subscription

Comment on lines +26 to +34
* Build a frozen Math object with only allowlisted methods/constants.
*/
function buildSafeMath(): Readonly<Record<string, unknown>> {
const safeMath: Record<string, unknown> = {}
for (const key of ALLOWED_MATH_MEMBERS) {
safeMath[key] = Math[key as keyof typeof Math]
}
return Object.freeze(safeMath)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildSafeMath is reimplemented verbatim in this interpreter (lines 26‑34) and again in packages/engine/src/runner/evaluator.ts (lines 12‑20). Both constructors iterate over ALLOWED_MATH_MEMBERS, pack them into a new object, and freeze it, so any later change to which Math members are allowed or how the object is built must be made twice. Could we extract this into a shared helper (e.g. expressions/safeMath.ts exporting both buildSafeMath/SAFE_MATH or move it into allowlist.js) and import it from both places? That would keep the Math sanitization logic in one place, making updates (new constants, stricter freezing, etc.) happen once instead of needing identical edits in two files.

Finding type: Code Dedup and Conventions | Severity: 🟢 Low


Want Baz to fix this for you? Activate Fixer

Heads up!

Your free trial ends tomorrow.
To keep getting your PRs reviewed by Baz, update your team's subscription

Comment on lines +55 to +57
export function interpretExpression(source: string, context: InterpreterContext): unknown {
const ast = acorn.parseExpressionAt(source, 0, { ecmaVersion: 2022 })
return evaluate(ast as any, context)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interpretExpression doesn't check for trailing tokens while parseExpression rejects them — should we throw when ast.end doesn't consume the whole source so the interpreter mirrors the parser?

Finding type: Logical Bugs | Severity: 🔴 High


Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents:

In packages/engine/src/expressions/interpreter.ts around lines 55 to 57, the
interpretExpression function parses the source but does not reject trailing tokens.
Modify interpretExpression to compute the parsed node end (ast.end) and compare it to
source.trimEnd().length; if they differ, throw an InterpreterError indicating unexpected
trailing tokens. This ensures the interpreter rejects inputs with extra content just
like the parser does.

Heads up!

Your free trial ends tomorrow.
To keep getting your PRs reviewed by Baz, update your team's subscription

Comment on lines +55 to +57
export function interpretExpression(source: string, context: InterpreterContext): unknown {
const ast = acorn.parseExpressionAt(source, 0, { ecmaVersion: 2022 })
return evaluate(ast as any, context)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interpretExpression lets acorn.parseExpressionAt throw raw SyntaxError — should we catch it and rethrow an InterpreterError?

Finding type: Logical Bugs | Severity: 🔴 High


Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents:

In packages/engine/src/expressions/interpreter.ts around lines 55 to 57, the
interpretExpression function directly calls acorn.parseExpressionAt and does not handle
parse-time failures. Wrap the parseExpressionAt call in a try/catch that catches any
thrown Error (including acorn SyntaxError) and rethrow a new InterpreterError with a
clear message that includes the original error message (and optionally location) before
returning evaluate(ast, context). Ensure TypeScript types are preserved and that
interpreter behavior is unchanged for successful parses.

Heads up!

Your free trial ends tomorrow.
To keep getting your PRs reviewed by Baz, update your team's subscription

@albertgwo
Copy link
Copy Markdown
Contributor Author

Code Review Findings

Critical (90-100 confidence)

1. interpretExpression does not validate source was fully consumed (95)

  • File: packages/engine/src/expressions/interpreter.ts:~62
  • Uses acorn.parseExpressionAt(source, 0, ...) which parses the first expression and stops. "1 + 2; process.exit()" silently ignores everything after 1 + 2. The parser (parser.ts) correctly checks ast.end < source.trimEnd().length but the interpreter has no such check.
  • Fix: Add trailing-content check after parsing:
const ast = acorn.parseExpressionAt(source, 0, { ecmaVersion: 2022 })
if (ast.end < source.trimEnd().length) {
  throw new InterpreterError('Unexpected content after expression')
}

2. Interpreter does not enforce ALLOWED_AST_TYPES allowlist (91)

  • File: packages/engine/src/expressions/interpreter.ts, evaluate function
  • Parser validates every AST node against ALLOWED_AST_TYPES. Interpreter relies on exhaustive switch cases instead of positive allowlist check. Defense-in-depth gap — if future code adds a case branch without updating the allowlist, the two enforcement points diverge.
  • Fix: Add allowlist check at top of evaluate:
if (!ALLOWED_AST_TYPES.has(type)) {
  throw new InterpreterError(`Disallowed AST node type: ${type}`)
}

3. ** (exponentiation) advertised but missing (92)

  • File: packages/engine/src/expressions/allowlist.ts and interpreter.ts
  • PR title mentions ** but it's not in ALLOWED_BINARY_OPS and no case '**' in interpreter.
  • Fix: Either add ** support or remove from PR description.

Important (80-89 confidence)

4. interpretExpression bypasses LRU parse cache (85)

  • File: packages/engine/src/expressions/interpreter.ts:~62
  • Calls acorn.parseExpressionAt directly, bypassing the cache. Same expression re-parsed from scratch on every call, defeating the cache's purpose.

5. Module-level PARSE_CACHE has no test isolation (82)

  • File: packages/engine/src/__tests__/expressions/parser.test.ts
  • No clearParseCache() in beforeEach — tests may exercise cache retrieval instead of actual parsing depending on execution order.

6. evaluateMemberExpression allows __proto__/constructor/prototype access (83)

  • File: packages/engine/src/expressions/interpreter.ts, evaluateMemberExpression
  • Resolves (object as any)[property] for any property name. Prototype chain objects readable.
  • Fix: Add deny-list: const FORBIDDEN_PROPERTIES = new Set(['__proto__', 'constructor', 'prototype'])

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant