Skip to content

Add unsafe_untracked_call for impure lazy directories#1161

Open
mbouaziz wants to merge 5 commits intomainfrom
untracked
Open

Add unsafe_untracked_call for impure lazy directories#1161
mbouaziz wants to merge 5 commits intomainfrom
untracked

Conversation

@mbouaziz
Copy link
Copy Markdown
Contributor

@mbouaziz mbouaziz commented Mar 19, 2026

Summary

  • Fixes lambda tracked_context in the type checker: lambda bodies now derive their tracking context from the lambda's own modifiers rather than always inheriting from the enclosing function
  • Adds @allow_tracked_call annotation to selectively bypass the tracking check at call sites
  • Adds Unsafe.unsafe_untracked_call<T>, a generic escape hatch that allows tracked code to call untracked functions

Motivation

SKStore lazy directory compute functions run in a tracked context, which means they cannot call untracked functions. This is correct for pure reactive computations, but becomes a blocker for use cases like lazy file system access in mappers, where:

  1. We don't know all needed files upfront — they're discovered during computation
  2. File reads are inherently side-effectful and should be untracked
  3. The caller takes responsibility for correctness and invalidation

Currently, volatile functions like Time.time_ms(), print_string, and FileSystem.readTextFile are not marked untracked even 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 properly untracked without breaking existing code.

Commits

  1. Set tracked_context for untracked lambda bodies — Previously lambda bodies always inherited the outer function's tracked_context. Now they derive it from their own modifiers: untracked lambdas get an untracked context, tracked (~>) lambdas enforce tracking even inside untracked functions.

  2. Add @allow_tracked_call annotation — In tfun_call, before calling check_tracking, the type checker looks up the callee's annotations. If @allow_tracked_call is present, the tracking check is skipped.

  3. Add AllowTrackedCall compiler test — Tests two variants: wrapping in an untracked lambda, and passing an untracked function directly.

  4. Add Unsafe.unsafe_untracked_call to stdlib — A simple generic function annotated with @allow_tracked_call that takes an untracked () -> T lambda and calls it.

  5. Better tracked_context for lambdas — Improved the lambda tracking logic to be bidirectional: derives context from modifiers when present, falls back to enclosing context otherwise.

Usage

fun myLazyCompute(context: mutable Context, path: String): String {
  Unsafe.unsafe_untracked_call(untracked () -> FileSystem.readTextFile(path))
}

Test plan

  • AllowTrackedCall test: tracked function calls untracked function via both lambda and direct reference
  • Existing untracked_fun2 and untracked_lambda2 invalid tests still fail
  • CI passes

🤖 Generated with Claude Code

@mbouaziz mbouaziz force-pushed the untracked branch 2 times, most recently from a68ebee to e116db8 Compare March 20, 2026 10:14
@mbouaziz mbouaziz requested a review from beauby March 20, 2026 10:19
@mbouaziz mbouaziz force-pushed the untracked branch 2 times, most recently from ba776b7 to 629b2d7 Compare March 20, 2026 11:58
mbouaziz and others added 5 commits March 20, 2026 18:04
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
// computations where you take responsibility for correctness and
// invalidation.
@allow_tracked_call
untracked fun unsafe_untracked_call<T>(f: untracked () -> T): T {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does unsafe_untracked_call need to be untracked itself? Isn't the idea that it "appears tracked" although it calls the untracked function f?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's untracked so that we can call untracked stuff in it. The annotation applies to callers only

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants