[WIP] opt: do not fold !! of non bool - #3560
Open
aleksisch wants to merge 8 commits into
Open
Conversation
aleksisch
force-pushed
the
aleksisch/no-folding-for-non-bool
branch
2 times, most recently
from
July 27, 2026 14:42
976bcc3 to
e886888
Compare
It's incorrect to fold !!x if x of any other type than bool. For example it can be user provided type.
aleksisch
force-pushed
the
aleksisch/no-folding-for-non-bool
branch
from
July 27, 2026 15:58
e886888 to
ee03cc1
Compare
We support it in same way it works in aot.
We can't test it in CI, however das allow das_config to be configured with various standard libraries and various setups. Some of them have stronger requirements.
An expression-bodied callee takes the pure-graft path, which handed the body
rewriter an empty rename map on the grounds that "expression bodies declare no
locals". They declare no locals, but a block literal inside one declares
ARGUMENTS, and after the graft those sit in the caller's scope chain. infer
re-resolves every ExprVar by name, so an unrenamed argument captures a
substituted parameter expression that happens to spell the same name:
def callee(squad_team : int) : int {
return apply_team() $ [shadowable] (team : int) : int {
return squad_team * 100 + team
}
}
def caller(team : int) : int { return callee(team) } // 505, want 705
// spliced: return (team * 100) + team - both operands are the block's team
The statement path already renames block arguments for exactly this reason
(LocalNameCollect::preVisitBlockArgument); only the graft path skipped it. Run
the same collector there.
Renaming rather than declining to splice matters, because the capture has three
faces and a candidacy gate only reaches one of them:
* a can_shadow block argument (host query blocks - dagor's ecs marks every
[es] argument - and daslib/linq_boost's generated blocks) is silently
wrong, since can_shadow is exactly what switches error 30701 off. dagor hit
`squad_team != team` emitted as `team != team`
* an explicit [inline] callee of the same shape is equally wrong, and no
heuristic gate applies to it - the author opted into splicing, not into
name capture
* the same shape WITHOUT can_shadow does not compile at all: error[30701]
"block argument 'team' is shadowed by function argument 'team'", pointing
at the callee's block and blaming a caller argument
All three are probe-verified, and the added tests cover them. Renaming also
keeps every splice that a gate would have declined - nested splices re-prefix
correctly, four levels of colliding `team` yielding
__inl0_l___inl0_l___inl0_l___inl0_l_team and still evaluating to 705.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
aleksisch
force-pushed
the
aleksisch/no-folding-for-non-bool
branch
from
July 27, 2026 16:05
ee03cc1 to
3e59b84
Compare
The splice substitutes the caller's argument expression for a parameter.
That breaks when the body passes the parameter on to a REF parameter:
the substituted value has no storage, so the inner call stops matching
("can't pass non-ref to ref"), and a program that compiled before the
pass now does not.
Record, per callee parameter, whether it is read as the argument of a ref
parameter, and bind a temp for those instead of substituting - for the
constant case as well as the computed one. A by-value parameter is a copy
in the real call anyway, so the temp is what the un-inlined code did.
Worse than a diagnostic when the substituted value is constant: the
folder then evaluates the native with a value where the C++ signature
wants `const T &` and dereferences storage that was never materialized,
which segfaults the compiler:
das::FoldingVisitor::evalAndFold -> Context::evalWithCatch -> <native>
The added test covers that shape - a private (so budget-exempt) callee
whose parameter goes to an implicit const-ref native, called with an
expression built from const locals. It crashed the compiler before this
change.
Found in dagor's enlisted: traceray_normalized in shell_fx_common.das
reported as no matching function after get_floor_material_name spliced.
Invoke devirtualization splices the block literal in place and consumes the
holder local's only read. The patch slot runs before lint, and optimize -
which reaps the now-dead `let` - runs after it, so lint saw a variable with
no access flag left and reported it unused:
def apply_via_call(x : int) : int
let add_two = $(v : int) : int { return v + 2 }
return apply_block(add_two, x) // LINT002: unused variable add_two
At lint time add_two carries only access_init, while a function-pointer local
in the same position carries access_get|access_init|access_pass. That
asymmetry makes the gap look specific to block-typed variables, but it is not
- @@fn simply never devirtualizes. `options disable_auto_inline` compiles the
same file clean, which is what pins this on the splice rather than on passing
a variable as an argument.
The devirtualizer now marks the holder marked_used, the same "a transform ate
the reads" contract the unused-argument checks in ast_lint.cpp already honor,
and daslib/lint.das honors that flag for locals as it already did for
arguments. Optimize still reaps the dead `let`, so there is no codegen cost.
Found in dagor, where the reported variables could not be renamed away
without breaking the build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The folder evaluates a builtin call at compile time when the callee is pure,
returns a foldable type and every argument is constant. That is wrong when
the body is not really there: an AOT compiler build (DAS_AOT_COMPILER)
replaces C++ bound bodies with an asserting stub, so folding such a call runs
the stub and aborts the compiler. dagor hit this on
rel_hp_float_to_fixed_floor(1.0)
which only became constant after a small wrapper was inlined at a constant
call site.
Add BuiltInFunction::noFolding with a chainable noFold(), and test it at the
four builtin fold gates, so a binding that must not run at compile time can
say so:
DAS_ADD_FUN_BIND("rel_hp_float_to_fixed_floor", none, ...)->noFold();
Nothing is marked here. Folding stays on by default for every function.
The flag sits on BuiltInFunction because both Function flag words are full,
and builtins are registered by the host in every process, so nothing has to
be serialized. Mark at the binding rather than under DAS_AOT_COMPILER: the
same TU is compiled into both the AOT compiler and the runtime, so both fold
the same way and the AOT hashes keep matching.
register_<Module> is extern "C" so C and D hosts can call it unmangled
between das_initialize_modules() and das_initialize_finalize(). Two side
effects of that linkage broke every C++ host that pulls modules.
The macro can no longer declare the entry point: it expands inside a function
body, a linkage specification is only valid at namespace scope, and declaring
it without one names a different, C++ mangled symbol that nothing defines. So
each host had to add a file scope DECLARE_MODULE per module, boilerplate the
macro already has the name for. REGISTER_MODULE and
REGISTER_MODULE_IN_NAMESPACE now also emit das_pull_<Module>, a C++ linkage
tail call to register_<Module>, and NEED_MODULE declares that instead, which
at block scope is both legal and correct.
The macro also stopped ending in a semicolon, so two bare uses on consecutive
lines spliced into one expression and the second leading * became a
multiplication:
error: invalid operands to binary expression ('unsigned int' and
'DasThreadLocal<unsigned int, ...>')
Put the semicolon back. A call site that writes its own gets an empty
statement.
Pulling a module from C++ needs no declaration again:
void pull_game_das() {
NEED_MODULE(SomeModule)
}
The C side is untouched: register_<Module> and jit_register_<Module> keep C
linkage and their unmangled names, so the C API embedding flow works exactly
as documented. DECLARE_MODULE and PULL_MODULE keep working for callers that
would rather declare at file scope.
aleksisch
force-pushed
the
aleksisch/no-folding-for-non-bool
branch
from
July 27, 2026 16:52
3e59b84 to
dc7a7fe
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.
It's incorrect to fold !!x if x of any other type than bool. For example it can be user provided type.