Close #15/#16: stamp errors with their call site; expose it to builtins - #17
Conversation
Both issues are the same gap seen from two sides: the interpreter knows
*where* evaluation is, bridges and builtins know *what* went wrong, and
no channel connected the two.
The channel is a task-local. The expression dispatcher binds
`Interpreter.evaluationOffset` to each node's UTF-8 offset for the
duration of its evaluation, so the innermost binding always names the
expression being evaluated — which, while a builtin or bridge body
runs, is the call that invoked it. Task-local rather than instance
state so script `Task { … }` concurrency can't corrupt a save/restore
stack, and unwinding is automatic.
Issue #15 — thrown errors:
- `RuntimeError` gains an `indirect case positioned(RuntimeError, at:)`
plus `positioned(at:)`, which never double-wraps: the earliest
(innermost, most precise) stamp wins. Existing cases and call sites
are untouched.
- `ScriptError` gains an `offset` — the catchable-error vehicle from
#13 can now say where it was raised.
- `callingBridge` stamps everything it wraps with the invoking
expression's offset: RuntimeErrors (both the boxed error and the
signal), raw host errors, and pre-wrapped signals from generated
Foundation bridges. Script throws are stamped at the `throw`
statement itself.
- The dispatcher and statement executor back-stop anything still
unpositioned, so `fatalError`, division by zero, and plain
`.invalid` diagnostics now render with carets too.
Issue #16 — recorded failures that never become an Error:
- `currentCallOffset` exposes the task-local while a builtin runs, so
an assertion-shaped global that records-and-continues (Swift
Testing / XCTest semantics) can capture its call site.
- `renderSourceContext(at:message:)` drives the existing caret
renderer from a bare (offset, message) pair; `renderRuntimeError`
is now a thin wrapper over it and also understands positioned
`ScriptError`s.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8efeb4f329
ℹ️ 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".
Codex review: a custom host error thrown from a `registerGlobal` closure escaped `invoke` raw — neither dispatcher catch matched it, so it rendered as a bare `error:` line with no call site, unlike the same error thrown from a bridge body. Route `.builtin` / `.builtinMethod` invocation through a new `callingBuiltin` wrapper: an arbitrary host error becomes a catchable `ScriptError` stamped with the invoking call's offset (the same #13 contract bridges follow), while a `RuntimeError` still passes through raw so the diagnostic builtins (`fatalError`, `precondition`, `assert`) keep terminating the script trap-style — positioned by the dispatcher on the way out. Control-flow signals and `ScriptExit` pass through untouched. Also wrap the three `.staticComputed` bridge invocation sites in `callingBridge` — same gap, same fix as instance computed properties. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Reviewed this from the perspective of a downstream host — Loupe embeds the interpreter and registers its own bridges and globals, so it exercises most of the surface this PR touches. I built the PR branch against it and probed each claim below against The positioning itself is goodThe core of #15/#16 works, and it needed no host changes at all to start paying off. A thrown bridge error now renders with the line and caret for free, and after capturing That is exactly what #16 was for. The offset also survives the shapes I expected to break it — inside a script Two things I'd change before merging, and one follow-up. Both of the first two are the same shape: the PR adds wrapping in two places and unwrapping in neither. 1.
|
…n anchors Three changes from the Loupe-based review of #17: 1. Drop `indirect case positioned` — position is now an `at: Int?` payload on `.invalid` and `.divisionByZero` directly, matching the shape `.unsupported` / `.unknownIdentifier` already had. Attaching a position no longer changes what the error *is*, so host-side `if case .invalid` / `catch RuntimeError.<case>` patterns keep matching. A `static func invalid(_:)` factory keeps every existing one-argument raise site source-compatible; `positioned(at:)` now reconstructs the case instead of wrapping. 2. Host control-flow sentinels can no longer be swallowed by scripts. New `ScriptUncatchableError` marker protocol: errors conforming to it pass through both invocation wrappers raw — like `ScriptExit` — so an XCTSkip-style sentinel thrown from a registered global or a bridge always reaches the host, immune to script `catch` / `try?`. Also `ScriptError.hostError` unboxes the wrapped host error so embedders (and our own tests) stop unpacking `.opaque` by hand. 3. Postfix chains anchor diagnostics on their own token — the member name, or a subscript's opening bracket — instead of the start of the receiver, so a line-broken chain (`app\n .buttons[…]\n .tap()`) blames the failing member's line, matching stock Swift attribution. Tests: case-shape matching after positioning, sentinel passthrough from both globals and bridges, `hostError`, and a line-broken chain. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
All three addressed in 5966c2d — thank you for probing this against a real host; the Loupe sentinel regression and the case-matching asymmetry were both things the suite could never have caught from inside this repo. 1. Took your first option: 2. Both suggestions taken. 3. Did the chain-anchor fix now rather than as a follow-up — your sketch was right and it's ~15 lines. Single-line calls moved with it ( Full suite: 561 tests green. 🤖 Addressed by Claude Code |
…he positioned-error model RuntimeError.noMacro keeps its always-present offset, joining unsupported / unknownIdentifier in positioned(at:)'s already-stamped group; invalid / divisionByZero carry main's optional positions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes #15, closes #16.
Both issues are the same gap seen from two sides: the interpreter knows where evaluation is, bridges and builtins know what went wrong, and no channel connected the two. The channel is a task-local: the expression dispatcher binds
Interpreter.evaluationOffsetto each node's UTF-8 offset for the duration of its evaluation, so the innermost binding always names the expression being evaluated — which, while a builtin or bridge body runs, is the call that invoked it. Task-local rather than instance state so scriptTask { … }concurrency can't corrupt a save/restore stack, and unwinding is automatic.#15 — thrown errors name their line
RuntimeErrorgainsindirect case positioned(RuntimeError, at: Int)pluspositioned(at:), which never double-wraps — the earliest (innermost, most precise) stamp wins. Existing cases, call sites, anddescriptionare untouched, socatch-and-match code keeps working.ScriptErrorgains anoffset: Int?— the catchable-error vehicle from Close #11/#12: defer leading-dot member args to the callee; make bridge errors catchable #13 can now say where it was raised.callingBridgestamps everything it wraps with the invoking expression's offset:RuntimeErrors (both the boxed error and the signal around it), raw host errors, and pre-wrapped signals thrown by generated Foundation bridges. Scriptthrows are stamped at the throw statement itself.fatalError, division by zero, and plain.invaliddiagnostics render with carets now too.A failing flow now reads:
#16 — recorded failures that never become an
Errorpublic var currentCallOffset: Int?exposes the task-local while a builtin runs, so an assertion-shaped global registered viaregisterGlobalthat records-and-continues (Swift Testing / XCTest semantics) can capture its call site at the moment it fires.public func renderSourceContext(at:message:)drives the existing caret renderer from a bare(offset, message)pair — out-of-range offsets clamp, no source in scope falls back to a plainerror:line.renderRuntimeErroris now a thin wrapper over it and also understands positionedScriptErrors.Tests
13 new tests in
CallSitePositionTestscover: uncaught bridgeRuntimeError/ raw host error / generated-bridge subscript error rendering withfile:line:col+ caret, the boxedRuntimeErrorcarrying the offset for hosts that dig it out, catchability + message unchanged (#13 regression), uncaught scriptthrow, division by zero,fatalError, precise offsets not being overwritten, the record-and-continue assertion flow end to end,currentCallOffsetnil outside evaluation, and both renderer fallbacks. Full suite: 554 tests / 61 suites green, probe conformance included.🤖 Generated with Claude Code