Skip to content

A multi-line binary operator's runtime error reports the operator's line (#1425) - #1444

Closed
InauguralPhysicist wants to merge 4 commits into
mainfrom
operator-line
Closed

InauguralPhysicist wants to merge 4 commits into
mainfrom
operator-line

Conversation

@InauguralPhysicist

Copy link
Copy Markdown
Collaborator

Closes #1425

This implements the owner's decision on #1425 (option a): a runtime error raised by a binary operator whose operands span lines reports the operator's line. That applies to e.line, the Error line N header, the excerpt and caret, and the traceback. Temporal filing (the #1381 first-line rule) is unchanged.

How. compile_operand_then_op in src/compiler.c records whether the right operand asked emit_line for another line. It re-stamps the operator's line before the op only in that case, so single-line code compiles to identical bytecode. An f-string's synthesized + is excluded (new binop.synth flag); an operator the user writes inside an interpolation follows the rule.

One judgement call (owner may veto): the rule also covers compound += on a field or index when it spans lines. Plain x += on a name already parses as a binary operator, so leaving the field and index forms out would make one operator report two different lines. Other multi-line constructs keep their old line, since #1425 leaves them undecided: call arguments, indexing, a non-compound index or field store, and unary -/not.

Verified.

  • New tests/test_operator_line.eigs (22 rows) plus module and function fixtures in [0g], covering the interpreter, the JIT and OSR, each with a compiled-code witness. On the base: 6 of 22 rows pass. Plants:

    Plant Result
    never re-stamp all nine new rows red, in all three tiers
    field/index compound forms put back 3 red
    JIT forced off over [0g] every JIT/OSR row reports that it compiled nothing
  • Bytecode: 244 programs and 78 lib modules are byte-identical to the base, so there is no cost to single-line code. Ir is unchanged: bench_perf −0.003%, bench_dmg_shape −0.001%.

  • Tapes: the only added L records come from eval'd multi-line f-strings, and none are removed. Strict replay is identical on 36/36 tier pairs.

  • make test-changed: 1327/1327. tools/jit_diff.sh: OK.

  • Docs: SPEC.md (an executed example), TRACE.md and llms.txt.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KC99CmwatKssQYggkBCgwF

xxxxxxxxxxxxx and others added 2 commits September 30, 2026 20:40
…ine (#1425)

Owner decision on #1425, option (a): a runtime error raised by a binary
operation whose operands span lines reports the physical line of the
OPERATOR in e.line, the `Error line N` header, the excerpt and caret, and the
traceback. Temporal filing (#1381, the first line) is unchanged.

    x is 0
    z is 1 / (
     0)

Before: `Error line 3: division by zero`, no excerpt (the `/` column does not
exist on line 3). After: `Error line 2`, excerpt of line 2, caret under `/`.

Mechanism: the VM line when an op runs is the last OP_LINE executed in the
frame (a call's return restores the caller's line since #1424/#1434). The
compiler now tracks the min/max line its right operand REQUESTED from
emit_line (compile_operand_then_op). Only when that operand requested a line
other than the operator's does it re-stamp the operator's line before the op.
Single-line code therefore compiles to identical bytecode (244 test programs
and 78 lib modules: 0 differing dumps vs ea9ee90). Used by AST_BINOP and the
compound-assignment ops on a field and an index (`x += ...` on a name already
desugars to AST_BINOP). An f-string's lexer-synthesized `+` keeps the old
stamping (binop.synth); an operator written inside an interpolation follows
the rule.

Tests: tests/test_operator_line.eigs (22 rows: / % + - * & <, a chain whose
second + faults, a multi-line call operand on each side, an operator on the
line after its left operand, += on a name/field/index, an operator inside an
f-string interpolation, hot JIT callee and OSR loop rows, and #1381 filing)
plus two uncaught fixtures pinning header, excerpt, caret and traceback, all
enrolled in [0g] under the interpreter, the JIT and forced OSR. Base ea9ee90:
16 of 22 rows fail; both fixtures report the operand's line.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KC99CmwatKssQYggkBCgwF
TRACE.md: the L record a multi-line binary operator adds (or moves) on the
tape, and that single-line code and f-string concatenation emit nothing
extra. llms.txt: the error-line sentence names the operator's line for a
binary operator whose operands span lines.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KC99CmwatKssQYggkBCgwF
@codspeed

codspeed Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 12 untouched benchmarks


Comparing operator-line (97c0eb5) with main (d7398b5)

Open in CodSpeed

@InauguralPhysicist

Copy link
Copy Markdown
Collaborator Author

Round-1 blind critic (GPT-6 Astra, 252 executions): FAIL, on two counts, both being fixed in round 2.

  1. A successful multi-line binary op leaves its operator line stamped, so an enclosing unary, call or index error reports the operator's line. For example r is -("a" +\n "b") reports line 1, where it used to report 2. The fix restores the attribution after the op.
  2. The compound field/index += extension is reverted. Those forms are separate AST nodes (AST_DOT_ASSIGN/AST_INDEX_ASSIGN), outside the binary-only decision on Multi-line binary expression: a runtime error reports the OPERATOR's line (decided: option a) #1425, and need their own owner decision. Plain x += rhs (which desugars to a binary op) stays covered.
    Everything else the critic attacked matched the decision: chains, precedence, eval, imports, recursion, OSR mid-expression, and the f-string exclusion.

Save the executed line around a multiline binary instruction and restore it
on success in the VM, JIT and OSR. Runtime state preserves short-circuit
attribution; one-line code emits no new bytecode. Append and verify adjacent
scope opcodes, disallow jump/handler entries into them, and restore worker
trace lines without reading the main thread cache.

Revert dedicated compound field/index restamping to ea9ee90 behavior.
Compound field/index forms are left for a separate owner decision; plain
name += remains a binary operation. Document the exact binary-only scope.

Validation receipts against ea9ee90:
- Bytecode: tests=244/244 lib=78/78 identical; differing=0.
- Parent controls: base/fixed 20/20, head 5b25a89 3/20 in each of 3 tiers.
- Final parent regression: 35/35 in each tier; eight per-expression native
  stamp witnesses in each compiled tier.
- Targeted sections 0g/42a/42f/42f2: 304/304, 0 failed/skipped; chunks=5 bearing=5.
- make test-changed BASE=ea9ee90: 1398/1398, 0 failed/skipped;
  chunks=38 bearing=37; full=no.
- ASan targeted plus 42h, detect_leaks=1: 326/326, 0 failed/skipped;
  chunks=6 bearing=6; no leak report.
- precheck: 18 passed, 0 failed/skipped; JIT smoke: all cases passed.
- Strict differential: 99 raises, 0 silent/misattributed; 31 pins, 0 broken;
  valid-input 255/255 unchanged in all 3 modes; no-gfx rows skipped.
- Tape diff: 246 programs x 3 tiers=738 pairs; 691 identical after pinning,
  35 changed, 12 boundary-nondeterministic; exact records retained.
- Strict replay: 738 recordings x 3 tiers=2214 combinations; 2115 matched,
  99 documented boundary refusals, 0 stable divergences, 0 unresolved.
- Critic matrix: 312 executions; 99 identical cases, 57 intended binary
  diagnostic changes.
- Plants: head fails 17/20 parent controls per tier; missing verifier
  constraints fail 4/29 checks; stale MT trace save fails 1/22 checks
  (2000 zero-line records), restored 22/22.
- jit_diff: OK (246 programs x {jit, osr} vs the interpreter; OBS
  examined=591 denied=12; 31 arms adjudicated; 3 ledgered), unchanged.
- replay_diff: OK (246 programs record+replay; 11 at the documented
  boundary; 0 nondeterministic; 0 ledgered).

Full suites were intentionally not run on this machine per the builder brief.
Tape differences include known thread scheduling/transition deduplication,
checkout paths in import warnings, and untaped mktemp paths (filed #1464).
The complete changed-record inventory and native witness receipts are retained
under /tmp/es-1425c-evidence; RESULT.md documents caveats.

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
@InauguralPhysicist
InauguralPhysicist deleted the operator-line branch October 1, 2026 07:03
@InauguralPhysicist
InauguralPhysicist restored the operator-line branch October 1, 2026 07:03
@InauguralPhysicist

Copy link
Copy Markdown
Collaborator Author

@codex review (head 97c0eb5)

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-01T11:42:30.832095Z 97c0eb5 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 97c0eb57cc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/compiler.c
Comment on lines +570 to +571
if (scope) emit_op_u32(c, OP_BINARY_LINE, (uint32_t)op_line, op_line);
emit(c, opc, op_line);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the compound operator column for diagnostics

When this path handles a multiline plain-name compound assignment such as x += (\n "s"), the desugared AST_BINOP still carries the variable's column (parser.c:1841) rather than the += token's column. Consequently the new operator-line scope reports the correct line but places the caret under x, contradicting the documented rule that the caret identifies the faulting operator. Preserve the compound token's column when constructing the binary node so the opcode emitted here is attributed to +=.

Useful? React with 👍 / 👎.

@InauguralPhysicist

Copy link
Copy Markdown
Collaborator Author

Codex review (agentic, GPT-6.1 Sol): Didn't find any major issues (P0/P1). Reviewed commit: 97c0eb57cc

@InauguralPhysicist
InauguralPhysicist added this pull request to the merge queue Oct 1, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 2, 2026
@InauguralPhysicist
InauguralPhysicist marked this pull request as draft October 2, 2026 11:52
@InauguralPhysicist

Copy link
Copy Markdown
Collaborator Author

Closing as superseded by drift, not rejected. The design (#1425, option a: report the operator's line) stands, and Codex's review of 97c0eb5 found no P0/P1 issues.

Since this branch was cut, #1665 arm D (B0–B10, #1701–#1726) rewrote large parts of vm.c, jit.c and compiler.c. A merge against main now conflicts in 23 files.

The plan is to re-implement #1425 on current main, using this diff as the reference (compile_operand_then_op re-stamping the operator's line only when the right operand moved the line, so single-line bytecode is unchanged). Suite enrolment goes through a tests/sections/ fragment, since run_all_tests.sh is no longer edited to enrol tests. The branch stays available for reference.

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.

Multi-line binary expression: a runtime error reports the OPERATOR's line (decided: option a)

2 participants