From 1f8f23f05d9e17a5eb11ef1484c93d8c5e162324 Mon Sep 17 00:00:00 2001 From: Mehdi Bouaziz Date: Fri, 20 Mar 2026 09:59:21 +0000 Subject: [PATCH 1/5] Set tracked_context for untracked lambda bodies Previously, lambda bodies always inherited the outer function's tracked_context. This meant an `untracked` lambda inside a tracked function would still be checked as tracked, preventing it from calling untracked functions. Now the type checker sets tracked_context=false for untracked lambda bodies, consistent with how method_body_ handles untracked functions. Co-Authored-By: Claude Opus 4.6 --- skiplang/compiler/src/skipTyping.sk | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/skiplang/compiler/src/skipTyping.sk b/skiplang/compiler/src/skipTyping.sk index 446095678..80d91410f 100644 --- a/skiplang/compiler/src/skipTyping.sk +++ b/skiplang/compiler/src/skipTyping.sk @@ -5312,10 +5312,15 @@ fun lambda( ) { throw TUtils.Delay() } else { + (_, lam_tracking) = ml; + lam_is_untracked = + lam_tracking.size() == 1 && + lam_tracking[0] is N.Funtracked(); !env = env with { break_type => None(), frozen_level => lam.lam_frozen_level, locals => lam_locals, + tracked_context => if (lam_is_untracked) false else env.tracked_context, }; !env = setReturnType(context, next_id, env, acc, rty, lam.yields); ordered_args = args.map(t -> From 4dac086b1e1b168fa88d18586ab6940aafd0a4f4 Mon Sep 17 00:00:00 2001 From: Mehdi Bouaziz Date: Fri, 20 Mar 2026 09:59:42 +0000 Subject: [PATCH 2/5] Add @allow_tracked_call annotation to bypass tracking check Functions annotated with @allow_tracked_call can be called from tracked contexts even if they are untracked. This provides a controlled escape hatch for cases like unsafe_untracked_call where the caller takes responsibility for correctness. Co-Authored-By: Claude Opus 4.6 --- skiplang/compiler/src/skipTyping.sk | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/skiplang/compiler/src/skipTyping.sk b/skiplang/compiler/src/skipTyping.sk index 80d91410f..8b6afaa74 100644 --- a/skiplang/compiler/src/skipTyping.sk +++ b/skiplang/compiler/src/skipTyping.sk @@ -1882,7 +1882,17 @@ fun tfun_call( ); args1 = TUtils.add_default_arguments(add_args, args); e = (pos, TAst.Call(f, args1)); - check_tracking(pos, env, tracking); + allowTrackedCall = f.i1.i1 match { + | TAst.Fun(name, _) -> + SkipNaming.maybeGetFun(context, name.i1) match { + | Some(fd) -> annotationsContain(fd.annotations, "@allow_tracked_call", pos) + | None() -> false + } + | _ -> false + }; + if (!allowTrackedCall) { + check_tracking(pos, env, tracking); + }; (acc1, (return_ty, e)) } From dc8220a913265faed496b6dea60f9c05e215db3e Mon Sep 17 00:00:00 2001 From: Mehdi Bouaziz Date: Fri, 20 Mar 2026 10:00:19 +0000 Subject: [PATCH 3/5] Add AllowTrackedCall compiler test Tests @allow_tracked_call with two variants: - trackedCaller1: wraps untracked call in an untracked lambda - trackedCaller2: passes untracked function directly Co-Authored-By: Claude Opus 4.6 --- .../tests/Typechecking/AllowTrackedCall.exp | 1 + .../tests/Typechecking/AllowTrackedCall.sk | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 skiplang/compiler/tests/Typechecking/AllowTrackedCall.exp create mode 100644 skiplang/compiler/tests/Typechecking/AllowTrackedCall.sk diff --git a/skiplang/compiler/tests/Typechecking/AllowTrackedCall.exp b/skiplang/compiler/tests/Typechecking/AllowTrackedCall.exp new file mode 100644 index 000000000..e6bc70f11 --- /dev/null +++ b/skiplang/compiler/tests/Typechecking/AllowTrackedCall.exp @@ -0,0 +1 @@ +OKOK \ No newline at end of file diff --git a/skiplang/compiler/tests/Typechecking/AllowTrackedCall.sk b/skiplang/compiler/tests/Typechecking/AllowTrackedCall.sk new file mode 100644 index 000000000..5774b9cac --- /dev/null +++ b/skiplang/compiler/tests/Typechecking/AllowTrackedCall.sk @@ -0,0 +1,27 @@ +module TypecheckingAllowTrackedCall; +// @allow_tracked_call lets a tracked function call an untracked function + +@allow_tracked_call +untracked fun unsafeUntrackedCall(f: untracked () -> T): T { + f() +} + +untracked fun sideEffect(): String { + "OK" +} + +// trackedCaller1: wraps sideEffect() in an untracked lambda +fun trackedCaller1(): String { + unsafeUntrackedCall(untracked () -> sideEffect()) +} + +// trackedCaller2: calls sideEffect() directly via unsafeUntrackedCall +fun trackedCaller2(): String { + unsafeUntrackedCall(sideEffect) +} + +untracked fun main(): void { + print_raw(trackedCaller1()); + print_raw(trackedCaller2()); +} +module end; From ec5c75571b7d9b3267fad1f24d1a9d1aa58cc523 Mon Sep 17 00:00:00 2001 From: Mehdi Bouaziz Date: Fri, 20 Mar 2026 10:00:27 +0000 Subject: [PATCH 4/5] Add Unsafe.unsafe_untracked_call to stdlib Provides a controlled escape hatch for calling untracked functions from tracked contexts (e.g., performing I/O inside reactive computations where the caller takes responsibility for correctness). Co-Authored-By: Claude Opus 4.6 --- skiplang/prelude/src/core/Unsafe.sk | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/skiplang/prelude/src/core/Unsafe.sk b/skiplang/prelude/src/core/Unsafe.sk index 8d5aef383..d6e77e4ff 100644 --- a/skiplang/prelude/src/core/Unsafe.sk +++ b/skiplang/prelude/src/core/Unsafe.sk @@ -100,4 +100,13 @@ native fun array_set_byte(v: mutable Array, i: Int, b: UInt8): void; @cpp_extern native fun string_ptr(v: String, i: Int): Runtime.NonGCPointer; +// Calls an untracked function from a tracked context. +// Use this to perform side effects (e.g., file I/O) inside reactive +// computations where you take responsibility for correctness and +// invalidation. +@allow_tracked_call +untracked fun unsafe_untracked_call(f: untracked () -> T): T { + f() +} + module end; From 56ba3e9ce5b7b117a04e6d7c7e6a3bfc25f77458 Mon Sep 17 00:00:00 2001 From: Mehdi Bouaziz Date: Fri, 20 Mar 2026 10:13:22 +0000 Subject: [PATCH 5/5] Better tracked_context for lambdas --- skiplang/compiler/src/skipTyping.sk | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/skiplang/compiler/src/skipTyping.sk b/skiplang/compiler/src/skipTyping.sk index 8b6afaa74..b6ba83287 100644 --- a/skiplang/compiler/src/skipTyping.sk +++ b/skiplang/compiler/src/skipTyping.sk @@ -5322,15 +5322,16 @@ fun lambda( ) { throw TUtils.Delay() } else { - (_, lam_tracking) = ml; - lam_is_untracked = - lam_tracking.size() == 1 && - lam_tracking[0] is N.Funtracked(); + (_lam_purity, lam_tracking) = ml; + lam_tracked_context = + env.tracked_context && + (lam_tracking.size() < 1 || + lam_tracking.any(tracking ~> tracking is N.Ftracked())); !env = env with { break_type => None(), frozen_level => lam.lam_frozen_level, locals => lam_locals, - tracked_context => if (lam_is_untracked) false else env.tracked_context, + tracked_context => lam_tracked_context, }; !env = setReturnType(context, next_id, env, acc, rty, lam.yields); ordered_args = args.map(t ->