Skip to content

Commit b120dcf

Browse files
mbouazizclaude
andcommitted
Add unsafe_untracked_call to allow tracked code to call untracked functions
SKStore lazy mappers run in a tracked context, which prevents them from calling untracked (side-effectful) functions. This is correct for most cases, but some use cases—such as lazy file system access in mappers— legitimately need to perform I/O during reactive computation, with the caller taking responsibility for correctness and invalidation. This adds: - A new @allow_tracked_call annotation that exempts an untracked function from the "Cannot call an untracked function from a tracked context" error - Unsafe.unsafe_untracked_call<T>, a generic function that takes an untracked lambda and calls it, usable from tracked contexts - A typechecker test exercising the new functionality The annotation check is implemented in tfun_call (skipTyping.sk) by looking up the callee's annotations before invoking check_tracking. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a897f79 commit b120dcf

4 files changed

Lines changed: 38 additions & 1 deletion

File tree

skiplang/compiler/src/skipTyping.sk

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1882,7 +1882,17 @@ fun tfun_call(
18821882
);
18831883
args1 = TUtils.add_default_arguments(add_args, args);
18841884
e = (pos, TAst.Call(f, args1));
1885-
check_tracking(pos, env, tracking);
1885+
allowTrackedCall = f.i1.i1 match {
1886+
| TAst.Fun(name, _) ->
1887+
SkipNaming.maybeGetFun(context, name.i1) match {
1888+
| Some(fd) -> annotationsContain(fd.annotations, "@allow_tracked_call", pos)
1889+
| None() -> false
1890+
}
1891+
| _ -> false
1892+
};
1893+
if (!allowTrackedCall) {
1894+
check_tracking(pos, env, tracking);
1895+
};
18861896
(acc1, (return_ty, e))
18871897
}
18881898

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
OK
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module TypecheckingAllowTrackedCall;
2+
// @allow_tracked_call lets a tracked function call an untracked function
3+
4+
untracked fun sideEffect(): String {
5+
"OK"
6+
}
7+
8+
// This function is tracked (default), but can call sideEffect()
9+
// via Unsafe.unsafe_untracked_call
10+
fun trackedCaller(): String {
11+
Unsafe.unsafe_untracked_call(() -> sideEffect())
12+
}
13+
14+
untracked fun main(): void {
15+
print_raw(trackedCaller())
16+
}
17+
module end;

skiplang/prelude/src/core/Unsafe.sk

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,13 @@ native fun array_set_byte<T>(v: mutable Array<T>, i: Int, b: UInt8): void;
100100
@cpp_extern
101101
native fun string_ptr(v: String, i: Int): Runtime.NonGCPointer;
102102

103+
// Calls an untracked function from a tracked context.
104+
// Use this to perform side effects (e.g., file I/O) inside reactive
105+
// computations where you take responsibility for correctness and
106+
// invalidation.
107+
@allow_tracked_call
108+
untracked fun unsafe_untracked_call<T>(f: untracked () -> T): T {
109+
f()
110+
}
111+
103112
module end;

0 commit comments

Comments
 (0)