Skip to content

fix(compiler): pop stale bool from stack in emitLoopBackwards#954

Merged
antonmedv merged 1 commit intoexpr-lang:masterfrom
lawrence3699:fix/findlast-map-literal-crash
Apr 12, 2026
Merged

fix(compiler): pop stale bool from stack in emitLoopBackwards#954
antonmedv merged 1 commit intoexpr-lang:masterfrom
lawrence3699:fix/findlast-map-literal-crash

Conversation

@lawrence3699
Copy link
Copy Markdown
Contributor

Fixes #950

findLast and findLastIndex crash with interface conversion: interface {} is bool, not string when used inside map literal expressions.

Failing scenario:

expr.Eval(`{"r": findLast([1, 2, 3, 4, 5], # > 3)}`, nil)
// runtime error: interface {} is bool, not string

The forward-iterating equivalents (find, findIndex) work fine in the same position.

Root cause:
emitLoopBackwards uses OpMoreOrEqual + OpJumpIfFalse to check the loop condition but never pops the comparison result. The stale bool corrupts the value stack — when OpMap tries to pop a string key, it gets the leftover bool instead.

The forward emitLoop avoids this by using OpJumpIfEnd, which checks scope.Index directly without touching the stack.

Fix:
Add OpPop after OpJumpIfFalse (continue path) and after patchJump (exit path). This matches the convention used by emitCond and every other OpJumpIfFalse callsite in the compiler.

After fix:

expr.Eval(`{"r": findLast([1, 2, 3, 4, 5], # > 3)}`, nil)
// => map[r:5]

Validation:

  • Full test suite passes (60+ packages)
  • Added regression tests covering: findLast/findLastIndex in map literals, no-match case, multiple backward-iterating builtins in one map

emitLoopBackwards uses OpMoreOrEqual + OpJumpIfFalse to check the loop
condition, but never pops the comparison result from the stack. This
leaves a stale bool that corrupts the value stack for the parent context.

When findLast or findLastIndex is used inside a map literal like
{"r": findLast([1, 2, 3], # > 3)}, the OpMap opcode tries to pop a
string key but finds the leftover bool instead, causing:
  interface conversion: interface {} is bool, not string

The forward-iterating emitLoop avoids this by using OpJumpIfEnd, which
checks scope.Index directly without touching the stack.

Fix: add OpPop after OpJumpIfFalse (continue path) and after patchJump
(exit path), matching the convention used by emitCond and every other
OpJumpIfFalse callsite in the compiler.

Fixes expr-lang#950
Copilot AI review requested due to automatic review settings April 12, 2026 11:27
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes a VM stack corruption bug triggered by reverse-iterating builtins (findLast, findLastIndex) when used inside map literal expressions by ensuring the loop-condition boolean is popped in emitLoopBackwards.

Changes:

  • Pop the OpMoreOrEqual condition result on both the continue and exit paths in emitLoopBackwards.
  • Add regression tests for findLast / findLastIndex used within map literals (including multiple reverse builtins in the same map and a no-match case).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
expr_test.go Adds regression coverage for reverse-iterating builtins inside map literals to prevent the reported runtime crash.
compiler/compiler.go Fixes stack hygiene in emitLoopBackwards by popping the loop-condition boolean on both control-flow paths.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@antonmedv antonmedv merged commit 3a46b19 into expr-lang:master Apr 12, 2026
23 of 24 checks passed
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.

findLast/findLastIndex crash inside map literal expressions

3 participants