Frame-based dynamic scope: 870/870 — all tests passing
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 13m34s

provide/context and scope/emit!/emitted now use CEK continuation
frames instead of an imperative global stack. Scope state is part
of the continuation — captured by shift, restored by k invocation.

New frame types:
- ProvideFrame: holds name + value, consumed when body completes
- ScopeAccFrame: holds name + mutable emitted list

New CEK special forms:
- context: walks kont for nearest ProvideFrame, returns value
- emit!: walks kont for nearest ScopeAccFrame, appends to emitted
- emitted: walks kont for nearest ScopeAccFrame, returns list

Kont walkers: kont-find-provide, kont-find-scope-acc

This fixes the last 2 test failures:
- provide survives resume: scope captured by shift, restored by k
- scope and emit across shift: accumulator preserved in continuation

JS Full: 870/870 (100%)
JS Standard: 747/747 (100%)
Python: 679/679 (100%)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 14:40:14 +00:00
parent 719da7914e
commit 9f32c8cf0d
3 changed files with 293 additions and 12 deletions

View File

@@ -152,11 +152,22 @@
(fn (f remaining env)
{:type "every" :f f :remaining remaining :env env}))
;; ScopeFrame: scope-pop! when frame pops
;; ScopeFrame: remaining body expressions for scope special form
(define make-scope-frame
(fn (name remaining env)
{:type "scope" :name name :remaining remaining :env env}))
;; ProvideFrame: dynamic variable binding (context reads this from kont)
(define make-provide-frame
(fn (name value remaining env)
{:type "provide" :name name :value value :remaining remaining :env env}))
;; ScopeAccFrame: accumulator scope (emit! appends, emitted reads)
(define make-scope-acc-frame
(fn (name value remaining env)
{:type "scope-acc" :name name :value (or value nil)
:emitted (list) :remaining remaining :env env}))
;; ResetFrame: delimiter for shift/reset continuations
(define make-reset-frame
(fn (env)
@@ -253,6 +264,26 @@
(scan (rest k) (append captured (list frame))))))))
(scan kont (list))))
;; Walk kont for nearest ProvideFrame with matching name
(define kont-find-provide
(fn (kont name)
(if (empty? kont) nil
(let ((frame (first kont)))
(if (and (= (frame-type frame) "provide")
(= (get frame "name") name))
frame
(kont-find-provide (rest kont) name))))))
;; Walk kont for nearest ScopeAccFrame with matching name
(define kont-find-scope-acc
(fn (kont name)
(if (empty? kont) nil
(let ((frame (first kont)))
(if (and (= (frame-type frame) "scope-acc")
(= (get frame "name") name))
frame
(kont-find-scope-acc (rest kont) name))))))
;; Check if a ReactiveResetFrame exists anywhere in the continuation
(define has-reactive-reset-frame?
(fn (kont)
@@ -1254,9 +1285,12 @@
;; Reactive deref-as-shift
(= name "deref") (step-sf-deref args env kont)
;; Scoped effects
;; Scoped effects — frame-based dynamic scope
(= name "scope") (step-sf-scope args env kont)
(= name "provide") (step-sf-provide args env kont)
(= name "context") (step-sf-context args env kont)
(= name "emit!") (step-sf-emit args env kont)
(= name "emitted") (step-sf-emitted args env kont)
;; Dynamic wind
(= name "dynamic-wind") (make-cek-value (sf-dynamic-wind args env) env kont)
@@ -1460,15 +1494,83 @@
(make-cek-value (sf-lambda args env) env kont)))
;; scope: evaluate name, then push ScopeFrame
;; scope: push ScopeAccFrame, evaluate body. emit!/emitted walk kont.
;; (scope name body...) or (scope name :value v body...)
(define step-sf-scope
(fn (args env kont)
;; Delegate to existing sf-scope for now — scope involves mutation
(make-cek-value (sf-scope args env) env kont)))
(let ((name (trampoline (eval-expr (first args) env)))
(rest-args (slice args 1))
(val nil)
(body nil))
;; Check for :value keyword
(if (and (>= (len rest-args) 2)
(= (type-of (first rest-args)) "keyword")
(= (keyword-name (first rest-args)) "value"))
(do (set! val (trampoline (eval-expr (nth rest-args 1) env)))
(set! body (slice rest-args 2)))
(set! body rest-args))
;; Push ScopeAccFrame and start evaluating body
(if (empty? body)
(make-cek-value nil env kont)
(if (= (len body) 1)
(make-cek-state (first body) env
(kont-push (make-scope-acc-frame name val (list) env) kont))
(make-cek-state (first body) env
(kont-push
(make-scope-acc-frame name val (rest body) env)
kont)))))))
;; provide: delegate to existing handler
;; provide: push ProvideFrame, evaluate body. context walks kont to read.
;; (provide name value body...)
(define step-sf-provide
(fn (args env kont)
(make-cek-value (sf-provide args env) env kont)))
(let ((name (trampoline (eval-expr (first args) env)))
(val (trampoline (eval-expr (nth args 1) env)))
(body (slice args 2)))
;; Push ProvideFrame and start evaluating body
(if (empty? body)
(make-cek-value nil env kont)
(if (= (len body) 1)
(make-cek-state (first body) env
(kont-push (make-provide-frame name val (list) env) kont))
(make-cek-state (first body) env
(kont-push
(make-provide-frame name val (rest body) env)
kont)))))))
;; context: walk kont for nearest ProvideFrame with matching name
(define step-sf-context
(fn (args env kont)
(let ((name (trampoline (eval-expr (first args) env)))
(default-val (if (>= (len args) 2)
(trampoline (eval-expr (nth args 1) env))
nil))
(frame (kont-find-provide kont name)))
(if frame
(make-cek-value (get frame "value") env kont)
(if (>= (len args) 2)
(make-cek-value default-val env kont)
(error (str "No provider for: " name)))))))
;; emit!: walk kont for nearest ScopeAccFrame, append value
(define step-sf-emit
(fn (args env kont)
(let ((name (trampoline (eval-expr (first args) env)))
(val (trampoline (eval-expr (nth args 1) env)))
(frame (kont-find-scope-acc kont name)))
(if frame
(do (append! (get frame "emitted") val)
(make-cek-value nil env kont))
(error (str "No scope for emit!: " name))))))
;; emitted: walk kont for nearest ScopeAccFrame, return accumulated list
(define step-sf-emitted
(fn (args env kont)
(let ((name (trampoline (eval-expr (first args) env)))
(frame (kont-find-scope-acc kont name)))
(if frame
(make-cek-value (get frame "emitted") env kont)
(error (str "No scope for emitted: " name))))))
;; reset: push ResetFrame, evaluate body
(define step-sf-reset
@@ -2013,6 +2115,41 @@
(make-scope-frame name (rest remaining) fenv)
rest-k))))
;; --- ProvideFrame: body expression evaluated ---
(= ft "provide")
(let ((remaining (get frame "remaining"))
(fenv (get frame "env")))
(if (empty? remaining)
;; Body done — return value, frame consumed
(make-cek-value value fenv rest-k)
;; More body expressions — keep frame on kont
(make-cek-state
(first remaining) fenv
(kont-push
(make-provide-frame
(get frame "name") (get frame "value")
(rest remaining) fenv)
rest-k))))
;; --- ScopeAccFrame: body expression evaluated ---
(= ft "scope-acc")
(let ((remaining (get frame "remaining"))
(fenv (get frame "env")))
(if (empty? remaining)
;; Body done — return value, frame consumed
(make-cek-value value fenv rest-k)
;; More body expressions — carry emitted list forward
(make-cek-state
(first remaining) fenv
(kont-push
(let ((new-frame (make-scope-acc-frame
(get frame "name") (get frame "value")
(rest remaining) fenv)))
;; Preserve accumulated emitted from current frame
(dict-set! new-frame "emitted" (get frame "emitted"))
new-frame)
rest-k))))
;; --- MapFrame: callback result for map/map-indexed ---
(= ft "map")
(let ((f (get frame "f"))