Add unsafe_untracked_call for impure lazy directories#1161
Open
mbouaziz wants to merge 7 commits into
Open
Conversation
a68ebee to
e116db8
Compare
629b2d7 to
56ba3e9
Compare
beauby
reviewed
Mar 27, 2026
Contributor
Author
Code review findings (multi-angle adversarial review; every finding below was probe-verified against a compiler built from this branch)Correctness
Cleanup / consistency
The review also surfaced a handful of lower-severity notes not listed here. Items 2-5 and 12-15 are concentrated in the pipe path and largely collapse into one refactor; item 1 is tracked in #1270; item 6 is the one open semantics decision. 🤖 Generated with Claude Code |
canCSECall's method branch used a deliberately lax per-callsite rule: CSE was allowed if any of the possible implementations could CSE. Two identical virtual calls were therefore merged even when the receiver could dispatch to a @debug (or untracked) implementation, silently deleting its second execution. A @debug implementation sharing a callsite with any CSE-able sibling implementation was enough to trigger it, at -O0 as well as -O2. Invert the rule: allow CSE only when every implementation the call might invoke passes funCanCSE. The allMethodImplementations iteration stops at the first implementation that cannot CSE, and the scan only runs for calls that already passed the deep-frozen gates, so there is no measurable compile-time impact (prelude release build: 17.7s before, 17.3s after). Fixes #1270 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Previously lambda bodies always inherited the enclosing function's tracked_context. Now the body's tracking context is derived from the lambda's own modifiers: untracked lambdas get an untracked context, other lambdas keep the context of their definition site. The context is snapshot at lambda creation (TUtils.Lambda's lam_tracked_context, mirroring lam_frozen_level) rather than read from the env at solve time: lambda bodies are solved lazily (possibly at their first call site, or at function end), so the solve-site env can have a different tracked_context than the definition site, which would make the verdict depend on solve order. The tracked_lambda_solve_order invalid test pins this down.
An untracked function or method annotated with @allow_tracked_call appears tracked to callers: the annotation is consulted where the declaration's function type is built (make_tfun_tracking), so it is part of the type. The body is still typechecked in an untracked context. Because the semantics lives in the type, no special call-site logic is needed: direct calls, calls through function values, and override compatibility all follow from the ordinary tracking rules. In particular, an unannotated untracked override of an annotated method is an invalid override (the existing tracking lattice), so dynamic dispatch can never reach an implementation that did not opt in; conversely, an annotated method may implement a tracked signature. The annotation only affects the type checker. The backend keeps deriving trackedness from the untracked modifier, so the optimizer still treats annotated declarations as side-effecting and never CSEs calls to them, like @debug. The annotation is rejected on declarations that are not untracked, where it would be meaningless.
Tracked callers exercise an annotated untracked function directly, through an untracked lambda argument, through a function value, and via a declared tracked function-typed parameter.
Annotated untracked methods called from tracked code: through a base-typed receiver, as a method value passed to a tracked function-typed parameter, via this from a tracked method of the same class, as a static, implementing a tracked signature, and overriding a plain untracked method (the covariant direction).
A generic escape hatch annotated with @allow_tracked_call that takes an untracked () -> T lambda and calls it, letting tracked code run an untracked computation. The AllowTrackedCall compiler test exercises it cross-module.
The N.Pipe case built its call node directly and never reached check_tracking, so piping into an untracked function silently bypassed the tracking check. Resolve the callee's tracking from its type (Tfun, Tlambda, or the upper bounds of a type parameter) and apply the same check as ordinary calls. Annotated declarations appear tracked to pipes like everywhere else.
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.
Summary
@allow_tracked_callannotation: anuntrackedfunction or method carrying it appears tracked to callers — the annotation is part of the declaration's type. Its body is still typechecked in an untracked context. Because the semantics lives in the type, everything follows from the ordinary tracking rules with no special call-site logic:untrackedoverride of an annotated method is rejected by the existing override lattice (untracked (() -> T)is not a subtype of(() -> T)), so dynamic dispatch can never reach an implementation that didn't opt in;@debug).untracked(it would be meaningless).tracked_contextin the type checker: a lambda body's tracking context is derived from the lambda's own modifiers and snapshot at its definition site (TUtils.Lambda.lam_tracked_context, mirroringlam_frozen_level), sountrackedlambdas get an untracked body context and the verdict does not depend on where or when the lambda body happens to be solved.x |> f) now run the same tracking check as ordinary calls (previously they bypassed it entirely).Unsafe.unsafe_untracked_call<T>, a generic escape hatch that allows tracked code to run an untracked computation.Fixes #1268.
Motivation
SKStore lazy directory compute functions run in a tracked context, which means they cannot call
untrackedfunctions. This is correct for pure reactive computations, but becomes a blocker for use cases like lazy file system access in mappers, where:untrackedCurrently, volatile functions like
Time.time_ms(),print_string, andFileSystem.readTextFileare not markeduntrackedeven though they should be — precisely because doing so would make them uncallable from mapper contexts. This escape hatch allows us to eventually mark these functions as properlyuntrackedwithout breaking existing code.The opt-in is transitive by design: closures created inside the annotated body have tracked types, so an annotated declaration can hand untracked computations to tracked callers. The annotation's author takes responsibility for the full closure of behavior they expose.
Commits
tracked_lambda_solve_order).make_tfun_trackingconsults the annotation at the two declaration-type construction sites (functions and methods) and in the backend lowering; naming rejects the annotation on non-untrackeddeclarations; invalid tests pin the placement rules and the override direction.thiscalls from tracked methods, statics, implementing a tracked signature, and the covariant override direction.|>previously bypassedcheck_trackingentirely; it now resolves the callee's tracking (including through type-parameter upper bounds) and applies the same rule.Usage
Test plan
AllowTrackedCall: direct, lambda-wrapped, through-a-value, declared-param, stdlib hatch, and pipe callersAllowTrackedCallMethod: base-receiver dispatch, method values,this, statics, tracked-signature implementation, covariant overrideuntracked_fun2/untracked_lambda2still fail🤖 Generated with Claude Code