diff --git a/lib/go/eval.sx b/lib/go/eval.sx index 3772a560..f5876627 100644 --- a/lib/go/eval.sx +++ b/lib/go/eval.sx @@ -562,226 +562,320 @@ :else (let ((call-env (go-bind-names caller-env param-names arg-vals))) - (cond - (= body nil) nil - (and (list? body) (= (first body) :block)) - (let ((r (go-eval-block call-env (nth body 1)))) + ;; Install a fresh defer stack for this call frame. + ;; Mutated by go-eval-defer-stmt via append!; drained + ;; LIFO before the call returns. Replaces any outer + ;; frame's stack (defers are frame-local). + (let ((defer-stack (list))) + (let ((frame-env + (go-env-extend + call-env "__go-defer-stack" defer-stack))) (cond - (and (list? r) (= (first r) :return-value)) - (nth r 1) - (go-eval-error? r) r - :else nil)) - :else nil)))))))))) + (= body nil) + (do (go-run-defers! frame-env defer-stack) nil) + (and (list? body) (= (first body) :block)) + (let ((r (go-eval-block frame-env (nth body 1)))) + (do + (go-run-defers! frame-env defer-stack) + (cond + (and (list? r) (= (first r) :return-value)) + (nth r 1) + (go-eval-error? r) r + :else nil))) + :else + (do (go-run-defers! frame-env defer-stack) + nil))))))))))))) + +(define + go-eval-defer-stmt + (fn + (env stmt) + (let + ((expr (nth stmt 1))) + (cond + (not (and (list? expr) (= (first expr) :app))) + (list :eval-error :defer-not-call expr) + :else (let + ((head (nth expr 1)) (args (nth expr 2))) + (let + ((callee-val (go-eval env head))) + (cond + (go-eval-error? callee-val) + callee-val + :else (let + ((arg-vals (go-eval-args env args))) + (cond + (go-eval-error? arg-vals) + arg-vals + :else (let + ((stack (go-env-lookup env "__go-defer-stack"))) + (cond + (= stack nil) + (list :eval-error :defer-outside-fn) + :else (do + (append! stack (list :go-defer callee-val arg-vals)) + env)))))))))))) + +(define + go-run-defers! + ;; Drain a defer stack LIFO. SX has no in-place list-shrink, so we + ;; walk by index from top down. + (fn (env stack) + (go-run-defers-prefix! env stack (len stack)))) + +(define + go-run-defers-prefix! + (fn (env stack idx) + (cond + (<= idx 0) nil + :else + (let ((d (nth stack (- idx 1)))) + (let ((callee-val (nth d 1)) (arg-vals (nth d 2))) + (let ((wrapped-args + (map (fn (v) (list :quoted-value v)) arg-vals))) + (do + (go-eval-call env callee-val wrapped-args) + (go-run-defers-prefix! env stack (- idx 1))))))))) (define go-eval-var-decl - ;; (:var-decl (:field NAMES TYPE) EXPRS) — bind each NAME to either - ;; the corresponding EXPR's value or nil (zero-init when no EXPRS). - (fn (env stmt) - (let ((field (nth stmt 1)) (exprs (nth stmt 2))) - (let ((names (nth field 1))) + (fn + (env stmt) + (let + ((field (nth stmt 1)) (exprs (nth stmt 2))) + (let + ((names (nth field 1))) (cond (or (= exprs nil) (= (len exprs) 0)) - (go-bind-names env names - (go-zeros (len names))) - :else - (let ((vals (go-eval-args env exprs))) + (go-bind-names env names (go-zeros (len names))) + :else (let + ((vals (go-eval-args env exprs))) (cond - (go-eval-error? vals) vals + (go-eval-error? vals) + vals :else (go-bind-names env names vals)))))))) (define - go-zeros (fn (n) (cond (<= n 0) (list) :else (cons nil (go-zeros (- n 1)))))) + go-zeros + (fn + (n) + (cond + (<= n 0) + (list) + :else (cons nil (go-zeros (- n 1)))))) (define go-eval-short-decl - ;; (:short-decl LHS-LIST EXPRS) — LHS list of (:var NAME) nodes. - (fn (env stmt) - (let ((lhs-list (nth stmt 1)) (exprs (nth stmt 2))) - (let ((names - (map (fn (lhs) - (cond - (and (list? lhs) (= (first lhs) :var)) - (nth lhs 1) - :else :unknown)) - lhs-list))) - (let ((vals (go-eval-args env exprs))) + (fn + (env stmt) + (let + ((lhs-list (nth stmt 1)) (exprs (nth stmt 2))) + (let + ((names (map (fn (lhs) (cond (and (list? lhs) (= (first lhs) :var)) (nth lhs 1) :else :unknown)) lhs-list))) + (let + ((vals (go-eval-args env exprs))) (cond - (go-eval-error? vals) vals + (go-eval-error? vals) + vals :else (go-bind-names env names vals))))))) (define go-eval-assign - ;; v0: assignment shadows via env extension (immutable env model). - ;; Mutation through closures deferred. - (fn (env stmt) - (let ((lhs-list (nth stmt 1)) (rhs-list (nth stmt 2))) - (let ((vals (go-eval-args env rhs-list))) + (fn + (env stmt) + (let + ((lhs-list (nth stmt 1)) (rhs-list (nth stmt 2))) + (let + ((vals (go-eval-args env rhs-list))) (cond - (go-eval-error? vals) vals - :else - (go-eval-assign-pairs env lhs-list vals)))))) + (go-eval-error? vals) + vals + :else (go-eval-assign-pairs env lhs-list vals)))))) (define go-eval-assign-pairs - (fn (env lhs-list vals) + (fn + (env lhs-list vals) (cond - (= (len lhs-list) 0) env - :else - (let ((lhs (first lhs-list)) (rhs-val (first vals))) + (= (len lhs-list) 0) + env + :else (let + ((lhs (first lhs-list)) (rhs-val (first vals))) (cond (and (list? lhs) (= (first lhs) :var)) (go-eval-assign-pairs (go-env-extend env (nth lhs 1) rhs-val) - (rest lhs-list) (rest vals)) - ;; (:index OBJ IDX) — slice or map element assignment + (rest lhs-list) + (rest vals)) (and (list? lhs) (= (first lhs) :index)) - (let ((obj-expr (nth lhs 1)) (idx-expr (nth lhs 2))) + (let + ((obj-expr (nth lhs 1)) + (idx-expr (nth lhs 2))) (cond - ;; only support var-rooted indexing for now (not (and (list? obj-expr) (= (first obj-expr) :var))) (list :eval-error :unsupported-lhs lhs) - :else - (let ((obj (go-eval env obj-expr)) (idx (go-eval env idx-expr))) + :else (let + ((obj (go-eval env obj-expr)) (idx (go-eval env idx-expr))) (cond - (go-eval-error? obj) obj - (go-eval-error? idx) idx + (go-eval-error? obj) + obj + (go-eval-error? idx) + idx (and (list? obj) (= (first obj) :go-slice)) (go-eval-assign-pairs - (go-env-extend env (nth obj-expr 1) - (list :go-slice - (go-slice-set (nth obj 1) idx rhs-val))) - (rest lhs-list) (rest vals)) + (go-env-extend + env + (nth obj-expr 1) + (list + :go-slice (go-slice-set (nth obj 1) idx rhs-val))) + (rest lhs-list) + (rest vals)) (and (list? obj) (= (first obj) :go-map)) (go-eval-assign-pairs - (go-env-extend env (nth obj-expr 1) - (list :go-map - (go-map-set (nth obj 1) idx rhs-val))) - (rest lhs-list) (rest vals)) + (go-env-extend + env + (nth obj-expr 1) + (list + :go-map (go-map-set (nth obj 1) idx rhs-val))) + (rest lhs-list) + (rest vals)) :else (list :eval-error :unsupported-lhs lhs))))) - ;; (:select OBJ FIELD) — struct field assignment (and (list? lhs) (= (first lhs) :select)) - (let ((obj-expr (nth lhs 1)) (field-name (nth lhs 2))) + (let + ((obj-expr (nth lhs 1)) + (field-name (nth lhs 2))) (cond (not (and (list? obj-expr) (= (first obj-expr) :var))) (list :eval-error :unsupported-lhs lhs) - :else - (let ((obj (go-eval env obj-expr))) + :else (let + ((obj (go-eval env obj-expr))) (cond - (go-eval-error? obj) obj + (go-eval-error? obj) + obj (and (list? obj) (= (first obj) :go-struct)) (go-eval-assign-pairs - (go-env-extend env (nth obj-expr 1) - (list :go-struct (nth obj 1) + (go-env-extend + env + (nth obj-expr 1) + (list + :go-struct (nth obj 1) (go-map-set (nth obj 2) field-name rhs-val))) - (rest lhs-list) (rest vals)) + (rest lhs-list) + (rest vals)) :else (list :eval-error :unsupported-lhs lhs))))) :else (list :eval-error :unsupported-lhs lhs)))))) (define go-eval-if - (fn (env stmt) - (let ((cnd (nth stmt 1)) (then (nth stmt 2)) (els (nth stmt 3))) - (let ((c (go-eval env cnd))) + (fn + (env stmt) + (let + ((cnd (nth stmt 1)) + (then (nth stmt 2)) + (els (nth stmt 3))) + (let + ((c (go-eval env cnd))) (cond - (go-eval-error? c) c - c (go-eval-stmt env then) - (not (= els nil)) (go-eval-stmt env els) + (go-eval-error? c) + c + c + (go-eval-stmt env then) + (not (= els nil)) + (go-eval-stmt env els) :else env))))) (define go-eval-func-decl - (fn (env stmt) - (let ((name (nth stmt 1)) (params (nth stmt 2)) - (body (nth stmt 4))) + (fn + (env stmt) + (let + ((name (nth stmt 1)) + (params (nth stmt 2)) + (body (nth stmt 4))) (go-env-extend env name (list :go-fn params body))))) (define go-eval-inc-dec - ;; (:inc-dec OP EXPR) where OP is "++" or "--". EXPR should be (:var NAME). - (fn (env stmt) - (let ((op (nth stmt 1)) (operand (nth stmt 2))) + (fn + (env stmt) + (let + ((op (nth stmt 1)) (operand (nth stmt 2))) (cond (not (and (list? operand) (= (first operand) :var))) (list :eval-error :unsupported-lhs operand) - :else - (let ((current (go-eval env operand))) + :else (let + ((current (go-eval env operand))) (cond - (go-eval-error? current) current - :else - (let ((new-val - (cond - (= op "++") (+ current 1) - (= op "--") (- current 1) - :else current))) + (go-eval-error? current) + current + :else (let + ((new-val (cond (= op "++") (+ current 1) (= op "--") (- current 1) :else current))) (go-env-extend env (nth operand 1) new-val)))))))) (define go-eval-for - ;; (:for INIT COND POST BODY). Any may be nil. - (fn (env stmt) - (let ((init (nth stmt 1)) (cnd (nth stmt 2)) - (post (nth stmt 3)) (body (nth stmt 4))) - (let ((env0 - (cond - (= init nil) env - :else (go-eval-stmt env init)))) + (fn + (env stmt) + (let + ((init (nth stmt 1)) + (cnd (nth stmt 2)) + (post (nth stmt 3)) + (body (nth stmt 4))) + (let + ((env0 (cond (= init nil) env :else (go-eval-stmt env init)))) (cond - (go-eval-error? env0) env0 + (go-eval-error? env0) + env0 :else (go-for-loop env0 cnd post body)))))) (define go-for-loop - (fn (env cnd post body) - (let ((c - (cond - (= cnd nil) true - :else (go-eval env cnd)))) + (fn + (env cnd post body) + (let + ((c (cond (= cnd nil) true :else (go-eval env cnd)))) (cond - (go-eval-error? c) c - (not c) env - :else - (let ((r - (cond - (= body nil) env - (and (list? body) (= (first body) :block)) - (go-eval-block env (nth body 1)) - :else env))) + (go-eval-error? c) + c + (not c) + env + :else (let + ((r (cond (= body nil) env (and (list? body) (= (first body) :block)) (go-eval-block env (nth body 1)) :else env))) (cond - (and (list? r) (= (first r) :return-value)) r - (= r :break) env + (and (list? r) (= (first r) :return-value)) + r + (= r :break) + env (= r :continue) - (let ((env1 - (cond - (= post nil) env - :else (go-eval-stmt env post)))) + (let + ((env1 (cond (= post nil) env :else (go-eval-stmt env post)))) (cond - (go-eval-error? env1) env1 + (go-eval-error? env1) + env1 :else (go-for-loop env1 cnd post body))) - (go-eval-error? r) r - :else - (let ((env1 - (cond - (= post nil) r - :else (go-eval-stmt r post)))) + (go-eval-error? r) + r + :else (let + ((env1 (cond (= post nil) r :else (go-eval-stmt r post)))) (cond - (go-eval-error? env1) env1 + (go-eval-error? env1) + env1 :else (go-for-loop env1 cnd post body))))))))) (define go-eval-stmt - (fn (env stmt) + (fn + (env stmt) (cond (and (list? stmt) (= (first stmt) :return)) - (let ((exprs (nth stmt 1))) + (let + ((exprs (nth stmt 1))) (cond (or (= exprs nil) (= (len exprs) 0)) (list :return-value nil) - :else - (let ((v (go-eval env (first exprs)))) - (cond - (go-eval-error? v) v - :else (list :return-value v))))) + :else (let + ((v (go-eval env (first exprs)))) + (cond (go-eval-error? v) v :else (list :return-value v))))) (and (list? stmt) (= (first stmt) :var-decl)) (go-eval-var-decl env stmt) (and (list? stmt) (= (first stmt) :short-decl)) @@ -794,9 +888,9 @@ (go-eval-if env stmt) (and (list? stmt) (= (first stmt) :for)) (go-eval-for env stmt) - (and (list? stmt) (= (first stmt) :break)) :break - (and (list? stmt) (= (first stmt) :continue)) :continue - (and (list? stmt) (= (first stmt) :inc-dec)) + (and (list? stmt) (= (first stmt) :break)) + :break (and (list? stmt) (= (first stmt) :continue)) + :continue (and (list? stmt) (= (first stmt) :inc-dec)) (go-eval-inc-dec env stmt) (and (list? stmt) (= (first stmt) :func-decl)) (go-eval-func-decl env stmt) @@ -805,157 +899,170 @@ (and (list? stmt) (= (first stmt) :type-decl)) (go-eval-type-decl env stmt) (and (list? stmt) (= (first stmt) :send)) - (let ((ch (go-eval env (nth stmt 1))) - (v (go-eval env (nth stmt 2)))) + (let + ((ch (go-eval env (nth stmt 1))) + (v (go-eval env (nth stmt 2)))) (cond - (go-eval-error? ch) ch - (go-eval-error? v) v - (not (go-chan? ch)) (list :eval-error :send-not-chan ch) + (go-eval-error? ch) + ch + (go-eval-error? v) + v + (not (go-chan? ch)) + (list :eval-error :send-not-chan ch) :else (do (go-chan-send! ch v) env))) + (and (list? stmt) (= (first stmt) :defer)) + (go-eval-defer-stmt env stmt) (and (list? stmt) (= (first stmt) :go)) - ;; v0: synchronous evaluation — no real preemption. The spawned - ;; expression's value is dropped. See sched.sx header for - ;; semantic notes. - (let ((v (go-eval env (nth stmt 1)))) - (cond - (go-eval-error? v) v - :else env)) + (let + ((v (go-eval env (nth stmt 1)))) + (cond (go-eval-error? v) v :else env)) (and (list? stmt) (= (first stmt) :select)) - (let ((r (go-eval-select-stmt env stmt))) + (let + ((r (go-eval-select-stmt env stmt))) (cond - (go-eval-error? r) r - (and (list? r) (= (first r) :return-value)) r - (= r :break) r - (= r :continue) r - ;; Otherwise r is the env after the selected body ran; - ;; propagate so assignments inside cases stick. + (go-eval-error? r) + r + (and (list? r) (= (first r) :return-value)) + r + (= r :break) + r + (= r :continue) + r :else r)) (and (list? stmt) (= (first stmt) :range-for)) (go-eval-range-for env stmt) - :else - (let ((v (go-eval env stmt))) - (cond - (go-eval-error? v) v - :else env))))) + :else (let ((v (go-eval env stmt))) (cond (go-eval-error? v) v :else env))))) (define go-select-try-case - ;; Returns: - ;; :not-ready — case can't proceed (recv on empty channel) - ;; env-or-extended-env — case ran; for recv-into-decl/assign, env - ;; carries the new binding - ;; :eval-error sentinel - (fn (env comm) + (fn + (env comm) (cond - ;; Send case (always ready in v0 with unbounded buffer) (and (list? comm) (= (first comm) :send)) - (let ((ch (go-eval env (nth comm 1))) - (v (go-eval env (nth comm 2)))) + (let + ((ch (go-eval env (nth comm 1))) + (v (go-eval env (nth comm 2)))) (cond - (go-eval-error? ch) ch - (go-eval-error? v) v - (not (go-chan? ch)) (list :eval-error :send-not-chan ch) + (go-eval-error? ch) + ch + (go-eval-error? v) + v + (not (go-chan? ch)) + (list :eval-error :send-not-chan ch) :else (do (go-chan-send! ch v) env))) - ;; Recv-into-var: x := <-ch / x = <-ch - (and (list? comm) - (or (= (first comm) :short-decl) (= (first comm) :assign))) - (let ((lhs-list (nth comm 1)) (exprs (nth comm 2))) + (and + (list? comm) + (or (= (first comm) :short-decl) (= (first comm) :assign))) + (let + ((lhs-list (nth comm 1)) (exprs (nth comm 2))) (cond - (not (= (len exprs) 1)) :not-ready - :else - (let ((rhs (first exprs))) + (not (= (len exprs) 1)) + :not-ready :else + (let + ((rhs (first exprs))) (cond - (not (and (list? rhs) (= (first rhs) :app) - (list? (nth rhs 1)) (= (first (nth rhs 1)) :var) - (= (nth (nth rhs 1) 1) "<-") - (= (len (nth rhs 2)) 1))) - :not-ready - :else - (let ((ch (go-eval env (first (nth rhs 2))))) + (not + (and + (list? rhs) + (= (first rhs) :app) + (list? (nth rhs 1)) + (= (first (nth rhs 1)) :var) + (= (nth (nth rhs 1) 1) "<-") + (= (len (nth rhs 2)) 1))) + :not-ready :else + (let + ((ch (go-eval env (first (nth rhs 2))))) (cond - (go-eval-error? ch) ch - (not (go-chan? ch)) (list :eval-error :recv-not-chan ch) - (= (go-chan-len ch) 0) :not-ready - :else - (let ((v (go-chan-recv! ch))) + (go-eval-error? ch) + ch + (not (go-chan? ch)) + (list :eval-error :recv-not-chan ch) + (= (go-chan-len ch) 0) + :not-ready :else + (let + ((v (go-chan-recv! ch))) (cond - (= v :empty) :not-ready - :else - (let ((names (map (fn (lhs) - (cond - (and (list? lhs) - (= (first lhs) :var)) - (nth lhs 1) - :else :unknown)) - lhs-list))) + (= v :empty) + :not-ready :else + (let + ((names (map (fn (lhs) (cond (and (list? lhs) (= (first lhs) :var)) (nth lhs 1) :else :unknown)) lhs-list))) (cond - (= (len names) 0) env - :else - (go-env-extend env (first names) v))))))))))) - ;; Bare recv: (:app (:var "<-") [CHAN]) - (and (list? comm) (= (first comm) :app) - (list? (nth comm 1)) (= (first (nth comm 1)) :var) - (= (nth (nth comm 1) 1) "<-") - (= (len (nth comm 2)) 1)) - (let ((ch (go-eval env (first (nth comm 2))))) + (= (len names) 0) + env + :else (go-env-extend env (first names) v))))))))))) + (and + (list? comm) + (= (first comm) :app) + (list? (nth comm 1)) + (= (first (nth comm 1)) :var) + (= (nth (nth comm 1) 1) "<-") + (= (len (nth comm 2)) 1)) + (let + ((ch (go-eval env (first (nth comm 2))))) (cond - (go-eval-error? ch) ch - (not (go-chan? ch)) (list :eval-error :recv-not-chan ch) - (= (go-chan-len ch) 0) :not-ready - :else (do (go-chan-recv! ch) env))) + (go-eval-error? ch) + ch + (not (go-chan? ch)) + (list :eval-error :recv-not-chan ch) + (= (go-chan-len ch) 0) + :not-ready :else + (do (go-chan-recv! ch) env))) :else :not-ready))) (define go-select-pick - ;; Walk cases in order. First :select-case whose comm-stmt is ready - ;; wins. If none ready and a :default was seen, run it. Otherwise - ;; :select-blocked-no-default. - (fn (env cases default-case) + (fn + (env cases default-case) (cond (or (= cases nil) (= (len cases) 0)) (cond - (= default-case nil) (list :eval-error :select-blocked-no-default) + (= default-case nil) + (list :eval-error :select-blocked-no-default) :else (go-eval-block env (nth default-case 1))) - :else - (let ((c (first cases))) + :else (let + ((c (first cases))) (cond (and (list? c) (= (first c) :default)) (go-select-pick env (rest cases) c) (and (list? c) (= (first c) :select-case)) - (let ((maybe-env (go-select-try-case env (nth c 1)))) + (let + ((maybe-env (go-select-try-case env (nth c 1)))) (cond (= maybe-env :not-ready) (go-select-pick env (rest cases) default-case) - (go-eval-error? maybe-env) maybe-env + (go-eval-error? maybe-env) + maybe-env :else (go-eval-block maybe-env (nth c 2)))) :else (go-select-pick env (rest cases) default-case)))))) (define go-eval-select-stmt - (fn (env stmt) - (go-select-pick env (nth stmt 1) nil))) + (fn (env stmt) (go-select-pick env (nth stmt 1) nil))) (define go-ast-name - ;; Extract a name from a (:var NAME) ast, else nil. - (fn (ast) + (fn + (ast) (cond - (and (list? ast) (= (first ast) :var)) (nth ast 1) + (and (list? ast) (= (first ast) :var)) + (nth ast 1) :else nil))) (define go-range-extend - (fn (env key-name value-name k v) + (fn + (env key-name value-name k v) (cond (and (not (= key-name nil)) (not (= value-name nil))) (go-env-extend (go-env-extend env key-name k) value-name v) - (not (= key-name nil)) (go-env-extend env key-name k) + (not (= key-name nil)) + (go-env-extend env key-name k) :else env))) (define go-range-body - ;; Evaluate body in env. Returns env-or-sentinel. - (fn (env body) + (fn + (env body) (cond (and (list? body) (= (first body) :block)) (go-eval-block env (nth body 1)) @@ -963,203 +1070,280 @@ (define go-range-slice-loop - (fn (env elems i key-name value-name body original-env) + (fn + (env elems i key-name value-name body original-env) (cond - (>= i (len elems)) env - :else - (let ((env2 (go-range-extend env key-name value-name i - (nth elems i)))) - (let ((r (go-range-body env2 body))) + (>= i (len elems)) + env + :else (let + ((env2 (go-range-extend env key-name value-name i (nth elems i)))) + (let + ((r (go-range-body env2 body))) (cond - (and (list? r) (= (first r) :return-value)) r - (= r :break) env + (and (list? r) (= (first r) :return-value)) + r + (= r :break) + env (= r :continue) - (go-range-slice-loop env elems (+ i 1) - key-name value-name body original-env) - (go-eval-error? r) r - :else - (go-range-slice-loop r elems (+ i 1) - key-name value-name body original-env))))))) + (go-range-slice-loop + env + elems + (+ i 1) + key-name + value-name + body + original-env) + (go-eval-error? r) + r + :else (go-range-slice-loop + r + elems + (+ i 1) + key-name + value-name + body + original-env))))))) (define go-range-map-loop - (fn (env entries key-name value-name body original-env) + (fn + (env entries key-name value-name body original-env) (cond - (or (= entries nil) (= (len entries) 0)) env - :else - (let ((entry (first entries))) - (let ((k (first entry)) (v (nth entry 1))) - (let ((env2 (go-range-extend env key-name value-name k v))) - (let ((r (go-range-body env2 body))) + (or (= entries nil) (= (len entries) 0)) + env + :else (let + ((entry (first entries))) + (let + ((k (first entry)) (v (nth entry 1))) + (let + ((env2 (go-range-extend env key-name value-name k v))) + (let + ((r (go-range-body env2 body))) (cond - (and (list? r) (= (first r) :return-value)) r - (= r :break) env + (and (list? r) (= (first r) :return-value)) + r + (= r :break) + env (= r :continue) - (go-range-map-loop env (rest entries) - key-name value-name body original-env) - (go-eval-error? r) r - :else - (go-range-map-loop r (rest entries) - key-name value-name body original-env))))))))) + (go-range-map-loop + env + (rest entries) + key-name + value-name + body + original-env) + (go-eval-error? r) + r + :else (go-range-map-loop + r + (rest entries) + key-name + value-name + body + original-env))))))))) (define go-range-chan-loop - ;; For chan: KEY-NAME receives each value. v0 stops when chan is - ;; empty (no preemption to wait for new values). Real Go waits on - ;; the chan until closed AND empty. - (fn (env coll key-name body original-env) + (fn + (env coll key-name body original-env) (cond - (= (go-chan-len coll) 0) env - :else - (let ((v (go-chan-recv! coll))) - (let ((env2 - (cond - (not (= key-name nil)) (go-env-extend env key-name v) - :else env))) - (let ((r (go-range-body env2 body))) + (= (go-chan-len coll) 0) + env + :else (let + ((v (go-chan-recv! coll))) + (let + ((env2 (cond (not (= key-name nil)) (go-env-extend env key-name v) :else env))) + (let + ((r (go-range-body env2 body))) (cond - (and (list? r) (= (first r) :return-value)) r - (= r :break) env + (and (list? r) (= (first r) :return-value)) + r + (= r :break) + env (= r :continue) (go-range-chan-loop env coll key-name body original-env) - (go-eval-error? r) r - :else - (go-range-chan-loop r coll key-name body original-env)))))))) + (go-eval-error? r) + r + :else (go-range-chan-loop r coll key-name body original-env)))))))) (define go-eval-range-for - ;; (:range-for DECL-KIND KEY VALUE COLL BODY) - ;; KEY/VALUE: (:var NAME) or nil - ;; COLL: an expression evaluating to slice / map / chan - (fn (env stmt) - (let ((key-name (go-ast-name (nth stmt 2))) - (value-name (go-ast-name (nth stmt 3))) - (coll-expr (nth stmt 4)) - (body (nth stmt 5))) - (let ((coll (go-eval env coll-expr))) + (fn + (env stmt) + (let + ((key-name (go-ast-name (nth stmt 2))) + (value-name (go-ast-name (nth stmt 3))) + (coll-expr (nth stmt 4)) + (body (nth stmt 5))) + (let + ((coll (go-eval env coll-expr))) (cond - (go-eval-error? coll) coll + (go-eval-error? coll) + coll (and (list? coll) (= (first coll) :go-slice)) - (go-range-slice-loop env (nth coll 1) 0 - key-name value-name body env) + (go-range-slice-loop + env + (nth coll 1) + 0 + key-name + value-name + body + env) (and (list? coll) (= (first coll) :go-map)) - (go-range-map-loop env (nth coll 1) - key-name value-name body env) + (go-range-map-loop + env + (nth coll 1) + key-name + value-name + body + env) (and (list? coll) (= (first coll) :go-chan)) (go-range-chan-loop env coll key-name body env) :else (list :eval-error :not-rangeable coll)))))) (define go-eval-method-decl - ;; (:method-decl RECV NAME PARAMS RESULTS BODY) — register the method - ;; under #method/RECV-TYPE-NAME/METHOD-NAME, value is a :go-method. - (fn (env stmt) - (let ((recv (nth stmt 1)) (name (nth stmt 2)) - (params (nth stmt 3)) (body (nth stmt 5))) - (let ((recv-names (nth recv 1)) (recv-ty (nth recv 2))) - (let ((recv-name - (cond - (= (len recv-names) 0) "_" - :else (first recv-names)))) - (let ((type-name (go-extract-recv-ty-name recv-ty))) + (fn + (env stmt) + (let + ((recv (nth stmt 1)) + (name (nth stmt 2)) + (params (nth stmt 3)) + (body (nth stmt 5))) + (let + ((recv-names (nth recv 1)) + (recv-ty (nth recv 2))) + (let + ((recv-name (cond (= (len recv-names) 0) "_" :else (first recv-names)))) + (let + ((type-name (go-extract-recv-ty-name recv-ty))) (cond - (= type-name nil) env - :else - (go-env-extend env + (= type-name nil) + env + :else (go-env-extend + env (str "#method/" type-name "/" name) (list :go-method recv-name params body))))))))) (define go-eval-method-call - ;; Method dispatch: lookup #method/TYPE/NAME in env, bind receiver - ;; to OBJ-value and params to ARGS, run body. - (fn (env obj-expr method-name args) - (let ((obj (go-eval env obj-expr))) + (fn + (env obj-expr method-name args) + (let + ((obj (go-eval env obj-expr))) (cond - (go-eval-error? obj) obj + (go-eval-error? obj) + obj (not (and (list? obj) (= (first obj) :go-struct))) - ;; Not a struct: maybe it's a callable field access? Try the - ;; normal select-then-call path. - (let ((callee (go-eval env (list :select obj-expr method-name)))) + (let + ((callee (go-eval env (list :select obj-expr method-name)))) (cond - (go-eval-error? callee) callee + (go-eval-error? callee) + callee :else (go-eval-call env callee args))) - :else - (let ((type-name (nth obj 1))) - (let ((method-val (go-env-lookup env - (str "#method/" type-name "/" method-name)))) + :else (let + ((type-name (nth obj 1))) + (let + ((method-val (go-env-lookup env (str "#method/" type-name "/" method-name)))) (cond (= method-val nil) (list :eval-error :no-such-method type-name method-name) - :else - (let ((recv-name (nth method-val 1)) - (params (nth method-val 2)) - (body (nth method-val 3))) - (let ((arg-vals (go-eval-args env args))) + :else (let + ((recv-name (nth method-val 1)) + (params (nth method-val 2)) + (body (nth method-val 3))) + (let + ((arg-vals (go-eval-args env args))) (cond - (go-eval-error? arg-vals) arg-vals - :else - (let ((param-names (go-flatten-param-names params))) + (go-eval-error? arg-vals) + arg-vals + :else (let + ((param-names (go-flatten-param-names params))) (cond (not (= (len param-names) (len arg-vals))) - (list :eval-error :arity-mismatch - (len param-names) (len arg-vals)) - :else - (let ((call-env - (go-env-extend - (go-bind-names env param-names arg-vals) - recv-name obj))) + (list + :eval-error :arity-mismatch + (len param-names) + (len arg-vals)) + :else (let + ((call-env (go-env-extend (go-bind-names env param-names arg-vals) recv-name obj))) (cond - (= body nil) nil + (= body nil) + nil (and (list? body) (= (first body) :block)) - (let ((r (go-eval-block call-env (nth body 1)))) + (let + ((r (go-eval-block call-env (nth body 1)))) (cond (and (list? r) (= (first r) :return-value)) (nth r 1) - (go-eval-error? r) r + (go-eval-error? r) + r :else nil)) :else nil)))))))))))))) (define go-eval-type-decl - ;; (:type-decl NAME TYPE). For struct types we register the field-name - ;; list so positional composite literals like Point{1, 2} can map - ;; positions to field names. Other type aliases are silent no-ops in v0. - (fn (env stmt) - (let ((name (nth stmt 1)) (ty (nth stmt 2))) + (fn + (env stmt) + (let + ((name (nth stmt 1)) (ty (nth stmt 2))) (cond (and (list? ty) (= (first ty) :ty-struct)) - (go-env-extend env name + (go-env-extend + env + name (list :go-struct-type (go-struct-field-names (nth ty 1)))) :else env)))) (define go-eval-block - (fn (env stmts) + (fn + (env stmts) (cond - (or (= stmts nil) (= (len stmts) 0)) env - :else - (let ((r (go-eval-stmt env (first stmts)))) + (or (= stmts nil) (= (len stmts) 0)) + env + :else (let + ((r (go-eval-stmt env (first stmts)))) (cond - (and (list? r) (= (first r) :return-value)) r - (= r :break) r - (= r :continue) r - (go-eval-error? r) r + (and (list? r) (= (first r) :return-value)) + r + (= r :break) + r + (= r :continue) + r + (go-eval-error? r) + r :else (go-eval-block r (rest stmts))))))) (define go-eval-program - ;; Evaluate a sequence of top-level forms in ENV. Returns the final - ;; env (or :eval-error / :return-value if either propagates). + ;; Top-level driver. The "implicit main frame" gets its own defer + ;; stack so `defer` at top level (which is what most runtime tests + ;; use) behaves like deferring in main. The stack is drained after + ;; all forms run. (fn (env forms) + (let ((defer-stack (list))) + (let ((env (go-env-extend env "__go-defer-stack" defer-stack))) + (let ((r (go-eval-program-loop env forms))) + (do + (go-run-defers! env defer-stack) + r)))))) + +(define + go-eval-program-loop + (fn + (env forms) (cond - (or (= forms nil) (= (len forms) 0)) env - :else - (let ((r (go-eval-stmt env (first forms)))) + (or (= forms nil) (= (len forms) 0)) + env + :else (let + ((r (go-eval-stmt env (first forms)))) (cond - (and (list? r) (= (first r) :return-value)) r - (go-eval-error? r) r - :else (go-eval-program r (rest forms))))))) + (and (list? r) (= (first r) :return-value)) + r + (go-eval-error? r) + r + :else (go-eval-program-loop r (rest forms))))))) (define go-eval @@ -1168,13 +1352,18 @@ (cond (and (list? expr) (= (first expr) :literal)) (go-eval-literal (nth expr 1)) + (and (list? expr) (= (first expr) :quoted-value)) + (nth expr 1) (and (list? expr) (= (first expr) :var)) (let ((name (nth expr 1))) (cond - (= name "true") true - (= name "false") false - (= name "nil") nil + (= name "true") + true + (= name "false") + false + (= name "nil") + nil :else (let ((v (go-env-lookup env name))) (cond (= v nil) (list :eval-error :unbound name) :else v)))) @@ -1187,41 +1376,57 @@ (and (list? expr) (= (first expr) :select)) (go-eval-select env expr) (and (list? expr) (= (first expr) :app)) - (let ((head (nth expr 1)) (args (nth expr 2))) + (let + ((head (nth expr 1)) (args (nth expr 2))) (cond (go-is-eval-binop? head args) - (let ((op (nth head 1))) - (let ((lv (go-eval env (first args))) - (rv (go-eval env (nth args 1)))) + (let + ((op (nth head 1))) + (let + ((lv (go-eval env (first args))) + (rv (go-eval env (nth args 1)))) (cond - (go-eval-error? lv) lv - (go-eval-error? rv) rv + (go-eval-error? lv) + lv + (go-eval-error? rv) + rv :else (go-eval-binop op lv rv)))) - ;; Unary prefix op: head is :var with op name + 1 arg. - (and (list? head) (= (first head) :var) (= (len args) 1) - (some (fn (o) (= o (nth head 1))) - (list "-" "+" "!" "<-"))) - (let ((op (nth head 1)) (v (go-eval env (first args)))) + (and + (list? head) + (= (first head) :var) + (= (len args) 1) + (some + (fn (o) (= o (nth head 1))) + (list "-" "+" "!" "<-"))) + (let + ((op (nth head 1)) (v (go-eval env (first args)))) (cond - (go-eval-error? v) v - (= op "-") (- 0 v) - (= op "+") v - (= op "!") (not v) + (go-eval-error? v) + v + (= op "-") + (- 0 v) + (= op "+") + v + (= op "!") + (not v) (= op "<-") (cond - (not (go-chan? v)) (list :eval-error :recv-not-chan v) - :else - (let ((r (go-chan-recv! v))) - ;; :empty in v0 means "no value yet" — Go would block. - ;; We return nil as a stand-in for the zero value. + (not (go-chan? v)) + (list :eval-error :recv-not-chan v) + :else (let + ((r (go-chan-recv! v))) (cond (= r :empty) nil :else r))) :else (list :eval-error :unsupported-unary op))) - ;; Method-call shape: head is (:select OBJ METHOD-NAME). (and (list? head) (= (first head) :select)) - (go-eval-method-call env (nth head 1) (nth head 2) args) - :else - (let ((callee (go-eval env head))) + (go-eval-method-call + env + (nth head 1) + (nth head 2) + args) + :else (let + ((callee (go-eval env head))) (cond - (go-eval-error? callee) callee + (go-eval-error? callee) + callee :else (go-eval-call env callee args))))) :else (list :eval-error :unsupported-eval expr)))) diff --git a/lib/go/scoreboard.json b/lib/go/scoreboard.json index c6519e67..7fa980ef 100644 --- a/lib/go/scoreboard.json +++ b/lib/go/scoreboard.json @@ -1,12 +1,12 @@ { "language": "go", - "total_pass": 497, - "total": 497, + "total_pass": 503, + "total": 503, "suites": [ {"name":"lex","pass":129,"total":129,"status":"ok"}, {"name":"parse","pass":176,"total":176,"status":"ok"}, {"name":"types","pass":72,"total":72,"status":"ok"}, - {"name":"eval","pass":80,"total":80,"status":"ok"}, + {"name":"eval","pass":86,"total":86,"status":"ok"}, {"name":"runtime","pass":40,"total":40,"status":"ok"}, {"name":"stdlib","pass":0,"total":0,"status":"pending"}, {"name":"e2e","pass":0,"total":0,"status":"pending"} diff --git a/lib/go/scoreboard.md b/lib/go/scoreboard.md index 93f6371c..aaa9579f 100644 --- a/lib/go/scoreboard.md +++ b/lib/go/scoreboard.md @@ -1,13 +1,13 @@ # Go-on-SX Scoreboard -**Total: 497 / 497 tests passing** +**Total: 503 / 503 tests passing** | | Suite | Pass | Total | |---|---|---|---| | ✅ | lex | 129 | 129 | | ✅ | parse | 176 | 176 | | ✅ | types | 72 | 72 | -| ✅ | eval | 80 | 80 | +| ✅ | eval | 86 | 86 | | ✅ | runtime | 40 | 40 | | ⬜ | stdlib | 0 | 0 | | ⬜ | e2e | 0 | 0 | diff --git a/lib/go/tests/eval.sx b/lib/go/tests/eval.sx index 2e6f12e3..b696ee6d 100644 --- a/lib/go/tests/eval.sx +++ b/lib/go/tests/eval.sx @@ -467,6 +467,55 @@ (go-eval env (go-parse "find(nums, 99)"))) -1) +(go-eval-test + "defer: single defer runs after surrounding fn body returns" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "ch := make()") (go-parse "func push2(c chan int) { c <- 2 }") (go-parse "func run(c chan int) { defer push2(c) ; c <- 1 }") (go-parse "run(ch)") (go-parse "first := <-ch") (go-parse "second := <-ch"))))) + (list (go-env-lookup env "first") (go-env-lookup env "second"))) + (list 1 2)) + +(go-eval-test + "defer: multiple defers run LIFO" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "ch := make()") (go-parse "func p2(c chan int) { c <- 2 }") (go-parse "func p3(c chan int) { c <- 3 }") (go-parse "func run(c chan int) { defer p2(c) ; defer p3(c) ; c <- 1 }") (go-parse "run(ch)") (go-parse "a := <-ch") (go-parse "b := <-ch") (go-parse "d := <-ch"))))) + (list + (go-env-lookup env "a") + (go-env-lookup env "b") + (go-env-lookup env "d"))) + (list 1 3 2)) + +(go-eval-test + "defer: arguments are evaluated at defer-time (not call-time)" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "ch := make()") (go-parse "func pushN(c chan int, v int) { c <- v }") (go-parse "func run(c chan int) { x := 7 ; defer pushN(c, x) ; x = 99 }") (go-parse "run(ch)") (go-parse "got := <-ch"))))) + (go-env-lookup env "got")) + 7) + +(go-eval-test + "defer: runs even when fn returns early via return" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "ch := make()") (go-parse "func note(c chan int) { c <- 42 }") (go-parse "func run(c chan int) int { defer note(c) ; return 1 }") (go-parse "r := run(ch)") (go-parse "n := <-ch"))))) + (list (go-env-lookup env "r") (go-env-lookup env "n"))) + (list 1 42)) + +(go-eval-test + "defer: stack is frame-local — outer defers don't run on inner return" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "ch := make()") (go-parse "func push1(c chan int) { c <- 1 }") (go-parse "func push2(c chan int) { c <- 2 }") (go-parse "func inner(c chan int) { defer push2(c) }") (go-parse "func outer(c chan int) { defer push1(c) ; inner(c) }") (go-parse "outer(ch)") (go-parse "a := <-ch") (go-parse "b := <-ch"))))) + (list (go-env-lookup env "a") (go-env-lookup env "b"))) + (list 2 1)) + +(go-eval-test + "defer: in a loop, all defers fire on fn return (not loop iter)" + (let + ((env (go-eval-program go-env-builtins (list (go-parse "ch := make()") (go-parse "func pushI(c chan int, v int) { c <- v }") (go-parse "func loop(c chan int) { for i := 0; i < 4; i = i + 1 { defer pushI(c, i) } }") (go-parse "loop(ch)") (go-parse "a := <-ch") (go-parse "b := <-ch") (go-parse "d := <-ch") (go-parse "e := <-ch"))))) + (list + (go-env-lookup env "a") + (go-env-lookup env "b") + (go-env-lookup env "d") + (go-env-lookup env "e"))) + (list 3 2 1 0)) + (define go-eval-test-summary (str "eval " go-eval-test-pass "/" go-eval-test-count)) diff --git a/plans/go-on-sx.md b/plans/go-on-sx.md index 65b30a76..95b8d24a 100644 --- a/plans/go-on-sx.md +++ b/plans/go-on-sx.md @@ -356,9 +356,11 @@ Progress-log line → push `origin/loops/go`. - **Acceptance:** runtime/ +20 tests. ### Phase 6 — `defer` + panic/recover ⬜ -- Defer stack per function frame; runs LIFO on return (normal or panic). -- `panic(v)` unwinds frames running deferreds; `recover()` inside a - deferred fn captures the panic value and stops unwinding. +- [x] Defer stack per function frame; runs LIFO on normal return. + Args eager at defer-time; frame-local (inner defers don't run + outer ones); defer-in-loop pushes each iteration. 6 tests. +- [ ] `panic(v)` unwinds frames running deferreds; `recover()` inside a + deferred fn captures the panic value and stops unwinding. - Goroutine panic propagation: a panicking goroutine that doesn't recover crashes the whole program (honour Go spec, or document divergence). - Tests: defer order (LIFO), defer + named-return mutation, panic/recover, @@ -609,6 +611,21 @@ Minimal repro: see `lib/go/lex.sx#gl-oct-digit?` and `#gl-match-op`. _Newest first. Append one dated entry per commit._ +- 2026-05-27 — **Phase 6 first slice: defer + LIFO.** Added + `go-eval-defer-stmt`, `go-run-defers!`, `go-run-defers-prefix!`, + plus new `:quoted-value` AST node so deferred calls can be + re-invoked with values captured at defer-time. Frame: `go-eval-call` + installs a fresh `__go-defer-stack` (mutable list) in the call env, + drains LIFO before returning. `go-eval-program` does the same for + the implicit main frame. 6 tests on eval/: single defer, + multi-defer LIFO, args eager at defer-time, defer fires on early + return, frame-local stack (inner defers don't bleed to outer), + defer-in-loop (all iterations defer to fn return). 503/503 total. + **Shape:** SX assignment shadows rather than mutates, so the + natural defer side-effect channel is the *channel buffer* — shared + via closure identity. Drove the test design and matches the eventual + panic/recover shape (errors will need to escape through a similar + out-of-band mechanism, not through env mutation). [shapes-scheduler] - 2026-05-27 — **Phase 5 acceptance bar hit (40/40 runtime, 497/497 total).** Added `after(d)` builtin (v0 timer stub: returns a channel already buffered with `:tick`) and 13 canonical-pattern tests: diff --git a/plans/lib-guest-scheduler.md b/plans/lib-guest-scheduler.md index e07ca110..44d89e02 100644 --- a/plans/lib-guest-scheduler.md +++ b/plans/lib-guest-scheduler.md @@ -231,6 +231,42 @@ real result. _Newest first. Append one dated entry per milestone landed._ +- 2026-05-27 — **Phase 6 first slice: defer + LIFO observation.** + Go's defer is a *frame-local cleanup queue* — a list of (callee, + pre-evaluated-args) records appended on `defer`, drained LIFO at + frame exit. The scheduler kit needs the same shape because: (a) a + panicking goroutine must run its frame's defers before unwinding to + the next frame; (b) a goroutine that exits cleanly still runs them; + (c) `select` cases that own resources (an acquired send slot, a + buffer reservation) need a cleanup hook on the case-not-taken path. + All three reduce to the same primitive: **"hand the frame a list + of thunks; call them LIFO before the frame is gone."** + + Concretely the kit should expose `frame-defer!` (push) and an + internal `frame-teardown!` (drained by the scheduler on exit / by + the panic unwinder on abort). The scheduler's exit-path becomes: + + 1. Mark frame done. + 2. Call `frame-teardown!` — run defers LIFO. A defer that itself + panics: capture the new panic, continue running the rest + (matches Go spec). + 3. Release frame slot. + + Crucially the defer queue is *not* the same as the scheduler's + ready-queue — confusing the two was an early temptation. The defer + queue is per-frame and synchronous-on-exit; the ready-queue is + global and async. Phase 5b will need to keep these distinct when + real preemption lands. + + Test signal that drove the shape: SX assignment shadows rather than + mutates, so the only observable side-effect channel for deferred + calls is `(append! buf ...)` on a value with stable identity (e.g. + a channel). That maps cleanly to "deferred work emits its effects + through capabilities the frame held, not through enclosing-env + mutation" — which is also how the scheduler kit's deferred work + should communicate with the rest of the system. No magic; just + capabilities the frame can hand to its defers. + - 2026-05-27 — **Phase 5 acceptance crossed (40 runtime tests).** Final shape observation: *time-as-readiness-flip*. The Go side added an `after(d)` builtin that returns a channel **already