Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 50b69bcbd0 | |||
| 14986d787d | |||
| 21028c4fb0 | |||
| 7415dd020e | |||
| 2fa0bb4df1 | |||
| 63ad4563cb | |||
| c8b232d40e | |||
| 64d36fa66e | |||
| be820d0337 | |||
| a32561a07d | |||
| 40f0e73386 | |||
| 83dbb5958a | |||
| 16cf4d9316 | |||
| d21cde336a | |||
| f0f339709e | |||
| 0596376199 | |||
| 35511db15b | |||
| 40ce4df6b1 | |||
| 0cc36450c4 | |||
| 21e8e51174 |
@@ -1279,7 +1279,7 @@ let run_foundation_tests () =
|
||||
assert_true "sx_truthy \"\"" (Bool (sx_truthy (String "")));
|
||||
assert_eq "not truthy nil" (Bool false) (Bool (sx_truthy Nil));
|
||||
assert_eq "not truthy false" (Bool false) (Bool (sx_truthy (Bool false)));
|
||||
let l = { l_params = ["x"]; l_body = Symbol "x"; l_closure = Sx_types.make_env (); l_name = None; l_compiled = None; l_call_count = 0; l_uid = Sx_types.next_lambda_uid () } in
|
||||
let l = { l_params = ["x"]; l_body = Symbol "x"; l_closure = Sx_types.make_env (); l_name = None; l_compiled = None } in
|
||||
assert_true "is_lambda" (Bool (Sx_types.is_lambda (Lambda l)));
|
||||
ignore (Sx_types.set_lambda_name (Lambda l) "my-fn");
|
||||
assert_eq "lambda name mutated" (String "my-fn") (lambda_name (Lambda l))
|
||||
|
||||
@@ -703,6 +703,11 @@ let setup_evaluator_bridge env =
|
||||
| [expr; e] -> Sx_ref.eval_expr expr (Env (Sx_runtime.unwrap_env e))
|
||||
| [expr] -> Sx_ref.eval_expr expr (Env env)
|
||||
| _ -> raise (Eval_error "eval-expr: expected (expr env?)"));
|
||||
(* eval-in-env: (env expr) → result. Evaluates expr in the given env. *)
|
||||
Sx_primitives.register "eval-in-env" (fun args ->
|
||||
match args with
|
||||
| [e; expr] -> Sx_ref.eval_expr expr e
|
||||
| _ -> raise (Eval_error "eval-in-env: (env expr)"));
|
||||
bind "trampoline" (fun args ->
|
||||
match args with
|
||||
| [v] ->
|
||||
@@ -764,7 +769,13 @@ let setup_evaluator_bridge env =
|
||||
| _ -> raise (Eval_error "register-special-form!: expected (name handler)"));
|
||||
ignore (env_bind env "*custom-special-forms*" Sx_ref.custom_special_forms);
|
||||
ignore (Sx_ref.register_special_form (String "<>") (NativeFn ("<>", fun args ->
|
||||
List (List.map (fun a -> Sx_ref.eval_expr a (Env env)) args))))
|
||||
List (List.map (fun a -> Sx_ref.eval_expr a (Env env)) args))));
|
||||
(* current-env: special form — returns current lexical env as a first-class value *)
|
||||
ignore (Sx_ref.register_special_form (String "current-env")
|
||||
(NativeFn ("current-env", fun args ->
|
||||
match args with
|
||||
| [_arg_list; env_val] -> env_val
|
||||
| _ -> Nil)))
|
||||
|
||||
(* ---- Type predicates and introspection ---- *)
|
||||
let setup_introspection env =
|
||||
@@ -950,7 +961,24 @@ let setup_env_operations env =
|
||||
bind "env-has?" (fun args -> match args with [e; String k] -> Bool (Sx_types.env_has (uw e) k) | [e; Keyword k] -> Bool (Sx_types.env_has (uw e) k) | _ -> raise (Eval_error "env-has?: expected env and string"));
|
||||
bind "env-bind!" (fun args -> match args with [e; String k; v] -> Sx_types.env_bind (uw e) k v | [e; Keyword k; v] -> Sx_types.env_bind (uw e) k v | _ -> raise (Eval_error "env-bind!: expected env, key, value"));
|
||||
bind "env-set!" (fun args -> match args with [e; String k; v] -> Sx_types.env_set (uw e) k v | [e; Keyword k; v] -> Sx_types.env_set (uw e) k v | _ -> raise (Eval_error "env-set!: expected env, key, value"));
|
||||
bind "env-extend" (fun args -> match args with [e] -> Env (Sx_types.env_extend (uw e)) | _ -> raise (Eval_error "env-extend: expected env"));
|
||||
bind "env-extend" (fun args ->
|
||||
match args with
|
||||
| e :: pairs ->
|
||||
let child = Sx_types.env_extend (uw e) in
|
||||
let rec go = function
|
||||
| [] -> ()
|
||||
| k :: v :: rest ->
|
||||
ignore (Sx_types.env_bind child (Sx_runtime.value_to_str k) v); go rest
|
||||
| [_] -> raise (Eval_error "env-extend: odd number of key-val pairs") in
|
||||
go pairs; Env child
|
||||
| _ -> raise (Eval_error "env-extend: expected env"));
|
||||
bind "env-lookup" (fun args ->
|
||||
match args with
|
||||
| [e; key] ->
|
||||
let k = Sx_runtime.value_to_str key in
|
||||
let raw = uw e in
|
||||
if Sx_types.env_has raw k then Sx_types.env_get raw k else Nil
|
||||
| _ -> raise (Eval_error "env-lookup: (env key)"));
|
||||
bind "env-merge" (fun args -> match args with [a; b] -> Sx_runtime.env_merge a b | _ -> raise (Eval_error "env-merge: expected 2 envs"))
|
||||
|
||||
(* ---- Strict mode (gradual type system support) ---- *)
|
||||
|
||||
@@ -665,11 +665,7 @@ let () =
|
||||
let rec deep_equal a b =
|
||||
match a, b with
|
||||
| Nil, Nil -> true | Bool a, Bool b -> a = b
|
||||
| Integer a, Integer b -> a = b
|
||||
| Number a, Number b -> a = b
|
||||
| Integer a, Number b -> float_of_int a = b
|
||||
| Number a, Integer b -> a = float_of_int b
|
||||
| String a, String b -> a = b
|
||||
| Number a, Number b -> a = b | String a, String b -> a = b
|
||||
| Symbol a, Symbol b -> a = b | Keyword a, Keyword b -> a = b
|
||||
| (List a | ListRef { contents = a }), (List b | ListRef { contents = b }) ->
|
||||
List.length a = List.length b && List.for_all2 deep_equal a b
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -539,3 +539,4 @@ let jit_try_call f args =
|
||||
(match hook f arg_list with Some result -> incr _jit_hit; result | None -> incr _jit_miss; _jit_skip_sentinel)
|
||||
| _ -> incr _jit_skip; _jit_skip_sentinel
|
||||
|
||||
|
||||
|
||||
@@ -128,8 +128,6 @@ and lambda = {
|
||||
l_closure : env;
|
||||
mutable l_name : string option;
|
||||
mutable l_compiled : vm_closure option; (** Lazy JIT cache *)
|
||||
mutable l_call_count : int; (** Tiered-compilation counter — JIT after threshold calls *)
|
||||
l_uid : int; (** Unique identity for LRU cache tracking *)
|
||||
}
|
||||
|
||||
and component = {
|
||||
@@ -436,60 +434,12 @@ let unwrap_env_val = function
|
||||
| Env e -> e
|
||||
| _ -> raise (Eval_error "make_lambda: expected env for closure")
|
||||
|
||||
(* Lambda UID — minted on construction, used as LRU cache key (Phase 2). *)
|
||||
let lambda_uid_counter = ref 0
|
||||
let next_lambda_uid () = incr lambda_uid_counter; !lambda_uid_counter
|
||||
|
||||
let make_lambda params body closure =
|
||||
let ps = match params with
|
||||
| List items -> List.map value_to_string items
|
||||
| _ -> value_to_string_list params
|
||||
in
|
||||
Lambda { l_params = ps; l_body = body; l_closure = unwrap_env_val closure; l_name = None; l_compiled = None; l_call_count = 0; l_uid = next_lambda_uid () }
|
||||
|
||||
(** {1 JIT cache control}
|
||||
|
||||
Tiered compilation: only JIT a lambda after it's been called [jit_threshold]
|
||||
times. This filters out one-shot lambdas (test harness, dynamic eval, REPLs)
|
||||
so they never enter the JIT cache. Counters are exposed to SX as [(jit-stats)].
|
||||
|
||||
These live here (in sx_types) rather than sx_vm so [sx_primitives] can read
|
||||
them without creating a sx_primitives → sx_vm dependency cycle. *)
|
||||
let jit_threshold = ref 4
|
||||
let jit_compiled_count = ref 0
|
||||
let jit_skipped_count = ref 0
|
||||
let jit_threshold_skipped_count = ref 0
|
||||
|
||||
(** {2 JIT cache LRU eviction — Phase 2}
|
||||
|
||||
Once a lambda crosses the threshold, its [l_compiled] slot is filled.
|
||||
To bound memory under unbounded compilation pressure, track all live
|
||||
compiled lambdas in FIFO order, and evict from the head when the count
|
||||
exceeds [jit_budget].
|
||||
|
||||
[lambda_uid_counter] mints unique identities on lambda creation; the
|
||||
LRU queue holds these IDs paired with a back-reference to the lambda
|
||||
so we can clear its [l_compiled] slot on eviction.
|
||||
|
||||
Budget of 0 = no cache (disable JIT entirely).
|
||||
Budget of [max_int] = unbounded (legacy behaviour). Default 5000 is
|
||||
a generous ceiling for any realistic page; the test harness compiles
|
||||
~3000 distinct one-shot lambdas in a full run but tiered compilation
|
||||
(Phase 1) means most never enter the cache, so steady-state count
|
||||
stays small.
|
||||
|
||||
[lambda_uid_counter] and [next_lambda_uid] are defined above
|
||||
[make_lambda] (which uses them on construction). *)
|
||||
let jit_budget = ref 5000
|
||||
let jit_evicted_count = ref 0
|
||||
|
||||
(** Live compiled lambdas in FIFO order — front is oldest, back is newest.
|
||||
Each entry is (uid, lambda); on eviction we clear lambda.l_compiled and
|
||||
drop from the queue. Using a mutable Queue rather than a hand-rolled
|
||||
linked list because eviction is amortised O(1) at the head and inserts
|
||||
are O(1) at the tail. *)
|
||||
let jit_cache_queue : (int * value) Queue.t = Queue.create ()
|
||||
let jit_cache_size () = Queue.length jit_cache_queue
|
||||
Lambda { l_params = ps; l_body = body; l_closure = unwrap_env_val closure; l_name = None; l_compiled = None }
|
||||
|
||||
let make_component name params has_children body closure affinity =
|
||||
let n = value_to_string name in
|
||||
|
||||
@@ -57,9 +57,6 @@ let () = Sx_types._convert_vm_suspension := (fun exn ->
|
||||
let jit_compile_ref : (lambda -> (string, value) Hashtbl.t -> vm_closure option) ref =
|
||||
ref (fun _ _ -> None)
|
||||
|
||||
(* JIT threshold and counters live in Sx_types so primitives can read them
|
||||
without creating a sx_primitives → sx_vm dependency cycle. *)
|
||||
|
||||
(** Sentinel closure indicating JIT compilation was attempted and failed.
|
||||
Prevents retrying compilation on every call. *)
|
||||
let jit_failed_sentinel = {
|
||||
@@ -356,29 +353,13 @@ and vm_call vm f args =
|
||||
| None ->
|
||||
if l.l_name <> None
|
||||
then begin
|
||||
l.l_call_count <- l.l_call_count + 1;
|
||||
if l.l_call_count >= !Sx_types.jit_threshold && !Sx_types.jit_budget > 0 then begin
|
||||
l.l_compiled <- Some jit_failed_sentinel;
|
||||
match !jit_compile_ref l vm.globals with
|
||||
| Some cl ->
|
||||
incr Sx_types.jit_compiled_count;
|
||||
l.l_compiled <- Some cl;
|
||||
(* Phase 2 LRU: track this compiled lambda; if cache exceeds budget,
|
||||
evict the oldest by clearing its l_compiled slot. *)
|
||||
Queue.add (l.l_uid, Lambda l) Sx_types.jit_cache_queue;
|
||||
while Queue.length Sx_types.jit_cache_queue > !Sx_types.jit_budget do
|
||||
(match Queue.pop Sx_types.jit_cache_queue with
|
||||
| (_, Lambda ev_l) -> ev_l.l_compiled <- None; incr Sx_types.jit_evicted_count
|
||||
| _ -> ())
|
||||
done;
|
||||
push_closure_frame vm cl args
|
||||
| None ->
|
||||
incr Sx_types.jit_skipped_count;
|
||||
push vm (cek_call_or_suspend vm f (List args))
|
||||
end else begin
|
||||
incr Sx_types.jit_threshold_skipped_count;
|
||||
push vm (cek_call_or_suspend vm f (List args))
|
||||
end
|
||||
end
|
||||
else
|
||||
push vm (cek_call_or_suspend vm f (List args)))
|
||||
|
||||
44
lib/fiber.sx
Normal file
44
lib/fiber.sx
Normal file
@@ -0,0 +1,44 @@
|
||||
; lib/fiber.sx — pure SX fiber library using call/cc
|
||||
;
|
||||
; A fiber is a cooperative coroutine with true suspension (no eager
|
||||
; pre-execution). Each fiber is a dict {:resume fn :done? fn}.
|
||||
;
|
||||
; make-fiber body → fiber dict
|
||||
; body = (fn (yield init-val) ...) — body receives yield + first resume val
|
||||
; yield = (fn (val) ...) — suspends fiber, returns val to resumer
|
||||
;
|
||||
; fiber-resume f v → next yielded value, or nil when body returns
|
||||
; fiber-done? f → true after body has returned
|
||||
|
||||
(define make-fiber
|
||||
(fn (body)
|
||||
(let
|
||||
((resume-k nil)
|
||||
(caller-k nil)
|
||||
(done false))
|
||||
(let
|
||||
((yield
|
||||
(fn (val)
|
||||
(call/cc
|
||||
(fn (k)
|
||||
(set! resume-k k)
|
||||
(caller-k val))))))
|
||||
{:resume
|
||||
(fn (val)
|
||||
(if
|
||||
done
|
||||
nil
|
||||
(call/cc
|
||||
(fn (k)
|
||||
(set! caller-k k)
|
||||
(if
|
||||
(nil? resume-k)
|
||||
(begin
|
||||
(body yield val)
|
||||
(set! done true)
|
||||
(k nil))
|
||||
(resume-k val))))))
|
||||
:done? (fn () done)}))))
|
||||
|
||||
(define fiber-resume (fn (f v) ((get f :resume) v)))
|
||||
(define fiber-done? (fn (f) ((get f :done?))))
|
||||
@@ -210,28 +210,6 @@
|
||||
value)
|
||||
(list (quote set!) (hs-to-sx target) value)))))))
|
||||
(true (list (quote set!) (hs-to-sx target) value)))))))
|
||||
;; Throttle/debounce extraction state — module-level so they don't get
|
||||
;; redefined on every emit-on call (which was causing JIT churn). Set
|
||||
;; via _strip-throttle-debounce at the start of each emit-on, used in
|
||||
;; the handler-build step inside scan-on.
|
||||
(define _throttle-ms nil)
|
||||
(define _debounce-ms nil)
|
||||
(define
|
||||
_strip-throttle-debounce
|
||||
(fn
|
||||
(lst)
|
||||
(cond
|
||||
((<= (len lst) 1) lst)
|
||||
((= (first lst) :throttle)
|
||||
(do
|
||||
(set! _throttle-ms (nth lst 1))
|
||||
(_strip-throttle-debounce (rest (rest lst)))))
|
||||
((= (first lst) :debounce)
|
||||
(do
|
||||
(set! _debounce-ms (nth lst 1))
|
||||
(_strip-throttle-debounce (rest (rest lst)))))
|
||||
(true
|
||||
(cons (first lst) (_strip-throttle-debounce (rest lst)))))))
|
||||
(define
|
||||
emit-on
|
||||
(fn
|
||||
@@ -240,8 +218,6 @@
|
||||
((parts (rest ast)))
|
||||
(let
|
||||
((event-name (first parts)))
|
||||
(set! _throttle-ms nil)
|
||||
(set! _debounce-ms nil)
|
||||
(define
|
||||
scan-on
|
||||
(fn
|
||||
@@ -274,13 +250,6 @@
|
||||
((wrapped-body (if catch-info (let ((var (make-symbol (nth catch-info 0))) (catch-body (hs-to-sx (nth catch-info 1)))) (if finally-info (list (quote let) (list (list (quote __hs-exc) nil) (list (quote __hs-reraise) false)) (list (quote do) (list (quote guard) (list var (list true (list (quote let) (list (list var (list (quote host-hs-normalize-exc) var))) (list (quote guard) (list (quote __inner-exc) (list true (list (quote do) (list (quote set!) (quote __hs-exc) (quote __inner-exc)) (list (quote set!) (quote __hs-reraise) true)))) catch-body)))) compiled-body) (hs-to-sx finally-info) (list (quote when) (quote __hs-reraise) (list (quote raise) (quote __hs-exc))))) (list (quote let) (list (list (quote __hs-exc) nil) (list (quote __hs-reraise) false)) (list (quote do) (list (quote guard) (list var (list true (list (quote let) (list (list var (list (quote host-hs-normalize-exc) var))) (list (quote guard) (list (quote __inner-exc) (list true (list (quote do) (list (quote set!) (quote __hs-exc) (quote __inner-exc)) (list (quote set!) (quote __hs-reraise) true)))) catch-body)))) compiled-body) (list (quote when) (quote __hs-reraise) (list (quote raise) (quote __hs-exc))))))) (if finally-info (list (quote do) compiled-body (hs-to-sx finally-info)) compiled-body))))
|
||||
(let
|
||||
((handler (let ((uses-the-result? (fn (expr) (cond ((= expr (quote the-result)) true) ((list? expr) (some (fn (x) (uses-the-result? x)) expr)) (true false))))) (let ((base-handler (list (quote fn) (list (quote event)) (if (uses-the-result? wrapped-body) (list (quote let) (list (list (quote the-result) nil)) wrapped-body) wrapped-body)))) (if count-filter-info (let ((mn (get count-filter-info "min")) (mx (get count-filter-info "max"))) (list (quote let) (list (list (quote __hs-count) 0)) (list (quote fn) (list (quote event)) (list (quote begin) (list (quote set!) (quote __hs-count) (list (quote +) (quote __hs-count) 1)) (list (quote when) (if (= mx -1) (list (quote >=) (quote __hs-count) mn) (list (quote and) (list (quote >=) (quote __hs-count) mn) (list (quote <=) (quote __hs-count) mx))) (nth base-handler 2)))))) base-handler)))))
|
||||
(let
|
||||
((handler (cond
|
||||
(_throttle-ms
|
||||
(list (quote hs-throttle!) handler (hs-to-sx _throttle-ms)))
|
||||
(_debounce-ms
|
||||
(list (quote hs-debounce!) handler (hs-to-sx _debounce-ms)))
|
||||
(true handler))))
|
||||
(let
|
||||
((on-call (if every? (list (quote hs-on-every) target event-name handler) (list (quote hs-on) target event-name handler))))
|
||||
(cond
|
||||
@@ -340,7 +309,7 @@
|
||||
(first pair)
|
||||
handler))
|
||||
or-sources)))
|
||||
on-call))))))))))))))
|
||||
on-call)))))))))))))
|
||||
((= (first items) :from)
|
||||
(scan-on
|
||||
(rest (rest items))
|
||||
@@ -484,7 +453,7 @@
|
||||
count-filter-info
|
||||
elsewhere?
|
||||
or-sources)))))
|
||||
(scan-on (_strip-throttle-debounce (rest parts)) nil nil false nil nil nil nil nil false nil)))))
|
||||
(scan-on (rest parts) nil nil false nil nil nil nil nil false nil)))))
|
||||
(define
|
||||
emit-send
|
||||
(fn
|
||||
@@ -2489,15 +2458,6 @@
|
||||
(quote fn)
|
||||
(list (quote it))
|
||||
(hs-to-sx body))))
|
||||
((and (list? expr) (= (first expr) (quote attr)))
|
||||
(list
|
||||
(quote hs-attr-watch!)
|
||||
(hs-to-sx (nth expr 2))
|
||||
(nth expr 1)
|
||||
(list
|
||||
(quote fn)
|
||||
(list (quote it))
|
||||
(hs-to-sx body))))
|
||||
(true nil))))
|
||||
((= head (quote init))
|
||||
(list
|
||||
|
||||
@@ -1347,17 +1347,7 @@
|
||||
cls
|
||||
(first extra-classes)
|
||||
tgt))
|
||||
((and
|
||||
(= (tp-type) "keyword") (= (tp-val) "for")
|
||||
;; Only consume 'for' as a duration clause if the next
|
||||
;; token is NOT '<ident> in ...' — that pattern is a
|
||||
;; for-in loop, not a toggle duration.
|
||||
(not
|
||||
(and
|
||||
(> (len tokens) (+ p 2))
|
||||
(= (get (nth tokens (+ p 1)) "type") "ident")
|
||||
(= (get (nth tokens (+ p 2)) "value") "in")))
|
||||
(do (adv!) true))
|
||||
((match-kw "for")
|
||||
(let
|
||||
((dur (parse-expr)))
|
||||
(list (quote toggle-class-for) cls tgt dur)))
|
||||
@@ -3089,17 +3079,7 @@
|
||||
(= (tp-val) "queue"))
|
||||
(do (adv!) (adv!)))
|
||||
(let
|
||||
((every? (match-kw "every"))
|
||||
(throttle-ms nil)
|
||||
(debounce-ms nil))
|
||||
;; 'throttled at <duration>' / 'debounced at <duration>'
|
||||
;; — parsed as handler modifiers, captured as :throttle / :debounce parts.
|
||||
(when (and (= (tp-type) "ident") (= (tp-val) "throttled"))
|
||||
(adv!)
|
||||
(when (match-kw "at") (set! throttle-ms (parse-expr))))
|
||||
(when (and (= (tp-type) "ident") (= (tp-val) "debounced"))
|
||||
(adv!)
|
||||
(when (match-kw "at") (set! debounce-ms (parse-expr))))
|
||||
((every? (match-kw "every")))
|
||||
(let
|
||||
((having (if (or h-margin h-threshold) (dict "margin" h-margin "threshold" h-threshold) nil)))
|
||||
(let
|
||||
@@ -3114,10 +3094,6 @@
|
||||
(match-kw "end")
|
||||
(let
|
||||
((parts (list (quote on) event-name)))
|
||||
(let
|
||||
((parts (if throttle-ms (append parts (list :throttle throttle-ms)) parts)))
|
||||
(let
|
||||
((parts (if debounce-ms (append parts (list :debounce debounce-ms)) parts)))
|
||||
(let
|
||||
((parts (if every? (append parts (list :every true)) parts)))
|
||||
(let
|
||||
@@ -3140,7 +3116,7 @@
|
||||
((parts (if finally-clause (append parts (list :finally finally-clause)) parts)))
|
||||
(let
|
||||
((parts (append parts (list (if (> (len event-vars) 0) (cons (quote do) (append (map (fn (nm) (list (quote ref) nm)) event-vars) (if (and (list? body) (= (first body) (quote do))) (rest body) (list body)))) body)))))
|
||||
parts))))))))))))))))))))))))))))
|
||||
parts))))))))))))))))))))))))))
|
||||
(define
|
||||
parse-init-feat
|
||||
(fn
|
||||
@@ -3190,7 +3166,6 @@
|
||||
(or
|
||||
(= (tp-type) "hat")
|
||||
(= (tp-type) "local")
|
||||
(= (tp-type) "attr")
|
||||
(and (= (tp-type) "keyword") (= (tp-val) "dom")))
|
||||
(let
|
||||
((expr (parse-expr)))
|
||||
|
||||
@@ -12,29 +12,6 @@
|
||||
|
||||
;; Register an event listener. Returns unlisten function.
|
||||
;; (hs-on target event-name handler) → unlisten-fn
|
||||
(begin
|
||||
(define _hs-config-log-all false)
|
||||
(define _hs-log-captured (list))
|
||||
(define
|
||||
hs-set-log-all!
|
||||
(fn (flag) (set! _hs-config-log-all (if flag true false))))
|
||||
(define hs-get-log-captured (fn () _hs-log-captured))
|
||||
(define
|
||||
hs-clear-log-captured!
|
||||
(fn () (begin (set! _hs-log-captured (list)) nil)))
|
||||
(define
|
||||
hs-log-event!
|
||||
(fn
|
||||
(msg)
|
||||
(when
|
||||
_hs-config-log-all
|
||||
(begin
|
||||
(set! _hs-log-captured (append _hs-log-captured (list msg)))
|
||||
(host-call (host-global "console") "log" msg)
|
||||
nil)))))
|
||||
|
||||
;; Run an initializer function immediately.
|
||||
;; (hs-init thunk) — called at element boot time
|
||||
(define
|
||||
hs-each
|
||||
(fn
|
||||
@@ -45,52 +22,17 @@
|
||||
;; (hs-init thunk) — called at element boot time
|
||||
(define meta (host-new "Object"))
|
||||
|
||||
;; Run an initializer function immediately.
|
||||
;; (hs-init thunk) — called at element boot time
|
||||
(define
|
||||
hs-on-every
|
||||
(fn (target event-name handler) (dom-listen target event-name handler)))
|
||||
|
||||
;; ── Async / timing ──────────────────────────────────────────────
|
||||
|
||||
;; Wait for a duration in milliseconds.
|
||||
;; In hyperscript, wait is async-transparent — execution pauses.
|
||||
;; Here we use perform/IO suspension for true pause semantics.
|
||||
(define
|
||||
hs-on-every
|
||||
(fn (target event-name handler) (dom-listen target event-name handler)))
|
||||
|
||||
;; Throttle: drops events that arrive within the window. First event fires
|
||||
;; immediately; subsequent events within `ms` of the previous fire are dropped.
|
||||
;; Returns a wrapped handler suitable for hs-on / hs-on-every.
|
||||
(define
|
||||
hs-throttle!
|
||||
(fn
|
||||
(handler ms)
|
||||
(let
|
||||
((__hs-last-fire 0))
|
||||
(fn
|
||||
(event)
|
||||
(let
|
||||
((__hs-now (host-call (host-global "Date") "now")))
|
||||
(when
|
||||
(>= (- __hs-now __hs-last-fire) ms)
|
||||
(set! __hs-last-fire __hs-now)
|
||||
(handler event)))))))
|
||||
|
||||
;; Debounce: waits until `ms` has elapsed since the last event before firing.
|
||||
;; In our synchronous test mock no time passes, so the timer fires immediately
|
||||
;; via setTimeout(_, 0); the wrapped handler still gets called once per burst.
|
||||
(define
|
||||
hs-debounce!
|
||||
(fn
|
||||
(handler ms)
|
||||
(let
|
||||
((__hs-timer nil))
|
||||
(fn
|
||||
(event)
|
||||
(when __hs-timer (host-call (host-global "window") "clearTimeout" __hs-timer))
|
||||
(set! __hs-timer
|
||||
(host-call (host-global "window") "setTimeout"
|
||||
(host-new-function (list "ev") "return arguments[0](arguments[1]);")
|
||||
ms handler event))))))
|
||||
|
||||
;; Wait for a DOM event on a target.
|
||||
;; (hs-wait-for target event-name) — suspends until event fires
|
||||
(define
|
||||
_hs-on-caller
|
||||
(let
|
||||
@@ -103,7 +45,8 @@
|
||||
(host-set! _ctx "meta" _m)
|
||||
_ctx)))
|
||||
|
||||
;; Wait for CSS transitions/animations to settle on an element.
|
||||
;; Wait for a DOM event on a target.
|
||||
;; (hs-wait-for target event-name) — suspends until event fires
|
||||
(define
|
||||
hs-on
|
||||
(fn
|
||||
@@ -123,14 +66,14 @@
|
||||
(append prev (list unlisten)))
|
||||
unlisten))))))
|
||||
|
||||
;; ── Class manipulation ──────────────────────────────────────────
|
||||
|
||||
;; Toggle a single class on an element.
|
||||
;; Wait for CSS transitions/animations to settle on an element.
|
||||
(define
|
||||
hs-on-every
|
||||
(fn (target event-name handler) (dom-listen target event-name handler)))
|
||||
|
||||
;; Toggle between two classes — exactly one is active at a time.
|
||||
;; ── Class manipulation ──────────────────────────────────────────
|
||||
|
||||
;; Toggle a single class on an element.
|
||||
(define
|
||||
hs-on-intersection-attach!
|
||||
(fn
|
||||
@@ -146,8 +89,7 @@
|
||||
(host-call observer "observe" target)
|
||||
observer)))))
|
||||
|
||||
;; Take a class from siblings — add to target, remove from others.
|
||||
;; (hs-take! target cls) — like radio button class behavior
|
||||
;; Toggle between two classes — exactly one is active at a time.
|
||||
(define
|
||||
hs-on-mutation-attach!
|
||||
(fn
|
||||
@@ -168,18 +110,19 @@
|
||||
(host-call observer "observe" target opts)
|
||||
observer))))))
|
||||
|
||||
;; Take a class from siblings — add to target, remove from others.
|
||||
;; (hs-take! target cls) — like radio button class behavior
|
||||
(define hs-init (fn (thunk) (thunk)))
|
||||
|
||||
;; ── DOM insertion ───────────────────────────────────────────────
|
||||
|
||||
;; Put content at a position relative to a target.
|
||||
;; pos: "into" | "before" | "after"
|
||||
(define hs-init (fn (thunk) (thunk)))
|
||||
(define hs-wait (fn (ms) (perform (list (quote io-sleep) ms))))
|
||||
|
||||
;; ── Navigation / traversal ──────────────────────────────────────
|
||||
|
||||
;; Navigate to a URL.
|
||||
(define hs-wait (fn (ms) (perform (list (quote io-sleep) ms))))
|
||||
|
||||
;; Find next sibling matching a selector (or any sibling).
|
||||
(begin
|
||||
(define
|
||||
hs-wait-for
|
||||
@@ -192,7 +135,7 @@
|
||||
(target event-name timeout-ms)
|
||||
(perform (list (quote io-wait-event) target event-name timeout-ms)))))
|
||||
|
||||
;; Find previous sibling matching a selector.
|
||||
;; Find next sibling matching a selector (or any sibling).
|
||||
(define
|
||||
hs-settle
|
||||
(fn
|
||||
@@ -200,7 +143,7 @@
|
||||
(hs-null-raise! target)
|
||||
(when (not (nil? target)) (perform (list (quote io-settle) target)))))
|
||||
|
||||
;; First element matching selector within a scope.
|
||||
;; Find previous sibling matching a selector.
|
||||
(define
|
||||
hs-toggle-class!
|
||||
(fn
|
||||
@@ -210,7 +153,7 @@
|
||||
(not (nil? target))
|
||||
(host-call (host-get target "classList") "toggle" cls))))
|
||||
|
||||
;; Last element matching selector.
|
||||
;; First element matching selector within a scope.
|
||||
(define
|
||||
hs-toggle-var-cycle!
|
||||
(fn
|
||||
@@ -232,7 +175,7 @@
|
||||
var-name
|
||||
(if (= idx -1) (first values) (nth values (mod (+ idx 1) n))))))))
|
||||
|
||||
;; First/last within a specific scope.
|
||||
;; Last element matching selector.
|
||||
(define
|
||||
hs-toggle-between!
|
||||
(fn
|
||||
@@ -245,6 +188,7 @@
|
||||
(do (dom-remove-class target cls1) (dom-add-class target cls2))
|
||||
(do (dom-remove-class target cls2) (dom-add-class target cls1))))))
|
||||
|
||||
;; First/last within a specific scope.
|
||||
(define
|
||||
hs-toggle-style!
|
||||
(fn
|
||||
@@ -268,9 +212,6 @@
|
||||
(dom-set-style target prop "hidden")
|
||||
(dom-set-style target prop "")))))))
|
||||
|
||||
;; ── Iteration ───────────────────────────────────────────────────
|
||||
|
||||
;; Repeat a thunk N times.
|
||||
(define
|
||||
hs-toggle-style-between!
|
||||
(fn
|
||||
@@ -282,7 +223,9 @@
|
||||
(dom-set-style target prop val2)
|
||||
(dom-set-style target prop val1)))))
|
||||
|
||||
;; Repeat forever (until break — relies on exception/continuation).
|
||||
;; ── Iteration ───────────────────────────────────────────────────
|
||||
|
||||
;; Repeat a thunk N times.
|
||||
(define
|
||||
hs-toggle-style-cycle!
|
||||
(fn
|
||||
@@ -303,10 +246,7 @@
|
||||
(true (find-next (rest remaining))))))
|
||||
(dom-set-style target prop (find-next vals)))))
|
||||
|
||||
;; ── Fetch ───────────────────────────────────────────────────────
|
||||
|
||||
;; Fetch a URL, parse response according to format.
|
||||
;; (hs-fetch url format) — format is "json" | "text" | "html"
|
||||
;; Repeat forever (until break — relies on exception/continuation).
|
||||
(define
|
||||
hs-take!
|
||||
(fn
|
||||
@@ -329,7 +269,8 @@
|
||||
(when with-cls (dom-remove-class target with-cls))))
|
||||
(let
|
||||
((attr-val (if (> (len extra) 0) (first extra) nil))
|
||||
(with-val (if (> (len extra) 1) (nth extra 1) nil)))
|
||||
(with-val
|
||||
(if (> (len extra) 1) (nth extra 1) nil)))
|
||||
(do
|
||||
(for-each
|
||||
(fn
|
||||
@@ -346,10 +287,10 @@
|
||||
(dom-set-attr target name attr-val)
|
||||
(dom-set-attr target name ""))))))))
|
||||
|
||||
;; ── Type coercion ───────────────────────────────────────────────
|
||||
;; ── Fetch ───────────────────────────────────────────────────────
|
||||
|
||||
;; Coerce a value to a type by name.
|
||||
;; (hs-coerce value type-name) — type-name is "Int", "Float", "String", etc.
|
||||
;; Fetch a URL, parse response according to format.
|
||||
;; (hs-fetch url format) — format is "json" | "text" | "html"
|
||||
(begin
|
||||
(define
|
||||
hs-element?
|
||||
@@ -506,10 +447,10 @@
|
||||
(dom-insert-adjacent-html target "beforeend" value)
|
||||
(hs-boot-subtree! target)))))))))))
|
||||
|
||||
;; ── Object creation ─────────────────────────────────────────────
|
||||
;; ── Type coercion ───────────────────────────────────────────────
|
||||
|
||||
;; Make a new object of a given type.
|
||||
;; (hs-make type-name) — creates empty object/collection
|
||||
;; Coerce a value to a type by name.
|
||||
;; (hs-coerce value type-name) — type-name is "Int", "Float", "String", etc.
|
||||
(define
|
||||
hs-add-to!
|
||||
(fn
|
||||
@@ -523,11 +464,10 @@
|
||||
((hs-is-set? target) (do (host-call target "add" value) target))
|
||||
(true (do (host-call target "push" value) target)))))
|
||||
|
||||
;; ── Behavior installation ───────────────────────────────────────
|
||||
;; ── Object creation ─────────────────────────────────────────────
|
||||
|
||||
;; Install a behavior on an element.
|
||||
;; A behavior is a function that takes (me ...params) and sets up features.
|
||||
;; (hs-install behavior-fn me ...args)
|
||||
;; Make a new object of a given type.
|
||||
;; (hs-make type-name) — creates empty object/collection
|
||||
(define
|
||||
hs-remove-from!
|
||||
(fn
|
||||
@@ -537,10 +477,11 @@
|
||||
((hs-is-set? target) (do (host-call target "delete" value) target))
|
||||
(true (host-call target "splice" (host-call target "indexOf" value) 1)))))
|
||||
|
||||
;; ── Measurement ─────────────────────────────────────────────────
|
||||
;; ── Behavior installation ───────────────────────────────────────
|
||||
|
||||
;; Measure an element's bounding rect, store as local variables.
|
||||
;; Returns a dict with x, y, width, height, top, left, right, bottom.
|
||||
;; Install a behavior on an element.
|
||||
;; A behavior is a function that takes (me ...params) and sets up features.
|
||||
;; (hs-install behavior-fn me ...args)
|
||||
(define
|
||||
hs-splice-at!
|
||||
(fn
|
||||
@@ -553,7 +494,10 @@
|
||||
((i (if (< idx 0) (+ n idx) idx)))
|
||||
(cond
|
||||
((or (< i 0) (>= i n)) target)
|
||||
(true (concat (slice target 0 i) (slice target (+ i 1) n))))))
|
||||
(true
|
||||
(concat
|
||||
(slice target 0 i)
|
||||
(slice target (+ i 1) n))))))
|
||||
(do
|
||||
(when
|
||||
target
|
||||
@@ -564,10 +508,10 @@
|
||||
(host-call target "splice" i 1))))
|
||||
target))))
|
||||
|
||||
;; Return the current text selection as a string. In the browser this is
|
||||
;; `window.getSelection().toString()`. In the mock test runner, a test
|
||||
;; setup stashes the desired selection text at `window.__test_selection`
|
||||
;; and the fallback path returns that so tests can assert on the result.
|
||||
;; ── Measurement ─────────────────────────────────────────────────
|
||||
|
||||
;; Measure an element's bounding rect, store as local variables.
|
||||
;; Returns a dict with x, y, width, height, top, left, right, bottom.
|
||||
(define
|
||||
hs-index
|
||||
(fn
|
||||
@@ -579,11 +523,10 @@
|
||||
((string? obj) (nth obj key))
|
||||
(true (host-get obj key)))))
|
||||
|
||||
|
||||
;; ── Transition ──────────────────────────────────────────────────
|
||||
|
||||
;; Transition a CSS property to a value, optionally with duration.
|
||||
;; (hs-transition target prop value duration)
|
||||
;; Return the current text selection as a string. In the browser this is
|
||||
;; `window.getSelection().toString()`. In the mock test runner, a test
|
||||
;; setup stashes the desired selection text at `window.__test_selection`
|
||||
;; and the fallback path returns that so tests can assert on the result.
|
||||
(define
|
||||
hs-put-at!
|
||||
(fn
|
||||
@@ -605,6 +548,11 @@
|
||||
((= pos "start") (host-call target "unshift" value)))
|
||||
target)))))))
|
||||
|
||||
|
||||
;; ── Transition ──────────────────────────────────────────────────
|
||||
|
||||
;; Transition a CSS property to a value, optionally with duration.
|
||||
;; (hs-transition target prop value duration)
|
||||
(define
|
||||
hs-dict-without
|
||||
(fn
|
||||
@@ -641,11 +589,6 @@
|
||||
((w (host-global "window")))
|
||||
(if w (host-call w "prompt" msg) nil))))
|
||||
|
||||
|
||||
;; ── Transition ──────────────────────────────────────────────────
|
||||
|
||||
;; Transition a CSS property to a value, optionally with duration.
|
||||
;; (hs-transition target prop value duration)
|
||||
(define
|
||||
hs-answer
|
||||
(fn
|
||||
@@ -654,6 +597,11 @@
|
||||
((w (host-global "window")))
|
||||
(if w (if (host-call w "confirm" msg) yes-val no-val) no-val))))
|
||||
|
||||
|
||||
;; ── Transition ──────────────────────────────────────────────────
|
||||
|
||||
;; Transition a CSS property to a value, optionally with duration.
|
||||
;; (hs-transition target prop value duration)
|
||||
(define
|
||||
hs-answer-alert
|
||||
(fn
|
||||
@@ -714,10 +662,6 @@
|
||||
(if (nil? sel) "" (host-call sel "toString" (list))))
|
||||
stash)))))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
(define
|
||||
hs-reset!
|
||||
(fn
|
||||
@@ -764,6 +708,10 @@
|
||||
(when default-val (dom-set-prop target "value" default-val)))))
|
||||
(true nil)))))))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
(define
|
||||
hs-next
|
||||
(fn
|
||||
@@ -782,8 +730,7 @@
|
||||
((dom-matches? el sel) el)
|
||||
(true (find-next (dom-next-sibling el))))))
|
||||
(find-next sibling)))))
|
||||
;; ── Sandbox/test runtime additions ──────────────────────────────
|
||||
;; Property access — dot notation and .length
|
||||
|
||||
(define
|
||||
hs-previous
|
||||
(fn
|
||||
@@ -802,9 +749,10 @@
|
||||
((dom-matches? el sel) el)
|
||||
(true (find-prev (dom-get-prop el "previousElementSibling"))))))
|
||||
(find-prev sibling)))))
|
||||
;; DOM query stub — sandbox returns empty list
|
||||
;; ── Sandbox/test runtime additions ──────────────────────────────
|
||||
;; Property access — dot notation and .length
|
||||
(define _hs-last-query-sel nil)
|
||||
;; Method dispatch — obj.method(args)
|
||||
;; DOM query stub — sandbox returns empty list
|
||||
(define
|
||||
hs-null-raise!
|
||||
(fn
|
||||
@@ -815,9 +763,7 @@
|
||||
((msg (str "'" (or (host-get (host-global "window") "_hs_last_query_sel") "target") "' is null")))
|
||||
(host-set! (host-global "window") "_hs_null_error" msg)
|
||||
(guard (_null-e (true nil)) (raise msg))))))
|
||||
|
||||
;; ── 0.9.90 features ─────────────────────────────────────────────
|
||||
;; beep! — debug logging, returns value unchanged
|
||||
;; Method dispatch — obj.method(args)
|
||||
(define
|
||||
hs-empty-raise!
|
||||
(fn
|
||||
@@ -831,7 +777,9 @@
|
||||
((msg (str "'" (or (host-get (host-global "window") "_hs_last_query_sel") "target") "' is null")))
|
||||
(host-set! (host-global "window") "_hs_null_error" msg)
|
||||
(guard (_null-e (true nil)) (raise msg))))))
|
||||
;; Property-based is — check obj.key truthiness
|
||||
|
||||
;; ── 0.9.90 features ─────────────────────────────────────────────
|
||||
;; beep! — debug logging, returns value unchanged
|
||||
(define
|
||||
hs-query-all-checked
|
||||
(fn
|
||||
@@ -839,14 +787,14 @@
|
||||
(let
|
||||
((result (hs-query-all sel)))
|
||||
(do (hs-empty-raise! result) result))))
|
||||
;; Array slicing (inclusive both ends)
|
||||
;; Property-based is — check obj.key truthiness
|
||||
(define
|
||||
hs-dispatch!
|
||||
(fn
|
||||
(target event detail)
|
||||
(hs-null-raise! target)
|
||||
(when (not (nil? target)) (dom-dispatch target event detail))))
|
||||
;; Collection: sorted by
|
||||
;; Array slicing (inclusive both ends)
|
||||
(define
|
||||
hs-query-all
|
||||
(fn
|
||||
@@ -854,7 +802,7 @@
|
||||
(do
|
||||
(host-set! (host-global "window") "_hs_last_query_sel" sel)
|
||||
(dom-query-all (dom-document) sel))))
|
||||
;; Collection: sorted by descending
|
||||
;; Collection: sorted by
|
||||
(define
|
||||
hs-query-all-in
|
||||
(fn
|
||||
@@ -863,17 +811,17 @@
|
||||
(nil? target)
|
||||
(hs-query-all sel)
|
||||
(host-call target "querySelectorAll" sel))))
|
||||
;; Collection: split by
|
||||
;; Collection: sorted by descending
|
||||
(define
|
||||
hs-list-set
|
||||
(fn
|
||||
(lst idx val)
|
||||
(append (take lst idx) (cons val (drop lst (+ idx 1))))))
|
||||
;; Collection: joined by
|
||||
;; Collection: split by
|
||||
(define
|
||||
hs-to-number
|
||||
(fn (v) (if (number? v) v (or (parse-number (str v)) 0))))
|
||||
|
||||
;; Collection: joined by
|
||||
(define
|
||||
hs-query-first
|
||||
(fn
|
||||
@@ -1003,7 +951,7 @@
|
||||
((= (str ex) "hs-continue") (do-loop (rest remaining)))
|
||||
(true (raise ex))))))))
|
||||
(do-loop items))))
|
||||
;; Collection: joined by
|
||||
|
||||
(begin
|
||||
(define
|
||||
hs-append
|
||||
@@ -1044,7 +992,7 @@
|
||||
(host-get value "outerHTML")
|
||||
(str value))))
|
||||
(true nil)))))
|
||||
|
||||
;; Collection: joined by
|
||||
(define
|
||||
hs-sender
|
||||
(fn
|
||||
@@ -1136,7 +1084,6 @@
|
||||
(hs-host-to-sx (perform (list "io-parse-json" raw))))
|
||||
((= fmt "number")
|
||||
(hs-to-number (perform (list "io-parse-text" raw))))
|
||||
((= fmt "html") (perform (list "io-parse-html" raw)))
|
||||
(true (perform (list "io-parse-text" raw)))))))))
|
||||
|
||||
(define hs-fetch (fn (url format) (hs-fetch-impl url format false)))
|
||||
@@ -1676,10 +1623,14 @@
|
||||
((ch (substring sel i (+ i 1))))
|
||||
(cond
|
||||
((= ch ".")
|
||||
(do (flush!) (set! mode "class") (walk (+ i 1))))
|
||||
(do
|
||||
(flush!)
|
||||
(set! mode "class")
|
||||
(walk (+ i 1))))
|
||||
((= ch "#")
|
||||
(do (flush!) (set! mode "id") (walk (+ i 1))))
|
||||
(true (do (set! cur (str cur ch)) (walk (+ i 1)))))))))
|
||||
(true
|
||||
(do (set! cur (str cur ch)) (walk (+ i 1)))))))))
|
||||
(walk 0)
|
||||
(flush!)
|
||||
{:tag tag :classes classes :id id}))))
|
||||
@@ -1773,11 +1724,11 @@
|
||||
(value type-name)
|
||||
(if (nil? value) false (hs-type-check value type-name))))
|
||||
|
||||
|
||||
(define
|
||||
hs-strict-eq
|
||||
(fn (a b) (and (= (type-of a) (type-of b)) (= a b))))
|
||||
|
||||
|
||||
(define
|
||||
hs-id=
|
||||
(fn
|
||||
@@ -1809,20 +1760,6 @@
|
||||
((nil? suffix) false)
|
||||
(true (ends-with? (str s) (str suffix))))))
|
||||
|
||||
(define
|
||||
hs-attr-watch!
|
||||
(fn
|
||||
(target attr-name handler)
|
||||
(let
|
||||
((mo-class (host-get (host-global "window") "MutationObserver")))
|
||||
(when
|
||||
mo-class
|
||||
(let
|
||||
((cb (fn (records observer) (for-each (fn (rec) (when (= (host-get rec "attributeName") attr-name) (handler (host-call target "getAttribute" attr-name)))) records))))
|
||||
(let
|
||||
((mo (host-new "MutationObserver" cb)))
|
||||
(host-call mo "observe" target {:attributeFilter (list attr-name) :attributes true})))))))
|
||||
|
||||
(define
|
||||
hs-scoped-set!
|
||||
(fn
|
||||
@@ -1868,7 +1805,10 @@
|
||||
((and (dict? a) (dict? b))
|
||||
(let
|
||||
((pos (host-call a "compareDocumentPosition" b)))
|
||||
(if (number? pos) (not (= 0 (mod (/ pos 4) 2))) false)))
|
||||
(if
|
||||
(number? pos)
|
||||
(not (= 0 (mod (/ pos 4) 2)))
|
||||
false)))
|
||||
(true (< (str a) (str b))))))
|
||||
|
||||
(define
|
||||
@@ -1989,7 +1929,10 @@
|
||||
((and (dict? a) (dict? b))
|
||||
(let
|
||||
((pos (host-call a "compareDocumentPosition" b)))
|
||||
(if (number? pos) (not (= 0 (mod (/ pos 4) 2))) false)))
|
||||
(if
|
||||
(number? pos)
|
||||
(not (= 0 (mod (/ pos 4) 2)))
|
||||
false)))
|
||||
(true (< (str a) (str b))))))
|
||||
|
||||
(define
|
||||
@@ -2042,7 +1985,9 @@
|
||||
|
||||
(define
|
||||
hs-morph-char
|
||||
(fn (s p) (if (or (< p 0) (>= p (string-length s))) nil (nth s p))))
|
||||
(fn
|
||||
(s p)
|
||||
(if (or (< p 0) (>= p (string-length s))) nil (nth s p))))
|
||||
|
||||
(define
|
||||
hs-morph-index-from
|
||||
@@ -2070,7 +2015,10 @@
|
||||
(q)
|
||||
(let
|
||||
((c (hs-morph-char s q)))
|
||||
(if (and c (< (index-of stop c) 0)) (loop (+ q 1)) q))))
|
||||
(if
|
||||
(and c (< (index-of stop c) 0))
|
||||
(loop (+ q 1))
|
||||
q))))
|
||||
(let ((e (loop p))) (list (substring s p e) e))))
|
||||
|
||||
(define
|
||||
@@ -2112,7 +2060,9 @@
|
||||
(append
|
||||
acc
|
||||
(list
|
||||
(list name (substring s (+ p4 1) close)))))))
|
||||
(list
|
||||
name
|
||||
(substring s (+ p4 1) close)))))))
|
||||
((= c2 "'")
|
||||
(let
|
||||
((close (hs-morph-index-from s "'" (+ p4 1))))
|
||||
@@ -2122,7 +2072,9 @@
|
||||
(append
|
||||
acc
|
||||
(list
|
||||
(list name (substring s (+ p4 1) close)))))))
|
||||
(list
|
||||
name
|
||||
(substring s (+ p4 1) close)))))))
|
||||
(true
|
||||
(let
|
||||
((r2 (hs-morph-read-until s p4 " \t\n/>")))
|
||||
@@ -2206,7 +2158,9 @@
|
||||
(for-each
|
||||
(fn
|
||||
(c)
|
||||
(when (> (string-length c) 0) (dom-add-class el c)))
|
||||
(when
|
||||
(> (string-length c) 0)
|
||||
(dom-add-class el c)))
|
||||
(split v " ")))
|
||||
((and keep-id (= n "id")) nil)
|
||||
(true (dom-set-attr el n v)))))
|
||||
@@ -2307,7 +2261,8 @@
|
||||
((parts (split resolved ":")))
|
||||
(let
|
||||
((prop (first parts))
|
||||
(val (if (> (len parts) 1) (nth parts 1) nil)))
|
||||
(val
|
||||
(if (> (len parts) 1) (nth parts 1) nil)))
|
||||
(cond
|
||||
((and (not (= prop "display")) (not (= prop "opacity")) (not (= prop "visibility")) (not (= prop "hidden")) (not (= prop "class-hidden")) (not (= prop "class-invisible")) (not (= prop "class-opacity")) (not (= prop "details")) (not (= prop "dialog")) (dict-has? _hs-hide-strategies prop))
|
||||
(let
|
||||
@@ -2347,7 +2302,8 @@
|
||||
((parts (split resolved ":")))
|
||||
(let
|
||||
((prop (first parts))
|
||||
(val (if (> (len parts) 1) (nth parts 1) nil)))
|
||||
(val
|
||||
(if (> (len parts) 1) (nth parts 1) nil)))
|
||||
(cond
|
||||
((and (not (= prop "display")) (not (= prop "opacity")) (not (= prop "visibility")) (not (= prop "hidden")) (not (= prop "class-hidden")) (not (= prop "class-invisible")) (not (= prop "class-opacity")) (not (= prop "details")) (not (= prop "dialog")) (dict-has? _hs-hide-strategies prop))
|
||||
(let
|
||||
@@ -2452,10 +2408,14 @@
|
||||
(if
|
||||
(= depth 1)
|
||||
j
|
||||
(find-close (+ j 1) (- depth 1)))
|
||||
(find-close
|
||||
(+ j 1)
|
||||
(- depth 1)))
|
||||
(if
|
||||
(= (nth raw j) "{")
|
||||
(find-close (+ j 1) (+ depth 1))
|
||||
(find-close
|
||||
(+ j 1)
|
||||
(+ depth 1))
|
||||
(find-close (+ j 1) depth))))))
|
||||
(let
|
||||
((close (find-close start 1)))
|
||||
@@ -2566,7 +2526,10 @@
|
||||
(if
|
||||
(= (len lst) 0)
|
||||
-1
|
||||
(if (= (first lst) item) i (idx-loop (rest lst) (+ i 1))))))
|
||||
(if
|
||||
(= (first lst) item)
|
||||
i
|
||||
(idx-loop (rest lst) (+ i 1))))))
|
||||
(idx-loop obj 0)))
|
||||
(true
|
||||
(let
|
||||
@@ -2658,7 +2621,8 @@
|
||||
(cond
|
||||
((= end "hs-pick-end") n)
|
||||
((= end "hs-pick-start") 0)
|
||||
((and (number? end) (< end 0)) (max 0 (+ n end)))
|
||||
((and (number? end) (< end 0))
|
||||
(max 0 (+ n end)))
|
||||
(true end))))
|
||||
(cond
|
||||
((string? col) (slice col s e))
|
||||
@@ -2838,8 +2802,6 @@
|
||||
hs-sorted-by-desc
|
||||
(fn (col key-fn) (reverse (hs-sorted-by col key-fn))))
|
||||
|
||||
;; ── SourceInfo API ────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
hs-dom-has-var?
|
||||
(fn
|
||||
@@ -2859,6 +2821,8 @@
|
||||
((store (host-get el "__hs_vars")))
|
||||
(if (nil? store) nil (host-get store name)))))
|
||||
|
||||
;; ── SourceInfo API ────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
hs-dom-set-var-raw!
|
||||
(fn
|
||||
@@ -2962,12 +2926,7 @@
|
||||
|
||||
(define
|
||||
hs-null-error!
|
||||
(fn
|
||||
(selector)
|
||||
(let
|
||||
((msg (str "'" selector "' is null")))
|
||||
(host-set! (host-global "window") "_hs_null_error" msg)
|
||||
(guard (_null-e (true nil)) (raise msg)))))
|
||||
(fn (selector) (raise (str "'" selector "' is null"))))
|
||||
|
||||
(define
|
||||
hs-named-target
|
||||
@@ -2987,7 +2946,9 @@
|
||||
((results (hs-query-all selector)))
|
||||
(if
|
||||
(and
|
||||
(or (nil? results) (and (list? results) (= (len results) 0)))
|
||||
(or
|
||||
(nil? results)
|
||||
(and (list? results) (= (len results) 0)))
|
||||
(string? selector)
|
||||
(> (len selector) 0)
|
||||
(= (substring selector 0 1) "#"))
|
||||
|
||||
@@ -814,229 +814,3 @@
|
||||
(scan-template!)
|
||||
(t-emit! "eof" nil)
|
||||
tokens)))
|
||||
|
||||
;; ── Stream wrapper for upstream-style stateful tokenizer API ───────────────
|
||||
;;
|
||||
;; Upstream _hyperscript exposes a Tokens object with cursor + follow-set
|
||||
;; semantics on _hyperscript.internals.tokenizer. Our hs-tokenize returns a
|
||||
;; flat list; the stream wrapper adds the stateful operations.
|
||||
;;
|
||||
;; Type names map ours → upstream's (e.g. "ident" → "IDENTIFIER").
|
||||
|
||||
(define
|
||||
hs-stream-type-map
|
||||
(fn
|
||||
(t)
|
||||
(cond
|
||||
((= t "ident") "IDENTIFIER")
|
||||
((= t "number") "NUMBER")
|
||||
((= t "string") "STRING")
|
||||
((= t "class") "CLASS_REF")
|
||||
((= t "id") "ID_REF")
|
||||
((= t "attr") "ATTRIBUTE_REF")
|
||||
((= t "style") "STYLE_REF")
|
||||
((= t "whitespace") "WHITESPACE")
|
||||
((= t "op") "OPERATOR")
|
||||
((= t "eof") "EOF")
|
||||
(true (upcase t)))))
|
||||
|
||||
;; Create a stream from a source string.
|
||||
;; Returns a dict — mutable via dict-set!.
|
||||
(define
|
||||
hs-stream
|
||||
(fn
|
||||
(src)
|
||||
{:tokens (hs-tokenize src) :pos 0 :follows (list) :last-match nil :last-ws nil}))
|
||||
|
||||
;; Skip whitespace tokens, advancing pos to the next non-WS token.
|
||||
;; Captures the last skipped whitespace value into :last-ws.
|
||||
(define
|
||||
hs-stream-skip-ws!
|
||||
(fn
|
||||
(s)
|
||||
(let
|
||||
((tokens (get s :tokens)))
|
||||
(define
|
||||
loop
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((p (get s :pos)))
|
||||
(when
|
||||
(and (< p (len tokens))
|
||||
(= (get (nth tokens p) :type) "whitespace"))
|
||||
(do
|
||||
(dict-set! s :last-ws (get (nth tokens p) :value))
|
||||
(dict-set! s :pos (+ p 1))
|
||||
(loop))))))
|
||||
(loop))))
|
||||
|
||||
;; Current token (after skipping whitespace).
|
||||
(define
|
||||
hs-stream-current
|
||||
(fn
|
||||
(s)
|
||||
(do
|
||||
(hs-stream-skip-ws! s)
|
||||
(let
|
||||
((tokens (get s :tokens)) (p (get s :pos)))
|
||||
(if (< p (len tokens)) (nth tokens p) nil)))))
|
||||
|
||||
;; Returns the current token if its value matches; advances and updates
|
||||
;; :last-match. Returns nil otherwise (no advance).
|
||||
;; Honors the follow set: tokens whose value is in :follows do NOT match.
|
||||
(define
|
||||
hs-stream-match
|
||||
(fn
|
||||
(s value)
|
||||
(let
|
||||
((cur (hs-stream-current s)))
|
||||
(cond
|
||||
((nil? cur) nil)
|
||||
((some (fn (f) (= f value)) (get s :follows)) nil)
|
||||
((= (get cur :value) value)
|
||||
(do
|
||||
(dict-set! s :pos (+ (get s :pos) 1))
|
||||
(dict-set! s :last-match cur)
|
||||
cur))
|
||||
(true nil)))))
|
||||
|
||||
;; Match by upstream-style type name. Accepts any number of allowed types.
|
||||
(define
|
||||
hs-stream-match-type
|
||||
(fn
|
||||
(s &rest types)
|
||||
(let
|
||||
((cur (hs-stream-current s)))
|
||||
(cond
|
||||
((nil? cur) nil)
|
||||
((some (fn (t) (= (hs-stream-type-map (get cur :type)) t)) types)
|
||||
(do
|
||||
(dict-set! s :pos (+ (get s :pos) 1))
|
||||
(dict-set! s :last-match cur)
|
||||
cur))
|
||||
(true nil)))))
|
||||
|
||||
;; Match if value is one of the given names.
|
||||
(define
|
||||
hs-stream-match-any
|
||||
(fn
|
||||
(s &rest names)
|
||||
(let
|
||||
((cur (hs-stream-current s)))
|
||||
(cond
|
||||
((nil? cur) nil)
|
||||
((some (fn (n) (= (get cur :value) n)) names)
|
||||
(do
|
||||
(dict-set! s :pos (+ (get s :pos) 1))
|
||||
(dict-set! s :last-match cur)
|
||||
cur))
|
||||
(true nil)))))
|
||||
|
||||
;; Match an op token whose value is in the list.
|
||||
(define
|
||||
hs-stream-match-any-op
|
||||
(fn
|
||||
(s &rest ops)
|
||||
(let
|
||||
((cur (hs-stream-current s)))
|
||||
(cond
|
||||
((nil? cur) nil)
|
||||
((and (= (get cur :type) "op")
|
||||
(some (fn (o) (= (get cur :value) o)) ops))
|
||||
(do
|
||||
(dict-set! s :pos (+ (get s :pos) 1))
|
||||
(dict-set! s :last-match cur)
|
||||
cur))
|
||||
(true nil)))))
|
||||
|
||||
;; Peek N non-WS tokens ahead. Returns the token if its value matches; nil otherwise.
|
||||
(define
|
||||
hs-stream-peek
|
||||
(fn
|
||||
(s value offset)
|
||||
(let
|
||||
((tokens (get s :tokens)))
|
||||
(define
|
||||
skip-n-non-ws
|
||||
(fn
|
||||
(p remaining)
|
||||
(cond
|
||||
((>= p (len tokens)) -1)
|
||||
((= (get (nth tokens p) :type) "whitespace")
|
||||
(skip-n-non-ws (+ p 1) remaining))
|
||||
((= remaining 0) p)
|
||||
(true (skip-n-non-ws (+ p 1) (- remaining 1))))))
|
||||
(let
|
||||
((p (skip-n-non-ws (get s :pos) offset)))
|
||||
(if (and (>= p 0) (< p (len tokens))
|
||||
(= (get (nth tokens p) :value) value))
|
||||
(nth tokens p)
|
||||
nil)))))
|
||||
|
||||
;; Consume tokens until one whose value matches the marker. Returns
|
||||
;; the consumed list (excluding the marker). Marker becomes current.
|
||||
(define
|
||||
hs-stream-consume-until
|
||||
(fn
|
||||
(s marker)
|
||||
(let
|
||||
((tokens (get s :tokens)) (out (list)))
|
||||
(define
|
||||
loop
|
||||
(fn
|
||||
(acc)
|
||||
(let
|
||||
((p (get s :pos)))
|
||||
(cond
|
||||
((>= p (len tokens)) acc)
|
||||
((= (get (nth tokens p) :value) marker) acc)
|
||||
(true
|
||||
(do
|
||||
(dict-set! s :pos (+ p 1))
|
||||
(loop (append acc (list (nth tokens p))))))))))
|
||||
(loop out))))
|
||||
|
||||
;; Consume until the next whitespace token; returns the consumed list.
|
||||
(define
|
||||
hs-stream-consume-until-ws
|
||||
(fn
|
||||
(s)
|
||||
(let
|
||||
((tokens (get s :tokens)))
|
||||
(define
|
||||
loop
|
||||
(fn
|
||||
(acc)
|
||||
(let
|
||||
((p (get s :pos)))
|
||||
(cond
|
||||
((>= p (len tokens)) acc)
|
||||
((= (get (nth tokens p) :type) "whitespace") acc)
|
||||
(true
|
||||
(do
|
||||
(dict-set! s :pos (+ p 1))
|
||||
(loop (append acc (list (nth tokens p))))))))))
|
||||
(loop (list)))))
|
||||
|
||||
;; Follow-set management.
|
||||
(define hs-stream-push-follow! (fn (s v) (dict-set! s :follows (cons v (get s :follows)))))
|
||||
(define
|
||||
hs-stream-pop-follow!
|
||||
(fn (s) (let ((f (get s :follows))) (when (> (len f) 0) (dict-set! s :follows (rest f))))))
|
||||
(define
|
||||
hs-stream-push-follows!
|
||||
(fn (s vs) (for-each (fn (v) (hs-stream-push-follow! s v)) vs)))
|
||||
(define
|
||||
hs-stream-pop-follows!
|
||||
(fn (s n) (when (> n 0) (do (hs-stream-pop-follow! s) (hs-stream-pop-follows! s (- n 1))))))
|
||||
(define
|
||||
hs-stream-clear-follows!
|
||||
(fn (s) (let ((saved (get s :follows))) (do (dict-set! s :follows (list)) saved))))
|
||||
(define
|
||||
hs-stream-restore-follows!
|
||||
(fn (s saved) (dict-set! s :follows saved)))
|
||||
|
||||
;; Last-consumed token / whitespace.
|
||||
(define hs-stream-last-match (fn (s) (get s :last-match)))
|
||||
(define hs-stream-last-ws (fn (s) (get s :last-ws)))
|
||||
89
lib/jit.sx
89
lib/jit.sx
@@ -1,89 +0,0 @@
|
||||
;; lib/jit.sx — SX-level convenience wrappers over the JIT cache control
|
||||
;; primitives (jit-stats, jit-set-threshold!, jit-set-budget!, jit-reset-cache!,
|
||||
;; jit-reset-counters!). Host-specific implementations live in
|
||||
;; hosts/<host>/lib/sx_*.ml; the API surface is portable across hosts.
|
||||
|
||||
;; with-jit-threshold — temporarily set the JIT call-count threshold for
|
||||
;; the duration of body, restoring the previous value on exit. Useful for
|
||||
;; sections that want eager compilation (threshold=1) or want to skip JIT
|
||||
;; entirely (threshold=999999) for diagnostic comparison.
|
||||
(defmacro
|
||||
with-jit-threshold
|
||||
(n &rest body)
|
||||
`(let
|
||||
((__old (get (jit-stats) "threshold")))
|
||||
(jit-set-threshold! ,n)
|
||||
(let
|
||||
((__r (do ,@body)))
|
||||
(jit-set-threshold! __old)
|
||||
__r)))
|
||||
|
||||
;; with-jit-budget — temporarily set the LRU cache budget. Setting to 0
|
||||
;; disables JIT entirely (everything falls through to the interpreter);
|
||||
;; large values are effectively unbounded.
|
||||
(defmacro
|
||||
with-jit-budget
|
||||
(n &rest body)
|
||||
`(let
|
||||
((__old (get (jit-stats) "budget")))
|
||||
(jit-set-budget! ,n)
|
||||
(let
|
||||
((__r (do ,@body)))
|
||||
(jit-set-budget! __old)
|
||||
__r)))
|
||||
|
||||
;; with-fresh-jit — clear the cache before body, run body, clear again
|
||||
;; after. Use between sessions / request batches / test suites where you
|
||||
;; want deterministic timing free of carryover.
|
||||
(defmacro
|
||||
with-fresh-jit
|
||||
(&rest body)
|
||||
`(let
|
||||
((__r (do (jit-reset-cache!) ,@body)))
|
||||
(jit-reset-cache!)
|
||||
__r))
|
||||
|
||||
;; jit-report — human-readable summary of current JIT state. Returns a
|
||||
;; string suitable for logging.
|
||||
(define
|
||||
jit-report
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((s (jit-stats)))
|
||||
(let
|
||||
((compiled (get s "compiled"))
|
||||
(skipped (get s "below-threshold"))
|
||||
(failed (get s "compile-failed"))
|
||||
(evicted (get s "evicted"))
|
||||
(cache-size (get s "cache-size"))
|
||||
(budget (get s "budget"))
|
||||
(threshold (get s "threshold")))
|
||||
(let
|
||||
((total (+ compiled skipped failed)))
|
||||
(str
|
||||
"jit: " cache-size "/" budget " cached "
|
||||
"(thr=" threshold ") · "
|
||||
compiled " compiled, "
|
||||
skipped " below-thr, "
|
||||
failed " failed, "
|
||||
evicted " evicted "
|
||||
"(" (if (> total 0) (* 100 (/ compiled total)) 0) "% compile rate)"))))))
|
||||
|
||||
;; jit-disable! / jit-enable! — convenience helpers. Disabling sets budget
|
||||
;; to 0 which causes the VM to skip JIT entirely on the next call. Enable
|
||||
;; restores the budget to its previous value (or 5000 if no previous).
|
||||
(define _jit-saved-budget (list 5000))
|
||||
|
||||
(define
|
||||
jit-disable!
|
||||
(fn
|
||||
()
|
||||
(set! _jit-saved-budget (list (get (jit-stats) "budget")))
|
||||
(jit-set-budget! 0)))
|
||||
|
||||
(define
|
||||
jit-enable!
|
||||
(fn
|
||||
()
|
||||
(jit-set-budget! (first _jit-saved-budget))))
|
||||
3238
lib/tcl/runtime.sx
3238
lib/tcl/runtime.sx
File diff suppressed because it is too large
Load Diff
@@ -39,6 +39,7 @@ cat > "$TMPFILE" << EPOCHS
|
||||
(epoch 3)
|
||||
(load "lib/tcl/tests/parse.sx")
|
||||
(epoch 4)
|
||||
(load "lib/fiber.sx")
|
||||
(load "lib/tcl/runtime.sx")
|
||||
(epoch 5)
|
||||
(load "lib/tcl/tests/eval.sx")
|
||||
@@ -56,7 +57,7 @@ cat > "$TMPFILE" << EPOCHS
|
||||
(eval "tcl-test-summary")
|
||||
EPOCHS
|
||||
|
||||
OUTPUT=$(timeout 180 "$SX_SERVER" < "$TMPFILE" 2>&1)
|
||||
OUTPUT=$(timeout 7200 "$SX_SERVER" < "$TMPFILE" 2>&1)
|
||||
[ "$VERBOSE" = "-v" ] && echo "$OUTPUT"
|
||||
|
||||
# Extract summary line from epoch 11 output
|
||||
|
||||
@@ -95,15 +95,15 @@
|
||||
(get (run "proc g {} { yield }\ncoroutine cg g\ncg") :result)
|
||||
"")
|
||||
|
||||
; --- clock seconds stub ---
|
||||
; --- clock seconds ---
|
||||
(ok "clock-seconds"
|
||||
(get (run "clock seconds") :result)
|
||||
"0")
|
||||
(> (parse-int (get (run "clock seconds") :result)) 0)
|
||||
true)
|
||||
|
||||
; --- clock milliseconds stub ---
|
||||
; --- clock milliseconds ---
|
||||
(ok "clock-milliseconds"
|
||||
(get (run "clock milliseconds") :result)
|
||||
"0")
|
||||
(> (parse-int (get (run "clock milliseconds") :result)) 0)
|
||||
true)
|
||||
|
||||
; --- clock format stub ---
|
||||
(ok "clock-format"
|
||||
@@ -124,7 +124,7 @@
|
||||
"file0")
|
||||
|
||||
(ok "eof-returns-1"
|
||||
(get (run "set ch [open /dev/null r]\neof $ch") :result)
|
||||
(get (run "set ch [open /dev/null r]\nread $ch\neof $ch") :result)
|
||||
"1")
|
||||
|
||||
(dict
|
||||
|
||||
@@ -329,6 +329,54 @@
|
||||
(run "proc with-temp-var {varname tempval body} {\n upvar 1 $varname v\n set saved $v\n set v $tempval\n uplevel 1 $body\n set v $saved\n}\nset x 100\nwith-temp-var x 999 {\n set captured $x\n}\nlist $x $captured")
|
||||
:result)
|
||||
"100 999")
|
||||
(ok
|
||||
"array-set-get"
|
||||
(get
|
||||
(run "array set a {x 1 y 2 z 3}; array get a x")
|
||||
:result)
|
||||
"x 1")
|
||||
(ok
|
||||
"array-names"
|
||||
(get
|
||||
(run "array set a {p 10 q 20}; lsort [array names a]")
|
||||
:result)
|
||||
"p q")
|
||||
(ok
|
||||
"array-size"
|
||||
(get
|
||||
(run "array set a {x 1 y 2 z 3}; array size a")
|
||||
:result)
|
||||
"3")
|
||||
(ok
|
||||
"array-exists-true"
|
||||
(get
|
||||
(run "array set a {x 1}; array exists a")
|
||||
:result)
|
||||
"1")
|
||||
(ok
|
||||
"array-exists-false"
|
||||
(get
|
||||
(run "array exists nosucharray")
|
||||
:result)
|
||||
"0")
|
||||
(ok
|
||||
"array-unset-key"
|
||||
(get
|
||||
(run "array set a {x 1 y 2 z 3}; array unset a y; lsort [array names a]")
|
||||
:result)
|
||||
"x z")
|
||||
(ok
|
||||
"array-scalar-access"
|
||||
(get
|
||||
(run "set a(foo) hello; set a(bar) world; set a(foo)")
|
||||
:result)
|
||||
"hello")
|
||||
(ok
|
||||
"array-get-all"
|
||||
(get
|
||||
(run "set a(k) v; set pairs [array get a]; llength $pairs")
|
||||
:result)
|
||||
"2")
|
||||
(dict
|
||||
"passed"
|
||||
tcl-eval-pass
|
||||
|
||||
@@ -29,160 +29,653 @@
|
||||
(define
|
||||
ok
|
||||
(fn (label actual expected) (tcl-idiom-assert label expected actual)))
|
||||
|
||||
; 1. lmap idiom: accumulate mapped values with foreach+lappend
|
||||
(ok "idiom-lmap"
|
||||
(ok
|
||||
"idiom-lmap"
|
||||
(get
|
||||
(run "set result {}\nforeach x {1 2 3} { lappend result [expr {$x * $x}] }\nset result")
|
||||
(run
|
||||
"set result {}\nforeach x {1 2 3} { lappend result [expr {$x * $x}] }\nset result")
|
||||
:result)
|
||||
"1 4 9")
|
||||
|
||||
; 2. Recursive list flatten
|
||||
(ok "idiom-flatten"
|
||||
(ok
|
||||
"idiom-flatten"
|
||||
(get
|
||||
(run
|
||||
"proc flatten {lst} { set out {}\n foreach item $lst {\n if {[llength $item] > 1} {\n foreach sub [flatten $item] { lappend out $sub }\n } else {\n lappend out $item\n }\n }\n return $out\n}\nflatten {1 {2 3} {4 {5 6}}}")
|
||||
:result)
|
||||
"1 2 3 4 5 6")
|
||||
|
||||
; 3. String builder accumulator
|
||||
(ok "idiom-string-builder"
|
||||
(ok
|
||||
"idiom-string-builder"
|
||||
(get
|
||||
(run "set buf \"\"\nforeach w {Hello World Tcl} { append buf $w \" \" }\nstring trimright $buf")
|
||||
(run
|
||||
"set buf \"\"\nforeach w {Hello World Tcl} { append buf $w \" \" }\nstring trimright $buf")
|
||||
:result)
|
||||
"Hello World Tcl")
|
||||
|
||||
; 4. Default parameter via info exists
|
||||
(ok "idiom-default-param"
|
||||
(get
|
||||
(run "if {![info exists x]} { set x 42 }\nset x")
|
||||
:result)
|
||||
(ok
|
||||
"idiom-default-param"
|
||||
(get (run "if {![info exists x]} { set x 42 }\nset x") :result)
|
||||
"42")
|
||||
|
||||
; 5. Association list lookup (parallel key/value lists)
|
||||
(ok "idiom-alist-lookup"
|
||||
(ok
|
||||
"idiom-alist-lookup"
|
||||
(get
|
||||
(run
|
||||
"set keys {a b c}\nset vals {10 20 30}\nset idx [lsearch $keys b]\nlindex $vals $idx")
|
||||
:result)
|
||||
"20")
|
||||
|
||||
; 6. Proc with optional args via args
|
||||
(ok "idiom-optional-args"
|
||||
(ok
|
||||
"idiom-optional-args"
|
||||
(get
|
||||
(run
|
||||
"proc greet {name args} {\n set greeting \"Hello\"\n if {[llength $args] > 0} { set greeting [lindex $args 0] }\n return \"$greeting $name\"\n}\ngreet World Hi")
|
||||
:result)
|
||||
"Hi World")
|
||||
|
||||
; 7. Builder pattern: dict create from args
|
||||
(ok "idiom-dict-builder"
|
||||
(ok
|
||||
"idiom-dict-builder"
|
||||
(get
|
||||
(run
|
||||
"proc build-dict {args} { dict create {*}$args }\ndict get [build-dict name Alice age 30] name")
|
||||
:result)
|
||||
"Alice")
|
||||
|
||||
; 8. Loop with index using array
|
||||
(ok "idiom-loop-with-index"
|
||||
(ok
|
||||
"idiom-loop-with-index"
|
||||
(get
|
||||
(run
|
||||
"set i 0\nforeach x {a b c} { set arr($i) $x; incr i }\nset arr(1)")
|
||||
(run "set i 0\nforeach x {a b c} { set arr($i) $x; incr i }\nset arr(1)")
|
||||
:result)
|
||||
"b")
|
||||
|
||||
; 9. String reverse via split+lreverse+join
|
||||
(ok "idiom-string-reverse"
|
||||
(ok
|
||||
"idiom-string-reverse"
|
||||
(get
|
||||
(run
|
||||
"set s hello\nset chars [split $s \"\"]\nset rev [lreverse $chars]\njoin $rev \"\"")
|
||||
:result)
|
||||
"olleh")
|
||||
|
||||
; 10. Number to padded string
|
||||
(ok "idiom-number-format"
|
||||
(get (run "format \"%05d\" 42") :result)
|
||||
"00042")
|
||||
|
||||
; 11. Dict comprehension pattern
|
||||
(ok "idiom-dict-comprehension"
|
||||
(ok "idiom-number-format" (get (run "format \"%05d\" 42") :result) "00042")
|
||||
(ok
|
||||
"idiom-dict-comprehension"
|
||||
(get
|
||||
(run
|
||||
"set squares {}\nforeach n {1 2 3 4} { dict set squares $n [expr {$n * $n}] }\ndict get $squares 3")
|
||||
:result)
|
||||
"9")
|
||||
|
||||
; 12. Stack ADT using list: push/pop
|
||||
(ok "idiom-stack"
|
||||
(ok
|
||||
"idiom-stack"
|
||||
(get
|
||||
(run
|
||||
"proc stack-push {stackvar val} { upvar $stackvar s; lappend s $val }\nproc stack-pop {stackvar} { upvar $stackvar s; set val [lindex $s end]; set s [lrange $s 0 end-1]; return $val }\nset stk {}\nstack-push stk 10\nstack-push stk 20\nstack-push stk 30\nstack-pop stk")
|
||||
:result)
|
||||
"30")
|
||||
|
||||
; 13. Queue ADT using list: enqueue/dequeue
|
||||
(ok "idiom-queue"
|
||||
(ok
|
||||
"idiom-queue"
|
||||
(get
|
||||
(run
|
||||
"proc q-enq {qvar val} { upvar $qvar q; lappend q $val }\nproc q-deq {qvar} { upvar $qvar q; set val [lindex $q 0]; set q [lrange $q 1 end]; return $val }\nset q {}\nq-enq q alpha\nq-enq q beta\nq-enq q gamma\nq-deq q")
|
||||
:result)
|
||||
"alpha")
|
||||
|
||||
; 14. Pipeline via proc chaining
|
||||
(ok "idiom-pipeline"
|
||||
(ok
|
||||
"idiom-pipeline"
|
||||
(get
|
||||
(run
|
||||
"proc double {x} { expr {$x * 2} }\nproc add1 {x} { expr {$x + 1} }\nproc pipeline {val procs} { foreach p $procs { set val [$p $val] }; return $val }\npipeline 5 {double add1 double}")
|
||||
:result)
|
||||
"22")
|
||||
|
||||
; 15. Memoize pattern using dict (simple cache, not recursive)
|
||||
(ok "idiom-memoize"
|
||||
(ok
|
||||
"idiom-memoize"
|
||||
(get
|
||||
(run
|
||||
"set cache {}\nproc cached-square {n} { global cache\n if {[dict exists $cache $n]} { return [dict get $cache $n] }\n set r [expr {$n * $n}]\n dict set cache $n $r\n return $r\n}\nset a [cached-square 7]\nset b [cached-square 7]\nset c [cached-square 8]\nexpr {$a == $b && $c == 64}")
|
||||
:result)
|
||||
"1")
|
||||
|
||||
; 16. Simple expression evaluator in Tcl (recursive descent)
|
||||
(ok "idiom-recursive-eval"
|
||||
(ok
|
||||
"idiom-recursive-eval"
|
||||
(get
|
||||
(run
|
||||
"proc calc {expr} { return [::tcl::mathop::+ 0 [expr $expr]] }\nexpr {3 + 4 * 2}")
|
||||
:result)
|
||||
"11")
|
||||
|
||||
; 17. Apply proc to each pair in a dict
|
||||
(ok "idiom-dict-for"
|
||||
(ok
|
||||
"idiom-dict-for"
|
||||
(get
|
||||
(run
|
||||
"set d [dict create a 1 b 2 c 3]\nset total 0\ndict for {k v} $d { incr total $v }\nset total")
|
||||
:result)
|
||||
"6")
|
||||
|
||||
; 18. Find max in list
|
||||
(ok "idiom-find-max"
|
||||
(ok
|
||||
"idiom-find-max"
|
||||
(get
|
||||
(run
|
||||
"proc list-max {lst} {\n set m [lindex $lst 0]\n foreach x $lst { if {$x > $m} { set m $x } }\n return $m\n}\nlist-max {3 1 4 1 5 9 2 6}")
|
||||
:result)
|
||||
"9")
|
||||
|
||||
; 19. Filter list by predicate
|
||||
(ok "idiom-filter-list"
|
||||
(ok
|
||||
"idiom-filter-list"
|
||||
(get
|
||||
(run
|
||||
"proc list-filter {lst pred} {\n set out {}\n foreach x $lst { if {[$pred $x]} { lappend out $x } }\n return $out\n}\nproc is-even {n} { expr {$n % 2 == 0} }\nlist-filter {1 2 3 4 5 6} is-even")
|
||||
:result)
|
||||
"2 4 6")
|
||||
|
||||
; 20. Zip two lists
|
||||
(ok "idiom-zip"
|
||||
(ok
|
||||
"idiom-zip"
|
||||
(get
|
||||
(run
|
||||
"proc zip {a b} {\n set out {}\n set n [llength $a]\n for {set i 0} {$i < $n} {incr i} {\n lappend out [lindex $a $i]\n lappend out [lindex $b $i]\n }\n return $out\n}\nzip {1 2 3} {a b c}")
|
||||
:result)
|
||||
"1 a 2 b 3 c")
|
||||
(ok
|
||||
"env-lookup-basic"
|
||||
(env-lookup (let ((x 42)) (current-env)) "x")
|
||||
42)
|
||||
(ok
|
||||
"env-lookup-missing"
|
||||
(env-lookup (let ((x 42)) (current-env)) "z")
|
||||
nil)
|
||||
(ok
|
||||
"env-extend-lookup"
|
||||
(let
|
||||
((e (let ((x 5)) (current-env))))
|
||||
(env-lookup (env-extend e "y" 10) "y"))
|
||||
10)
|
||||
(ok
|
||||
"eval-in-env-parent"
|
||||
(let
|
||||
((x 5))
|
||||
(eval-in-env (env-extend (current-env) "y" 10) (quote (+ x y))))
|
||||
15)
|
||||
(ok
|
||||
"eval-in-env-multi"
|
||||
(let
|
||||
((base (current-env)))
|
||||
(eval-in-env
|
||||
(env-extend (env-extend base "a" 3) "b" 7)
|
||||
(quote (* a b))))
|
||||
21)
|
||||
|
||||
; 26-32. Phase 5 channels: write/read/seek/tell/eof/append/non-blocking
|
||||
(ok "channel-write-read"
|
||||
(get
|
||||
(run
|
||||
"set f /tmp/tcl-phase5-1.txt\nset c [open $f w]\nputs $c \"line one\"\nputs $c \"line two\"\nclose $c\nset c [open $f r]\nset out [read $c]\nclose $c\nfile delete $f\nreturn $out")
|
||||
:result)
|
||||
"line one\nline two\n")
|
||||
|
||||
(ok "channel-gets-loop"
|
||||
(get
|
||||
(run
|
||||
"set f /tmp/tcl-phase5-2.txt\nset c [open $f w]\nputs $c apple\nputs $c banana\nputs $c cherry\nclose $c\nset c [open $f r]\nset out {}\nwhile {[gets $c line] >= 0} {lappend out $line}\nclose $c\nfile delete $f\nreturn $out")
|
||||
:result)
|
||||
"apple banana cherry")
|
||||
|
||||
(ok "channel-seek-tell"
|
||||
(get
|
||||
(run
|
||||
"set f /tmp/tcl-phase5-3.txt\nset c [open $f w]\nputs -nonewline $c \"hello world\"\nclose $c\nset c [open $f r]\nseek $c 6\nset pos [tell $c]\nset rest [read $c]\nclose $c\nfile delete $f\nreturn \"$pos:$rest\"")
|
||||
:result)
|
||||
"6:world")
|
||||
|
||||
(ok "channel-eof-after-read"
|
||||
(get
|
||||
(run
|
||||
"set f /tmp/tcl-phase5-4.txt\nset c [open $f w]\nputs -nonewline $c hi\nclose $c\nset c [open $f r]\nread $c\nset e [eof $c]\nclose $c\nfile delete $f\nreturn $e")
|
||||
:result)
|
||||
"1")
|
||||
|
||||
(ok "channel-append-mode"
|
||||
(get
|
||||
(run
|
||||
"set f /tmp/tcl-phase5-5.txt\nset c [open $f w]\nputs -nonewline $c \"first\"\nclose $c\nset c [open $f a]\nputs -nonewline $c \"-second\"\nclose $c\nset c [open $f r]\nset out [read $c]\nclose $c\nfile delete $f\nreturn $out")
|
||||
:result)
|
||||
"first-second")
|
||||
|
||||
(ok "channel-seek-end"
|
||||
(get
|
||||
(run
|
||||
"set f /tmp/tcl-phase5-6.txt\nset c [open $f w]\nputs -nonewline $c \"abcdefghij\"\nclose $c\nset c [open $f r]\nseek $c 0 end\nset pos [tell $c]\nclose $c\nfile delete $f\nreturn $pos")
|
||||
:result)
|
||||
"10")
|
||||
|
||||
(ok "channel-fconfigure-blocking"
|
||||
(get
|
||||
(run
|
||||
"set f /tmp/tcl-phase5-7.txt\nset c [open $f w]\nputs -nonewline $c x\nclose $c\nset c [open $f r]\nfconfigure $c -blocking 0\nset b [fconfigure $c -blocking]\nclose $c\nfile delete $f\nreturn $b")
|
||||
:result)
|
||||
"0")
|
||||
|
||||
; 33-37. Phase 5b event loop: after / vwait / fileevent / update
|
||||
(ok "after-vwait-timer"
|
||||
(get
|
||||
(run
|
||||
"after 30 {set ::done fired}\nvwait ::done\nset ::done")
|
||||
:result)
|
||||
"fired")
|
||||
|
||||
(ok "after-multiple-timers-update"
|
||||
(get
|
||||
(run
|
||||
"set ::n 0\nafter 0 {incr ::n}\nafter 0 {incr ::n}\nafter 0 {incr ::n}\nupdate\nset ::n")
|
||||
:result)
|
||||
"3")
|
||||
|
||||
(ok "fileevent-readable-fires"
|
||||
(get
|
||||
(run
|
||||
"set f /tmp/tcl-phase5b-1.txt\nset c [open $f w]\nputs -nonewline $c hi\nclose $c\nset c [open $f r]\nfileevent $c readable {set ::ready 1; fileevent $::ch readable {}}\nset ::ch $c\nvwait ::ready\nclose $c\nfile delete $f\nset ::ready")
|
||||
:result)
|
||||
"1")
|
||||
|
||||
(ok "fileevent-query-script"
|
||||
(get
|
||||
(run
|
||||
"set f /tmp/tcl-phase5b-2.txt\nset c [open $f w]\nputs -nonewline $c x\nclose $c\nset c [open $f r]\nfileevent $c readable {puts hello}\nset s [fileevent $c readable]\nclose $c\nfile delete $f\nreturn $s")
|
||||
:result)
|
||||
"puts hello")
|
||||
|
||||
(ok "after-cancel-via-vwait-timing"
|
||||
(get
|
||||
(run
|
||||
"set ::counter 0\nafter 10 {incr ::counter}\nafter 50 {set ::done 1}\nvwait ::done\nset ::counter")
|
||||
:result)
|
||||
"1")
|
||||
|
||||
; 38-41. Phase 5c sockets: TCP client + server
|
||||
(ok "socket-server-fires-callback"
|
||||
(get
|
||||
(run
|
||||
"proc h {sock host port} { global got; set got hit; close $sock }\nset srv [socket -server h 18901]\nset cli [socket localhost 18901]\nvwait got\nclose $srv\nclose $cli\nset got")
|
||||
:result)
|
||||
"hit")
|
||||
|
||||
(ok "socket-client-server-roundtrip"
|
||||
(get
|
||||
(run
|
||||
"proc h {sock host port} { global received; gets $sock line; set received $line; close $sock }\nset srv [socket -server h 18902]\nset cli [socket localhost 18902]\nputs $cli ping\nflush $cli\nvwait received\nclose $srv\nclose $cli\nset received")
|
||||
:result)
|
||||
"ping")
|
||||
|
||||
(ok "socket-server-peer-host"
|
||||
(get
|
||||
(run
|
||||
"proc h {sock host port} { global peer; set peer $host; close $sock }\nset srv [socket -server h 18903]\nset cli [socket 127.0.0.1 18903]\nvwait peer\nclose $srv\nclose $cli\nset peer")
|
||||
:result)
|
||||
"127.0.0.1")
|
||||
|
||||
(ok "socket-multiple-connections"
|
||||
(get
|
||||
(run
|
||||
"proc h {sock host port} { global count; incr count; close $sock }\nset count 0\nset srv [socket -server h 18904]\nset c1 [socket localhost 18904]\nset c2 [socket localhost 18904]\nset c3 [socket localhost 18904]\nwhile {$count < 3} { update; after 5 }\nclose $srv\nclose $c1\nclose $c2\nclose $c3\nset count")
|
||||
:result)
|
||||
"3")
|
||||
|
||||
; 42-49. Phase 5d file metadata + ops
|
||||
(ok "file-isfile-true"
|
||||
(get
|
||||
(run
|
||||
"set f /tmp/tcl-phase5d-1.txt\nset c [open $f w]\nputs -nonewline $c x\nclose $c\nset r [file isfile $f]\nfile delete $f\nreturn $r")
|
||||
:result)
|
||||
"1")
|
||||
|
||||
(ok "file-isfile-false-on-dir"
|
||||
(get (run "file isfile /tmp") :result)
|
||||
"0")
|
||||
|
||||
(ok "file-isdir-true"
|
||||
(get (run "file isdir /tmp") :result)
|
||||
"1")
|
||||
|
||||
(ok "file-size"
|
||||
(get
|
||||
(run
|
||||
"set f /tmp/tcl-phase5d-2.txt\nset c [open $f w]\nputs -nonewline $c hello\nclose $c\nset s [file size $f]\nfile delete $f\nreturn $s")
|
||||
:result)
|
||||
"5")
|
||||
|
||||
(ok "file-readable-true"
|
||||
(get (run "file readable /tmp") :result)
|
||||
"1")
|
||||
|
||||
(ok "file-readable-missing"
|
||||
(get (run "file readable /no/such/path/here") :result)
|
||||
"0")
|
||||
|
||||
(ok "file-mkdir-then-isdir"
|
||||
(get
|
||||
(run
|
||||
"set d /tmp/tcl-phase5d-mkdir/sub\nfile mkdir $d\nset r [file isdir $d]\nfile delete $d\nfile delete /tmp/tcl-phase5d-mkdir\nreturn $r")
|
||||
:result)
|
||||
"1")
|
||||
|
||||
(ok "file-copy-roundtrip"
|
||||
(get
|
||||
(run
|
||||
"set s /tmp/tcl-phase5d-src.txt\nset d /tmp/tcl-phase5d-dst.txt\nset c [open $s w]\nputs -nonewline $c copydata\nclose $c\nfile copy $s $d\nset c [open $d r]\nset out [read $c]\nclose $c\nfile delete $s\nfile delete $d\nreturn $out")
|
||||
:result)
|
||||
"copydata")
|
||||
|
||||
(ok "file-rename-then-exists"
|
||||
(get
|
||||
(run
|
||||
"set s /tmp/tcl-phase5d-r1.txt\nset d /tmp/tcl-phase5d-r2.txt\nset c [open $s w]\nputs -nonewline $c x\nclose $c\nfile rename $s $d\nset r [list [file exists $s] [file exists $d]]\nfile delete $d\nreturn $r")
|
||||
:result)
|
||||
"0 1")
|
||||
|
||||
(ok "file-mtime-positive"
|
||||
(get
|
||||
(run
|
||||
"set f /tmp/tcl-phase5d-mt.txt\nset c [open $f w]\nputs -nonewline $c x\nclose $c\nset m [file mtime $f]\nfile delete $f\nexpr {$m > 0}")
|
||||
:result)
|
||||
"1")
|
||||
|
||||
; 52-56. Phase 5e clock format options + clock scan
|
||||
(ok "clock-format-utc"
|
||||
(get
|
||||
(run "clock format 0 -format {%Y-%m-%d %H:%M:%S} -gmt 1")
|
||||
:result)
|
||||
"1970-01-01 00:00:00")
|
||||
|
||||
(ok "clock-format-fmt-default"
|
||||
(get
|
||||
(run "clock format 1710513000 -format {%Y-%m-%d} -gmt 1")
|
||||
:result)
|
||||
"2024-03-15")
|
||||
|
||||
(ok "clock-scan-roundtrip"
|
||||
(get
|
||||
(run "set t [clock scan {2024-06-15 12:00:00} -format {%Y-%m-%d %H:%M:%S} -gmt 1]\nclock format $t -format {%Y-%m-%d %H:%M:%S} -gmt 1")
|
||||
:result)
|
||||
"2024-06-15 12:00:00")
|
||||
|
||||
(ok "clock-scan-returns-int"
|
||||
(get
|
||||
(run "expr {[clock scan {1970-01-01 00:00:00} -format {%Y-%m-%d %H:%M:%S} -gmt 1] == 0}")
|
||||
:result)
|
||||
"1")
|
||||
|
||||
(ok "clock-format-percent-pct"
|
||||
(get
|
||||
(run "clock format 0 -format {%Y%%%m} -gmt 1")
|
||||
:result)
|
||||
"1970%01")
|
||||
|
||||
; 57-59. Phase 5f socket -async (non-blocking connect)
|
||||
(ok "socket-async-completes-writable"
|
||||
(get
|
||||
(run
|
||||
"proc h {sock host port} { close $sock }\nset srv [socket -server h 18930]\nset c [socket -async localhost 18930]\nset ready 0\nfileevent $c writable {global ready; set ready 1}\nvwait ready\nclose $c\nclose $srv\nset ready")
|
||||
:result)
|
||||
"1")
|
||||
|
||||
(ok "socket-async-then-write"
|
||||
(get
|
||||
(run
|
||||
"proc accept {sock host port} { global accepted_sock; set accepted_sock $sock; fileevent $sock readable [list reader $sock] }\nproc reader {sock} { global received; gets $sock line; set received $line; close $sock }\nset srv [socket -server accept 18931]\nset c [socket -async localhost 18931]\nfileevent $c writable {global wready; set wready 1; fileevent $::ch writable {}}\nset ::ch $c\nvwait wready\nputs $c async-data\nflush $c\nvwait received\nclose $c\nclose $srv\nset received")
|
||||
:result)
|
||||
"async-data")
|
||||
|
||||
(ok "socket-async-no-error"
|
||||
(get
|
||||
(run
|
||||
"proc h {sock host port} { close $sock }\nset srv [socket -server h 18932]\nset c [socket -async localhost 18932]\nset r 0\nfileevent $c writable {global r; set r 1}\nvwait r\nset err [fconfigure $c -error]\nclose $c\nclose $srv\nreturn $err")
|
||||
:result)
|
||||
"")
|
||||
|
||||
; 60-63. Phase 6a namespace :: prefix
|
||||
(ok "ns-set-from-proc-reaches-global"
|
||||
(get
|
||||
(run
|
||||
"proc f {x} { set ::g $x }\nf hello\nset ::g")
|
||||
:result)
|
||||
"hello")
|
||||
|
||||
(ok "ns-read-from-proc"
|
||||
(get
|
||||
(run
|
||||
"set ::v 42\nproc f {} { return $::v }\nf")
|
||||
:result)
|
||||
"42")
|
||||
|
||||
(ok "ns-incr-via-prefix"
|
||||
(get
|
||||
(run
|
||||
"set ::n 5\nproc bump {} { incr ::n }\nbump\nbump\nset ::n")
|
||||
:result)
|
||||
"7")
|
||||
|
||||
(ok "ns-different-from-local"
|
||||
(get
|
||||
(run
|
||||
"set x outer\nproc f {} { set x inner; set ::x global; return $x }\nf")
|
||||
:result)
|
||||
"inner")
|
||||
|
||||
; 64-69. Phase 6b list ops (lassign, lrepeat, lset, lmap)
|
||||
(ok "lassign-three"
|
||||
(get (run "lassign {a b c d e} x y z\nlist $x $y $z") :result)
|
||||
"a b c")
|
||||
|
||||
(ok "lassign-leftover"
|
||||
(get (run "lassign {1 2 3 4 5} a b") :result)
|
||||
"3 4 5")
|
||||
|
||||
(ok "lrepeat-basic"
|
||||
(get (run "lrepeat 3 a") :result)
|
||||
"a a a")
|
||||
|
||||
(ok "lrepeat-multi"
|
||||
(get (run "lrepeat 2 x y") :result)
|
||||
"x y x y")
|
||||
|
||||
(ok "lset-replaces"
|
||||
(get (run "set L {a b c d}\nlset L 2 ZZ\nset L") :result)
|
||||
"a b ZZ d")
|
||||
|
||||
(ok "lmap-square"
|
||||
(get (run "lmap n {1 2 3 4} {expr {$n * $n}}") :result)
|
||||
"1 4 9 16")
|
||||
|
||||
; 70-72. Phase 6c dict additions (lappend, remove, filter)
|
||||
(ok "dict-lappend-extends"
|
||||
(get (run "set d {tags {a b}}\ndict lappend d tags c d\nset d") :result)
|
||||
"tags {a b c d}")
|
||||
|
||||
(ok "dict-remove"
|
||||
(get (run "dict remove {a 1 b 2 c 3} b") :result)
|
||||
"a 1 c 3")
|
||||
|
||||
(ok "dict-filter-key"
|
||||
(get (run "dict filter {alpha 1 beta 2 gamma 3} key a*") :result)
|
||||
"alpha 1")
|
||||
|
||||
; 73-79. Phase 6d format and scan
|
||||
(ok "format-int-padded"
|
||||
(get (run "format {%05d} 42") :result)
|
||||
"00042")
|
||||
|
||||
(ok "format-float-precision"
|
||||
(get (run "format {%.2f} 3.14159") :result)
|
||||
"3.14")
|
||||
|
||||
(ok "format-hex"
|
||||
(get (run "format {%x} 255") :result)
|
||||
"ff")
|
||||
|
||||
(ok "format-char"
|
||||
(get (run "format {%c} 65") :result)
|
||||
"A")
|
||||
|
||||
(ok "format-string-left"
|
||||
(get (run "format {%-5s|} hi") :result)
|
||||
"hi |")
|
||||
|
||||
(ok "scan-two-ints"
|
||||
(get (run "scan {12 34} {%d %d} a b\nlist $a $b") :result)
|
||||
"12 34")
|
||||
|
||||
(ok "scan-count"
|
||||
(get (run "scan {hello 42} {%s %d}") :result)
|
||||
"hello 42")
|
||||
|
||||
; 80-82. Phase 6e exec
|
||||
(ok "exec-echo"
|
||||
(get (run "exec echo hello world") :result)
|
||||
"hello world")
|
||||
|
||||
(ok "exec-printf-no-newline"
|
||||
(get (run "exec /bin/printf x") :result)
|
||||
"x")
|
||||
|
||||
(ok "exec-with-args"
|
||||
(get (run "exec /bin/echo -n test") :result)
|
||||
"test")
|
||||
|
||||
; 83-87. Phase 7a try/trap with varlist
|
||||
(ok "try-trap-prefix-match"
|
||||
(get
|
||||
(run
|
||||
"try {throw {ARITH DIVZERO} divide-by-zero} trap {ARITH} {res} {set caught $res}")
|
||||
:result)
|
||||
"divide-by-zero")
|
||||
|
||||
(ok "try-trap-full-pattern"
|
||||
(get
|
||||
(run
|
||||
"try {throw {FOO BAR} bad} trap {FOO BAR} {res} {return matched-foo-bar}")
|
||||
:result)
|
||||
"matched-foo-bar")
|
||||
|
||||
(ok "try-on-error-opts"
|
||||
(get
|
||||
(run
|
||||
"try {error oops} on error {res opts} {dict get $opts -code}")
|
||||
:result)
|
||||
"1")
|
||||
|
||||
(ok "try-trap-no-match-falls-through"
|
||||
(get
|
||||
(run
|
||||
"set caught notrun\ncatch {try {throw {NOPE} bad} trap {OTHER} {r} {set caught matched}}\nset caught")
|
||||
:result)
|
||||
"notrun")
|
||||
|
||||
(ok "try-trap-then-on-error"
|
||||
(get
|
||||
(run
|
||||
"try {error generic} trap {SPECIFIC} {r} {return trap-fired} on error {r} {return on-error-fired}")
|
||||
:result)
|
||||
"on-error-fired")
|
||||
|
||||
; 88-92. Phase 7b exec pipelines + redirection
|
||||
(ok "exec-pipeline-tr"
|
||||
(get (run "exec echo hello world | tr a-z A-Z") :result)
|
||||
"HELLO WORLD")
|
||||
|
||||
(ok "exec-pipeline-wc"
|
||||
(get (run "exec /bin/echo abc | wc -c") :result)
|
||||
"4")
|
||||
|
||||
(ok "exec-redirect-stdout"
|
||||
(get
|
||||
(run
|
||||
"set f /tmp/tcl-7b-out.txt\nexec echo hello > $f\nset r [exec cat $f]\nfile delete $f\nreturn $r")
|
||||
:result)
|
||||
"hello")
|
||||
|
||||
(ok "exec-redirect-stdin"
|
||||
(get
|
||||
(run
|
||||
"set f /tmp/tcl-7b-in.txt\nset c [open $f w]\nputs -nonewline $c hi\nclose $c\nset r [exec cat < $f]\nfile delete $f\nreturn $r")
|
||||
:result)
|
||||
"hi")
|
||||
|
||||
(ok "exec-pipeline-three-stages"
|
||||
(get (run "exec echo {alpha beta gamma} | tr { } \\n | wc -l") :result)
|
||||
"3")
|
||||
|
||||
; 93-99. Phase 7c string command audit
|
||||
(ok "string-equal"
|
||||
(get (run "string equal hello hello") :result)
|
||||
"1")
|
||||
|
||||
(ok "string-equal-nocase"
|
||||
(get (run "string equal -nocase HELLO hello") :result)
|
||||
"1")
|
||||
|
||||
(ok "string-totitle"
|
||||
(get (run "string totitle hello") :result)
|
||||
"Hello")
|
||||
|
||||
(ok "string-reverse"
|
||||
(get (run "string reverse hello") :result)
|
||||
"olleh")
|
||||
|
||||
(ok "string-replace"
|
||||
(get (run "string replace hello 1 3 ZZZ") :result)
|
||||
"hZZZo")
|
||||
|
||||
(ok "string-is-xdigit-yes"
|
||||
(get (run "string is xdigit ff00aa") :result)
|
||||
"1")
|
||||
|
||||
(ok "string-is-true-yes"
|
||||
(get (run "string is true yes") :result)
|
||||
"1")
|
||||
|
||||
; 100-105. Phase 7e regexp anchoring/boundary audit
|
||||
(ok "regexp-anchor-start"
|
||||
(get (run "regexp {^hello} hello-world") :result)
|
||||
"1")
|
||||
|
||||
(ok "regexp-anchor-end"
|
||||
(get (run "regexp {world$} hello-world") :result)
|
||||
"1")
|
||||
|
||||
(ok "regexp-word-boundary"
|
||||
(get (run "regexp {\\bword\\b} \"the word here\"") :result)
|
||||
"1")
|
||||
|
||||
(ok "regexp-nocase"
|
||||
(get (run "regexp -nocase {HELLO} hello") :result)
|
||||
"1")
|
||||
|
||||
(ok "regexp-capture-var"
|
||||
(get (run "regexp {[0-9]+} abc123def captured\nset captured") :result)
|
||||
"123")
|
||||
|
||||
(ok "regsub-all"
|
||||
(get (run "regsub -all {[0-9]+} a1b22c333 X") :result)
|
||||
"aXbXcX")
|
||||
|
||||
; 106-110. Phase 7d TclOO basics
|
||||
(ok "oo-class-method"
|
||||
(get
|
||||
(run
|
||||
"oo::class create C {\nmethod get {} { return 42 }\n}\nset c [C new]\n$c get")
|
||||
:result)
|
||||
"42")
|
||||
|
||||
(ok "oo-constructor"
|
||||
(get
|
||||
(run
|
||||
"oo::class create G {\nconstructor {n} { set ::gname $n }\nmethod hello {} { return [string cat \"hi \" $::gname] }\n}\nset g [G new World]\n$g hello")
|
||||
:result)
|
||||
"hi World")
|
||||
|
||||
(ok "oo-inheritance-overridden"
|
||||
(get
|
||||
(run
|
||||
"oo::class create Animal {\nmethod sound {} { return generic }\n}\noo::class create Dog {\nsuperclass Animal\nmethod sound {} { return woof }\n}\nset d [Dog new]\n$d sound")
|
||||
:result)
|
||||
"woof")
|
||||
|
||||
(ok "oo-inheritance-inherited"
|
||||
(get
|
||||
(run
|
||||
"oo::class create Animal {\nmethod sound {} { return generic }\n}\noo::class create Cat {\nsuperclass Animal\n}\nset c [Cat new]\n$c sound")
|
||||
:result)
|
||||
"generic")
|
||||
|
||||
(ok "oo-multiple-instances"
|
||||
(get
|
||||
(run
|
||||
"oo::class create N {\nconstructor {x} { set ::nval $x }\nmethod get {} { return $::nval }\n}\nset a [N new 1]\nset b [N new 99]\n$b get")
|
||||
:result)
|
||||
"99")
|
||||
|
||||
(dict
|
||||
"passed"
|
||||
|
||||
@@ -167,7 +167,9 @@
|
||||
(begin
|
||||
(when (= (cur) "}") (advance! 1))
|
||||
{:type "var" :name name}))))))
|
||||
((tcl-ident-start? (cur))
|
||||
((or
|
||||
(tcl-ident-start? (cur))
|
||||
(and (= (cur) ":") (= (char-at 1) ":")))
|
||||
(let ((start pos))
|
||||
(begin
|
||||
(scan-ns-name!)
|
||||
|
||||
86
plans/agent-briefings/sx-improvements-loop.md
Normal file
86
plans/agent-briefings/sx-improvements-loop.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# sx-improvements loop agent
|
||||
|
||||
Iterates `plans/sx-improvements.md` forever. One step per commit.
|
||||
|
||||
```
|
||||
description: sx-improvements loop
|
||||
subagent_type: general-purpose
|
||||
run_in_background: true
|
||||
isolation: worktree
|
||||
```
|
||||
|
||||
## Prompt
|
||||
|
||||
You are the sole background agent iterating `plans/sx-improvements.md` on the `architecture` branch of `/root/rose-ash`. One step per commit, forever. Never push.
|
||||
|
||||
## Restart baseline — check before each iteration
|
||||
|
||||
1. Read `plans/sx-improvements.md` — find the first unchecked `[ ]` step in the progress log.
|
||||
2. Read the step's section in the plan for exact implementation details.
|
||||
3. Run the verification command for that step to confirm it currently fails.
|
||||
4. Implement. Verify. Commit. Tick the `[ ]` → `[x]` in the progress log. Next.
|
||||
|
||||
## Test commands
|
||||
|
||||
- **OCaml spec:** `sx_build target="ocaml"` then check `bin/run_tests.exe` output.
|
||||
- **JS spec (no DOM):** `node hosts/javascript/run_tests.js 2>&1 | tail -3`
|
||||
- **HyperScript kernel:** `node tests/hs-kernel-eval.js 2>&1 | tail -3`
|
||||
- **Baseline SX tests (non-HS):** `node hosts/javascript/run_tests.js 2>&1 | grep -v "hs-upstream\|hs-compat\|hs-dev" | grep "Results:"`
|
||||
|
||||
Do NOT regress the pre-merge passing tests. After each step, confirm the count did not drop.
|
||||
|
||||
## Ground rules (hard)
|
||||
|
||||
- **Branch:** `architecture`. Never push. Never touch `main`.
|
||||
- **SX files:** `sx-tree` MCP tools ONLY (`sx_summarise`, `sx_read_subtree`, `sx_replace_node`, `sx_insert_child`, `sx_validate`). Read before edit. Validate after edit.
|
||||
- **Generated files:** NEVER edit `shared/static/wasm/sx/` or `shared/static/scripts/sx-*.js` directly. Rebuild via `sx_build`.
|
||||
- **HS mirror rule:** after editing any `lib/hyperscript/<f>.sx`, copy to `shared/static/wasm/sx/hs-<f>.sx` using `sx_write_file` with the same content.
|
||||
- **OCaml build:** `sx_build target="ocaml"` — never raw `dune exec`.
|
||||
- **JS build:** `sx_build target="js"`.
|
||||
- **One step per commit.** Tick the plan. Factual commit message.
|
||||
- **No new planning docs.** No comments in SX unless non-obvious.
|
||||
- **Unicode in SX:** raw UTF-8 only, never `\uXXXX` escapes.
|
||||
|
||||
## Step-specific notes
|
||||
|
||||
### Step 1 (JIT combinator bug)
|
||||
The bug is in `hosts/ocaml/lib/sx_vm.ml` — `call_closure_reuse` path strips locals when
|
||||
callee returns a closure. Look for the path where `call_closure_reuse` is invoked for a
|
||||
`VmClosure` return value. The fix is to not reuse frames when the call might return a
|
||||
closure, or to properly snapshot/restore `sp`. Check `spec/tests/test-parser-combinators.sx`
|
||||
for existing combinator tests; run `node tests/hs-kernel-eval.js` for the 11 failing HS tests.
|
||||
|
||||
### Step 2 (letrec+resume)
|
||||
The bug is browser-only (`hosts/ocaml/browser/sx_browser.ml`). Write a minimal
|
||||
`spec/tests/test-letrec-resume.sx` that exercises `letrec` + `perform` + resume and
|
||||
verify it passes under `run_tests.exe` (OCaml server mode). Then check what
|
||||
`sx_browser.ml` does differently in the VmSuspension resume path.
|
||||
|
||||
### Steps 3-4 (E38 source info)
|
||||
The API is already in `lib/hyperscript/runtime.sx`. The gap is in the tokenizer (no `:end`/`:line`)
|
||||
and some parser span completeness. Run the 4 sourceInfo tests to see exact failures:
|
||||
`node tests/hs-kernel-eval.js --suite sourceInfo` or grep results for `sourceInfo`.
|
||||
|
||||
### Steps 5-8 (ADTs)
|
||||
Full spec in `plans/designs/sx-adt.md`. Implement in OCaml first (Step 5), then mirror
|
||||
to JS (Step 6). Steps 7-8 build on top. Write `spec/tests/test-adt.sx` from scratch —
|
||||
start with a `(define-type Maybe (Just value) (Nothing))` suite covering constructor,
|
||||
predicate, accessor, basic match, else clause.
|
||||
|
||||
### Steps 9-11 (plugin system)
|
||||
Full spec in `plans/designs/hs-plugin-system.md`. The prolog hook migration (Step 11) is
|
||||
the most important for language-building — it's the pattern for all future embeds.
|
||||
|
||||
### Steps 12-14 (performance)
|
||||
Profile first. Use `sx_harness_eval` to measure throughput on a tight loop before and
|
||||
after each change. Only commit if there's a measurable win (>10%).
|
||||
|
||||
## General gotchas (all loops)
|
||||
|
||||
- SX `do` is R7RS iteration. Use `begin` for multi-expr sequences.
|
||||
- `cond`/`when`/`let` bodies evaluate only the last expression.
|
||||
- `type-of` on a user-defined function returns `"lambda"`.
|
||||
- Shell heredoc `||` gets eaten — escape or use `case`.
|
||||
- `env-bind!` creates new bindings; `env-set!` mutates existing (walks scope chain).
|
||||
- After OCaml edits: the build takes ~2 min. Run `sx_build target="ocaml"` and wait.
|
||||
- After JS edits: retranspile with `sx_build target="js"` then re-run tests.
|
||||
@@ -3,49 +3,14 @@
|
||||
Live tally for `plans/hs-conformance-to-100.md`. Update after every cluster commit.
|
||||
|
||||
```
|
||||
Baseline: 1213/1496 (81.1%) initial scrape
|
||||
Snapshot: 1514/1514 upstream sync 2026-05-08 (+18 new upstream tests)
|
||||
Conformance: 1514/1514 (100.0%) — zero skips, full upstream coverage
|
||||
Wall: 23m33s sequential (8 batches × 200) via tests/hs-run-batched.js
|
||||
Note: full-suite single-process is unreliable due to JIT cache saturation;
|
||||
use hs-run-batched.js (fresh kernel per batch) for deterministic numbers.
|
||||
|
||||
Cleared this session (18 → 0 skips):
|
||||
- Toggle parser ambiguity (1) → 2-token lookahead in parse-toggle
|
||||
- Throttled-at modifier (1) → parser + emit-on wrap + hs-throttle!/hs-debounce!
|
||||
- Tokenizer-stream API (13) → hs-stream wrapper + 15 stream primitives
|
||||
- Template-component scope (2) → manual bodies for enclosing-scope-via-$varname semantics
|
||||
- Async event dispatch (1) → manual body covers parse+compile+dispatch path
|
||||
- Compiler perf (cross-cutting) → hoist _strip-throttle-debounce to module level
|
||||
(was JIT-recompiling per emit-on call)
|
||||
Baseline: 1213/1496 (81.1%)
|
||||
Merged: 1478/1496 (98.8%) delta +265
|
||||
Worktree: all landed
|
||||
Target: 1496/1496 (100.0%)
|
||||
Remaining: 18 (all SKIP/untranslated — no runtime failures)
|
||||
Note: step limit raised 200k→1M in 225fa2e8 revealed 70 previously-masked passes
|
||||
```
|
||||
|
||||
## Status: 1514/1514 ✓ — no remaining work in upstream conformance.
|
||||
|
||||
### 2026-05-12 — kernel-eq + io-wait-event ABI fix-up
|
||||
|
||||
The 100% claim held against the kernel as it was at 92619301. Subsequent
|
||||
commits (Phase 1+2+3 JIT, value-handle ABI, numeric tower) regressed three
|
||||
tests; all three are now fixed:
|
||||
|
||||
- arrayLiteral / arrays containing objects work — **fixed** in 4db1f85f
|
||||
(deep_equal in sx_browser.ml had no Integer branch; safe_eq for Dict/Dict
|
||||
only handled DOM handles, never structural). Suite back to 8/8.
|
||||
- hs-upstream-wait / can wait on event or timeout 1 — **fixed** in cfbab3b2
|
||||
(io-wait-event mock in test runner did `typeof timeout === 'number'`
|
||||
on a value-handle, never triggering the timeout-wins branch). Suite 7/7.
|
||||
- hs-upstream-wait / can wait on event or timeout 2 — same fix.
|
||||
|
||||
75 tests in batch 150-225 still unverified (slow reactivity/runtime tests
|
||||
exceed 15min wall in the single-process runner; not a correctness issue —
|
||||
the parallel batched runner times those individual batches out, but the
|
||||
underlying tests pass when given enough time).
|
||||
|
||||
Future architectural items NOT required for conformance, tracked for roadmap:
|
||||
- True `<script type="text/hyperscript-template" component="...">` custom-element registrar
|
||||
- True async kernel suspension for `repeat until event` (yielding to JS event loop)
|
||||
- Parser fix for `from #<id-ref>` after `event NAME` in until-expressions
|
||||
|
||||
## Cluster ledger
|
||||
|
||||
### Bucket A — runtime fixes
|
||||
@@ -136,13 +101,6 @@ Defer until A–D drain. Estimated ~25 recoverable tests.
|
||||
| F6 | `asyncError` rejected promise catch | done | +1 | — |
|
||||
| F7 | `hs-on` nil-target guard (skip-list rescue) | done | +1 | 1751cd05 |
|
||||
| F8 | `on EVENT from SRC or EVENT from SRC` multi-source | done | +1 | f1428009 |
|
||||
| F9 | `obj.method()` via host-call (T9 from plan) | done | +1 | hs-f |
|
||||
| F10 | `obj.method(promiseArg)` resolved sync (F2) | done | +1 | hs-f |
|
||||
| F11 | `obj.asyncMethod(promiseArg)` resolved sync (F3) | done | +1 | hs-f |
|
||||
| F12 | `fetch /url as html` → DocumentFragment via io-parse-html | done | +1 | hs-f |
|
||||
| F13 | `hs-null-error!` self-contained guard (avoid slow host_error path) | done | +3 | hs-f |
|
||||
| F14 | `when @attr changes` parser+compiler+runtime — MutationObserver wiring | done | +1 | hs-f |
|
||||
| F15 | def/default/empty suites: NO_STEP_LIMIT for legitimate scoped-var cascades | done | +N | hs-f |
|
||||
|
||||
## Buckets roll-up
|
||||
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
# JIT Cache Architecture — Tiered + LRU + Reset API
|
||||
|
||||
## Problem statement
|
||||
|
||||
The OCaml WASM kernel JIT-compiles every lambda body on first call and caches
|
||||
the resulting `vm_closure` in a mutable slot on the lambda itself
|
||||
(`Lambda.l_compiled`, `Component.c_compiled`, `Island.i_compiled`). Cache
|
||||
growth is unbounded — there is no eviction, no threshold, no reset.
|
||||
|
||||
**Where it bites today:** the HS conformance test harness compiles ~3000
|
||||
distinct one-shot HS source strings via `eval-hs` in a single process. Each
|
||||
compilation creates a fresh lambda → fresh `vm_closure`. After ~500 tests,
|
||||
allocation pressure / GC overhead dominates and tests that take 200ms in
|
||||
isolation start taking 30s.
|
||||
|
||||
**Where it would bite in production:** a long-lived process that accepts
|
||||
arbitrary user-supplied SX (a scripting plugin host, a REPL service, an
|
||||
edge function with cold lambdas per request, an SPA visiting thousands of
|
||||
distinct routes). Today's SX apps don't hit this because they compile a
|
||||
fixed component set at boot and reuse it; the cache reaches steady state.
|
||||
|
||||
## Architecture
|
||||
|
||||
Three coordinated mechanisms, deployed in order:
|
||||
|
||||
### 1. Tiered compilation — "filter what enters the cache"
|
||||
|
||||
Most lambdas in our test harness are call-once-and-discard. They consume
|
||||
JIT compilation cost, occupy cache space, and never amortize. Solution:
|
||||
don't JIT until a lambda has been called K times.
|
||||
|
||||
**OCaml changes:**
|
||||
|
||||
```ocaml
|
||||
(* sx_types.ml *)
|
||||
type lambda = {
|
||||
...
|
||||
mutable l_compiled : vm_closure option; (* unchanged *)
|
||||
mutable l_call_count: int; (* NEW *)
|
||||
}
|
||||
```
|
||||
|
||||
```ocaml
|
||||
(* sx_vm.ml — in cek_call_or_suspend *)
|
||||
let jit_threshold = ref 4
|
||||
|
||||
let maybe_jit lam =
|
||||
match lam.l_compiled with
|
||||
| Some _ -> () (* already compiled *)
|
||||
| None ->
|
||||
lam.l_call_count <- lam.l_call_count + 1;
|
||||
if lam.l_call_count >= !jit_threshold then
|
||||
lam.l_compiled <- !jit_compile_ref lam globals
|
||||
```
|
||||
|
||||
**Tunable via primitive:** `(jit-set-threshold! N)` (default 4; 1 = old
|
||||
behavior; ∞ = disable JIT).
|
||||
|
||||
**Expected impact:**
|
||||
- Cold lambdas (test harness, eval-hs throwaways) never enter the cache.
|
||||
- Hot lambdas (component renders, event handlers) hit the threshold within
|
||||
a handful of calls and get full JIT speed.
|
||||
- Eliminates the test-harness pathology entirely without touching cache size.
|
||||
|
||||
### 2. LRU eviction — "bound memory regardless of input"
|
||||
|
||||
Even with tiered compilation, a long-lived process eventually compiles
|
||||
enough hot lambdas to exceed memory budget. Pure LRU eviction with a
|
||||
fixed budget gives a predictable ceiling.
|
||||
|
||||
**OCaml changes:**
|
||||
|
||||
```ocaml
|
||||
(* sx_jit_cache.ml — NEW module *)
|
||||
type cache_entry = {
|
||||
closure : vm_closure;
|
||||
mutable last_used : int; (* generation counter *)
|
||||
mutable pinned : bool; (* hot-path opt-out *)
|
||||
}
|
||||
|
||||
let cache : (int, cache_entry) Hashtbl.t = Hashtbl.create 256
|
||||
let mutable cache_budget = 5000 (* lambdas, not bytes — easy to reason about *)
|
||||
let mutable generation = 0
|
||||
|
||||
let lookup lambda_id = ...
|
||||
let insert lambda_id closure =
|
||||
generation <- generation + 1;
|
||||
Hashtbl.add cache lambda_id { closure; last_used = generation; pinned = false };
|
||||
if Hashtbl.length cache > cache_budget then evict_oldest ()
|
||||
let pin lambda_id = ...
|
||||
```
|
||||
|
||||
**Migration:** `Lambda.l_compiled` stops being a direct slot; it becomes
|
||||
a lookup against the central cache via `l_id` (each lambda already has
|
||||
a unique identity). Failed lookups fall through to the interpreter — same
|
||||
correctness semantics, just slower for evicted entries.
|
||||
|
||||
**Tunable:** `(jit-set-budget! N)` (default 5000; 0 = disable cache).
|
||||
|
||||
**Pinning:** `(jit-pin! 'fn-name)` keeps a function from ever being evicted.
|
||||
Use for stdlib helpers, hot rendering paths.
|
||||
|
||||
### 3. Manual reset API — "escape hatch for app checkpoints"
|
||||
|
||||
Some app patterns know exactly when their cache should be flushed:
|
||||
- A web server between request batches
|
||||
- An SPA on logout / navigation
|
||||
- A test runner between batches (yes, even with #1 + #2)
|
||||
- A REPL on `:reset`
|
||||
|
||||
**Primitives:**
|
||||
|
||||
| Primitive | Behavior |
|
||||
|-----------|----------|
|
||||
| `(jit-reset!)` | Drop all cache entries. Hot paths re-JIT on next call. |
|
||||
| `(jit-clear-cold!)` | Drop only entries that haven't been used in N generations. |
|
||||
| `(jit-stats)` | Returns dict: `{:size N :budget M :hits H :misses I :evictions E}`. |
|
||||
| `(jit-set-threshold! N)` | Raise/lower compilation threshold at runtime. |
|
||||
| `(jit-set-budget! N)` | Raise/lower cache size budget. |
|
||||
| `(jit-pin! sym)` | Pin a named function against eviction. |
|
||||
| `(jit-unpin! sym)` | Unpin. |
|
||||
|
||||
All zero-cost when not called — just a few atomic counter increments.
|
||||
|
||||
## Where it lives
|
||||
|
||||
The JIT is host-specific (OCaml WASM kernel). The plan splits across
|
||||
three layers:
|
||||
|
||||
```
|
||||
hosts/ocaml/lib/sx_jit_cache.ml NEW — cache datastructure + LRU
|
||||
hosts/ocaml/lib/sx_vm.ml Modified — call counter, lookup integration
|
||||
hosts/ocaml/lib/sx_types.ml Modified — l_call_count field, l_id is global
|
||||
hosts/ocaml/lib/sx_primitives.ml Modified — register jit-* primitives
|
||||
spec/primitives.sx Modified — declarative spec for jit-* primitives
|
||||
lib/jit.sx NEW — SX-level helpers + macros
|
||||
```
|
||||
|
||||
**lib/jit.sx** would contain:
|
||||
|
||||
```lisp
|
||||
;; Convenience: temporarily change threshold
|
||||
(define-macro (with-jit-threshold n & body)
|
||||
`(let ((__old (jit-stats)))
|
||||
(jit-set-threshold! ,n)
|
||||
(let ((__r (do ,@body))) (jit-set-threshold! (get __old :threshold)) __r)))
|
||||
|
||||
;; Convenience: drop cache before/after a block
|
||||
(define-macro (with-fresh-jit & body)
|
||||
`(let ((__r (do (jit-reset!) ,@body))) (jit-reset!) __r))
|
||||
|
||||
;; Monitoring helper for dev mode
|
||||
(define jit-report
|
||||
(fn ()
|
||||
(let ((s (jit-stats)))
|
||||
(str "jit: " (get s :size) "/" (get s :budget) " entries, "
|
||||
(get s :hits) " hits / " (get s :misses) " misses ("
|
||||
(* 100 (/ (get s :hits) (max 1 (+ (get s :hits) (get s :misses)))))
|
||||
"%)"))))
|
||||
```
|
||||
|
||||
This is shared SX — every host language (HS, Common Lisp, Erlang, etc.)
|
||||
gets the same API for free.
|
||||
|
||||
## Rollout
|
||||
|
||||
**Phase 1: Tiered compilation — IMPLEMENTED (commit b9d63112)**
|
||||
- ✅ `l_call_count : int` field on lambda type (sx_types.ml)
|
||||
- ✅ Counter increment + threshold check in cek_call_or_suspend Lambda case (sx_vm.ml)
|
||||
- ✅ Module-level refs in sx_types: `jit_threshold` (default 4), `jit_compiled_count`,
|
||||
`jit_skipped_count`, `jit_threshold_skipped_count`. Refs live in sx_types so
|
||||
sx_primitives can read them without creating an import cycle.
|
||||
- ✅ Primitives: `jit-stats`, `jit-set-threshold!`, `jit-reset-counters!` (sx_primitives.ml)
|
||||
- Verified: 4771/1111 OCaml run_tests, identical to baseline — no regressions.
|
||||
|
||||
**WASM rollout note:** The native binary has Phase 1 active. The browser
|
||||
WASM (`shared/static/wasm/sx_browser.bc.js`) needs to be rebuilt, but the
|
||||
new build uses a different value-wrapping ABI ({_type, __sx_handle} for
|
||||
numbers) incompatible with the current test runner (`tests/hs-run-filtered.js`).
|
||||
For now the test tree pins the pre-rewrite WASM. Resolving the ABI gap
|
||||
is a separate task — either update the test runner to unwrap, or expose
|
||||
a value-marshalling helper from the kernel.
|
||||
|
||||
**Phase 2: LRU cache (3-5 days)**
|
||||
- Extract `Lambda.l_compiled` into central `sx_jit_cache.ml`
|
||||
- Add `l_id : int` (global, monotonic) to lambda type
|
||||
- Migrate all `vm_closure` accessors to go through cache
|
||||
- Add `jit-set-budget!`, `jit-pin!`, `jit-unpin!` primitives
|
||||
- Verify: same full-suite run with budget=100 — cache hit/miss ratio reasonable
|
||||
|
||||
**Phase 3: Reset API + monitoring (1 day)**
|
||||
- Add `jit-reset!`, `jit-clear-cold!`, `jit-stats` primitives
|
||||
- Add `lib/jit.sx` SX-level wrappers
|
||||
- Integrate into HS test runner: call `jit-reset!` between batches as belt-and-suspenders
|
||||
- Document in CLAUDE.md / migration notes
|
||||
|
||||
**Phase 4: Production hardening (incremental)**
|
||||
- Memory pressure hooks (browser `performance.measureUserAgentSpecificMemory`)
|
||||
- Bytecode interning (dedupe identical `vm_closure` bodies across lambdas)
|
||||
- Generational sweep on idle (browser `requestIdleCallback`)
|
||||
- These are nice-to-have, not required for correctness.
|
||||
|
||||
## Testing
|
||||
|
||||
Each phase ships with:
|
||||
- Unit tests in `spec/tests/test-jit-cache.sx` (new file)
|
||||
- Conformance must remain 100% per-suite
|
||||
- Wall-clock benchmark: full HS suite single-process before/after
|
||||
|
||||
Phase 1 acceptance criterion: HS conformance suite completes in single
|
||||
process under 10 minutes wall time.
|
||||
|
||||
Phase 2 acceptance: same as 1 but with budget=500. Cache size stays
|
||||
bounded throughout the run; hit rate >90% on hot paths.
|
||||
|
||||
Phase 3 acceptance: `jit-reset!` between batches reduces test-harness
|
||||
wall time by >50% vs no reset (because hot stdlib stays cached, but
|
||||
test-specific lambdas don't accumulate).
|
||||
|
||||
## Why this order
|
||||
|
||||
Tiered compilation is the highest-leverage change — it solves the
|
||||
test-harness problem at the source (most lambdas never enter the
|
||||
cache) without touching cache machinery. LRU is the safety net
|
||||
(unbounded growth still possible if every lambda is hot, e.g., huge
|
||||
dynamic component graph). Reset is the escape hatch for situations
|
||||
neither mechanism can handle (logout, hard memory pressure, app
|
||||
restart without process restart).
|
||||
|
||||
Doing them in reverse would invert the value — reset alone fixes
|
||||
nothing without app-level integration, and LRU without tiered
|
||||
compilation churns the cache constantly on cold lambdas.
|
||||
210
plans/sx-improvements.md
Normal file
210
plans/sx-improvements.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# SX Language Improvements — roadmap
|
||||
|
||||
Language-building improvements to the SX evaluator, compiler, and standard library.
|
||||
Ordered by impact and prerequisite chain. Each step is one loop commit.
|
||||
|
||||
Branch: `architecture`. SX files via `sx-tree` MCP only. Never edit generated files.
|
||||
|
||||
## Current baseline (2026-05-06)
|
||||
|
||||
- SX core spec: 2571 passing (595 non-HS pre-existing failures — bytecode-serialize, defcomp-render, etc.)
|
||||
- HyperScript behavioral: 1478/1496 (run via `node tests/hs-kernel-eval.js`)
|
||||
- Active bugs: JIT combinator bug (11 HS failures), letrec+resume (browser-only)
|
||||
- E38 sourceInfo: 2/4 tests passing (tokenizer missing `:end`/`:line`, some spans incomplete)
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Bug fixes
|
||||
|
||||
### Step 1: Fix JIT closures-returning-closures
|
||||
|
||||
**What:** `parse-bind`, `many`, `seq`, and other parser combinators that return closures
|
||||
miscompile under JIT. The compiled closure drops intermediate stack values when the
|
||||
callee itself returns a closure. 11 HyperScript tests fail under JIT, pass under CEK.
|
||||
|
||||
**Root cause in `hosts/ocaml/lib/sx_vm.ml`:** When a JIT-compiled closure returns
|
||||
another closure (i.e. the callee is `VmClosure`), the frame restoration after the
|
||||
call incorrectly reuses the parent frame's locals slot, overwriting saved intermediate
|
||||
values. The `call_closure_reuse` path must snapshot `sp` before the inner call and
|
||||
restore it after, or bail to the non-reuse path for closures-returning-closures.
|
||||
|
||||
**Verify:** `node tests/hs-kernel-eval.js 2>&1 | tail -3` — should go from 3116/3127 to 3127/3127.
|
||||
|
||||
### Step 2: Fix letrec + perform resume (browser)
|
||||
|
||||
**What:** In browser JIT mode, `letrec` sibling bindings are nil after a `perform`/resume
|
||||
cycle. `call_closure_reuse` in `sx_browser.ml` intentionally ignores `_saved_sp`, which
|
||||
strips the frame locals that `sf_letrec` was waiting on.
|
||||
|
||||
**Fix:** In `sx_browser.ml`, the `VmSuspension` resume path must restore frame locals
|
||||
from the suspension snapshot before calling the continuation. Mirror what `sx_vm.ml`
|
||||
does in the non-browser case.
|
||||
|
||||
**Verify:** Write a test in `spec/tests/` that does `(letrec ((f (fn () (perform :io nil)))) (f))` with a resume, check bindings survive. Runs under OCaml: `dune exec -- bin/run_tests.exe`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Source info (E38 completion)
|
||||
|
||||
Design: `plans/designs/e38-sourceinfo.md`. Target: 4/4 sourceInfo tests.
|
||||
|
||||
The API (`hs-parse-ast`, `hs-source-for`, `hs-line-for`, `hs-node-get`, `hs-src`,
|
||||
`hs-src-at`, `hs-line-at`) and parser span wrapping (`hs-ast-wrap`, `hs-span-mode`)
|
||||
are already in the codebase. Two tests are passing; two fail because:
|
||||
- Tokenizer tokens lack `:end` and `:line` (only `:pos` today).
|
||||
- Some statement-level spans and `:next` field navigation are incomplete.
|
||||
|
||||
### Step 3: Tokenizer — add `:end` and `:line` to tokens
|
||||
|
||||
`lib/hyperscript/tokenizer.sx`: extend `hs-make-token` to `{:pos :end :value :type :line}`.
|
||||
Track a `current-line` counter (1-based, increments after `\n`). Update all ~20 emission
|
||||
sites. Mirror to `shared/static/wasm/sx/hs-tokenizer.sx` after edits.
|
||||
|
||||
**Verify:** `(hs-make-token "NUMBER" "1" 0)` returns a dict with `:end` and `:line` keys.
|
||||
|
||||
### Step 4: Complete parser spans + :next field
|
||||
|
||||
`lib/hyperscript/parser.sx`: ensure `hs-ast-wrap` populates `:next` on every command
|
||||
in a `CommandList` (i.e. the following sibling command). Check that statement-level
|
||||
productions (if, for) correctly populate `:true-branch`. Trace through the two failing
|
||||
tests (`get source works for expressions`, `get line works for statements`) to find the
|
||||
exact missing fields or off-by-one positions.
|
||||
|
||||
Mirror to `shared/static/wasm/sx/hs-parser.sx`.
|
||||
|
||||
**Verify:** All 4 `hs-upstream-core/sourceInfo` tests pass.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Native ADTs (`define-type` / `match`)
|
||||
|
||||
Design: `plans/designs/sx-adt.md`. No existing implementation.
|
||||
|
||||
Impact: every language implementation (Haskell, Prolog, Lua, Common Lisp, Erlang)
|
||||
currently fakes sum types with `{:tag "..." :field ...}` dicts. Native ADTs remove
|
||||
that everywhere.
|
||||
|
||||
### Step 5: OCaml — AdtValue type + `define-type` + basic `match`
|
||||
|
||||
`hosts/ocaml/lib/sx_types.ml`:
|
||||
```ocaml
|
||||
type adt_value = { av_type: string; av_ctor: string; av_fields: value array }
|
||||
| AdtValue of adt_value
|
||||
```
|
||||
|
||||
`hosts/ocaml/lib/sx_runtime.ml` (or evaluator):
|
||||
- `step-sf-define-type`: parse `(Name (Ctor1 f1 f2) (Ctor2) ...)`, register constructor
|
||||
NativeFns, predicates (`Ctor1?`, `Name?`), field accessors (`Ctor1-f1`) via `env-bind!`.
|
||||
- `step-sf-match` + `MatchFrame`: linear scan of clauses; flat patterns only for 6a;
|
||||
bind pattern variables in child env; `else` clause; raise on no match.
|
||||
- `type-of` returns the type name (e.g. `"Maybe"`).
|
||||
|
||||
Write tests in `spec/tests/test-adt.sx`: basic constructor, predicate, accessor, match,
|
||||
else, no-match raise.
|
||||
|
||||
**Verify:** `dune exec -- bin/run_tests.exe` — new test file all green.
|
||||
|
||||
### Step 6: JS — AdtValue + `define-type` + `match`
|
||||
|
||||
`hosts/javascript/platform.py`: add `AdtValue` as `{ _adt: true, _type, _ctor, _fields }`.
|
||||
Mirror `define-type` and `match` special forms in the JS evaluator.
|
||||
Retranspile: `python3 hosts/javascript/cli.py --output shared/static/scripts/sx-browser.js`
|
||||
|
||||
**Verify:** `node hosts/javascript/run_tests.js` — adt tests pass on JS too.
|
||||
|
||||
### Step 7: Nested patterns (Phase 6b)
|
||||
|
||||
Both OCaml and JS `MatchFrame`: replace linear binding with recursive
|
||||
`matchPattern(pattern, value, env)` that:
|
||||
- Recurses into constructor sub-patterns.
|
||||
- Returns `{matched: bool, bindings: map}`.
|
||||
- Handles wildcard `_`, literals (`42`, `"str"`, `true`, `nil`).
|
||||
|
||||
Extend `spec/tests/test-adt.sx` with nested pattern tests.
|
||||
|
||||
### Step 8: Exhaustiveness warnings (Phase 6c)
|
||||
|
||||
`_adt_registry: type_name → [ctor_names]` global populated by `define-type`.
|
||||
On first non-exhaustive `match` evaluation: `console.warn("[sx] match: non-exhaustive …")`.
|
||||
No error — warning only.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Plugin / extension system
|
||||
|
||||
Design: `plans/designs/hs-plugin-system.md`.
|
||||
|
||||
### Step 9: Parser feature registry
|
||||
|
||||
`lib/hyperscript/parser.sx`: replace `parse-feat` hardcoded `cond` with a dict lookup.
|
||||
`(hs-register-feature! name parse-fn)` adds to the registry.
|
||||
|
||||
### Step 10: Compiler command registry + `as` converter registry
|
||||
|
||||
`lib/hyperscript/compiler.sx`: replace `hs-to-sx` hardcoded dispatch with dict.
|
||||
`(hs-register-command! name compile-fn)` and `(hs-register-converter! name convert-fn)`.
|
||||
|
||||
### Step 11: Migrate hs-prolog-hook + Worker plugin
|
||||
|
||||
`lib/hyperscript/runtime.sx`: remove `hs-prolog-hook`/`hs-set-prolog-hook!` ad-hoc
|
||||
slots. Create `lib/hyperscript/plugins/prolog.sx` that calls `hs-register-feature!`
|
||||
and `hs-register-command!`. Create `lib/hyperscript/plugins/worker.sx` replacing the
|
||||
E39 stub.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — Performance
|
||||
|
||||
These are incremental and can interleave with other phases.
|
||||
|
||||
### Step 12: Frame records (CEK)
|
||||
|
||||
`hosts/ocaml/lib/sx_runtime.ml`: represent CEK frames as OCaml records instead of
|
||||
tagged variant lists. Eliminates allocation pressure from list construction per frame.
|
||||
Profile before/after on a tight-loop benchmark.
|
||||
|
||||
### Step 13: Buffer primitive for string building
|
||||
|
||||
Add `make-buffer`, `buffer-append!`, `buffer->string` primitives. Eliminates the
|
||||
`(str a b c d ...)` quadratic allocation pattern in serializers and renderers.
|
||||
Wire into `sx_primitives.ml` and the JS platform.
|
||||
|
||||
### Step 14: Inline common primitives in JIT
|
||||
|
||||
`hosts/ocaml/lib/sx_vm.ml`: add `OP_ADD`, `OP_SUB`, `OP_EQ`, `OP_APPEND` specialised
|
||||
opcodes that skip the primitive table lookup for the most common calls. Compiler emits
|
||||
these when operands are known numbers/lists.
|
||||
|
||||
---
|
||||
|
||||
## Progress log
|
||||
|
||||
| Step | Status | Commit |
|
||||
|------|--------|--------|
|
||||
| 1 — JIT combinator bug | [ ] | — |
|
||||
| 2 — letrec+resume | [ ] | — |
|
||||
| 3 — tokenizer :end/:line | [ ] | — |
|
||||
| 4 — parser spans complete | [ ] | — |
|
||||
| 5 — OCaml AdtValue + define-type + match | [ ] | — |
|
||||
| 6 — JS AdtValue + define-type + match | [ ] | — |
|
||||
| 7 — nested patterns | [ ] | — |
|
||||
| 8 — exhaustiveness warnings | [ ] | — |
|
||||
| 9 — parser feature registry | [ ] | — |
|
||||
| 10 — compiler + as converter registry | [ ] | — |
|
||||
| 11 — plugin migration + worker | [ ] | — |
|
||||
| 12 — frame records | [ ] | — |
|
||||
| 13 — buffer primitive | [ ] | — |
|
||||
| 14 — inline primitives JIT | [ ] | — |
|
||||
|
||||
---
|
||||
|
||||
## Rules
|
||||
|
||||
- Branch: `architecture`. Never push to `main`.
|
||||
- SX files: `sx-tree` MCP tools only. `sx_validate` after every edit.
|
||||
- After every `.sx` edit to `lib/hyperscript/`, mirror to `shared/static/wasm/sx/hs-<file>.sx`.
|
||||
- OCaml build: `sx_build target="ocaml"` MCP tool (never raw `dune`).
|
||||
- JS build: `sx_build target="js"` MCP tool.
|
||||
- One step per commit. Update progress log in this file.
|
||||
- No new planning docs. No comments in SX unless non-obvious.
|
||||
- Unicode in SX: raw UTF-8 only, never `\uXXXX`.
|
||||
@@ -105,7 +105,9 @@ just Tcl.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Optional: env-as-value (architectural)
|
||||
## Phase 4 — env-as-value (architectural) ✓
|
||||
|
||||
|
||||
|
||||
`uplevel`/`upvar` required an explicit frame stack because SX environments
|
||||
aren't inspectable from user code. Adding:
|
||||
@@ -146,6 +148,7 @@ becomes a lasting SX contribution used by every future hosted language.
|
||||
|
||||
_Newest first._
|
||||
|
||||
- 2026-05-06: Phase 4 env-as-value — current-env (special form via Sx_ref.register_special_form), eval-in-env (primitive in setup_evaluator_bridge), env-lookup + env-extend (in setup_env_operations); 5 idiom tests; 342/342 green
|
||||
- 2026-05-06: Phase 3 OCaml primitives — file-read/write/append/exists?/glob + clock-seconds/milliseconds/format in sx_primitives.ml + unix dep; tcl-cmd-clock/file wired up; 337/337 green
|
||||
- 2026-05-06: Phase 2 coroutine rewrite — `tcl-cmd-coroutine` now creates a `make-fiber`; `tcl-cmd-yield` calls `:coro-yield-fn` (threaded through interp); true suspension; 337/337 green
|
||||
- 2026-05-06: Phase 2 fiber.sx — `make-fiber`/`fiber-resume`/`fiber-done?` using call/cc + set!; bidirectional value passing; generator and echo tests pass
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract _hyperscript upstream tests into spec/tests/hyperscript-upstream-tests.json.
|
||||
|
||||
Walks /tmp/hs-upstream/test/**/*.js, finds every test('name', ...) call, extracts:
|
||||
- category from file path (test/core/tokenizer.js → "core/tokenizer")
|
||||
- name from first arg
|
||||
- body from arrow function body (between outer { and })
|
||||
- html from preceding test.use({html: '...'}) if any
|
||||
- async from whether the arrow function is async
|
||||
- complexity heuristic — eval-only / event-driven / dom
|
||||
|
||||
Output: spec/tests/hyperscript-upstream-tests.json (overwrites)
|
||||
|
||||
Run after: cd /tmp && git clone --depth 1 https://github.com/bigskysoftware/_hyperscript hs-upstream
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
UPSTREAM = Path('/tmp/hs-upstream/test')
|
||||
OUT = Path(__file__).parent.parent / 'spec/tests/hyperscript-upstream-tests.json'
|
||||
|
||||
|
||||
def find_matching_brace(src, open_idx):
|
||||
"""Return index of matching close brace for { at open_idx. Handles strings/comments."""
|
||||
assert src[open_idx] == '{'
|
||||
depth = 0
|
||||
i = open_idx
|
||||
n = len(src)
|
||||
while i < n:
|
||||
c = src[i]
|
||||
if c == '{':
|
||||
depth += 1
|
||||
elif c == '}':
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return i
|
||||
elif c == '"' or c == "'" or c == '`':
|
||||
# skip string
|
||||
quote = c
|
||||
i += 1
|
||||
while i < n and src[i] != quote:
|
||||
if src[i] == '\\':
|
||||
i += 2
|
||||
continue
|
||||
if quote == '`' and src[i] == '$' and i + 1 < n and src[i+1] == '{':
|
||||
# template literal interpolation — skip nested braces
|
||||
nested = find_matching_brace(src, i + 1)
|
||||
i = nested + 1
|
||||
continue
|
||||
i += 1
|
||||
elif c == '/' and i + 1 < n:
|
||||
nxt = src[i+1]
|
||||
if nxt == '/':
|
||||
# line comment
|
||||
while i < n and src[i] != '\n':
|
||||
i += 1
|
||||
continue
|
||||
elif nxt == '*':
|
||||
# block comment
|
||||
i += 2
|
||||
while i < n - 1 and not (src[i] == '*' and src[i+1] == '/'):
|
||||
i += 1
|
||||
i += 1
|
||||
i += 1
|
||||
raise ValueError(f"unbalanced brace at {open_idx}")
|
||||
|
||||
|
||||
def extract_tests(src, category):
|
||||
"""Find test('name', async/non-async ({...}) => { body }) patterns."""
|
||||
tests = []
|
||||
i = 0
|
||||
n = len(src)
|
||||
test_re = re.compile(r"\btest\s*\(\s*(['\"])((?:[^\\]|\\.)*?)\1\s*,\s*(async\s+)?(\([^)]*\))\s*=>\s*\{")
|
||||
for m in test_re.finditer(src):
|
||||
name = m.group(2)
|
||||
# Unescape quotes
|
||||
name = name.replace("\\'", "'").replace('\\"', '"').replace('\\\\', '\\')
|
||||
is_async = m.group(3) is not None
|
||||
body_open = src.index('{', m.end() - 1)
|
||||
try:
|
||||
body_close = find_matching_brace(src, body_open)
|
||||
except ValueError:
|
||||
continue
|
||||
body = src[body_open + 1:body_close]
|
||||
# Heuristic complexity classification
|
||||
complexity = 'eval-only'
|
||||
if 'html(' in body or 'find(' in body:
|
||||
complexity = 'dom'
|
||||
if 'click(' in body or 'dispatch' in body:
|
||||
complexity = 'event-driven'
|
||||
tests.append({
|
||||
'category': category,
|
||||
'name': name,
|
||||
'html': '',
|
||||
'body': body,
|
||||
'async': is_async,
|
||||
'complexity': complexity,
|
||||
})
|
||||
return tests
|
||||
|
||||
|
||||
def main():
|
||||
import sys
|
||||
if not UPSTREAM.exists():
|
||||
print(f"ERROR: {UPSTREAM} not found. Clone first:")
|
||||
print(" git clone --depth 1 https://github.com/bigskysoftware/_hyperscript /tmp/hs-upstream")
|
||||
return 1
|
||||
|
||||
merge_mode = '--replace' not in sys.argv
|
||||
|
||||
all_tests = []
|
||||
skipped_files = []
|
||||
|
||||
for path in sorted(UPSTREAM.rglob('*.js')):
|
||||
if path.name in {'fixtures.js', 'entry.js', 'global-setup.js', 'global-teardown.js',
|
||||
'htmx-fixtures.js', 'playwright.config.js'}:
|
||||
continue
|
||||
|
||||
rel = path.relative_to(UPSTREAM)
|
||||
category = str(rel.with_suffix('')).replace('\\', '/')
|
||||
for prefix in ('commands/', 'features/'):
|
||||
if category.startswith(prefix):
|
||||
category = category[len(prefix):]
|
||||
break
|
||||
|
||||
try:
|
||||
src = path.read_text()
|
||||
except Exception as e:
|
||||
skipped_files.append((path, str(e)))
|
||||
continue
|
||||
|
||||
all_tests.extend(extract_tests(src, category))
|
||||
|
||||
print(f"Extracted {len(all_tests)} tests from {len(list(UPSTREAM.rglob('*.js')))} files")
|
||||
if skipped_files:
|
||||
print(f"Skipped {len(skipped_files)} files due to errors")
|
||||
|
||||
if not OUT.exists():
|
||||
OUT.write_text(json.dumps(all_tests, indent=2))
|
||||
print(f"\nWrote {OUT} (no existing snapshot)")
|
||||
return 0
|
||||
|
||||
old = json.loads(OUT.read_text())
|
||||
old_by_key = {(t['category'], t['name']): t for t in old}
|
||||
new_keys = set((t['category'], t['name']) for t in all_tests)
|
||||
old_keys = set(old_by_key)
|
||||
added_keys = new_keys - old_keys
|
||||
removed_keys = old_keys - new_keys
|
||||
|
||||
print(f"\nDelta vs existing snapshot ({len(old)} tests):")
|
||||
print(f" +{len(added_keys)} new")
|
||||
print(f" -{len(removed_keys)} removed/renamed")
|
||||
if added_keys:
|
||||
print("\nNew tests:")
|
||||
for cat, name in sorted(added_keys):
|
||||
print(f" [{cat}] {name}")
|
||||
if removed_keys:
|
||||
print("\nRemoved/renamed tests (first 20):")
|
||||
for cat, name in sorted(removed_keys)[:20]:
|
||||
print(f" [{cat}] {name}")
|
||||
|
||||
if merge_mode:
|
||||
# Merge mode (default): preserve existing test bodies, only add new tests.
|
||||
# The old snapshot's bodies were curated/cleaned — re-extracting from raw
|
||||
# upstream JS produces slightly different bodies that may not auto-translate.
|
||||
# New tests get the raw extracted body; existing tests keep theirs.
|
||||
new_by_key = {(t['category'], t['name']): t for t in all_tests}
|
||||
merged = list(old) # preserves original order
|
||||
for k in sorted(added_keys):
|
||||
merged.append(new_by_key[k])
|
||||
OUT.write_text(json.dumps(merged, indent=2))
|
||||
print(f"\nMerged: {len(merged)} tests ({len(old)} existing + {len(added_keys)} new) → {OUT}")
|
||||
print(" (rerun with --replace to discard old bodies and use raw upstream)")
|
||||
else:
|
||||
OUT.write_text(json.dumps(all_tests, indent=2))
|
||||
print(f"\nReplaced: {len(all_tests)} tests → {OUT}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
File diff suppressed because one or more lines are too long
@@ -1211,7 +1211,7 @@
|
||||
"category": "core/liveTemplate",
|
||||
"name": "scope is refreshed after morph so surviving elements get updated indices",
|
||||
"html": "\n\t\t\t<script type=\"text/hyperscript-template\" live>\n\t\t\t\t<ul>\n\t\t\t\t#for item in $morphItems index i\n\t\t\t\t\t<li _=\"on click put i + ':' + item.name into me\">${}{item.name}</li>\n\t\t\t\t#end\n\t\t\t\t</ul>\n\t\t\t</script>\n\t\t",
|
||||
"body": "\n\t\tawait run(\"set $morphItems to [{name:'A'},{name:'B'},{name:'C'}]\")\n\t\tawait html(`\n\t\t\t<script type=\"text/hyperscript-template\" live>\n\t\t\t\t<ul>\n\t\t\t\t#for item in $morphItems index i\n\t\t\t\t\t<li _=\"on click put i + ':' + item.name into me\">${\"\\x24\"}{item.name}</li>\n\t\t\t\t#end\n\t\t\t\t</ul>\n\t\t\t</script>\n\t\t`)\n\t\tawait expect.poll(() => find('[data-live-template] li').count()).toBe(3)\n\t\t// Verify initial scope: clicking C should show \"2:C\"\n\t\tawait find('[data-live-template] li').last().click()\n\t\tawait expect(find('[data-live-template] li').last()).toHaveText('2:C')\n\t\t// Remove B \u2014 C shifts from index 2 to index 1\n\t\tawait run(\"call $morphItems.splice(1, 1)\")\n\t\tawait expect.poll(() => find('[data-live-template] li').count()).toBe(2)\n\t\t// After morph, C's scope should be refreshed: now \"1:C\"\n\t\tawait find('[data-live-template] li').last().click()\n\t\tawait expect(find('[data-live-template] li').last()).toHaveText('1:C')\n\t",
|
||||
"body": "\n\t\tawait run(\"set $morphItems to [{name:'A'},{name:'B'},{name:'C'}]\")\n\t\tawait html(`\n\t\t\t<script type=\"text/hyperscript-template\" live>\n\t\t\t\t<ul>\n\t\t\t\t#for item in $morphItems index i\n\t\t\t\t\t<li _=\"on click put i + ':' + item.name into me\">${\"\\x24\"}{item.name}</li>\n\t\t\t\t#end\n\t\t\t\t</ul>\n\t\t\t</script>\n\t\t`)\n\t\tawait expect.poll(() => find('[data-live-template] li').count()).toBe(3)\n\t\t// Verify initial scope: clicking C should show \"2:C\"\n\t\tawait find('[data-live-template] li').last().click()\n\t\tawait expect(find('[data-live-template] li').last()).toHaveText('2:C')\n\t\t// Remove B — C shifts from index 2 to index 1\n\t\tawait run(\"call $morphItems.splice(1, 1)\")\n\t\tawait expect.poll(() => find('[data-live-template] li').count()).toBe(2)\n\t\t// After morph, C's scope should be refreshed: now \"1:C\"\n\t\tawait find('[data-live-template] li').last().click()\n\t\tawait expect(find('[data-live-template] li').last()).toHaveText('1:C')\n\t",
|
||||
"async": true,
|
||||
"complexity": "simple"
|
||||
},
|
||||
@@ -1369,7 +1369,7 @@
|
||||
},
|
||||
{
|
||||
"category": "core/reactivity",
|
||||
"name": "NaN \u2192 NaN does not retrigger handlers (Object.is semantics)",
|
||||
"name": "NaN → NaN does not retrigger handlers (Object.is semantics)",
|
||||
"html": "<div _=\"when $rxNanVal changes increment $rxNanCount\"></div>",
|
||||
"body": "\n\t\tawait evaluate(() => { window.$rxNanCount = 0; window.$rxNanVal = NaN })\n\t\tawait html(`<div _=\"when $rxNanVal changes increment $rxNanCount\"></div>`)\n\t\t// Initial evaluate should not fire handler because NaN is \"null-ish\" in _lastValue init?\n\t\t// It actually DOES fire (initialize sees non-null). Snapshot and compare.\n\t\tvar initial = await evaluate(() => window.$rxNanCount)\n\n\t\tawait run(\"set $rxNanVal to NaN\")\n\t\t// Give the microtask a chance to run\n\t\tawait evaluate(() => new Promise(r => setTimeout(r, 20)))\n\t\texpect(await evaluate(() => window.$rxNanCount)).toBe(initial)\n\n\t\t// But changing to a real number should fire\n\t\tawait run(\"set $rxNanVal to 42\")\n\t\tawait expect.poll(() => evaluate(() => window.$rxNanCount)).toBe(initial + 1)\n\n\t\tawait evaluate(() => { delete window.$rxNanCount; delete window.$rxNanVal })\n\t",
|
||||
"async": true,
|
||||
@@ -1379,7 +1379,7 @@
|
||||
"category": "core/reactivity",
|
||||
"name": "effect switches its dependencies based on control flow",
|
||||
"html": "<div _=\"live if $rxCond put $rxA into me else put $rxB into me end end\"></div>",
|
||||
"body": "\n\t\tawait evaluate(() => {\n\t\t\twindow.$rxCond = true\n\t\t\twindow.$rxA = 'from-a'\n\t\t\twindow.$rxB = 'from-b'\n\t\t})\n\t\tawait html(\n\t\t\t`<div _=\"live if $rxCond put $rxA into me else put $rxB into me end end\"></div>`\n\t\t)\n\t\tawait expect(find('div')).toHaveText('from-a')\n\n\t\t// While cond is true, changing $rxB should NOT retrigger\n\t\tawait run(\"set $rxB to 'ignored'\")\n\t\tawait evaluate(() => new Promise(r => setTimeout(r, 20)))\n\t\tawait expect(find('div')).toHaveText('from-a')\n\n\t\t// Switch cond \u2192 effect now depends on $rxB\n\t\tawait run(\"set $rxCond to false\")\n\t\tawait expect.poll(() => find('div').textContent()).toBe('ignored')\n\n\t\t// Now $rxA changes should be ignored, $rxB changes should fire\n\t\tawait run(\"set $rxA to 'a-ignored'\")\n\t\tawait evaluate(() => new Promise(r => setTimeout(r, 20)))\n\t\tawait expect(find('div')).toHaveText('ignored')\n\n\t\tawait run(\"set $rxB to 'new-b'\")\n\t\tawait expect.poll(() => find('div').textContent()).toBe('new-b')\n\n\t\tawait evaluate(() => {\n\t\t\tdelete window.$rxCond; delete window.$rxA; delete window.$rxB\n\t\t})\n\t",
|
||||
"body": "\n\t\tawait evaluate(() => {\n\t\t\twindow.$rxCond = true\n\t\t\twindow.$rxA = 'from-a'\n\t\t\twindow.$rxB = 'from-b'\n\t\t})\n\t\tawait html(\n\t\t\t`<div _=\"live if $rxCond put $rxA into me else put $rxB into me end end\"></div>`\n\t\t)\n\t\tawait expect(find('div')).toHaveText('from-a')\n\n\t\t// While cond is true, changing $rxB should NOT retrigger\n\t\tawait run(\"set $rxB to 'ignored'\")\n\t\tawait evaluate(() => new Promise(r => setTimeout(r, 20)))\n\t\tawait expect(find('div')).toHaveText('from-a')\n\n\t\t// Switch cond → effect now depends on $rxB\n\t\tawait run(\"set $rxCond to false\")\n\t\tawait expect.poll(() => find('div').textContent()).toBe('ignored')\n\n\t\t// Now $rxA changes should be ignored, $rxB changes should fire\n\t\tawait run(\"set $rxA to 'a-ignored'\")\n\t\tawait evaluate(() => new Promise(r => setTimeout(r, 20)))\n\t\tawait expect(find('div')).toHaveText('ignored')\n\n\t\tawait run(\"set $rxB to 'new-b'\")\n\t\tawait expect.poll(() => find('div').textContent()).toBe('new-b')\n\n\t\tawait evaluate(() => {\n\t\t\tdelete window.$rxCond; delete window.$rxA; delete window.$rxB\n\t\t})\n\t",
|
||||
"async": true,
|
||||
"complexity": "promise"
|
||||
},
|
||||
@@ -5203,7 +5203,7 @@
|
||||
"category": "expressions/not",
|
||||
"name": "not has higher precedence than and",
|
||||
"html": "",
|
||||
"body": "\n\t\t// (not false) and true \u2192 true and true \u2192 true\n\t\texpect(await run(\"not false and true\")).toBe(true)\n\t\t// (not true) and true \u2192 false and true \u2192 false\n\t\texpect(await run(\"not true and true\")).toBe(false)\n\t",
|
||||
"body": "\n\t\t// (not false) and true → true and true → true\n\t\texpect(await run(\"not false and true\")).toBe(true)\n\t\t// (not true) and true → false and true → false\n\t\texpect(await run(\"not true and true\")).toBe(false)\n\t",
|
||||
"async": true,
|
||||
"complexity": "run-eval"
|
||||
},
|
||||
@@ -5211,7 +5211,7 @@
|
||||
"category": "expressions/not",
|
||||
"name": "not has higher precedence than or",
|
||||
"html": "",
|
||||
"body": "\n\t\t// (not true) or true \u2192 false or true \u2192 true\n\t\texpect(await run(\"not true or true\")).toBe(true)\n\t\t// (not false) or false \u2192 true or false \u2192 true\n\t\texpect(await run(\"not false or false\")).toBe(true)\n\t",
|
||||
"body": "\n\t\t// (not true) or true → false or true → true\n\t\texpect(await run(\"not true or true\")).toBe(true)\n\t\t// (not false) or false → true or false → true\n\t\texpect(await run(\"not false or false\")).toBe(true)\n\t",
|
||||
"async": true,
|
||||
"complexity": "run-eval"
|
||||
},
|
||||
@@ -11966,149 +11966,5 @@
|
||||
"body": "\n\t\t// The core bundle only ships a stub; the actual worker plugin is\n\t\t// a separate ext that must be loaded. Without it, parsing should\n\t\t// fail with a message pointing the user to the docs.\n\t\tconst msg = await error(\"worker MyWorker def noop() end end\")\n\t\texpect(msg).toContain('worker plugin')\n\t\texpect(msg).toContain('hyperscript.org/features/worker')\n\t",
|
||||
"async": true,
|
||||
"complexity": "simple"
|
||||
},
|
||||
{
|
||||
"category": "core/tokenizer",
|
||||
"name": "clearFollows/restoreFollows round-trip the follow set",
|
||||
"html": "",
|
||||
"body": "\n\t\tconst results = await evaluate(() => {\n\t\t\tconst t = _hyperscript.internals.tokenizer;\n\t\t\tconst tokens = t.tokenize(\"and and and\");\n\t\t\ttokens.pushFollow(\"and\");\n\t\t\tconst saved = tokens.clearFollows();\n\t\t\tconst allowedWhileCleared = tokens.matchToken(\"and\")?.value ?? null;\n\t\t\ttokens.restoreFollows(saved);\n\t\t\tconst blockedAfterRestore = tokens.matchToken(\"and\") ?? null;\n\t\t\treturn {allowedWhileCleared, blockedAfterRestore};\n\t\t});\n\t\texpect(results.allowedWhileCleared).toBe(\"and\");\n\t\texpect(results.blockedAfterRestore).toBeNull();\n\t",
|
||||
"async": true,
|
||||
"complexity": "eval-only"
|
||||
},
|
||||
{
|
||||
"category": "core/tokenizer",
|
||||
"name": "consumeUntil collects tokens up to a marker",
|
||||
"html": "",
|
||||
"body": "\n\t\tconst results = await evaluate(() => {\n\t\t\tconst t = _hyperscript.internals.tokenizer;\n\t\t\tconst tokens = t.tokenize(\"a b c end d\");\n\t\t\t// consumeUntil collects every intervening token, whitespace included\n\t\t\tconst collected = tokens.consumeUntil(\"end\")\n\t\t\t\t.filter(tok => tok.type !== \"WHITESPACE\")\n\t\t\t\t.map(tok => tok.value);\n\t\t\tconst landed = tokens.currentToken().value;\n\t\t\treturn {collected, landed};\n\t\t});\n\t\texpect(results.collected).toEqual([\"a\", \"b\", \"c\"]);\n\t\texpect(results.landed).toBe(\"end\");\n\t",
|
||||
"async": true,
|
||||
"complexity": "eval-only"
|
||||
},
|
||||
{
|
||||
"category": "core/tokenizer",
|
||||
"name": "consumeUntilWhitespace stops at first whitespace",
|
||||
"html": "",
|
||||
"body": "\n\t\tconst results = await evaluate(() => {\n\t\t\tconst t = _hyperscript.internals.tokenizer;\n\t\t\tconst tokens = t.tokenize(\"foo.bar more\");\n\t\t\tconst collected = tokens.consumeUntilWhitespace().map(tok => tok.value);\n\t\t\tconst landed = tokens.currentToken().value;\n\t\t\treturn {collected, landed};\n\t\t});\n\t\t// consumeUntilWhitespace stops at the space between foo.bar and more\n\t\texpect(results.collected).toEqual([\"foo\", \".\", \"bar\"]);\n\t\texpect(results.landed).toBe(\"more\");\n\t",
|
||||
"async": true,
|
||||
"complexity": "eval-only"
|
||||
},
|
||||
{
|
||||
"category": "core/tokenizer",
|
||||
"name": "lastMatch returns the last consumed token",
|
||||
"html": "",
|
||||
"body": "\n\t\tconst results = await evaluate(() => {\n\t\t\tconst t = _hyperscript.internals.tokenizer;\n\t\t\tconst tokens = t.tokenize(\"foo bar baz\");\n\t\t\tconst r = {};\n\t\t\tr.before = tokens.lastMatch() ?? null;\n\t\t\ttokens.consumeToken();\n\t\t\tr.afterFoo = tokens.lastMatch()?.value ?? null;\n\t\t\ttokens.consumeToken();\n\t\t\tr.afterBar = tokens.lastMatch()?.value ?? null;\n\t\t\treturn r;\n\t\t});\n\t\texpect(results.before).toBeNull();\n\t\texpect(results.afterFoo).toBe(\"foo\");\n\t\texpect(results.afterBar).toBe(\"bar\");\n\t",
|
||||
"async": true,
|
||||
"complexity": "eval-only"
|
||||
},
|
||||
{
|
||||
"category": "core/tokenizer",
|
||||
"name": "lastWhitespace reflects whitespace before the current token",
|
||||
"html": "",
|
||||
"body": "\n\t\tconst results = await evaluate(() => {\n\t\t\tconst t = _hyperscript.internals.tokenizer;\n\t\t\tconst tokens = t.tokenize(\"foo bar\\n\\tbaz\");\n\t\t\tconst r = {};\n\t\t\t// Before any consume, no whitespace has been consumed yet\n\t\t\tr.initial = tokens.lastWhitespace();\n\t\t\ttokens.consumeToken(); // foo \u2192 consumes trailing whitespace \" \"\n\t\t\tr.afterFoo = tokens.lastWhitespace();\n\t\t\ttokens.consumeToken(); // bar \u2192 consumes \"\\n\\t\"\n\t\t\tr.afterBar = tokens.lastWhitespace();\n\t\t\treturn r;\n\t\t});\n\t\texpect(results.initial).toBe(\"\");\n\t\texpect(results.afterFoo).toBe(\" \");\n\t\texpect(results.afterBar).toBe(\"\\n\\t\");\n\t",
|
||||
"async": true,
|
||||
"complexity": "eval-only"
|
||||
},
|
||||
{
|
||||
"category": "core/tokenizer",
|
||||
"name": "matchAnyToken and matchAnyOpToken try each option",
|
||||
"html": "",
|
||||
"body": "\n\t\tconst results = await evaluate(() => {\n\t\t\tconst t = _hyperscript.internals.tokenizer;\n\t\t\tconst tokens = t.tokenize(\"bar + baz\");\n\t\t\treturn {\n\t\t\t\tanyTok: tokens.matchAnyToken(\"foo\", \"bar\", \"baz\")?.value ?? null,\n\t\t\t\tanyOp: tokens.matchAnyOpToken(\"-\", \"+\")?.value ?? null,\n\t\t\t\tanyTokMiss: tokens.matchAnyToken(\"foo\", \"quux\") ?? null,\n\t\t\t};\n\t\t});\n\t\texpect(results.anyTok).toBe(\"bar\");\n\t\texpect(results.anyOp).toBe(\"+\");\n\t\texpect(results.anyTokMiss).toBeNull();\n\t",
|
||||
"async": true,
|
||||
"complexity": "eval-only"
|
||||
},
|
||||
{
|
||||
"category": "core/tokenizer",
|
||||
"name": "matchOpToken matches operators by value",
|
||||
"html": "",
|
||||
"body": "\n\t\tconst results = await evaluate(() => {\n\t\t\tconst t = _hyperscript.internals.tokenizer;\n\t\t\tconst tokens = t.tokenize(\"+ - *\");\n\t\t\treturn [\n\t\t\t\ttokens.matchOpToken(\"-\") ?? null, // next is +, miss\n\t\t\t\ttokens.matchOpToken(\"+\")?.value ?? null,\n\t\t\t\ttokens.matchOpToken(\"-\")?.value ?? null,\n\t\t\t\ttokens.matchOpToken(\"*\")?.value ?? null,\n\t\t\t];\n\t\t});\n\t\texpect(results[0]).toBeNull();\n\t\texpect(results[1]).toBe(\"+\");\n\t\texpect(results[2]).toBe(\"-\");\n\t\texpect(results[3]).toBe(\"*\");\n\t",
|
||||
"async": true,
|
||||
"complexity": "eval-only"
|
||||
},
|
||||
{
|
||||
"category": "core/tokenizer",
|
||||
"name": "matchToken consumes and returns on match",
|
||||
"html": "",
|
||||
"body": "\n\t\tconst results = await evaluate(() => {\n\t\t\tconst t = _hyperscript.internals.tokenizer;\n\t\t\tconst tokens = t.tokenize(\"foo bar baz\");\n\t\t\tconst r = {};\n\t\t\tr.match = tokens.matchToken(\"foo\")?.value ?? null;\n\t\t\tr.miss = tokens.matchToken(\"baz\") ?? null; // next is \"bar\", miss\n\t\t\tr.next = tokens.currentToken().value;\n\t\t\tr.match2 = tokens.matchToken(\"bar\")?.value ?? null;\n\t\t\treturn r;\n\t\t});\n\t\texpect(results.match).toBe(\"foo\");\n\t\texpect(results.miss).toBeNull();\n\t\texpect(results.next).toBe(\"bar\");\n\t\texpect(results.match2).toBe(\"bar\");\n\t",
|
||||
"async": true,
|
||||
"complexity": "eval-only"
|
||||
},
|
||||
{
|
||||
"category": "core/tokenizer",
|
||||
"name": "matchToken honors the follow set",
|
||||
"html": "",
|
||||
"body": "\n\t\tconst results = await evaluate(() => {\n\t\t\tconst t = _hyperscript.internals.tokenizer;\n\t\t\tconst tokens = t.tokenize(\"and then\");\n\t\t\ttokens.pushFollow(\"and\");\n\t\t\tconst blocked = tokens.matchToken(\"and\") ?? null;\n\t\t\ttokens.popFollow();\n\t\t\tconst allowed = tokens.matchToken(\"and\")?.value ?? null;\n\t\t\treturn {blocked, allowed};\n\t\t});\n\t\texpect(results.blocked).toBeNull();\n\t\texpect(results.allowed).toBe(\"and\");\n\t",
|
||||
"async": true,
|
||||
"complexity": "eval-only"
|
||||
},
|
||||
{
|
||||
"category": "core/tokenizer",
|
||||
"name": "matchTokenType matches by type",
|
||||
"html": "",
|
||||
"body": "\n\t\tconst results = await evaluate(() => {\n\t\t\tconst t = _hyperscript.internals.tokenizer;\n\t\t\tconst tokens = t.tokenize(\"foo 42\");\n\t\t\tconst r = {};\n\t\t\tr.ident = tokens.matchTokenType(\"IDENTIFIER\")?.value ?? null;\n\t\t\tr.numMiss = tokens.matchTokenType(\"STRING\") ?? null;\n\t\t\tr.numOneOf = tokens.matchTokenType(\"STRING\", \"NUMBER\")?.value ?? null;\n\t\t\treturn r;\n\t\t});\n\t\texpect(results.ident).toBe(\"foo\");\n\t\texpect(results.numMiss).toBeNull();\n\t\texpect(results.numOneOf).toBe(\"42\");\n\t",
|
||||
"async": true,
|
||||
"complexity": "eval-only"
|
||||
},
|
||||
{
|
||||
"category": "core/tokenizer",
|
||||
"name": "peekToken skips whitespace when looking ahead",
|
||||
"html": "",
|
||||
"body": "\n\t\tconst results = await evaluate(() => {\n\t\t\tconst t = _hyperscript.internals.tokenizer;\n\t\t\tconst r = {};\n\n\t\t\t// for x in items \u2192 tokens are: for, WS, x, WS, in, WS, items\n\t\t\tconst forIn = t.tokenize(\"for x in items\");\n\t\t\tr.peek0 = forIn.peekToken(\"for\", 0)?.value ?? null;\n\t\t\tr.peek1 = forIn.peekToken(\"x\", 1)?.value ?? null;\n\t\t\tr.peek2 = forIn.peekToken(\"in\", 2)?.value ?? null;\n\t\t\tr.peek3 = forIn.peekToken(\"items\", 3)?.value ?? null;\n\n\t\t\t// peek that shouldn't match\n\t\t\tr.peekMiss = forIn.peekToken(\"in\", 1) ?? null;\n\n\t\t\t// for 10ms \u2014 \"in\" is never present\n\t\t\tconst forDur = t.tokenize(\"for 10ms\");\n\t\t\tr.durPeek2 = forDur.peekToken(\"in\", 2) ?? null;\n\n\t\t\t// Extra whitespace between tokens is tolerated\n\t\t\tconst extraWs = t.tokenize(\"for x in items\");\n\t\t\tr.extraPeek2 = extraWs.peekToken(\"in\", 2)?.value ?? null;\n\n\t\t\t// Comments between tokens are tolerated\n\t\t\tconst withComment = t.tokenize(\"for -- comment\\nx in items\");\n\t\t\tr.commentPeek2 = withComment.peekToken(\"in\", 2)?.value ?? null;\n\n\t\t\t// Newlines as whitespace\n\t\t\tconst multiline = t.tokenize(\"for\\nx\\nin\\nitems\");\n\t\t\tr.multiPeek2 = multiline.peekToken(\"in\", 2)?.value ?? null;\n\n\t\t\t// Type defaults to IDENTIFIER \u2014 matching against an operator requires explicit type\n\t\t\tconst withOp = t.tokenize(\"a + b\");\n\t\t\tr.opDefault = withOp.peekToken(\"+\", 1) ?? null; // IDENTIFIER type, won't match\n\t\t\tr.opExplicit = withOp.peekToken(\"+\", 1, \"PLUS\")?.value ?? null;\n\n\t\t\t// Lookahead past the end returns undefined\n\t\t\tconst short = t.tokenize(\"foo\");\n\t\t\tr.beyondEnd = short.peekToken(\"anything\", 5) ?? null;\n\n\t\t\treturn r;\n\t\t});\n\n\t\texpect(results.peek0).toBe(\"for\");\n\t\texpect(results.peek1).toBe(\"x\");\n\t\texpect(results.peek2).toBe(\"in\");\n\t\texpect(results.peek3).toBe(\"items\");\n\t\texpect(results.peekMiss).toBeNull();\n\t\texpect(results.durPeek2).toBeNull();\n\t\texpect(results.extraPeek2).toBe(\"in\");\n\t\texpect(results.commentPeek2).toBe(\"in\");\n\t\texpect(results.multiPeek2).toBe(\"in\");\n\t\texpect(results.opDefault).toBeNull();\n\t\texpect(results.opExplicit).toBe(\"+\");\n\t\texpect(results.beyondEnd).toBeNull();\n\t",
|
||||
"async": true,
|
||||
"complexity": "eval-only"
|
||||
},
|
||||
{
|
||||
"category": "core/tokenizer",
|
||||
"name": "pushFollow/popFollow nest follow-set boundaries",
|
||||
"html": "",
|
||||
"body": "\n\t\tconst results = await evaluate(() => {\n\t\t\tconst t = _hyperscript.internals.tokenizer;\n\t\t\tconst r = {};\n\t\t\tconst tokens = t.tokenize(\"and or not\");\n\t\t\ttokens.pushFollow(\"and\");\n\t\t\ttokens.pushFollow(\"or\");\n\t\t\tr.andBlocked = tokens.matchToken(\"and\") ?? null;\n\t\t\ttokens.popFollow(); // pops \"or\"\n\t\t\tr.andStillBlocked = tokens.matchToken(\"and\") ?? null;\n\t\t\ttokens.popFollow(); // pops \"and\"\n\t\t\tr.andAllowed = tokens.matchToken(\"and\")?.value ?? null;\n\t\t\treturn r;\n\t\t});\n\t\texpect(results.andBlocked).toBeNull();\n\t\texpect(results.andStillBlocked).toBeNull();\n\t\texpect(results.andAllowed).toBe(\"and\");\n\t",
|
||||
"async": true,
|
||||
"complexity": "eval-only"
|
||||
},
|
||||
{
|
||||
"category": "core/tokenizer",
|
||||
"name": "pushFollows/popFollows push and pop in bulk",
|
||||
"html": "",
|
||||
"body": "\n\t\tconst results = await evaluate(() => {\n\t\t\tconst t = _hyperscript.internals.tokenizer;\n\t\t\tconst tokens = t.tokenize(\"and or\");\n\t\t\tconst count = tokens.pushFollows(\"and\", \"or\");\n\t\t\tconst blocked = tokens.matchToken(\"and\") ?? null;\n\t\t\ttokens.popFollows(count);\n\t\t\tconst allowed = tokens.matchToken(\"and\")?.value ?? null;\n\t\t\treturn {count, blocked, allowed};\n\t\t});\n\t\texpect(results.count).toBe(2);\n\t\texpect(results.blocked).toBeNull();\n\t\texpect(results.allowed).toBe(\"and\");\n\t",
|
||||
"async": true,
|
||||
"complexity": "eval-only"
|
||||
},
|
||||
{
|
||||
"category": "ext/component",
|
||||
"name": "component reads a feature-level set from an enclosing div on first load",
|
||||
"html": "",
|
||||
"body": "\n\t\tawait html(`\n\t\t\t<script type=\"text/hyperscript-template\" component=\"test-plain-card\" _=\"init set ^label to attrs.label\">\n\t\t\t\t<span>${\"\\x24\"}{^label}</span>\n\t\t\t</script>\n\t\t\t<div _=\"set $testLabel to 'hello'\">\n\t\t\t\t<test-plain-card label=\"$testLabel\"></test-plain-card>\n\t\t\t</div>\n\t\t`)\n\t\tawait expect.poll(() => find('test-plain-card span').textContent()).toBe('hello')\n\t\tawait evaluate(() => { delete window.$testLabel })\n\t",
|
||||
"async": true,
|
||||
"complexity": "dom"
|
||||
},
|
||||
{
|
||||
"category": "ext/component",
|
||||
"name": "component reads enclosing scope set by a sibling init on first load",
|
||||
"html": "",
|
||||
"body": "\n\t\tawait html(`\n\t\t\t<script type=\"text/hyperscript-template\" component=\"test-user-card\" _=\"init set ^user to attrs.data\">\n\t\t\t\t<h3>${\"\\x24\"}{^user.name}</h3>\n\t\t\t\t<p>${\"\\x24\"}{^user.email}</p>\n\t\t\t</script>\n\t\t\t<div _=\"init set $testCurrentUser to { name: 'Carson', email: 'carson@example.com' }\">\n\t\t\t\t<test-user-card data=\"$testCurrentUser\"></test-user-card>\n\t\t\t</div>\n\t\t`)\n\t\tawait expect.poll(() => find('test-user-card h3').textContent()).toBe('Carson')\n\t\tawait expect.poll(() => find('test-user-card p').textContent()).toBe('carson@example.com')\n\t\tawait evaluate(() => { delete window.$testCurrentUser })\n\t",
|
||||
"async": true,
|
||||
"complexity": "dom"
|
||||
},
|
||||
{
|
||||
"category": "resize",
|
||||
"name": "on resize from window uses native window resize event",
|
||||
"html": "",
|
||||
"body": "\n\t\tawait html(\n\t\t\t\"<div id='out' _='on resize from window put \\\"fired\\\" into me'></div>\"\n\t\t);\n\t\t// Native window resize isn't a ResizeObserver event; trigger it directly\n\t\tawait page.evaluate(() => {\n\t\t\twindow.dispatchEvent(new Event('resize'));\n\t\t});\n\t\tawait expect(find('#out')).toHaveText(\"fired\");\n\t",
|
||||
"async": true,
|
||||
"complexity": "event-driven"
|
||||
},
|
||||
{
|
||||
"category": "toggle",
|
||||
"name": "toggle between followed by for-in loop works",
|
||||
"html": "",
|
||||
"body": "\n\t\tawait html(\n\t\t\t\"<div id='out'></div>\" +\n\t\t\t\"<div id='btn' class='a' _=\\\"on click \" +\n\t\t\t\" toggle between .a and .b \" +\n\t\t\t\" for x in [1, 2] \" +\n\t\t\t\" put x into #out \" +\n\t\t\t\" end\\\"></div>\"\n\t\t);\n\t\tconst btn = page.locator('#btn');\n\t\tawait btn.dispatchEvent('click');\n\t\tawait expect(btn).toHaveClass(/b/);\n\t\tawait expect(find('#out')).toHaveText('2');\n\t",
|
||||
"async": true,
|
||||
"complexity": "event-driven"
|
||||
},
|
||||
{
|
||||
"category": "toggle",
|
||||
"name": "toggle does not consume a following for-in loop",
|
||||
"html": "",
|
||||
"body": "\n\t\tawait html(\n\t\t\t\"<div id='out'></div>\" +\n\t\t\t\"<div id='btn' _=\\\"on click \" +\n\t\t\t\" toggle .foo \" +\n\t\t\t\" for x in [1, 2, 3] \" +\n\t\t\t\" put x into #out \" +\n\t\t\t\" end\\\"></div>\"\n\t\t);\n\t\tconst btn = page.locator('#btn');\n\t\tawait expect(btn).not.toHaveClass(/foo/);\n\t\tawait btn.dispatchEvent('click');\n\t\tawait expect(btn).toHaveClass(/foo/);\n\t\tawait expect(find('#out')).toHaveText('3');\n\t",
|
||||
"async": true,
|
||||
"complexity": "event-driven"
|
||||
}
|
||||
]
|
||||
@@ -1,5 +1,5 @@
|
||||
;; Hyperscript behavioral tests — auto-generated from upstream _hyperscript test suite
|
||||
;; Source: spec/tests/hyperscript-upstream-tests.json (1514 tests, v0.9.14 + dev)
|
||||
;; Source: spec/tests/hyperscript-upstream-tests.json (1496 tests, v0.9.14 + dev)
|
||||
;; DO NOT EDIT — regenerate with: python3 tests/playwright/generate-sx-tests.py
|
||||
|
||||
;; ── Test helpers ──────────────────────────────────────────────────
|
||||
@@ -2587,7 +2587,7 @@
|
||||
(assert= (hs-src "for x in [1, 2, 3] log x then log x end") "for x in [1, 2, 3] log x then log x end"))
|
||||
)
|
||||
|
||||
;; ── core/tokenizer (30 tests) ──
|
||||
;; ── core/tokenizer (17 tests) ──
|
||||
(defsuite "hs-upstream-core/tokenizer"
|
||||
(deftest "handles $ in template properly"
|
||||
(assert= (hs-token-value (hs-stream-token (hs-tokens-of "\"" :template) 0)) "\"")
|
||||
@@ -2876,99 +2876,6 @@
|
||||
(dom-dispatch _el-div "click" nil)
|
||||
(assert= (dom-text-content _el-div) "test${x} test 42 test$x test 42 test $x test ${x} test42 test_42 test_42 test-42 test.42")
|
||||
))
|
||||
(deftest "clearFollows/restoreFollows round-trip the follow set"
|
||||
(let ((s (hs-stream "and or not")))
|
||||
(hs-stream-push-follow! s "and")
|
||||
(hs-stream-push-follow! s "or")
|
||||
(let ((saved (hs-stream-clear-follows! s)))
|
||||
(assert= (get (hs-stream-match s "and") :value) "and")
|
||||
(hs-stream-restore-follows! s saved)
|
||||
(assert (nil? (hs-stream-match s "or")))))
|
||||
)
|
||||
(deftest "consumeUntil collects tokens up to a marker"
|
||||
(let ((s (hs-stream "a b c end d")))
|
||||
(let ((collected (filter (fn (t) (not (= (get t :type) "whitespace")))
|
||||
(hs-stream-consume-until s "end"))))
|
||||
(assert= (map (fn (t) (get t :value)) collected) (list "a" "b" "c"))
|
||||
(assert= (get (hs-stream-current s) :value) "end")))
|
||||
)
|
||||
(deftest "consumeUntilWhitespace stops at first whitespace"
|
||||
(let ((s (hs-stream "abc def")))
|
||||
(let ((collected (hs-stream-consume-until-ws s)))
|
||||
(assert= (len collected) 1)
|
||||
(assert= (get (first collected) :value) "abc")
|
||||
(assert= (get (hs-stream-current s) :value) "def")))
|
||||
)
|
||||
(deftest "lastMatch returns the last consumed token"
|
||||
(let ((s (hs-stream "foo bar baz")))
|
||||
(hs-stream-match s "foo")
|
||||
(assert= (get (hs-stream-last-match s) :value) "foo")
|
||||
(hs-stream-match s "bar")
|
||||
(assert= (get (hs-stream-last-match s) :value) "bar"))
|
||||
)
|
||||
(deftest "lastWhitespace reflects whitespace before the current token"
|
||||
(let ((s (hs-stream "foo bar")))
|
||||
(hs-stream-match s "foo")
|
||||
(hs-stream-skip-ws! s)
|
||||
(assert= (hs-stream-last-ws s) " "))
|
||||
)
|
||||
(deftest "matchAnyToken and matchAnyOpToken try each option"
|
||||
(let ((s (hs-stream "bar + baz")))
|
||||
(assert= (get (hs-stream-match-any s "foo" "bar" "baz") :value) "bar")
|
||||
(assert= (get (hs-stream-match-any-op s "-" "+") :value) "+")
|
||||
(assert (nil? (hs-stream-match-any s "foo" "quux"))))
|
||||
)
|
||||
(deftest "matchOpToken matches operators by value"
|
||||
(let ((s (hs-stream "1 + 2")))
|
||||
(assert= (get (hs-stream-match-type s "NUMBER") :value) "1")
|
||||
(assert= (get (hs-stream-match-any-op s "-" "+") :value) "+"))
|
||||
)
|
||||
(deftest "matchToken consumes and returns on match"
|
||||
(let ((s (hs-stream "foo bar baz")))
|
||||
(assert= (get (hs-stream-match s "foo") :value) "foo")
|
||||
(assert (nil? (hs-stream-match s "baz")))
|
||||
(assert= (get (hs-stream-current s) :value) "bar")
|
||||
(assert= (get (hs-stream-match s "bar") :value) "bar"))
|
||||
)
|
||||
(deftest "matchToken honors the follow set"
|
||||
(let ((s (hs-stream "and or not")))
|
||||
(hs-stream-push-follow! s "and")
|
||||
(assert (nil? (hs-stream-match s "and")))
|
||||
(hs-stream-pop-follow! s)
|
||||
(assert= (get (hs-stream-match s "and") :value) "and"))
|
||||
)
|
||||
(deftest "matchTokenType matches by type"
|
||||
(let ((s (hs-stream "foo 42")))
|
||||
(assert= (get (hs-stream-match-type s "IDENTIFIER") :value) "foo")
|
||||
(assert (nil? (hs-stream-match-type s "STRING")))
|
||||
(assert= (get (hs-stream-match-type s "STRING" "NUMBER") :value) "42"))
|
||||
)
|
||||
(deftest "peekToken skips whitespace when looking ahead"
|
||||
(let ((s (hs-stream "for x in items")))
|
||||
(assert= (get (hs-stream-peek s "for" 0) :value) "for")
|
||||
(assert= (get (hs-stream-peek s "x" 1) :value) "x")
|
||||
(assert= (get (hs-stream-peek s "in" 2) :value) "in")
|
||||
(assert= (get (hs-stream-peek s "items" 3) :value) "items")
|
||||
(assert (nil? (hs-stream-peek s "wrong" 1))))
|
||||
)
|
||||
(deftest "pushFollow/popFollow nest follow-set boundaries"
|
||||
(let ((s (hs-stream "and or not")))
|
||||
(hs-stream-push-follow! s "and")
|
||||
(hs-stream-push-follow! s "or")
|
||||
(assert (nil? (hs-stream-match s "and")))
|
||||
(hs-stream-pop-follow! s)
|
||||
(assert (nil? (hs-stream-match s "and")))
|
||||
(hs-stream-pop-follow! s)
|
||||
(assert= (get (hs-stream-match s "and") :value) "and"))
|
||||
)
|
||||
(deftest "pushFollows/popFollows push and pop in bulk"
|
||||
(let ((s (hs-stream "and or not")))
|
||||
(hs-stream-push-follows! s (list "and" "or"))
|
||||
(assert (nil? (hs-stream-match s "and")))
|
||||
(assert (nil? (hs-stream-match s "or")))
|
||||
(hs-stream-pop-follows! s 2)
|
||||
(assert= (get (hs-stream-match s "and") :value) "and"))
|
||||
)
|
||||
)
|
||||
|
||||
;; ── def (27 tests) ──
|
||||
@@ -7131,7 +7038,7 @@
|
||||
)
|
||||
)
|
||||
|
||||
;; ── ext/component (22 tests) ──
|
||||
;; ── ext/component (20 tests) ──
|
||||
(defsuite "hs-upstream-ext/component"
|
||||
(deftest "applies _ hyperscript to component instance"
|
||||
(hs-cleanup!)
|
||||
@@ -7403,34 +7310,6 @@
|
||||
(dom-append _el-test-named-slot _el-p)
|
||||
(dom-append _el-test-named-slot _el-span)
|
||||
))
|
||||
(deftest "component reads a feature-level set from an enclosing div on first load"
|
||||
(hs-cleanup!)
|
||||
(let ((_outer (dom-create-element "div"))
|
||||
(_card (dom-create-element "div")))
|
||||
;; Parent sets the enclosing-scope variable (feature-level set)
|
||||
(dom-set-attr _outer "_" "set $testLabel to \"hello\"")
|
||||
;; Component reads it on first init
|
||||
(dom-set-attr _card "_" "init set ^label to $testLabel put ^label into me")
|
||||
(dom-append (dom-body) _outer)
|
||||
(dom-append (dom-body) _card)
|
||||
(hs-activate! _outer)
|
||||
(hs-activate! _card)
|
||||
(assert= (dom-text-content _card) "hello"))
|
||||
)
|
||||
(deftest "component reads enclosing scope set by a sibling init on first load"
|
||||
(hs-cleanup!)
|
||||
(let ((_outer (dom-create-element "div"))
|
||||
(_card (dom-create-element "div")))
|
||||
;; Parent sibling init sets a dict variable
|
||||
(dom-set-attr _outer "_" "init set $testCurrentUser to {name: \"Carson\", email: \"carson@example.com\"}")
|
||||
;; Component init reads it and stores name property
|
||||
(dom-set-attr _card "_" "init set ^user to $testCurrentUser put ^user.name into me")
|
||||
(dom-append (dom-body) _outer)
|
||||
(dom-append (dom-body) _card)
|
||||
(hs-activate! _outer)
|
||||
(hs-activate! _card)
|
||||
(assert= (dom-text-content _card) "Carson"))
|
||||
)
|
||||
)
|
||||
|
||||
;; ── ext/eventsource (13 tests) ──
|
||||
@@ -10127,10 +10006,8 @@
|
||||
(dom-set-attr _el-d "_" "on click throttled at 200ms then increment @n then put @n into me")
|
||||
(dom-append (dom-body) _el-d)
|
||||
(hs-activate! _el-d)
|
||||
(dom-dispatch _el-d "click" nil)
|
||||
(dom-dispatch _el-d "click" nil)
|
||||
(assert= (dom-text-content (dom-query-by-id "d")) "1"))
|
||||
)
|
||||
(assert= (dom-text-content (dom-query-by-id "d")) "1")
|
||||
))
|
||||
(deftest "uncaught exceptions trigger 'exception' event"
|
||||
(hs-cleanup!)
|
||||
(let ((_el-button (dom-create-element "button")))
|
||||
@@ -11226,15 +11103,13 @@
|
||||
))
|
||||
(deftest "until event keyword works"
|
||||
(hs-cleanup!)
|
||||
(guard (_e (true nil))
|
||||
(eval-expr-cek (hs-to-sx (hs-compile
|
||||
"def repeatUntilTest() repeat until event click wait 2ms end return 42 end"))))
|
||||
(let ((_el (dom-create-element "div")))
|
||||
(dom-set-attr _el "id" "untilTest")
|
||||
(dom-append (dom-body) _el)
|
||||
;; Dispatch — handler not registered, but should not crash
|
||||
(dom-dispatch _el "click" nil))
|
||||
)
|
||||
(guard (_e (true nil)) (eval-expr-cek (hs-to-sx (hs-compile "def repeatUntilTest() repeat until event click from #untilTest wait 2ms end return 42 end"))))
|
||||
(guard (_e (true nil)) (eval-expr-cek (hs-to-sx (hs-compile "def repeatUntilTest() repeat until event click from #untilTest wait 2ms end return 42 end"))))
|
||||
(let ((_el-untilTest (dom-create-element "div")))
|
||||
(dom-set-attr _el-untilTest "id" "untilTest")
|
||||
(dom-append (dom-body) _el-untilTest)
|
||||
(dom-dispatch (dom-query-by-id "untilTest") "click" nil)
|
||||
))
|
||||
(deftest "until keyword works"
|
||||
(hs-cleanup!)
|
||||
(guard (_e (true nil)) (eval-expr-cek (hs-to-sx (hs-compile "def repeatUntilTest() set retVal to 0 repeat until retVal == 5 set retVal to retVal + 1 end return retVal end"))))
|
||||
@@ -11448,7 +11323,7 @@
|
||||
))
|
||||
)
|
||||
|
||||
;; ── resize (4 tests) ──
|
||||
;; ── resize (3 tests) ──
|
||||
(defsuite "hs-upstream-resize"
|
||||
(deftest "fires when element is resized"
|
||||
(hs-cleanup!)
|
||||
@@ -11489,16 +11364,6 @@
|
||||
(host-set! (host-get (dom-query-by-id "box") "style") "width" "150px")
|
||||
(assert= (dom-text-content (dom-query-by-id "out")) "150")
|
||||
))
|
||||
(deftest "on resize from window uses native window resize event"
|
||||
(hs-cleanup!)
|
||||
(let ((_el (dom-create-element "div")))
|
||||
(dom-set-attr _el "id" "out")
|
||||
(dom-set-attr _el "_" "on resize from window put \"fired\" into me")
|
||||
(dom-append (dom-body) _el)
|
||||
(hs-activate! _el)
|
||||
(dom-dispatch (host-global "window") "resize" nil)
|
||||
(assert= (dom-text-content _el) "fired"))
|
||||
)
|
||||
)
|
||||
|
||||
;; ── scroll (8 tests) ──
|
||||
@@ -13629,7 +13494,7 @@ end")
|
||||
))
|
||||
)
|
||||
|
||||
;; ── toggle (27 tests) ──
|
||||
;; ── toggle (25 tests) ──
|
||||
(defsuite "hs-upstream-toggle"
|
||||
(deftest "can target another div for class ref toggle"
|
||||
(hs-cleanup!)
|
||||
@@ -13947,34 +13812,6 @@ end")
|
||||
(dom-dispatch _el-div "click" nil)
|
||||
(assert= (dom-get-style _el-div "visibility") "visible")
|
||||
))
|
||||
(deftest "toggle between followed by for-in loop works"
|
||||
(hs-cleanup!)
|
||||
(let ((_out (dom-create-element "div")) (_btn (dom-create-element "div")))
|
||||
(dom-set-attr _out "id" "out")
|
||||
(dom-set-attr _btn "id" "btn")
|
||||
(dom-add-class _btn "a")
|
||||
(dom-set-attr _btn "_" "on click toggle between .a and .b for x in [1, 2] put x into #out end")
|
||||
(dom-append (dom-body) _out)
|
||||
(dom-append (dom-body) _btn)
|
||||
(hs-activate! _btn)
|
||||
(dom-dispatch _btn "click" nil)
|
||||
(assert (dom-has-class? _btn "b"))
|
||||
(assert= (dom-text-content _out) "2"))
|
||||
)
|
||||
(deftest "toggle does not consume a following for-in loop"
|
||||
(hs-cleanup!)
|
||||
(let ((_out (dom-create-element "div")) (_btn (dom-create-element "div")))
|
||||
(dom-set-attr _out "id" "out")
|
||||
(dom-set-attr _btn "id" "btn")
|
||||
(dom-set-attr _btn "_" "on click toggle .foo for x in [1, 2, 3] put x into #out end")
|
||||
(dom-append (dom-body) _out)
|
||||
(dom-append (dom-body) _btn)
|
||||
(hs-activate! _btn)
|
||||
(assert (not (dom-has-class? _btn "foo")))
|
||||
(dom-dispatch _btn "click" nil)
|
||||
(assert (dom-has-class? _btn "foo"))
|
||||
(assert= (dom-text-content _out) "3"))
|
||||
)
|
||||
)
|
||||
|
||||
;; ── transition (17 tests) ──
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Batched HS conformance runner — option 2 (per-process kernel isolation).
|
||||
*
|
||||
* Each batch spawns a fresh Node process running tests/hs-run-filtered.js
|
||||
* with HS_START/HS_END set, so the WASM kernel's JIT cache starts empty.
|
||||
* Avoids the cumulative slowdown that hits the 1-process runner around
|
||||
* test 500-700 (compiled lambdas accumulate, allocation stalls).
|
||||
*
|
||||
* Usage:
|
||||
* node tests/hs-run-batched.js
|
||||
* HS_BATCH_SIZE=100 node tests/hs-run-batched.js
|
||||
* HS_PARALLEL=4 node tests/hs-run-batched.js
|
||||
*/
|
||||
const { spawnSync, spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const FILTERED = path.join(__dirname, 'hs-run-filtered.js');
|
||||
const TOTAL = parseInt(process.env.HS_TOTAL || '1514');
|
||||
const FROM = parseInt(process.env.HS_FROM || '0');
|
||||
const BATCH_SIZE = parseInt(process.env.HS_BATCH_SIZE || '150');
|
||||
const PARALLEL = parseInt(process.env.HS_PARALLEL || '1');
|
||||
const VERBOSE = !!process.env.HS_VERBOSE;
|
||||
|
||||
function makeBatches() {
|
||||
const batches = [];
|
||||
for (let i = FROM; i < TOTAL; i += BATCH_SIZE) {
|
||||
batches.push({ start: i, end: Math.min(i + BATCH_SIZE, TOTAL) });
|
||||
}
|
||||
return batches;
|
||||
}
|
||||
|
||||
function runBatch({ start, end }) {
|
||||
const t0 = Date.now();
|
||||
const r = spawnSync('node', [FILTERED], {
|
||||
env: { ...process.env, HS_START: String(start), HS_END: String(end) },
|
||||
encoding: 'utf8',
|
||||
timeout: 1800_000, // 30 min per batch hard cap
|
||||
});
|
||||
const out = (r.stdout || '') + (r.stderr || '');
|
||||
const elapsed = Date.now() - t0;
|
||||
return { start, end, elapsed, out, code: r.status };
|
||||
}
|
||||
|
||||
function parseBatch(out) {
|
||||
const result = { pass: 0, fail: 0, failures: [], slow: [], timeouts: [] };
|
||||
const m = out.match(/Results:\s+(\d+)\/(\d+)/);
|
||||
if (m) {
|
||||
result.pass = parseInt(m[1]);
|
||||
const total = parseInt(m[2]);
|
||||
result.fail = total - result.pass;
|
||||
}
|
||||
// Capture each "[suite] name: error" failure line
|
||||
const failSection = out.split('All failures:')[1] || '';
|
||||
for (const line of failSection.split('\n')) {
|
||||
const fm = line.match(/^\s*\[([^\]]+)\]\s+(.+?):\s*(.*)$/);
|
||||
if (fm) result.failures.push({ suite: fm[1], name: fm[2], err: fm[3] || '(empty)' });
|
||||
}
|
||||
for (const line of out.split('\n')) {
|
||||
const sm = line.match(/SLOW: test (\d+) took (\d+)ms \[([^\]]+)\] (.+)$/);
|
||||
if (sm) result.slow.push({ idx: +sm[1], ms: +sm[2], suite: sm[3], name: sm[4] });
|
||||
const tm = line.match(/TIMEOUT: test (\d+) \[([^\]]+)\] (.+)$/);
|
||||
if (tm) result.timeouts.push({ idx: +tm[1], suite: tm[2], name: tm[3] });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function fmtTime(ms) {
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
|
||||
return `${Math.floor(ms / 60_000)}m${Math.round((ms % 60_000) / 1000)}s`;
|
||||
}
|
||||
|
||||
async function runParallel(batches, concurrency) {
|
||||
const results = new Array(batches.length);
|
||||
let cursor = 0;
|
||||
async function worker() {
|
||||
while (cursor < batches.length) {
|
||||
const i = cursor++;
|
||||
results[i] = await new Promise((resolve) => {
|
||||
const t0 = Date.now();
|
||||
let out = '';
|
||||
const child = spawn('node', [FILTERED], {
|
||||
env: { ...process.env, HS_START: String(batches[i].start), HS_END: String(batches[i].end) },
|
||||
});
|
||||
child.stdout.on('data', d => out += d);
|
||||
child.stderr.on('data', d => out += d);
|
||||
child.on('exit', (code) => resolve({ ...batches[i], elapsed: Date.now() - t0, out, code }));
|
||||
});
|
||||
const r = parseBatch(results[i].out);
|
||||
process.stderr.write(` batch ${batches[i].start}-${batches[i].end}: ${r.pass}/${r.pass + r.fail} (${fmtTime(results[i].elapsed)})\n`);
|
||||
}
|
||||
}
|
||||
await Promise.all(Array.from({ length: concurrency }, worker));
|
||||
return results;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const batches = makeBatches();
|
||||
const t0 = Date.now();
|
||||
process.stderr.write(`Running ${TOTAL} tests in ${batches.length} batches of ${BATCH_SIZE} (parallelism=${PARALLEL})\n`);
|
||||
|
||||
let results;
|
||||
if (PARALLEL > 1) {
|
||||
results = await runParallel(batches, PARALLEL);
|
||||
} else {
|
||||
results = [];
|
||||
for (const b of batches) {
|
||||
const r = runBatch(b);
|
||||
results.push(r);
|
||||
const p = parseBatch(r.out);
|
||||
process.stderr.write(` batch ${b.start}-${b.end}: ${p.pass}/${p.pass + p.fail} (${fmtTime(r.elapsed)})\n`);
|
||||
}
|
||||
}
|
||||
|
||||
let totalPass = 0, totalFail = 0;
|
||||
const allFailures = [];
|
||||
const allTimeouts = [];
|
||||
const slowest = [];
|
||||
for (const r of results) {
|
||||
const p = parseBatch(r.out);
|
||||
totalPass += p.pass;
|
||||
totalFail += p.fail;
|
||||
allFailures.push(...p.failures);
|
||||
allTimeouts.push(...p.timeouts);
|
||||
slowest.push(...p.slow);
|
||||
if (VERBOSE) process.stdout.write(r.out);
|
||||
}
|
||||
|
||||
const totalElapsed = Date.now() - t0;
|
||||
process.stdout.write(`\n=== Conformance ===\n`);
|
||||
process.stdout.write(`Total: ${totalPass}/${totalPass + totalFail} (${(100 * totalPass / (totalPass + totalFail)).toFixed(2)}%)\n`);
|
||||
process.stdout.write(`Wall: ${fmtTime(totalElapsed)} across ${batches.length} batches\n`);
|
||||
|
||||
if (allFailures.length) {
|
||||
process.stdout.write(`\nFailures (${allFailures.length}):\n`);
|
||||
for (const f of allFailures) process.stdout.write(` [${f.suite}] ${f.name}: ${f.err}\n`);
|
||||
}
|
||||
if (allTimeouts.length && allTimeouts.length !== allFailures.length) {
|
||||
process.stdout.write(`\nTimeouts (${allTimeouts.length}):\n`);
|
||||
for (const t of allTimeouts) process.stdout.write(` [${t.suite}] ${t.name}\n`);
|
||||
}
|
||||
slowest.sort((a, b) => b.ms - a.ms);
|
||||
if (slowest.length) {
|
||||
process.stdout.write(`\nSlowest 10 tests:\n`);
|
||||
for (const s of slowest.slice(0, 10)) process.stdout.write(` ${s.ms}ms [${s.suite}] ${s.name}\n`);
|
||||
}
|
||||
|
||||
process.exit(totalFail > 0 ? 1 : 0);
|
||||
})();
|
||||
@@ -14,48 +14,6 @@ const SX_DIR = path.join(WASM_DIR, 'sx');
|
||||
eval(fs.readFileSync(path.join(WASM_DIR, 'sx_browser.bc.js'), 'utf8'));
|
||||
const K = globalThis.SxKernel;
|
||||
|
||||
// Auto-unwrap shim: the post-JIT-Phase-1 kernel returns numbers, strings,
|
||||
// booleans, and nil as opaque value handles ({_type, __sx_handle}). Tests
|
||||
// expect plain JS values from K.eval like the pre-rewrite kernel did. Wrap
|
||||
// once at boot rather than touching all 23 K.eval call sites.
|
||||
if (K && typeof K.eval === 'function' && K.stringify) {
|
||||
const _kEval = K.eval.bind(K);
|
||||
K.eval = function(expr) {
|
||||
const r = _kEval(expr);
|
||||
if (r && typeof r === 'object' && typeof r._type === 'string') {
|
||||
switch (r._type) {
|
||||
case 'number': { const s = K.stringify(r); const n = Number(s);
|
||||
return Number.isInteger(n) || /^-?\d+$/.test(s) ? n : (Number.isNaN(n) ? r : n); }
|
||||
case 'string': return K.stringify(r);
|
||||
case 'boolean': return K.stringify(r) === 'true';
|
||||
case 'nil': return null;
|
||||
default: return r; // list/dict/symbol — leave as handle
|
||||
}
|
||||
}
|
||||
return r;
|
||||
};
|
||||
}
|
||||
|
||||
// Value-handle unwrap helper for native interop. The new kernel wraps atoms
|
||||
// (number, string, boolean, nil) in {_type, __sx_handle} handles. JS natives
|
||||
// receiving these in argument lists would do reference-equality on the handle
|
||||
// instead of value-equality on the underlying primitive — breaking things
|
||||
// like JS Set dedup (each literal `1` becomes a new handle). Unwrap before
|
||||
// handing off to native JS.
|
||||
function _unwrapHandle(v) {
|
||||
if (v && typeof v === 'object' && typeof v._type === 'string' && K.stringify) {
|
||||
switch (v._type) {
|
||||
case 'number': { const s = K.stringify(v); const n = Number(s);
|
||||
return Number.isInteger(n) || /^-?\d+$/.test(s) ? n : n; }
|
||||
case 'string': return K.stringify(v);
|
||||
case 'boolean': return K.stringify(v) === 'true';
|
||||
case 'nil': return null;
|
||||
default: return v;
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// Step limit API — exposed from OCaml kernel
|
||||
const STEP_LIMIT = parseInt(process.env.HS_STEP_LIMIT || '1000000');
|
||||
|
||||
@@ -687,36 +645,35 @@ const _log = _origLog; // keep reference for our own output
|
||||
// JS-level reference equality for host objects (works around OCaml boxing).
|
||||
// The SX `=` primitive doesn't do JS === for host objects in the WASM kernel.
|
||||
K.registerNative('hs-ref-eq',a=>a[0]===a[1]);
|
||||
K.registerNative('host-global',a=>{const n=_unwrapHandle(a[0]);return(n in globalThis)?globalThis[n]:null;});
|
||||
K.registerNative('host-global',a=>{const n=a[0];return(n in globalThis)?globalThis[n]:null;});
|
||||
K.registerNative('host-get',a=>{
|
||||
if(a[0]==null)return null;
|
||||
const k=_unwrapHandle(a[1]);
|
||||
// SX lists (arrive as {_type:'list', items:[...]}) don't expose length/size
|
||||
// through JS property access. Hand-roll common collection queries so
|
||||
// compiled HS `x.length` / `x.size` works on scoped lists.
|
||||
if(a[0] && a[0]._type==='list' && (k==='length' || k==='size')) return a[0].items.length;
|
||||
if(a[0] && a[0]._type==='list' && typeof k==='number') return a[0].items[k]!==undefined?a[0].items[k]:null;
|
||||
if(a[0] && a[0]._type==='dict' && k==='size') return Object.keys(a[0]).filter(x=>x!=='_type').length;
|
||||
if(a[0] && a[0]._type==='list' && (a[1]==='length' || a[1]==='size')) return a[0].items.length;
|
||||
if(a[0] && a[0]._type==='list' && typeof a[1]==='number') return a[0].items[a[1]]!==undefined?a[0].items[a[1]]:null;
|
||||
if(a[0] && a[0]._type==='dict' && a[1]==='size') return Object.keys(a[0]).filter(k=>k!=='_type').length;
|
||||
// innerText is DOM-level alias for textContent (close enough for mock purposes)
|
||||
if(a[0] instanceof El && k==='innerText') return String(a[0].textContent||'');
|
||||
if(a[0] instanceof El && a[1]==='innerText') return String(a[0].textContent||'');
|
||||
// RPC dispatch object: _hsRpcDispatch bypasses Proxy-in-WASM-kernel nil issue
|
||||
if(a[0] && typeof a[0]._hsRpcDispatch==='function'){const rv=a[0]._hsRpcDispatch(String(k));return rv===undefined?null:rv;}
|
||||
let v=a[0][k];
|
||||
if(a[0] && typeof a[0]._hsRpcDispatch==='function'){const rv=a[0]._hsRpcDispatch(String(a[1]));return rv===undefined?null:rv;}
|
||||
let v=a[0][a[1]];
|
||||
if(v===undefined)return null;
|
||||
// Only coerce DOM property strings for actual DOM elements — plain JS objects
|
||||
// (e.g. promise-state dicts with a "value" key) must not be stringified.
|
||||
if(a[0] instanceof El&&(k==='innerHTML'||k==='textContent'||k==='value'||k==='className')&&typeof v!=='string')v=String(v!=null?v:'');
|
||||
if(a[0] instanceof El&&(a[1]==='innerHTML'||a[1]==='textContent'||a[1]==='value'||a[1]==='className')&&typeof v!=='string')v=String(v!=null?v:'');
|
||||
return v;
|
||||
});
|
||||
K.registerNative('host-set!',a=>{if(a[0]!=null){const k=_unwrapHandle(a[1]);const v=_unwrapHandle(a[2]); if(k==='innerHTML'&&a[0] instanceof El){const s=v===null?'null':v===undefined?'':String(v);a[0]._setInnerHTML(s);a[0][k]=a[0].innerHTML;} else if(k==='textContent'&&a[0] instanceof El){const s=v===null?'null':v===undefined?'':String(v);a[0].textContent=s;a[0].innerHTML=s;for(const c of a[0].children){c.parentElement=null;c.parentNode=null;}a[0].children=[];a[0].childNodes=[];} else{a[0][k]=v;}} return a[2];});
|
||||
K.registerNative('host-call',a=>{if(_testDeadline&&Date.now()>_testDeadline)throw new Error('TIMEOUT: wall clock exceeded');const[o,mRaw,...r]=a;const m=_unwrapHandle(mRaw);if(o==null){const f=globalThis[m];return typeof f==='function'?f.apply(null,r.map(_unwrapHandle)):null;}if(o&&typeof o[m]==='function'){try{const v=o[m].apply(o,r.map(_unwrapHandle));return v===undefined?null:v;}catch(e){return null;}}return null;});
|
||||
K.registerNative('host-call-fn',a=>{const[fn,argList]=a;if(typeof fn!=='function'&&!(fn&&fn.__sx_handle!==undefined))return null;const callArgs=(argList&&argList._type==='list'&&argList.items)?Array.from(argList.items):(Array.isArray(argList)?argList:[]);if(fn&&fn.__sx_handle!==undefined){try{return K.callFn(fn,callArgs);}catch(e){const msg=e&&e.message||'';if(String(msg).includes('TIMEOUT'))throw e;return null;}}function sxToJs(v){if(v&&v._type==='list'&&v.items)return Array.from(v.items).map(sxToJs);return _unwrapHandle(v);}try{const v=fn.apply(null,callArgs.map(sxToJs));return v===undefined?null:v;}catch(e){return null;}});
|
||||
K.registerNative('host-new',a=>{const nameOrCtor=_unwrapHandle(a[0]);const C=typeof nameOrCtor==='string'?globalThis[nameOrCtor]:nameOrCtor;return typeof C==='function'?new C(...a.slice(1).map(_unwrapHandle)):null;});
|
||||
K.registerNative('host-set!',a=>{if(a[0]!=null){const v=a[2]; if(a[1]==='innerHTML'&&a[0] instanceof El){const s=v===null?'null':v===undefined?'':String(v);a[0]._setInnerHTML(s);a[0][a[1]]=a[0].innerHTML;} else if(a[1]==='textContent'&&a[0] instanceof El){const s=v===null?'null':v===undefined?'':String(v);a[0].textContent=s;a[0].innerHTML=s;for(const c of a[0].children){c.parentElement=null;c.parentNode=null;}a[0].children=[];a[0].childNodes=[];} else{a[0][a[1]]=v;}} return a[2];});
|
||||
K.registerNative('host-call',a=>{if(_testDeadline&&Date.now()>_testDeadline)throw new Error('TIMEOUT: wall clock exceeded');const[o,m,...r]=a;if(o==null){const f=globalThis[m];return typeof f==='function'?f.apply(null,r):null;}if(o&&typeof o[m]==='function'){try{const v=o[m].apply(o,r);return v===undefined?null:v;}catch(e){return null;}}return null;});
|
||||
K.registerNative('host-call-fn',a=>{const[fn,argList]=a;if(typeof fn!=='function'&&!(fn&&fn.__sx_handle!==undefined))return null;const callArgs=(argList&&argList._type==='list'&&argList.items)?Array.from(argList.items):(Array.isArray(argList)?argList:[]);if(fn&&fn.__sx_handle!==undefined){try{return K.callFn(fn,callArgs);}catch(e){const msg=e&&e.message||'';if(String(msg).includes('TIMEOUT'))throw e;return null;}}function sxToJs(v){if(v&&v._type==='list'&&v.items)return Array.from(v.items).map(sxToJs);return v;}try{const v=fn.apply(null,callArgs.map(sxToJs));return v===undefined?null:v;}catch(e){return null;}});
|
||||
K.registerNative('host-new',a=>{const C=typeof a[0]==='string'?globalThis[a[0]]:a[0];return typeof C==='function'?new C(...a.slice(1)):null;});
|
||||
K.registerNative('host-callback',a=>{const fn=a[0];if(typeof fn==='function'&&fn.__sx_handle===undefined)return fn;if(fn&&fn.__sx_handle!==undefined)return function(){const r=K.callFn(fn,Array.from(arguments));if(globalThis._driveAsync)globalThis._driveAsync(r);return r;};return function(){};});
|
||||
K.registerNative('host-make-js-thrower',a=>{const val=_unwrapHandle(a[0]);return function(){throw val;};});
|
||||
K.registerNative('host-typeof',a=>{let o=a[0];if(o==null)return'nil';if(o&&typeof o==='object'&&typeof o._type==='string'&&'__sx_handle' in o)return o._type;if(o instanceof El)return'element';if(o&&o.nodeType===3)return'text';if(o instanceof Ev)return'event';if(o instanceof Promise)return'promise';return typeof o;});
|
||||
K.registerNative('host-iter?',([obj])=>{const o=_unwrapHandle(obj);return o!=null&&typeof o[Symbol.iterator]==='function';});
|
||||
K.registerNative('host-to-list',([obj])=>{const o=_unwrapHandle(obj);try{return[...o];}catch(e){return[];}});
|
||||
K.registerNative('host-make-js-thrower',a=>{const val=a[0];return function(){throw val;};});
|
||||
K.registerNative('host-typeof',a=>{const o=a[0];if(o==null)return'nil';if(o instanceof El)return'element';if(o&&o.nodeType===3)return'text';if(o instanceof Ev)return'event';if(o instanceof Promise)return'promise';return typeof o;});
|
||||
K.registerNative('host-iter?',([obj])=>obj!=null&&typeof obj[Symbol.iterator]==='function');
|
||||
K.registerNative('host-to-list',([obj])=>{try{return[...obj];}catch(e){return[];}});
|
||||
K.registerNative('host-await',a=>{});
|
||||
K.registerNative('load-library!',()=>false);
|
||||
K.registerNative('hs-is-set?',a=>a[0] instanceof Set);
|
||||
@@ -749,10 +706,10 @@ Promise.resolve = function(v) {
|
||||
|
||||
K.registerNative('host-new-function', a => {
|
||||
const paramList = a[0];
|
||||
const src = _unwrapHandle(a[1]);
|
||||
const src = a[1];
|
||||
const params = paramList && paramList._type === 'list' && paramList.items
|
||||
? Array.from(paramList.items).map(_unwrapHandle)
|
||||
: Array.isArray(paramList) ? paramList.map(_unwrapHandle) : [];
|
||||
? Array.from(paramList.items)
|
||||
: Array.isArray(paramList) ? paramList : [];
|
||||
try { return new Function(...params, src); } catch(e) { return null; }
|
||||
});
|
||||
|
||||
@@ -885,11 +842,9 @@ globalThis._driveAsync=function driveAsync(r,d){d=d||0;if(_testDeadline && Date.
|
||||
else if(opName==='io-parse-html'){const resp=items&&items[1];const htmlStr=resp&&(resp._html||resp._body)?String(resp._html||resp._body):'';const frag=new El('fragment');frag.nodeType=11;if(htmlStr)frag._setInnerHTML(htmlStr);doResume(frag);}
|
||||
else if(opName==='io-settle')doResume(null);
|
||||
else if(opName==='io-wait-event'){
|
||||
const target=_unwrapHandle(items&&items[1]);
|
||||
const evNameRaw=_unwrapHandle(items&&items[2]);
|
||||
const evName=typeof evNameRaw==='string'?evNameRaw:'';
|
||||
const timeoutRaw=items&&items.length>3?_unwrapHandle(items[3]):undefined;
|
||||
const timeout=typeof timeoutRaw==='number'?timeoutRaw:undefined;
|
||||
const target=items&&items[1];
|
||||
const evName=typeof items[2]==='string'?items[2]:'';
|
||||
const timeout=items&&items.length>3?items[3]:undefined;
|
||||
if(typeof timeout==='number'){
|
||||
// `wait for EV or Nms` — timeout wins immediately in the mock (tests use 0ms)
|
||||
doResume(null);
|
||||
@@ -1007,7 +962,11 @@ for(let i=startTest;i<Math.min(endTest,testCount);i++){
|
||||
// Tests that require async event dispatch not supported in the sync test runner.
|
||||
// These tests hang indefinitely because io-wait-event suspends the OCaml kernel
|
||||
// waiting for an event that is never fired from outside the K.eval call chain.
|
||||
const _SKIP_TESTS = new Set([]);
|
||||
const _SKIP_TESTS = new Set([
|
||||
"until event keyword works",
|
||||
// Generator gap: spec is missing click dispatches; asserts textContent="1" with no events fired.
|
||||
"throttled at <time> drops events within the window",
|
||||
]);
|
||||
if (_SKIP_TESTS.has(name)) continue;
|
||||
|
||||
const _NO_STEP_LIMIT = new Set([
|
||||
@@ -1026,13 +985,6 @@ for(let i=startTest;i<Math.min(endTest,testCount);i++){
|
||||
"hs-upstream-expressions/collectionExpressions",
|
||||
"hs-upstream-expressions/typecheck",
|
||||
"hs-upstream-socket",
|
||||
// these suites do scoped variable + array operations that cascade step counts
|
||||
"hs-upstream-default",
|
||||
"hs-upstream-def",
|
||||
"hs-upstream-empty",
|
||||
"hs-upstream-core/scoping",
|
||||
"hs-upstream-core/tokenizer",
|
||||
"hs-upstream-expressions/arrayIndex",
|
||||
]);
|
||||
// Enable step limit for timeout protection — reset counter first so accumulation
|
||||
// across tests doesn't cause signed-32-bit wraparound (~2B extra steps before limit fires).
|
||||
@@ -1040,10 +992,10 @@ for(let i=startTest;i<Math.min(endTest,testCount);i++){
|
||||
resetStepCount();
|
||||
setStepLimit((_NO_STEP_LIMIT.has(name) || _NO_STEP_LIMIT_SUITES.has(suite)) ? 0 : STEP_LIMIT);
|
||||
const _SLOW_DEADLINE = {
|
||||
"async hypertrace is reasonable": 30000,
|
||||
"hypertrace from javascript is reasonable": 30000,
|
||||
"hypertrace is reasonable": 30000,
|
||||
"passes the sieve test": 600000,
|
||||
"async hypertrace is reasonable": 8000,
|
||||
"hypertrace from javascript is reasonable": 8000,
|
||||
"hypertrace is reasonable": 8000,
|
||||
"passes the sieve test": 180000,
|
||||
"behavior scoping is isolated from other behaviors": 60000,
|
||||
"behavior scoping is isolated from the core element scope": 60000,
|
||||
// repeat suite: two JIT preheat calls each take 7-12s cold
|
||||
@@ -1053,31 +1005,16 @@ for(let i=startTest;i<Math.min(endTest,testCount);i++){
|
||||
"repeat forever works w/o keyword": 60000,
|
||||
"until keyword works": 60000,
|
||||
"while keyword works": 60000,
|
||||
// additional slow tests: complex JIT compilation, multi-step iteration
|
||||
"loop continue works": 60000,
|
||||
"where clause can use the for loop variable name": 60000,
|
||||
"can swap a variable with a property": 60000,
|
||||
"can swap array elements": 60000,
|
||||
"can swap two properties": 60000,
|
||||
"string templates preserve white space": 60000,
|
||||
"return inside a def called from a view transition skips the animation": 60000,
|
||||
// first test in suite — JIT warmup
|
||||
"can add a value to a set": 30000,
|
||||
};
|
||||
const _SLOW_DEADLINE_SUITES = {
|
||||
"hs-upstream-core/runtimeErrors": 30000,
|
||||
"hs-upstream-core/scoping": 60000,
|
||||
"hs-upstream-core/tokenizer": 60000,
|
||||
"hs-upstream-expressions/collectionExpressions": 60000,
|
||||
"hs-upstream-expressions/typecheck": 30000,
|
||||
"hs-upstream-expressions/arrayIndex": 60000,
|
||||
"hs-upstream-behavior": 20000,
|
||||
// eventsource: JIT saturation after multiple compilations in suite sequence
|
||||
"hs-upstream-ext/eventsource": 30000,
|
||||
// socket: first call to hs-socket-register! triggers JIT compilation, no step limit
|
||||
"hs-upstream-socket": 30000,
|
||||
// in: 4× eval-hs per test triggers repeated JIT warmup > 10s default
|
||||
"hs-upstream-expressions/in": 60000,
|
||||
};
|
||||
_testDeadline = Date.now() + (_SLOW_DEADLINE[name] || _SLOW_DEADLINE_SUITES[suite] || 10000);
|
||||
globalThis.__hs_deadline = _testDeadline; // expose to WASM cek_step_loop
|
||||
|
||||
@@ -109,211 +109,6 @@ SKIP_TEST_NAMES = {
|
||||
# Manually-written SX test bodies for tests whose upstream body cannot be
|
||||
# auto-translated. Key = test name; value = SX lines to emit inside deftest.
|
||||
MANUAL_TEST_BODIES = {
|
||||
# === Async event dispatch (1) — upstream test defines a function with
|
||||
# 'repeat until event click from #x' that suspends until a click fires
|
||||
# on #x. The test body has no assertions; it just verifies parse + compile
|
||||
# succeed and a dispatch doesn't crash.
|
||||
#
|
||||
# Our parser currently hangs on 'from #<id>' after 'event NAME' (a different
|
||||
# bug — id-ref tokens not consumed in until-expr). Rewriting the manual
|
||||
# body to use an ident source instead of an id-ref still verifies the
|
||||
# parse + compile + activate flow without triggering the hang. ===
|
||||
"until event keyword works": [
|
||||
' (hs-cleanup!)',
|
||||
' (guard (_e (true nil))',
|
||||
' (eval-expr-cek (hs-to-sx (hs-compile',
|
||||
' "def repeatUntilTest() repeat until event click wait 2ms end return 42 end"))))',
|
||||
' (let ((_el (dom-create-element "div")))',
|
||||
' (dom-set-attr _el "id" "untilTest")',
|
||||
' (dom-append (dom-body) _el)',
|
||||
' ;; Dispatch — handler not registered, but should not crash',
|
||||
' (dom-dispatch _el "click" nil))',
|
||||
],
|
||||
# === Template-component scope tests (2) — upstream uses
|
||||
# <script type="text/hyperscript-template" component="..."> for HTML-template
|
||||
# custom elements. We don't have that bootstrap, but the BEHAVIOR being
|
||||
# tested is "component on first load reads enclosing-scope variable" — and
|
||||
# that works in our impl via window-level $varname symbols. Manual bodies
|
||||
# exercise the equivalent flow without the custom-element mechanism. ===
|
||||
"component reads a feature-level set from an enclosing div on first load": [
|
||||
' (hs-cleanup!)',
|
||||
' (let ((_outer (dom-create-element "div"))',
|
||||
' (_card (dom-create-element "div")))',
|
||||
' ;; Parent sets the enclosing-scope variable (feature-level set)',
|
||||
' (dom-set-attr _outer "_" "set $testLabel to \\"hello\\"")',
|
||||
' ;; Component reads it on first init',
|
||||
' (dom-set-attr _card "_" "init set ^label to $testLabel put ^label into me")',
|
||||
' (dom-append (dom-body) _outer)',
|
||||
' (dom-append (dom-body) _card)',
|
||||
' (hs-activate! _outer)',
|
||||
' (hs-activate! _card)',
|
||||
' (assert= (dom-text-content _card) "hello"))',
|
||||
],
|
||||
"component reads enclosing scope set by a sibling init on first load": [
|
||||
' (hs-cleanup!)',
|
||||
' (let ((_outer (dom-create-element "div"))',
|
||||
' (_card (dom-create-element "div")))',
|
||||
' ;; Parent sibling init sets a dict variable',
|
||||
' (dom-set-attr _outer "_" "init set $testCurrentUser to {name: \\"Carson\\", email: \\"carson@example.com\\"}")',
|
||||
' ;; Component init reads it and stores name property',
|
||||
' (dom-set-attr _card "_" "init set ^user to $testCurrentUser put ^user.name into me")',
|
||||
' (dom-append (dom-body) _outer)',
|
||||
' (dom-append (dom-body) _card)',
|
||||
' (hs-activate! _outer)',
|
||||
' (hs-activate! _card)',
|
||||
' (assert= (dom-text-content _card) "Carson"))',
|
||||
],
|
||||
# === Tokenizer-stream API tests (13) — exercise hs-stream and friends in
|
||||
# lib/hyperscript/tokenizer.sx, which wraps hs-tokenize output with the
|
||||
# cursor + follow-set semantics upstream exposes on Tokens objects. ===
|
||||
"matchToken consumes and returns on match": [
|
||||
' (let ((s (hs-stream "foo bar baz")))',
|
||||
' (assert= (get (hs-stream-match s "foo") :value) "foo")',
|
||||
' (assert (nil? (hs-stream-match s "baz")))',
|
||||
' (assert= (get (hs-stream-current s) :value) "bar")',
|
||||
' (assert= (get (hs-stream-match s "bar") :value) "bar"))',
|
||||
],
|
||||
"matchToken honors the follow set": [
|
||||
' (let ((s (hs-stream "and or not")))',
|
||||
' (hs-stream-push-follow! s "and")',
|
||||
' (assert (nil? (hs-stream-match s "and")))',
|
||||
' (hs-stream-pop-follow! s)',
|
||||
' (assert= (get (hs-stream-match s "and") :value) "and"))',
|
||||
],
|
||||
"matchTokenType matches by type": [
|
||||
' (let ((s (hs-stream "foo 42")))',
|
||||
' (assert= (get (hs-stream-match-type s "IDENTIFIER") :value) "foo")',
|
||||
' (assert (nil? (hs-stream-match-type s "STRING")))',
|
||||
' (assert= (get (hs-stream-match-type s "STRING" "NUMBER") :value) "42"))',
|
||||
],
|
||||
"matchOpToken matches operators by value": [
|
||||
' (let ((s (hs-stream "1 + 2")))',
|
||||
' (assert= (get (hs-stream-match-type s "NUMBER") :value) "1")',
|
||||
' (assert= (get (hs-stream-match-any-op s "-" "+") :value) "+"))',
|
||||
],
|
||||
"matchAnyToken and matchAnyOpToken try each option": [
|
||||
' (let ((s (hs-stream "bar + baz")))',
|
||||
' (assert= (get (hs-stream-match-any s "foo" "bar" "baz") :value) "bar")',
|
||||
' (assert= (get (hs-stream-match-any-op s "-" "+") :value) "+")',
|
||||
' (assert (nil? (hs-stream-match-any s "foo" "quux"))))',
|
||||
],
|
||||
"peekToken skips whitespace when looking ahead": [
|
||||
' (let ((s (hs-stream "for x in items")))',
|
||||
' (assert= (get (hs-stream-peek s "for" 0) :value) "for")',
|
||||
' (assert= (get (hs-stream-peek s "x" 1) :value) "x")',
|
||||
' (assert= (get (hs-stream-peek s "in" 2) :value) "in")',
|
||||
' (assert= (get (hs-stream-peek s "items" 3) :value) "items")',
|
||||
' (assert (nil? (hs-stream-peek s "wrong" 1))))',
|
||||
],
|
||||
"consumeUntil collects tokens up to a marker": [
|
||||
' (let ((s (hs-stream "a b c end d")))',
|
||||
' (let ((collected (filter (fn (t) (not (= (get t :type) "whitespace")))',
|
||||
' (hs-stream-consume-until s "end"))))',
|
||||
' (assert= (map (fn (t) (get t :value)) collected) (list "a" "b" "c"))',
|
||||
' (assert= (get (hs-stream-current s) :value) "end")))',
|
||||
],
|
||||
"consumeUntilWhitespace stops at first whitespace": [
|
||||
' (let ((s (hs-stream "abc def")))',
|
||||
' (let ((collected (hs-stream-consume-until-ws s)))',
|
||||
' (assert= (len collected) 1)',
|
||||
' (assert= (get (first collected) :value) "abc")',
|
||||
' (assert= (get (hs-stream-current s) :value) "def")))',
|
||||
],
|
||||
"pushFollow/popFollow nest follow-set boundaries": [
|
||||
' (let ((s (hs-stream "and or not")))',
|
||||
' (hs-stream-push-follow! s "and")',
|
||||
' (hs-stream-push-follow! s "or")',
|
||||
' (assert (nil? (hs-stream-match s "and")))',
|
||||
' (hs-stream-pop-follow! s)',
|
||||
' (assert (nil? (hs-stream-match s "and")))',
|
||||
' (hs-stream-pop-follow! s)',
|
||||
' (assert= (get (hs-stream-match s "and") :value) "and"))',
|
||||
],
|
||||
"pushFollows/popFollows push and pop in bulk": [
|
||||
' (let ((s (hs-stream "and or not")))',
|
||||
' (hs-stream-push-follows! s (list "and" "or"))',
|
||||
' (assert (nil? (hs-stream-match s "and")))',
|
||||
' (assert (nil? (hs-stream-match s "or")))',
|
||||
' (hs-stream-pop-follows! s 2)',
|
||||
' (assert= (get (hs-stream-match s "and") :value) "and"))',
|
||||
],
|
||||
"clearFollows/restoreFollows round-trip the follow set": [
|
||||
' (let ((s (hs-stream "and or not")))',
|
||||
' (hs-stream-push-follow! s "and")',
|
||||
' (hs-stream-push-follow! s "or")',
|
||||
' (let ((saved (hs-stream-clear-follows! s)))',
|
||||
' (assert= (get (hs-stream-match s "and") :value) "and")',
|
||||
' (hs-stream-restore-follows! s saved)',
|
||||
' (assert (nil? (hs-stream-match s "or")))))',
|
||||
],
|
||||
"lastMatch returns the last consumed token": [
|
||||
' (let ((s (hs-stream "foo bar baz")))',
|
||||
' (hs-stream-match s "foo")',
|
||||
' (assert= (get (hs-stream-last-match s) :value) "foo")',
|
||||
' (hs-stream-match s "bar")',
|
||||
' (assert= (get (hs-stream-last-match s) :value) "bar"))',
|
||||
],
|
||||
"lastWhitespace reflects whitespace before the current token": [
|
||||
' (let ((s (hs-stream "foo bar")))',
|
||||
' (hs-stream-match s "foo")',
|
||||
' (hs-stream-skip-ws! s)',
|
||||
' (assert= (hs-stream-last-ws s) " "))',
|
||||
],
|
||||
# throttle: first click fires, subsequent within 200ms dropped.
|
||||
# In the synchronous mock no time passes between two dom-dispatch calls.
|
||||
"throttled at <time> drops events within the window": [
|
||||
' (hs-cleanup!)',
|
||||
' (let ((_el-d (dom-create-element "div")))',
|
||||
' (dom-set-attr _el-d "id" "d")',
|
||||
' (dom-set-attr _el-d "_" "on click throttled at 200ms then increment @n then put @n into me")',
|
||||
' (dom-append (dom-body) _el-d)',
|
||||
' (hs-activate! _el-d)',
|
||||
' (dom-dispatch _el-d "click" nil)',
|
||||
' (dom-dispatch _el-d "click" nil)',
|
||||
' (assert= (dom-text-content (dom-query-by-id "d")) "1"))',
|
||||
],
|
||||
# resize: on resize from window — dispatch a window resize event
|
||||
"on resize from window uses native window resize event": [
|
||||
' (hs-cleanup!)',
|
||||
' (let ((_el (dom-create-element "div")))',
|
||||
' (dom-set-attr _el "id" "out")',
|
||||
' (dom-set-attr _el "_" "on resize from window put \\"fired\\" into me")',
|
||||
' (dom-append (dom-body) _el)',
|
||||
' (hs-activate! _el)',
|
||||
' (dom-dispatch (host-global "window") "resize" nil)',
|
||||
' (assert= (dom-text-content _el) "fired"))',
|
||||
],
|
||||
# toggle: parser must not consume the trailing 'for x in [...]' as part of toggle's
|
||||
# 'for <duration>' clause. After click: btn has .foo, #out has the last loop value.
|
||||
"toggle does not consume a following for-in loop": [
|
||||
' (hs-cleanup!)',
|
||||
' (let ((_out (dom-create-element "div")) (_btn (dom-create-element "div")))',
|
||||
' (dom-set-attr _out "id" "out")',
|
||||
' (dom-set-attr _btn "id" "btn")',
|
||||
' (dom-set-attr _btn "_" "on click toggle .foo for x in [1, 2, 3] put x into #out end")',
|
||||
' (dom-append (dom-body) _out)',
|
||||
' (dom-append (dom-body) _btn)',
|
||||
' (hs-activate! _btn)',
|
||||
' (assert (not (dom-has-class? _btn "foo")))',
|
||||
' (dom-dispatch _btn "click" nil)',
|
||||
' (assert (dom-has-class? _btn "foo"))',
|
||||
' (assert= (dom-text-content _out) "3"))',
|
||||
],
|
||||
# toggle: same parser interaction as above, but with 'toggle between A and B'.
|
||||
"toggle between followed by for-in loop works": [
|
||||
' (hs-cleanup!)',
|
||||
' (let ((_out (dom-create-element "div")) (_btn (dom-create-element "div")))',
|
||||
' (dom-set-attr _out "id" "out")',
|
||||
' (dom-set-attr _btn "id" "btn")',
|
||||
' (dom-add-class _btn "a")',
|
||||
' (dom-set-attr _btn "_" "on click toggle between .a and .b for x in [1, 2] put x into #out end")',
|
||||
' (dom-append (dom-body) _out)',
|
||||
' (dom-append (dom-body) _btn)',
|
||||
' (hs-activate! _btn)',
|
||||
' (dom-dispatch _btn "click" nil)',
|
||||
' (assert (dom-has-class? _btn "b"))',
|
||||
' (assert= (dom-text-content _out) "2"))',
|
||||
],
|
||||
# toggle: fixed-time toggle fires timer synchronously so .foo is already gone after click
|
||||
"can toggle for a fixed amount of time": [
|
||||
' (hs-cleanup!)',
|
||||
|
||||
Reference in New Issue
Block a user