1. Pipe operator bypasses the tracking check (pre-existing soundness hole)
check_tracking ("Cannot call an untracked function from a tracked context") has a single call site, in tfun_call. The N.Pipe case in expr_ (skiplang/compiler/src/skipTyping.sk, | N.Pipe(e1, e2) ->) builds the TAst.Call node directly: it joins the callee's type against an expected Tfun whose modifiers are the laxest possible ({Fmutable, Funtracked}) and never goes through tfun_call, so the tracking check never runs.
Repro — the direct call is rejected, the pipe is accepted:
untracked fun sideEffect(x: String): String {
x
}
fun main(): void {
print_raw(sideEffect("direct")) // error: Cannot call an untracked function from a tracked context
}
untracked fun sideEffect(x: String): String {
x
}
fun main(): void {
print_raw("piped" |> sideEffect) // accepted; compiles and runs
}
Verified against both the current main-based compiler and the #1161 branch: x |> f lets tracked code call any untracked function with no diagnostic. This predates #1161, but it undermines the same guarantee that PR is careful to preserve (its escape hatch, @allow_tracked_call / Unsafe.unsafe_untracked_call, is explicit and opt-in; the pipe bypass is silent).
Suggested fix: in the N.Pipe case, derive the callee's actual Tfun_modifiers (from ty2 after the join) and call check_tracking with them — or route pipe application through tfun_call so it gets the same treatment as ordinary calls (including @allow_tracked_call support). An invalid test (tracked context piping into an untracked function) should pin the fix.
2. Hardening: @allow_tracked_call is silently inert on methods
The bypass added in #1161 only matches direct calls of top-level functions (TAst.Fun callees). Placing @allow_tracked_call on a method parses fine and does nothing — the tracking check still fires. This is intentional and pinned by the allow_tracked_call_method invalid test, but a diagnostic at naming/typing time ("@allow_tracked_call has no effect on methods") would fail fast instead of surprising the author at the call site.
3. Hardening: @allow_tracked_call on a tracked function is a silent no-op
The annotation is only consulted when the callee is untracked and the context is tracked. Annotating a tracked function does nothing; a warning would catch annotations that are stale (e.g. after removing untracked from the function) or misplaced.
1. Pipe operator bypasses the tracking check (pre-existing soundness hole)
check_tracking("Cannot call an untracked function from a tracked context") has a single call site, intfun_call. TheN.Pipecase inexpr_(skiplang/compiler/src/skipTyping.sk,| N.Pipe(e1, e2) ->) builds theTAst.Callnode directly: it joins the callee's type against an expectedTfunwhose modifiers are the laxest possible ({Fmutable, Funtracked}) and never goes throughtfun_call, so the tracking check never runs.Repro — the direct call is rejected, the pipe is accepted:
Verified against both the current
main-based compiler and the #1161 branch:x |> flets tracked code call any untracked function with no diagnostic. This predates #1161, but it undermines the same guarantee that PR is careful to preserve (its escape hatch,@allow_tracked_call/Unsafe.unsafe_untracked_call, is explicit and opt-in; the pipe bypass is silent).Suggested fix: in the
N.Pipecase, derive the callee's actualTfun_modifiers(fromty2after the join) and callcheck_trackingwith them — or route pipe application throughtfun_callso it gets the same treatment as ordinary calls (including@allow_tracked_callsupport). An invalid test (trackedcontext piping into anuntrackedfunction) should pin the fix.2. Hardening:
@allow_tracked_callis silently inert on methodsThe bypass added in #1161 only matches direct calls of top-level functions (
TAst.Funcallees). Placing@allow_tracked_callon a method parses fine and does nothing — the tracking check still fires. This is intentional and pinned by theallow_tracked_call_methodinvalid test, but a diagnostic at naming/typing time ("@allow_tracked_call has no effect on methods") would fail fast instead of surprising the author at the call site.3. Hardening:
@allow_tracked_callon a tracked function is a silent no-opThe annotation is only consulted when the callee is
untrackedand the context is tracked. Annotating a tracked function does nothing; a warning would catch annotations that are stale (e.g. after removinguntrackedfrom the function) or misplaced.