Extends 1036-errors-failable-smoke with an end-to-end Composition section
covering the E5.1 forms: a failable closure literal through a Closure(...)
param (try-propagated, caught), a non-failable closure literal widened
into a failable bare slot (∅-widening adapter), and generic ($T)
value-carrying failable composition. Completes E5.4 — the per-feature
examples (1039-1045) remain the focused units; this is the integrated
smoke.
A closure VALUE (a pre-bound variable) flowing into a bare (T)->U slot
was passed unsoundly: the bare ABI calls fn_ptr(ctx, args) with no env
channel, so the closure's underlying fn (which takes an env slot) had its
env dropped and args shifted — UB for a matching ABI, a wrong-tuple read
for the non-failable->failable widening (returned -1), and a segfault when
the closure captured.
coerceToType now rejects a .closure -> .function coercion with a
diagnostic pointing at the idiom (pass the literal directly, which gets
the static adapter, or type the parameter Closure(...) so the env is
carried). Closure LITERALS are unaffected — lowerLambda pre-adapts them to
a .function-typed value before coercion.
Regression: 1045-errors-closure-var-bare-slot-reject.sx.
Generic value-carrying failable composition works with the documented
$T: Type generic form (catch / destructure / failure-propagation / a
second monomorphization at a different T). Issue 0062 was an invalid-repro
report — it used the non-generic T: type form, which is a plain Type-valued
param, not a generic type parameter. Marked 0062 resolved (not a bug).
The only real residual: a non-$ T: Type function param used as a type
silently resolves to an empty {} (renders T{}) instead of erroring. Filed
as 0064 (deferred, orthogonal to ERR — the $T idiom works).
Regression: 1044-errors-generic-failable-composition.sx.
A free function called via UFCS (recv.fn(args)) whose first param is *T
was passed the receiver by value (LLVM "Call parameter type does not
match function signature"), and a function reached only via UFCS was
declared but never emitted (undefined symbol at link).
The bare-name UFCS fallback now mirrors the qualified-method path: it
lazily lowers the target body and calls fixupMethodReceiver +
coerceCallArgs, so the value receiver gets the same implicit address-of
as a struct-defined method and mutations through *T are visible.
Regression: 0039-basic-free-fn-ufcs-pointer-receiver.sx.
A closure literal whose body raises but is annotated non-failable (or has
no ! in its return) now gets a lambda-specific diagnostic telling the user
to declare the failable return explicitly, instead of the generic "raise
is only valid inside a failable function". Failability is never inferred
for a lambda, so a raising lambda with no ! is a hard error that should
point at the fix.
New in_lambda_body flag (save/restore for nesting) set around the lambda
body lowering in lowerLambda; diagRaiseNotFailable branches on it.
Top-level functions keep the generic message.
Test: 1043-errors-lambda-raise-annotation-hint.sx.
All occurrences of Closure(<sig>) -> (T, !) with a structurally identical
value-signature now share one inferred error-set node; every bare-!
closure literal of that shape unions its escape tags in, and a
`try slot(x)` against any matching-shape slot widens the caller's named
set against that union. This closes the gap where a slot call (no static
function name) skipped the widening check entirely.
- shape_inferred_sets keyed by closureShapeKey (params + value-return via
mangleTypeName, error slot excluded) so bare-!, non-failable, .function
and .closure of one value-sig collapse to a single key.
- convergeClosureShapeSets pre-pass (lowerRoot Pass 1d', after the
name-keyed convergeInferredErrorSets): collectClosureShapes walks fn
bodies through lambda boundaries; recordClosureShape resolves each
concrete bare-! literal's shape and unions its raises (+ try named_fn()
edges via calleeEscapeTags) into the shape node.
- checkEscapeWidening falls back to shapeKeyOfCallee for bare-! slot calls
(computed from the callee expr's .function/.closure type). Empty union
is silently allowed (sub-feature 6).
Scope: concrete shapes only (generic lambdas skipped); closure-to-closure
try edges are not fix-pointed (under-approximation = a missed diagnostic,
never a miscompile).
Tests: 1041 (positive — union composes, runs), 1042 (reject — two
widening diagnostics, exit 1).
A bare `return X;` / `raise` in the middle of a block closed the current
LLVM basic block, but lowerBlock / lowerBlockValue only stopped the
statement loop on the `block_terminated` flag — which lowerReturn
deliberately never sets (it would leak past an `if cond { return }` merge
block). So trailing dead statements were emitted into the already-closed
block, tripping the LLVM verifier with "Terminator found in the middle of
a basic block".
Fix: also stop the statement loop when currentBlockHasTerminator() is
true. That is CFG-level termination of the *current* block, which is
naturally false at an if / inline-if merge block, so conditional returns
still fall through to their trailing statements.
This unblocks ERR E5.1: the canonical failable-closure form
`closure((x) -> (s32,!) { raise error.X; return x; })` has a dead
`return x;` after the unconditional raise and tripped the verifier.
Regression: examples/0038-basic-dead-code-after-terminator.sx.
Two more E5.1 composition pieces:
- inferExprType .call: a callee that's a local variable of bare type
() now resolves to its declared return type (only
was handled before), so / on the call see the failable result
instead of .
- createClosureToBareFnAdapter now widens: when a NON-failable closure literal
flows into a failable bare slot (∅ ⊆ slot set, success type matches), the
adapter wraps the value into the slot's tuple via
lowerFailableSuccessReturn — previously rejected. The failable->non-failable
and capturing->bare crossings stay rejected.
Adapter generation fires for closure LITERALS flowing into a bare-fn slot; a
pre-bound closure VARIABLE into a bare-fn slot is a separate coercion-site path,
still unhandled (noted in CHECKPOINT-ERR). Regression:
examples/1040-errors-failable-closure-composition. Suite: 329 passed.
A closure's underlying function carries a hidden env arg that a bare (T)->U slot
doesn't pass, so a closure flowing into a bare function-type slot dropped the
env — the first user arg landed in the env slot and the rest read garbage
(apply(closure((x)->s64 { x*2 })) returned 192 instead of 10; non-failable too).
- createClosureToBareFnAdapter: a capture-free closure into a bare (T)->U slot is
bridged by a generated adapter carrying the bare ABI (forwards a null env);
lowerLambda returns its func_ref. Rejected (no silent miscompile): a capturing
closure into a bare slot (env has nowhere to live) and a failable closure into
a non-failable slot (the ERR E5.1 FFI-boundary rule).
- Arrow-body failable closures (-> (T,!) => expr) now wrap the bare success value
into {value, 0} via lowerFailableSuccessReturn (the implicit return previously
returned a malformed tuple → caught value read as 0).
The isLambda .bang parser fix (failable closure literals parse) already landed in
485b4fa. Regressions: examples/0309-closures-literal-as-bare-fn-param (non-
failable, block + arrow, called in callee) + 1039-errors-failable-closure-literal
(failable, block + arrow, direct + Closure(...) param). Resolves issue 0060
(remaining E5.1 follow-ups noted in the .md). Suite: 328 passed.
A bare failable `#run` (no catch/or) whose error escapes used to segfault (const
form `x :: #run f()`) or silently succeed (statement form `#run f();`). Now the
compiler reports the raised tag name + the resolved return trace at the #run site
and halts with a non-zero exit.
- lower.zig: a failable #run's comptime function returns the full failable tuple
(so the error slot is inspectable) while the global is typed as the success
value; failable side-effects return the tuple instead of void.
- emit_llvm.zig: read the always-on comptime trace buffer (extern sx_trace_*);
comptimeErrChannel + checkComptimeFailable split the result (non-zero tag →
reportComptimeEscape + comptime_failed flag; success → value part). Wired into
emitGlobals (const) and runComptimeSideEffects (statement, now filtered by the
__run name; buffer cleared before each eval).
- core.zig: generateCode returns error.ComptimeError when comptime_failed, so the
driver aborts before JIT/link.
catch / or / onfail compose at comptime exactly as at runtime; a successful bare
#run yields the value. Regressions: examples/1037-errors-comptime-run-escape
(diagnostic, exit 1) + 1038-errors-comptime-run-handled (exit 164). Suite: 326.
Break the monolithic examples/50-smoke.sx into 30 focused per-section examples,
filed into their category blocks (basic/types/comptime/memory/protocols/ffi),
each carrying only the top-level decls its section references (the protocols
section keeps the full preamble — its deps flow through UFCS method calls that
name-based extraction can't see). Outputs verified identical to the original
section blocks.
Add examples/1036-errors-failable-smoke.sx — an end-to-end error-handling example
(the E5.4 work): named + inferred error sets consumed via destructure, try (in
helpers), catch (bare-expr / match-body / diverging / no-binding), or
value-terminator, onfail+defer interleave, and error.X value + {} tag
interpolation.
Remove examples/50-smoke.sx. Suite: 324 passed, 0 failed.
A function with no explicit return type (arrow `=> expr`, or a block whose
`return <v>` drives the type) has its return type inferred from the body — but
the body references the function's own params. resolveReturnType ran that
inference before the params were pushed into self.scope (they're bound later, at
body lowering), so inferExprType couldn't resolve them and yielded .unresolved,
which reached LLVM emission and panicked. It only worked when a same-named
binding lingered in scope from earlier lowering (e.g. inside the big smoke file).
Bind the function's plain annotated value params into a temporary scope during
return-type inference. Resolve their types via resolveTypeWithBindings rather
than resolveParamType — the latter does variadic/pack bookkeeping that must run
exactly once, at body lowering; calling it here too corrupted the format/index
path. Variadic/pack/comptime/unannotated params are skipped (no by-name return
dependency; their types come from substitution).
Regression: examples/0308-closures-arrow-inferred-return.sx (arrow + block
inferred-return, top-level + local). Resolves issue 0059. Suite: 293 passed.
Rename all example tests/companions to the XXXX-category-test-name scheme
(per-category 100-blocks: basic 0010, types 0100, ... errors 1000,
diagnostics 1100, ffi 1200, ffi-objc 1300, ffi-jni 1400, vectors 1500,
platform 1600). Companions and dir/C fixtures move in lockstep with their
parent test; #import/#source/#include paths rewritten to match.
Expected output now lives in examples/expected/ (a sibling dir of the
tests) split into three streams per the new convention:
<name>.exit / <name>.stdout / <name>.stderr (+ optional <name>.ir)
run_examples.sh rewritten: scans examples/ and issues/ for an
expected/<name>.exit marker, captures stdout and stderr separately (no
more 2>&1), compares each stream + exit + optional IR snapshot.
Behavior validated unchanged: every renamed test reproduces its prior
merged output + exit (diffs limited to file paths/basenames embedded in
diagnostics + traces, which correctly reflect the new names). Suite:
292 passed, 0 failed. 50-smoke.sx split + issue relocation + docs follow
in subsequent commits.