Skip to content

Close #11/#12: defer leading-dot member args to the callee; make bridge errors catchable - #13

Merged
odrobnik merged 2 commits into
mainfrom
claude/swiftscript-issues-11-12
Aug 9, 2026
Merged

Close #11/#12: defer leading-dot member args to the callee; make bridge errors catchable#13
odrobnik merged 2 commits into
mainfrom
claude/swiftscript-issues-11-12

Conversation

@odrobnik

@odrobnik odrobnik commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Closes #11, #12 — the two follow-on blockers from the XCUITest work in #9.

#11 — leading-dot member access defers to the callee

A leading-dot member in argument position (app.descendants(matching: .any), element.typeKey(.escape)) no longer errors before any type context is consulted. In evaluateArg it resolves against the parameter's context type when one is known (a bridge static let like .utf8/.whitespaces, a user enum case), and otherwise — only when the receiver is a bridged (opaque) type — defers to the callee as an unresolved .enumValue(typeName: "", caseName:) marker. A bridged parameter has no declared type for the interpreter to consult, so the receiving bridge decides what the case means; a bridge expecting something else raises its own clearer error.

Deferral is deliberately narrow: a bare member passed to a builtin method ([1,2,3].contains(.foo)) or used in general position (let x = .any) stays a hard error, matching stock Swift's "no such member".

#12 — errors thrown from bridges are catchable

An error raised inside a bridge body (.method/.computed/.subscriptGet/…) used to fly past every catch and end the script. Bridge invocations now surface a non-control-flow error as a catchable .opaque(typeName: "Error", …) value, so catch { print(error) } binds it and try? yields nil.

Crucially this is scoped to the bridge-invocation boundary, not a blanket do/catch change: Swift's uncatchable traps (fatalError/precondition/assert, division-by-zero, out-of-bounds) and programming errors (undefined identifier, no-such-member) are raised by the interpreter itself — outside any bridge closure — so they keep terminating the script as they must. Control-flow signals (return/break/continue/fallthrough/exit) are re-thrown untouched.

Tests

New suites for both: implicit-member deferral (incl. a fake element-query bridge that reads the marker, plus builtin-collection and general-position stay-fatal cases) and catchable bridge errors (incl. trap/programming errors staying fatal, control-flow bypass, and exit).

🤖 Generated with Claude Code

…able

Closes #11, #12 — the two follow-on blockers from the XCUITest work
in #9.

#11 (implicit member): a leading-dot member in argument position
(`app.descendants(matching: .any)`, `element.typeKey(.escape)`) no
longer errors before any type context is consulted. In evaluateArg it
resolves against the parameter's context type when one is known (a
bridge static-let like `.utf8`/`.whitespaces`, a user enum case),
and otherwise defers to the callee as an unresolved
`.enumValue(typeName: "", caseName:)` marker — a bridged parameter
has no declared type for the interpreter to consult, so the receiving
bridge decides what the case means and a bridge expecting something
else raises its own clearer error. The deferral is scoped to argument
position; a stray `.foo` in general expressions stays a hard error.

#12 (catchable bridge errors): a RuntimeError (or raw host error)
raised inside a .method/.computed/.subscriptGet body used to fly past
every catch clause and end the script. execute(do:) and
evaluate(try:) now surface any non-control-flow error as a catchable
`.opaque(typeName: "Error", …)` value — `catch { print(error) }`
binds it, `try?` yields nil, and an unmatched clause re-raises the
original so an unhandled error still ends the script with its message.
Control-flow signals (return/break/continue/fallthrough/exit) are
re-thrown explicitly rather than swept up by the catch-all, via an
allowlist as the issue asks.

15 new tests (implicit-member deferral incl. a fake element-query
bridge reading the marker; catchable bridge errors incl. control-flow
bypass and exit); 531 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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: 7ebc71b730

ℹ️ 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 Sources/SwiftScriptInterpreter/Execution/Interpreter+Throws.swift Outdated
Comment thread Sources/SwiftScriptInterpreter/Execution/Interpreter+Calls.swift Outdated
An internal review found both #11 and #12 over-reached; narrowed both.

#11: the leading-dot deferral fired even when the receiver was a
builtin container, so `[1,2,3].contains(.foo)` silently returned
false (the marker never equals a real element) instead of erroring.
evaluateArg now only defers to the marker when the receiver is a
bridged (opaque) type — the only receiver with a bridge that can
interpret the case name. A bare `.foo` to a builtin method, or in
general position, stays the hard "no such member" error.

#12: the do/catch catch-all surfaced EVERY non-control-flow error as
catchable, which swallowed Swift's uncatchable traps (fatalError /
precondition / assert / division-by-zero) and programming errors
(undefined identifier, no-such-member) — masking script bugs. Reverted
the do/catch change and instead wrap at the bridge-invocation boundary
(callingBridge): only an error raised *inside* a bridge body becomes a
catchable UserThrowSignal. Traps, dispatch failures, and operator
errors are raised by the interpreter itself, outside any bridge, so
they keep terminating the script exactly as stock Swift traps.
Control-flow signals still pass through untouched.

New boundary tests lock it in: builtin-container bare members stay
fatal; fatalError / precondition / division-by-zero / undefined-id /
no-such-member stay uncatchable; try? doesn't suppress a trap; an
embedder bridge that throws is caught. 541 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@odrobnik
odrobnik merged commit a771bb9 into main Aug 9, 2026
5 checks passed
@odrobnik
odrobnik deleted the claude/swiftscript-issues-11-12 branch August 9, 2026 16:17
odrobnik added a commit that referenced this pull request Aug 10, 2026
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>
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.

Implicit member access (.any) is unsupported, blocking verbatim Apple-shaped calls

1 participant