Repository navigation
An unused local whose initializer can raise keeps its initializer - #4310
Merged
Merged
Conversation
aleksisch
force-pushed
the
aleksisch/unused-may-raise
branch
8 times, most recently
from
October 20, 2026 10:24
27c1fc6 to
60a54bb
Compare
aleksisch
force-pushed
the
aleksisch/unused-may-raise
branch
2 times, most recently
from
October 22, 2026 15:40
3bcdcb1 to
61ec181
Compare
aleksisch
force-pushed
the
aleksisch/unused-may-raise
branch
3 times, most recently
from
October 25, 2026 10:31
ee76322 to
fc7eec7
Compare
aleksisch
force-pushed
the
aleksisch/unused-may-raise
branch
2 times, most recently
from
October 26, 2026 07:50
dc61800 to
c17ffce
Compare
Under -q a clean run still printed the summary block, so every lint chunk of the build wrote three lines of noise. -q now prints the summary only when there is an issue or an error; skipped files alone do not count. The exit code is unchanged.
terminal_checkpoint_ansi wrote "\r\n" between rows with the last cell's style still active. When the replay scrolls, the emulator fills the new row from the pen, so a row ending in a styled cell left that style on every blank cell of the row scrolled in below it. terminal_semantics.das asserted this, but its assert_snapshots_equal call only asserts, so the optimizer deleted the call and the check never ran.
Every pass that asked whether a function is pure tested sideEffectFlags against zero, so any bit added to SideEffects would turn all of those checks off at once. Function::observableEffects and hasObservableEffects read the word through one mask (userScenario, modifyExternal, modifyArgument, accessGlobal, invoke, captureString), and the const folder, lint, escape analysis, the builtin registration checks and the AST printer call them. Every consumer needs the whole mask today, so nothing folds differently; src/ast/ARCHITECTURE_EFFECTS.md#observable-effects says why for each.
`noSideEffects` means "writes nothing an observer can see", not "cannot
throw", and every deleting pass read only that flag. So an unused
`var u = a[10]`, a dead store `y = a[10]`, `a[10] - a[10]`, `a[10] * 0` and
`if (a[10] > 0) {}` ran to the end of main with the optimizer on and raised
under `options optimize = false`.
Raising is one more side effect, `SideEffects::mayRaise`, outside the
observable-effects mask: a call that can raise still folds, rewrites and
passes escape analysis and the lint, and only a pass that deletes an
evaluation asks about it. A builtin declares it in its binding - integer `/`
and `%`, `panic`, the string conversions and character reads. A das function
gets it like every other inferred bit: getSideEffects sets it from the
function's own nodes and ORs it in from its callees, and a callee still
being analyzed counts as raising. Each expression carries `noRaise` beside
`noSideEffects`, computed by the same NoSideEffectVisitor for the whole
subtree. Both take the node rule from one place: `a[i]`, `as`, a
dereference, a field read through a pointer, `assert` / `verify` and an
invoke raise by their class, and making a block does not. A fold that keeps
one evaluation is untouched, and so is CSE. Raising builtins no longer get
`nounwind` / `willreturn` in the JIT.
`ExprMakeVariant` with a null field value no longer dereferences it while
stamping access flags.
aleksisch
force-pushed
the
aleksisch/unused-may-raise
branch
from
October 26, 2026 08:36
c17ffce to
43efd45
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Behavior change: an optimized build no longer skips a runtime error. Code that relied on a dead
a[i]orx / 0staying silent under optimization now throws, as it already did unoptimized.Why.
noSideEffectsanswers "writes nothing an observer can see", not "cannot throw", and every pass that deletes an evaluation read only that flag. So the optimizer removed the throw along with the value nobody wanted: an unusedvar u = a[10]on a two-element array, a dead storey = a[10]overwritten before any read,a[10] - a[10],a[10] * 0andif (a[10] > 0) {}all ran to the end ofmainwith the optimizer on and raised underoptions optimize = false— in the interpreter, under-jitand under-aotalike. A release build skipped an error its debug build reported, and arecoverthe program depends on never ran.What changes.
src/ast/ast_can_raise.hasks the question once for every pass, the wayast_alias.halready shares the alias model between CSE and DSE.canRaisewalks a subtree, not a root: the root ofa[10] + 1is anExprOp2that cannot throw on its own.ExprAt,ExprAsVariant,ExprPtr2Ref,ExprFieldthrough a pointer,/and%, and everyExprCall; the safe spellings have their own visitor hooks and stay out.E op Efolds that collapse to a constant, the dropped conditions and the top-level pure statement drop.x & xandx | xreturn the left operand, which still throws. CSE is untouched for the same reason.let, the wayast_dse.cppkeeps a dead store's rhs; keeping the variable grew the frame.ExprMakeVariantwith a null field value no longer dereferences it while stamping access flags.Observable behavior.
var u = a[10],y = a[10]theny = 5,a[10] - a[10],a[10] * 0,if (a[10] > 0) {}run to the end -> they raise, matchingoptions optimize = falsevar u = a[10] + 1runs to the end -> it raisesvar unused = a + b,x & x,x | x-> unchanged, still foldedWhere to look.
src/ast/ast_can_raise.hand its call sites inast_unused.cpp,ast_dse.cpp,ast_const_folding.cppandast_block_folding.cpp.#nightly
Validation, claims, ledger
Validation
tests/lsp/test_lsp_protocol.das:489: stack overflowunder-jit, 2 errors, all four platforms. Reproduced locally, bisected toast_unused.cppin two builds (reverting the four files passes 4/4, restoring only that one fails), and fixed by dropping the dead variable instead of keeping it. That test is one function with 115 locals and a captured lambda, so it was the first frame to blow.E op E, the folded operand, the dropped condition) and the earlier three fail with the unused-local guard reverted. The dead-store case passes either way in this shape - its guard is justified by the pass, not by that test.samePureValueitself and broketests/language/optimization_fold_shape.das("arr[i] & arr[i] keeps a single read"). That fold keeps one evaluation, so it is sound; only the twosamePureValueuses that collapse to a constant carry the question now.ast_unused.cpp, interleaved over three rounds: every delta (-1.27% to +3.23%) sits under the 1.9-4.8% within-binary spread. The question is the last term of each condition, so it runs only when a pass is about to delete.dasfmt --verifyclean.Claims - stated, not tested
ast_cse.cpp:277and its consumers rather than by a test. A break would show as an array read losing its bounds check after CSE.