hs: query targets, prolog hook, loop scripts, new plans, WASM regen
Hyperscript compiler/runtime:
- query target support in set/fire/put commands
- hs-set-prolog-hook! / hs-prolog-hook / hs-prolog in runtime
- runtime log-capture cleanup
Scripts: sx-loops-up/down, sx-hs-e-up/down, sx-primitives-down
Plans: datalog, elixir, elm, go, koka, minikanren, ocaml, hs-bucket-f,
designs (breakpoint, null-safety, step-limit, tell, cookies, eval,
plugin-system)
lib/prolog/hs-bridge.sx: initial hook-based bridge draft
lib/common-lisp/tests/runtime.sx: CL runtime tests
WASM: regenerate sx_browser.bc.js from updated hs sources
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
207
lib/common-lisp/tests/runtime.sx
Normal file
207
lib/common-lisp/tests/runtime.sx
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
;; lib/common-lisp/tests/runtime.sx — tests for CL runtime layer
|
||||||
|
|
||||||
|
(load "lib/common-lisp/runtime.sx")
|
||||||
|
|
||||||
|
(defsuite
|
||||||
|
"cl-types"
|
||||||
|
(deftest "cl-null? nil" (assert= true (cl-null? nil)))
|
||||||
|
(deftest "cl-null? false" (assert= false (cl-null? false)))
|
||||||
|
(deftest
|
||||||
|
"cl-consp? pair"
|
||||||
|
(assert= true (cl-consp? (list 1 2))))
|
||||||
|
(deftest "cl-consp? nil" (assert= false (cl-consp? nil)))
|
||||||
|
(deftest "cl-listp? nil" (assert= true (cl-listp? nil)))
|
||||||
|
(deftest
|
||||||
|
"cl-listp? list"
|
||||||
|
(assert= true (cl-listp? (list 1 2))))
|
||||||
|
(deftest "cl-atom? nil" (assert= true (cl-atom? nil)))
|
||||||
|
(deftest "cl-atom? pair" (assert= false (cl-atom? (list 1))))
|
||||||
|
(deftest "cl-integerp?" (assert= true (cl-integerp? 42)))
|
||||||
|
(deftest "cl-floatp?" (assert= true (cl-floatp? 3.14)))
|
||||||
|
(deftest
|
||||||
|
"cl-characterp?"
|
||||||
|
(assert= true (cl-characterp? (integer->char 65))))
|
||||||
|
(deftest "cl-stringp?" (assert= true (cl-stringp? "hello")))
|
||||||
|
(deftest "cl-symbolp?" (assert= true (cl-symbolp? (quote foo)))))
|
||||||
|
|
||||||
|
(defsuite
|
||||||
|
"cl-arithmetic"
|
||||||
|
(deftest "cl-mod" (assert= 1 (cl-mod 10 3)))
|
||||||
|
(deftest "cl-rem" (assert= 1 (cl-rem 10 3)))
|
||||||
|
(deftest
|
||||||
|
"cl-quotient"
|
||||||
|
(assert= 3 (cl-quotient 10 3)))
|
||||||
|
(deftest "cl-gcd" (assert= 4 (cl-gcd 12 8)))
|
||||||
|
(deftest "cl-lcm" (assert= 12 (cl-lcm 4 6)))
|
||||||
|
(deftest "cl-abs pos" (assert= 5 (cl-abs 5)))
|
||||||
|
(deftest "cl-abs neg" (assert= 5 (cl-abs -5)))
|
||||||
|
(deftest "cl-min" (assert= 2 (cl-min 2 7)))
|
||||||
|
(deftest "cl-max" (assert= 7 (cl-max 2 7)))
|
||||||
|
(deftest "cl-evenp? t" (assert= true (cl-evenp? 4)))
|
||||||
|
(deftest "cl-evenp? f" (assert= false (cl-evenp? 3)))
|
||||||
|
(deftest "cl-oddp? t" (assert= true (cl-oddp? 7)))
|
||||||
|
(deftest "cl-zerop?" (assert= true (cl-zerop? 0)))
|
||||||
|
(deftest "cl-plusp?" (assert= true (cl-plusp? 1)))
|
||||||
|
(deftest "cl-minusp?" (assert= true (cl-minusp? -1)))
|
||||||
|
(deftest "cl-signum pos" (assert= 1 (cl-signum 42)))
|
||||||
|
(deftest "cl-signum neg" (assert= -1 (cl-signum -7)))
|
||||||
|
(deftest "cl-signum zero" (assert= 0 (cl-signum 0))))
|
||||||
|
|
||||||
|
(defsuite
|
||||||
|
"cl-chars"
|
||||||
|
(deftest
|
||||||
|
"cl-char-code"
|
||||||
|
(assert= 65 (cl-char-code (integer->char 65))))
|
||||||
|
(deftest "cl-code-char" (assert= true (char? (cl-code-char 65))))
|
||||||
|
(deftest
|
||||||
|
"cl-char-upcase"
|
||||||
|
(assert=
|
||||||
|
(integer->char 65)
|
||||||
|
(cl-char-upcase (integer->char 97))))
|
||||||
|
(deftest
|
||||||
|
"cl-char-downcase"
|
||||||
|
(assert=
|
||||||
|
(integer->char 97)
|
||||||
|
(cl-char-downcase (integer->char 65))))
|
||||||
|
(deftest
|
||||||
|
"cl-alpha-char-p"
|
||||||
|
(assert= true (cl-alpha-char-p (integer->char 65))))
|
||||||
|
(deftest
|
||||||
|
"cl-digit-char-p"
|
||||||
|
(assert= true (cl-digit-char-p (integer->char 48))))
|
||||||
|
(deftest
|
||||||
|
"cl-char=?"
|
||||||
|
(assert=
|
||||||
|
true
|
||||||
|
(cl-char=? (integer->char 65) (integer->char 65))))
|
||||||
|
(deftest
|
||||||
|
"cl-char<?"
|
||||||
|
(assert=
|
||||||
|
true
|
||||||
|
(cl-char<? (integer->char 65) (integer->char 90))))
|
||||||
|
(deftest
|
||||||
|
"cl-char space"
|
||||||
|
(assert= (integer->char 32) cl-char-space))
|
||||||
|
(deftest
|
||||||
|
"cl-char newline"
|
||||||
|
(assert= (integer->char 10) cl-char-newline)))
|
||||||
|
|
||||||
|
(defsuite
|
||||||
|
"cl-format"
|
||||||
|
(deftest
|
||||||
|
"cl-format nil basic"
|
||||||
|
(assert= "hello" (cl-format nil "~a" "hello")))
|
||||||
|
(deftest
|
||||||
|
"cl-format nil number"
|
||||||
|
(assert= "42" (cl-format nil "~d" 42)))
|
||||||
|
(deftest
|
||||||
|
"cl-format nil hex"
|
||||||
|
(assert= "ff" (cl-format nil "~x" 255)))
|
||||||
|
(deftest
|
||||||
|
"cl-format nil template"
|
||||||
|
(assert= "x=3 y=4" (cl-format nil "x=~d y=~d" 3 4)))
|
||||||
|
(deftest "cl-format nil tilde" (assert= "a~b" (cl-format nil "a~~b"))))
|
||||||
|
|
||||||
|
(defsuite
|
||||||
|
"cl-gensym"
|
||||||
|
(deftest
|
||||||
|
"cl-gensym returns symbol"
|
||||||
|
(assert= "symbol" (type-of (cl-gensym))))
|
||||||
|
(deftest "cl-gensym unique" (assert= false (= (cl-gensym) (cl-gensym)))))
|
||||||
|
|
||||||
|
(defsuite
|
||||||
|
"cl-sets"
|
||||||
|
(deftest "cl-make-set empty" (assert= true (cl-set? (cl-make-set))))
|
||||||
|
(deftest
|
||||||
|
"cl-set-add/member"
|
||||||
|
(let
|
||||||
|
((s (cl-make-set)))
|
||||||
|
(do
|
||||||
|
(cl-set-add s 1)
|
||||||
|
(assert= true (cl-set-memberp s 1)))))
|
||||||
|
(deftest
|
||||||
|
"cl-set-memberp false"
|
||||||
|
(assert= false (cl-set-memberp (cl-make-set) 42)))
|
||||||
|
(deftest
|
||||||
|
"cl-list->set"
|
||||||
|
(let
|
||||||
|
((s (cl-list->set (list 1 2 3))))
|
||||||
|
(assert= true (cl-set-memberp s 2)))))
|
||||||
|
|
||||||
|
(defsuite
|
||||||
|
"cl-lists"
|
||||||
|
(deftest
|
||||||
|
"cl-nth 0"
|
||||||
|
(assert=
|
||||||
|
1
|
||||||
|
(cl-nth 0 (list 1 2 3))))
|
||||||
|
(deftest
|
||||||
|
"cl-nth 2"
|
||||||
|
(assert=
|
||||||
|
3
|
||||||
|
(cl-nth 2 (list 1 2 3))))
|
||||||
|
(deftest
|
||||||
|
"cl-last"
|
||||||
|
(assert=
|
||||||
|
(list 3)
|
||||||
|
(cl-last (list 1 2 3))))
|
||||||
|
(deftest
|
||||||
|
"cl-butlast"
|
||||||
|
(assert=
|
||||||
|
(list 1 2)
|
||||||
|
(cl-butlast (list 1 2 3))))
|
||||||
|
(deftest
|
||||||
|
"cl-nthcdr 1"
|
||||||
|
(assert=
|
||||||
|
(list 2 3)
|
||||||
|
(cl-nthcdr 1 (list 1 2 3))))
|
||||||
|
(deftest
|
||||||
|
"cl-assoc hit"
|
||||||
|
(assert=
|
||||||
|
(list "b" 2)
|
||||||
|
(cl-assoc "b" (list (list "a" 1) (list "b" 2)))))
|
||||||
|
(deftest
|
||||||
|
"cl-assoc miss"
|
||||||
|
(assert= nil (cl-assoc "z" (list (list "a" 1)))))
|
||||||
|
(deftest
|
||||||
|
"cl-getf hit"
|
||||||
|
(assert= 42 (cl-getf (list "x" 42 "y" 99) "x")))
|
||||||
|
(deftest "cl-getf miss" (assert= nil (cl-getf (list "x" 42) "z")))
|
||||||
|
(deftest
|
||||||
|
"cl-adjoin new"
|
||||||
|
(assert=
|
||||||
|
(list 0 1 2)
|
||||||
|
(cl-adjoin 0 (list 1 2))))
|
||||||
|
(deftest
|
||||||
|
"cl-adjoin dup"
|
||||||
|
(assert=
|
||||||
|
(list 1 2)
|
||||||
|
(cl-adjoin 1 (list 1 2))))
|
||||||
|
(deftest
|
||||||
|
"cl-flatten"
|
||||||
|
(assert=
|
||||||
|
(list 1 2 3 4)
|
||||||
|
(cl-flatten (list 1 (list 2 3) 4))))
|
||||||
|
(deftest
|
||||||
|
"cl-member hit"
|
||||||
|
(assert=
|
||||||
|
(list 2 3)
|
||||||
|
(cl-member 2 (list 1 2 3))))
|
||||||
|
(deftest
|
||||||
|
"cl-member miss"
|
||||||
|
(assert=
|
||||||
|
nil
|
||||||
|
(cl-member 9 (list 1 2 3)))))
|
||||||
|
|
||||||
|
(defsuite
|
||||||
|
"cl-radix"
|
||||||
|
(deftest "binary" (assert= "1010" (cl-format-binary 10)))
|
||||||
|
(deftest "octal" (assert= "17" (cl-format-octal 15)))
|
||||||
|
(deftest "hex" (assert= "ff" (cl-format-hex 255)))
|
||||||
|
(deftest "decimal" (assert= "42" (cl-format-decimal 42)))
|
||||||
|
(deftest
|
||||||
|
"n->s r16"
|
||||||
|
(assert= "1f" (cl-integer-to-string 31 16)))
|
||||||
|
(deftest
|
||||||
|
"s->n r16"
|
||||||
|
(assert= 31 (cl-string-to-integer "1f" 16))))
|
||||||
@@ -48,6 +48,15 @@
|
|||||||
prop
|
prop
|
||||||
value))
|
value))
|
||||||
(list (quote hs-query-all) (nth base-ast 1))))
|
(list (quote hs-query-all) (nth base-ast 1))))
|
||||||
|
((and (list? base-ast) (= (first base-ast) (quote query)))
|
||||||
|
(list
|
||||||
|
(quote dom-set-prop)
|
||||||
|
(list
|
||||||
|
(quote hs-named-target)
|
||||||
|
(nth base-ast 1)
|
||||||
|
(list (quote hs-query-first) (nth base-ast 1)))
|
||||||
|
prop
|
||||||
|
value))
|
||||||
((and (list? base-ast) (= (first base-ast) dot-sym) (let ((inner (nth base-ast 1))) (and (list? inner) (= (first inner) (quote query)) (let ((s (nth inner 1))) (and (string? s) (> (len s) 0) (= (substring s 0 1) "."))))))
|
((and (list? base-ast) (= (first base-ast) dot-sym) (let ((inner (nth base-ast 1))) (and (list? inner) (= (first inner) (quote query)) (let ((s (nth inner 1))) (and (string? s) (> (len s) 0) (= (substring s 0 1) "."))))))
|
||||||
(let
|
(let
|
||||||
((inner (nth base-ast 1))
|
((inner (nth base-ast 1))
|
||||||
@@ -146,6 +155,14 @@
|
|||||||
(nth prop-ast 1)
|
(nth prop-ast 1)
|
||||||
value)
|
value)
|
||||||
(list (quote set!) (hs-to-sx target) value))))))
|
(list (quote set!) (hs-to-sx target) value))))))
|
||||||
|
((= th (quote query))
|
||||||
|
(list
|
||||||
|
(quote hs-set-inner-html!)
|
||||||
|
(list
|
||||||
|
(quote hs-named-target)
|
||||||
|
(nth target 1)
|
||||||
|
(list (quote hs-query-first) (nth target 1)))
|
||||||
|
value))
|
||||||
(true (list (quote set!) (hs-to-sx target) value)))))))
|
(true (list (quote set!) (hs-to-sx target) value)))))))
|
||||||
(define
|
(define
|
||||||
emit-on
|
emit-on
|
||||||
@@ -274,17 +291,33 @@
|
|||||||
((name (nth ast 1)) (rest-parts (rest (rest ast))))
|
((name (nth ast 1)) (rest-parts (rest (rest ast))))
|
||||||
(cond
|
(cond
|
||||||
((and (= (len ast) 4) (list? (nth ast 2)) (= (first (nth ast 2)) (quote dict)))
|
((and (= (len ast) 4) (list? (nth ast 2)) (= (first (nth ast 2)) (quote dict)))
|
||||||
(list
|
(let
|
||||||
(quote dom-dispatch)
|
((tgt-ast (nth ast 3)))
|
||||||
(hs-to-sx (nth ast 3))
|
(list
|
||||||
name
|
(quote dom-dispatch)
|
||||||
(hs-to-sx (nth ast 2))))
|
(if
|
||||||
|
(and (list? tgt-ast) (= (first tgt-ast) (quote query)))
|
||||||
|
(list
|
||||||
|
(quote hs-named-target)
|
||||||
|
(nth tgt-ast 1)
|
||||||
|
(list (quote hs-query-first) (nth tgt-ast 1)))
|
||||||
|
(hs-to-sx tgt-ast))
|
||||||
|
name
|
||||||
|
(hs-to-sx (nth ast 2)))))
|
||||||
((= (len ast) 3)
|
((= (len ast) 3)
|
||||||
(list
|
(let
|
||||||
(quote dom-dispatch)
|
((tgt-ast (nth ast 2)))
|
||||||
(hs-to-sx (nth ast 2))
|
(list
|
||||||
name
|
(quote dom-dispatch)
|
||||||
(list (quote dict) "sender" (quote me))))
|
(if
|
||||||
|
(and (list? tgt-ast) (= (first tgt-ast) (quote query)))
|
||||||
|
(list
|
||||||
|
(quote hs-named-target)
|
||||||
|
(nth tgt-ast 1)
|
||||||
|
(list (quote hs-query-first) (nth tgt-ast 1)))
|
||||||
|
(hs-to-sx tgt-ast))
|
||||||
|
name
|
||||||
|
(list (quote dict) "sender" (quote me)))))
|
||||||
(true
|
(true
|
||||||
(list
|
(list
|
||||||
(quote dom-dispatch)
|
(quote dom-dispatch)
|
||||||
@@ -706,6 +739,33 @@
|
|||||||
(quote fn)
|
(quote fn)
|
||||||
(cons (quote me) (map make-symbol params))
|
(cons (quote me) (map make-symbol params))
|
||||||
(cons (quote do) (map hs-to-sx body)))))))
|
(cons (quote do) (map hs-to-sx body)))))))
|
||||||
|
(define
|
||||||
|
hs-safe-obj
|
||||||
|
(fn
|
||||||
|
(obj-ast)
|
||||||
|
(if
|
||||||
|
(and (list? obj-ast) (= (first obj-ast) (quote ref)))
|
||||||
|
(list (quote host-global) (nth obj-ast 1))
|
||||||
|
(if
|
||||||
|
(and (list? obj-ast) (= (first obj-ast) dot-sym))
|
||||||
|
(let
|
||||||
|
((inner (nth obj-ast 1)) (prop (nth obj-ast 2)))
|
||||||
|
(list (quote host-get) (hs-safe-obj inner) prop))
|
||||||
|
(hs-to-sx obj-ast)))))
|
||||||
|
(define
|
||||||
|
hs-chain-name
|
||||||
|
(fn
|
||||||
|
(obj-ast)
|
||||||
|
(if
|
||||||
|
(and (list? obj-ast) (= (first obj-ast) (quote ref)))
|
||||||
|
(nth obj-ast 1)
|
||||||
|
(if
|
||||||
|
(and (list? obj-ast) (= (first obj-ast) dot-sym))
|
||||||
|
(str (hs-chain-name (nth obj-ast 1)) "." (nth obj-ast 2))
|
||||||
|
(if
|
||||||
|
(and (list? obj-ast) (= (first obj-ast) (quote query)))
|
||||||
|
(nth obj-ast 1)
|
||||||
|
nil)))))
|
||||||
(fn
|
(fn
|
||||||
(ast)
|
(ast)
|
||||||
(cond
|
(cond
|
||||||
@@ -1226,12 +1286,21 @@
|
|||||||
(if
|
(if
|
||||||
(and (list? raw-tgt) (= (first raw-tgt) (quote query)))
|
(and (list? raw-tgt) (= (first raw-tgt) (quote query)))
|
||||||
(list
|
(list
|
||||||
(quote for-each)
|
(quote let)
|
||||||
(list
|
(list
|
||||||
(quote fn)
|
(list
|
||||||
(list (quote _el))
|
(quote _tgt)
|
||||||
(list (quote dom-add-class) (quote _el) (nth ast 1)))
|
(list (quote hs-query-named-all) (nth raw-tgt 1))))
|
||||||
(list (quote hs-query-all) (nth raw-tgt 1)))
|
(list
|
||||||
|
(quote for-each)
|
||||||
|
(list
|
||||||
|
(quote fn)
|
||||||
|
(list (quote _el))
|
||||||
|
(list
|
||||||
|
(quote dom-add-class)
|
||||||
|
(quote _el)
|
||||||
|
(nth ast 1)))
|
||||||
|
(quote _tgt)))
|
||||||
(list
|
(list
|
||||||
(quote dom-add-class)
|
(quote dom-add-class)
|
||||||
(hs-to-sx raw-tgt)
|
(hs-to-sx raw-tgt)
|
||||||
@@ -1244,14 +1313,20 @@
|
|||||||
(nth ast 2)))
|
(nth ast 2)))
|
||||||
((= head (quote set-styles))
|
((= head (quote set-styles))
|
||||||
(let
|
(let
|
||||||
((pairs (nth ast 1)) (tgt (hs-to-sx (nth ast 2))))
|
((pairs (nth ast 1)) (tgt-ast (nth ast 2)))
|
||||||
(cons
|
(let
|
||||||
(quote do)
|
((tgt (if (and (list? tgt-ast) (= (first tgt-ast) (quote query))) (list (quote hs-named-target) (nth tgt-ast 1) (list (quote hs-query-first) (nth tgt-ast 1))) (hs-to-sx tgt-ast))))
|
||||||
(map
|
(cons
|
||||||
(fn
|
(quote do)
|
||||||
(p)
|
(map
|
||||||
(list (quote dom-set-style) tgt (first p) (nth p 1)))
|
(fn
|
||||||
pairs))))
|
(p)
|
||||||
|
(list
|
||||||
|
(quote dom-set-style)
|
||||||
|
tgt
|
||||||
|
(first p)
|
||||||
|
(nth p 1)))
|
||||||
|
pairs)))))
|
||||||
((= head (quote multi-add-class))
|
((= head (quote multi-add-class))
|
||||||
(let
|
(let
|
||||||
((target (hs-to-sx (nth ast 1)))
|
((target (hs-to-sx (nth ast 1)))
|
||||||
@@ -1349,15 +1424,21 @@
|
|||||||
(if
|
(if
|
||||||
(and (list? raw-tgt) (= (first raw-tgt) (quote query)))
|
(and (list? raw-tgt) (= (first raw-tgt) (quote query)))
|
||||||
(list
|
(list
|
||||||
(quote for-each)
|
(quote let)
|
||||||
(list
|
(list
|
||||||
(quote fn)
|
|
||||||
(list (quote _el))
|
|
||||||
(list
|
(list
|
||||||
(quote dom-remove-class)
|
(quote _tgt)
|
||||||
(quote _el)
|
(list (quote hs-query-named-all) (nth raw-tgt 1))))
|
||||||
(nth ast 1)))
|
(list
|
||||||
(list (quote hs-query-all) (nth raw-tgt 1)))
|
(quote for-each)
|
||||||
|
(list
|
||||||
|
(quote fn)
|
||||||
|
(list (quote _el))
|
||||||
|
(list
|
||||||
|
(quote dom-remove-class)
|
||||||
|
(quote _el)
|
||||||
|
(nth ast 1)))
|
||||||
|
(quote _tgt)))
|
||||||
(list
|
(list
|
||||||
(quote dom-remove-class)
|
(quote dom-remove-class)
|
||||||
(if (nil? raw-tgt) (quote me) (hs-to-sx raw-tgt))
|
(if (nil? raw-tgt) (quote me) (hs-to-sx raw-tgt))
|
||||||
@@ -1401,15 +1482,32 @@
|
|||||||
((tgt (nth ast 3)))
|
((tgt (nth ast 3)))
|
||||||
(list
|
(list
|
||||||
(quote hs-set-attr!)
|
(quote hs-set-attr!)
|
||||||
(hs-to-sx tgt)
|
(if
|
||||||
|
(and (list? tgt) (= (first tgt) (quote query)))
|
||||||
|
(list
|
||||||
|
(quote hs-named-target)
|
||||||
|
(nth tgt 1)
|
||||||
|
(list (quote hs-query-first) (nth tgt 1)))
|
||||||
|
(hs-to-sx tgt))
|
||||||
(nth ast 1)
|
(nth ast 1)
|
||||||
(hs-to-sx (nth ast 2)))))
|
(hs-to-sx (nth ast 2)))))
|
||||||
((= head (quote remove-value))
|
((= head (quote remove-value))
|
||||||
(let
|
(let
|
||||||
((val (hs-to-sx (nth ast 1))) (tgt (nth ast 2)))
|
((val (hs-to-sx (nth ast 1))) (raw-tgt (nth ast 2)))
|
||||||
(emit-set
|
(emit-set
|
||||||
tgt
|
raw-tgt
|
||||||
(list (quote hs-remove-from!) val (hs-to-sx tgt)))))
|
(list
|
||||||
|
(quote hs-remove-from!)
|
||||||
|
val
|
||||||
|
(if
|
||||||
|
(and
|
||||||
|
(list? raw-tgt)
|
||||||
|
(= (first raw-tgt) (quote query)))
|
||||||
|
(list
|
||||||
|
(quote hs-named-target)
|
||||||
|
(nth raw-tgt 1)
|
||||||
|
(list (quote hs-query-first) (nth raw-tgt 1)))
|
||||||
|
(hs-to-sx raw-tgt))))))
|
||||||
((= head (quote empty-target))
|
((= head (quote empty-target))
|
||||||
(let
|
(let
|
||||||
((tgt (nth ast 1)))
|
((tgt (nth ast 1)))
|
||||||
@@ -1440,8 +1538,19 @@
|
|||||||
(hs-to-sx (nth ast 2))))
|
(hs-to-sx (nth ast 2))))
|
||||||
((= head (quote remove-attr))
|
((= head (quote remove-attr))
|
||||||
(let
|
(let
|
||||||
((tgt (if (nil? (nth ast 2)) (quote me) (hs-to-sx (nth ast 2)))))
|
((raw-tgt (nth ast 2)))
|
||||||
(list (quote dom-remove-attr) tgt (nth ast 1))))
|
(list
|
||||||
|
(quote dom-remove-attr)
|
||||||
|
(if
|
||||||
|
(and
|
||||||
|
(list? raw-tgt)
|
||||||
|
(= (first raw-tgt) (quote query)))
|
||||||
|
(list
|
||||||
|
(quote hs-named-target)
|
||||||
|
(nth raw-tgt 1)
|
||||||
|
(list (quote hs-query-first) (nth raw-tgt 1)))
|
||||||
|
(if (nil? raw-tgt) (quote me) (hs-to-sx raw-tgt)))
|
||||||
|
(nth ast 1))))
|
||||||
((= head (quote remove-css))
|
((= head (quote remove-css))
|
||||||
(let
|
(let
|
||||||
((tgt (if (nil? (nth ast 2)) (quote me) (hs-to-sx (nth ast 2))))
|
((tgt (if (nil? (nth ast 2)) (quote me) (hs-to-sx (nth ast 2))))
|
||||||
@@ -1452,10 +1561,20 @@
|
|||||||
(fn (p) (list (quote dom-set-style) tgt p ""))
|
(fn (p) (list (quote dom-set-style) tgt p ""))
|
||||||
props))))
|
props))))
|
||||||
((= head (quote toggle-class))
|
((= head (quote toggle-class))
|
||||||
(list
|
(let
|
||||||
(quote hs-toggle-class!)
|
((tgt-ast (nth ast 2)))
|
||||||
(hs-to-sx (nth ast 2))
|
(list
|
||||||
(nth ast 1)))
|
(quote hs-toggle-class!)
|
||||||
|
(if
|
||||||
|
(and
|
||||||
|
(list? tgt-ast)
|
||||||
|
(= (first tgt-ast) (quote query)))
|
||||||
|
(list
|
||||||
|
(quote hs-named-target)
|
||||||
|
(nth tgt-ast 1)
|
||||||
|
(list (quote hs-query-first) (nth tgt-ast 1)))
|
||||||
|
(hs-to-sx tgt-ast))
|
||||||
|
(nth ast 1))))
|
||||||
((= head (quote toggle-class-for))
|
((= head (quote toggle-class-for))
|
||||||
(list
|
(list
|
||||||
(quote do)
|
(quote do)
|
||||||
@@ -1510,11 +1629,21 @@
|
|||||||
(hs-to-sx tgt-ast)
|
(hs-to-sx tgt-ast)
|
||||||
(hs-to-sx val-ast)))))
|
(hs-to-sx val-ast)))))
|
||||||
((= head (quote toggle-between))
|
((= head (quote toggle-between))
|
||||||
(list
|
(let
|
||||||
(quote hs-toggle-between!)
|
((tgt-ast (nth ast 3)))
|
||||||
(hs-to-sx (nth ast 3))
|
(list
|
||||||
(nth ast 1)
|
(quote hs-toggle-between!)
|
||||||
(nth ast 2)))
|
(if
|
||||||
|
(and
|
||||||
|
(list? tgt-ast)
|
||||||
|
(= (first tgt-ast) (quote query)))
|
||||||
|
(list
|
||||||
|
(quote hs-named-target)
|
||||||
|
(nth tgt-ast 1)
|
||||||
|
(list (quote hs-query-first) (nth tgt-ast 1)))
|
||||||
|
(hs-to-sx tgt-ast))
|
||||||
|
(nth ast 1)
|
||||||
|
(nth ast 2))))
|
||||||
((= head (quote toggle-style))
|
((= head (quote toggle-style))
|
||||||
(let
|
(let
|
||||||
((raw-tgt (nth ast 2)))
|
((raw-tgt (nth ast 2)))
|
||||||
@@ -1538,10 +1667,20 @@
|
|||||||
(quote list)
|
(quote list)
|
||||||
(map hs-to-sx (slice ast 3 (len ast))))))
|
(map hs-to-sx (slice ast 3 (len ast))))))
|
||||||
((= head (quote toggle-attr))
|
((= head (quote toggle-attr))
|
||||||
(list
|
(let
|
||||||
(quote hs-toggle-attr!)
|
((tgt-ast (nth ast 2)))
|
||||||
(hs-to-sx (nth ast 2))
|
(list
|
||||||
(nth ast 1)))
|
(quote hs-toggle-attr!)
|
||||||
|
(if
|
||||||
|
(and
|
||||||
|
(list? tgt-ast)
|
||||||
|
(= (first tgt-ast) (quote query)))
|
||||||
|
(list
|
||||||
|
(quote hs-named-target)
|
||||||
|
(nth tgt-ast 1)
|
||||||
|
(list (quote hs-query-first) (nth tgt-ast 1)))
|
||||||
|
(hs-to-sx tgt-ast))
|
||||||
|
(nth ast 1))))
|
||||||
((= head (quote toggle-attr-between))
|
((= head (quote toggle-attr-between))
|
||||||
(list
|
(list
|
||||||
(quote hs-toggle-attr-between!)
|
(quote hs-toggle-attr-between!)
|
||||||
@@ -1575,7 +1714,22 @@
|
|||||||
(emit-set
|
(emit-set
|
||||||
raw-tgt
|
raw-tgt
|
||||||
(list (quote hs-put-at!) val pos (hs-to-sx raw-tgt))))
|
(list (quote hs-put-at!) val pos (hs-to-sx raw-tgt))))
|
||||||
(true (list (quote hs-put!) val pos (hs-to-sx raw-tgt))))))
|
(true
|
||||||
|
(let
|
||||||
|
((tgt-ast raw-tgt))
|
||||||
|
(list
|
||||||
|
(quote hs-put!)
|
||||||
|
val
|
||||||
|
pos
|
||||||
|
(if
|
||||||
|
(and
|
||||||
|
(list? tgt-ast)
|
||||||
|
(= (first tgt-ast) (quote query)))
|
||||||
|
(list
|
||||||
|
(quote hs-named-target)
|
||||||
|
(nth tgt-ast 1)
|
||||||
|
(list (quote hs-query-first) (nth tgt-ast 1)))
|
||||||
|
(hs-to-sx tgt-ast))))))))
|
||||||
((= head (quote if))
|
((= head (quote if))
|
||||||
(if
|
(if
|
||||||
(> (len ast) 3)
|
(> (len ast) 3)
|
||||||
@@ -1651,12 +1805,22 @@
|
|||||||
(detail (if (= (len ast) 4) (nth ast 2) nil)))
|
(detail (if (= (len ast) 4) (nth ast 2) nil)))
|
||||||
(list
|
(list
|
||||||
(quote dom-dispatch)
|
(quote dom-dispatch)
|
||||||
(hs-to-sx tgt)
|
(let
|
||||||
|
((tgt-ast tgt))
|
||||||
|
(if
|
||||||
|
(and
|
||||||
|
(list? tgt-ast)
|
||||||
|
(= (first tgt-ast) (quote query)))
|
||||||
|
(list
|
||||||
|
(quote hs-named-target)
|
||||||
|
(nth tgt-ast 1)
|
||||||
|
(list (quote hs-query-first) (nth tgt-ast 1)))
|
||||||
|
(hs-to-sx tgt-ast)))
|
||||||
name
|
name
|
||||||
(if has-detail (hs-to-sx detail) nil))))
|
(if has-detail (hs-to-sx detail) nil))))
|
||||||
((= head (quote hide))
|
((= head (quote hide))
|
||||||
(let
|
(let
|
||||||
((tgt (let ((raw-tgt (nth ast 1))) (if (and (list? raw-tgt) (= (first raw-tgt) (quote query))) (list (quote hs-query-all) (nth raw-tgt 1)) (hs-to-sx raw-tgt))))
|
((tgt (let ((raw-tgt (nth ast 1))) (if (and (list? raw-tgt) (= (first raw-tgt) (quote query))) (list (quote hs-query-named-all) (nth raw-tgt 1)) (hs-to-sx raw-tgt))))
|
||||||
(strategy (if (> (len ast) 2) (nth ast 2) "display"))
|
(strategy (if (> (len ast) 2) (nth ast 2) "display"))
|
||||||
(when-cond (if (> (len ast) 3) (nth ast 3) nil)))
|
(when-cond (if (> (len ast) 3) (nth ast 3) nil)))
|
||||||
(if
|
(if
|
||||||
@@ -1672,7 +1836,7 @@
|
|||||||
(hs-to-sx when-cond))))))
|
(hs-to-sx when-cond))))))
|
||||||
((= head (quote show))
|
((= head (quote show))
|
||||||
(let
|
(let
|
||||||
((tgt (let ((raw-tgt (nth ast 1))) (if (and (list? raw-tgt) (= (first raw-tgt) (quote query))) (list (quote hs-query-all) (nth raw-tgt 1)) (hs-to-sx raw-tgt))))
|
((tgt (let ((raw-tgt (nth ast 1))) (if (and (list? raw-tgt) (= (first raw-tgt) (quote query))) (list (quote hs-query-named-all) (nth raw-tgt 1)) (hs-to-sx raw-tgt))))
|
||||||
(strategy (if (> (len ast) 2) (nth ast 2) "display"))
|
(strategy (if (> (len ast) 2) (nth ast 2) "display"))
|
||||||
(when-cond (if (> (len ast) 3) (nth ast 3) nil)))
|
(when-cond (if (> (len ast) 3) (nth ast 3) nil)))
|
||||||
(if
|
(if
|
||||||
@@ -1735,13 +1899,28 @@
|
|||||||
((= head (quote call))
|
((= head (quote call))
|
||||||
(let
|
(let
|
||||||
((raw-fn (nth ast 1))
|
((raw-fn (nth ast 1))
|
||||||
(fn-expr
|
|
||||||
(if
|
|
||||||
(string? raw-fn)
|
|
||||||
(make-symbol raw-fn)
|
|
||||||
(hs-to-sx raw-fn)))
|
|
||||||
(args (map hs-to-sx (rest (rest ast)))))
|
(args (map hs-to-sx (rest (rest ast)))))
|
||||||
(cons fn-expr args)))
|
(if
|
||||||
|
(and (list? raw-fn) (= (first raw-fn) (quote ref)))
|
||||||
|
(let
|
||||||
|
((name (nth raw-fn 1)))
|
||||||
|
(list
|
||||||
|
(quote let)
|
||||||
|
(list
|
||||||
|
(list
|
||||||
|
(quote __hs-fn)
|
||||||
|
(list (quote host-global) name)))
|
||||||
|
(cons
|
||||||
|
(quote do)
|
||||||
|
(list
|
||||||
|
(list
|
||||||
|
(quote if)
|
||||||
|
(list (quote nil?) (quote __hs-fn))
|
||||||
|
(list (quote raise) (str "'" name "' is null"))
|
||||||
|
(cons (quote __hs-fn) args))))))
|
||||||
|
(let
|
||||||
|
((fn-expr (if (string? raw-fn) (make-symbol raw-fn) (hs-to-sx raw-fn))))
|
||||||
|
(cons fn-expr args)))))
|
||||||
((= head (quote return))
|
((= head (quote return))
|
||||||
(let
|
(let
|
||||||
((val (nth ast 1)))
|
((val (nth ast 1)))
|
||||||
@@ -1754,7 +1933,22 @@
|
|||||||
((= head (quote throw))
|
((= head (quote throw))
|
||||||
(list (quote raise) (hs-to-sx (nth ast 1))))
|
(list (quote raise) (hs-to-sx (nth ast 1))))
|
||||||
((= head (quote settle))
|
((= head (quote settle))
|
||||||
(list (quote hs-settle) (quote me)))
|
(let
|
||||||
|
((raw-tgt (nth ast 1)))
|
||||||
|
(list
|
||||||
|
(quote hs-settle)
|
||||||
|
(if
|
||||||
|
(nil? raw-tgt)
|
||||||
|
(quote me)
|
||||||
|
(if
|
||||||
|
(and
|
||||||
|
(list? raw-tgt)
|
||||||
|
(= (first raw-tgt) (quote query)))
|
||||||
|
(list
|
||||||
|
(quote hs-named-target)
|
||||||
|
(nth raw-tgt 1)
|
||||||
|
(list (quote hs-query-first) (nth raw-tgt 1)))
|
||||||
|
(hs-to-sx raw-tgt))))))
|
||||||
((= head (quote go))
|
((= head (quote go))
|
||||||
(list (quote hs-navigate!) (hs-to-sx (nth ast 1))))
|
(list (quote hs-navigate!) (hs-to-sx (nth ast 1))))
|
||||||
((= head (quote ask))
|
((= head (quote ask))
|
||||||
@@ -1874,7 +2068,11 @@
|
|||||||
((= head (quote install))
|
((= head (quote install))
|
||||||
(cons (quote hs-install) (map hs-to-sx (rest ast))))
|
(cons (quote hs-install) (map hs-to-sx (rest ast))))
|
||||||
((= head (quote measure))
|
((= head (quote measure))
|
||||||
(list (quote hs-measure) (hs-to-sx (nth ast 1))))
|
(let
|
||||||
|
((raw-tgt (nth ast 1)))
|
||||||
|
(let
|
||||||
|
((compiled-tgt (if (and (list? raw-tgt) (= (first raw-tgt) (quote query))) (list (quote hs-named-target) (nth raw-tgt 1) (list (quote hs-query-first) (nth raw-tgt 1))) (hs-to-sx raw-tgt))))
|
||||||
|
(list (quote hs-measure) compiled-tgt))))
|
||||||
((= head (quote increment!))
|
((= head (quote increment!))
|
||||||
(if
|
(if
|
||||||
(= (len ast) 3)
|
(= (len ast) 3)
|
||||||
|
|||||||
@@ -2455,7 +2455,16 @@
|
|||||||
((and (= typ "keyword") (= val "answer"))
|
((and (= typ "keyword") (= val "answer"))
|
||||||
(do (adv!) (parse-answer-cmd)))
|
(do (adv!) (parse-answer-cmd)))
|
||||||
((and (= typ "keyword") (= val "settle"))
|
((and (= typ "keyword") (= val "settle"))
|
||||||
(do (adv!) (list (quote settle))))
|
(do
|
||||||
|
(adv!)
|
||||||
|
(if
|
||||||
|
(or
|
||||||
|
(at-end?)
|
||||||
|
(and
|
||||||
|
(= (tp-type) "keyword")
|
||||||
|
(or (= (tp-val) "then") (= (tp-val) "end"))))
|
||||||
|
(list (quote settle))
|
||||||
|
(list (quote settle) (parse-expr)))))
|
||||||
((and (= typ "keyword") (= val "go"))
|
((and (= typ "keyword") (= val "go"))
|
||||||
(do (adv!) (parse-go-cmd)))
|
(do (adv!) (parse-go-cmd)))
|
||||||
((and (= typ "keyword") (= val "return"))
|
((and (= typ "keyword") (= val "return"))
|
||||||
|
|||||||
@@ -12,37 +12,14 @@
|
|||||||
|
|
||||||
;; Register an event listener. Returns unlisten function.
|
;; Register an event listener. Returns unlisten function.
|
||||||
;; (hs-on target event-name handler) → unlisten-fn
|
;; (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)))))
|
|
||||||
|
|
||||||
;; Register for every occurrence (no queuing — each fires independently).
|
|
||||||
;; Stock hyperscript queues by default; "every" disables queuing.
|
|
||||||
(define
|
(define
|
||||||
hs-each
|
hs-each
|
||||||
(fn
|
(fn
|
||||||
(target action)
|
(target action)
|
||||||
(if (list? target) (for-each action target) (action target))))
|
(if (list? target) (for-each action target) (action target))))
|
||||||
|
|
||||||
;; Run an initializer function immediately.
|
;; Register for every occurrence (no queuing — each fires independently).
|
||||||
;; (hs-init thunk) — called at element boot time
|
;; Stock hyperscript queues by default; "every" disables queuing.
|
||||||
(define
|
(define
|
||||||
hs-on
|
hs-on
|
||||||
(fn
|
(fn
|
||||||
@@ -55,17 +32,17 @@
|
|||||||
(dom-set-data target "hs-unlisteners" (append prev (list unlisten)))
|
(dom-set-data target "hs-unlisteners" (append prev (list unlisten)))
|
||||||
unlisten))))
|
unlisten))))
|
||||||
|
|
||||||
|
;; 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 ──────────────────────────────────────────────
|
;; ── Async / timing ──────────────────────────────────────────────
|
||||||
|
|
||||||
;; Wait for a duration in milliseconds.
|
;; Wait for a duration in milliseconds.
|
||||||
;; In hyperscript, wait is async-transparent — execution pauses.
|
;; In hyperscript, wait is async-transparent — execution pauses.
|
||||||
;; Here we use perform/IO suspension for true pause semantics.
|
;; 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)))
|
|
||||||
|
|
||||||
;; Wait for a DOM event on a target.
|
|
||||||
;; (hs-wait-for target event-name) — suspends until event fires
|
|
||||||
(define
|
(define
|
||||||
hs-on-intersection-attach!
|
hs-on-intersection-attach!
|
||||||
(fn
|
(fn
|
||||||
@@ -81,15 +58,16 @@
|
|||||||
(host-call observer "observe" target)
|
(host-call observer "observe" target)
|
||||||
observer)))))
|
observer)))))
|
||||||
|
|
||||||
;; 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-init (fn (thunk) (thunk)))
|
(define hs-init (fn (thunk) (thunk)))
|
||||||
|
|
||||||
|
;; Wait for CSS transitions/animations to settle on an element.
|
||||||
|
(define hs-wait (fn (ms) (perform (list (quote io-sleep) ms))))
|
||||||
|
|
||||||
;; ── Class manipulation ──────────────────────────────────────────
|
;; ── Class manipulation ──────────────────────────────────────────
|
||||||
|
|
||||||
;; Toggle a single class on an element.
|
;; Toggle a single class on an element.
|
||||||
(define hs-wait (fn (ms) (perform (list (quote io-sleep) ms))))
|
|
||||||
|
|
||||||
;; Toggle between two classes — exactly one is active at a time.
|
|
||||||
(begin
|
(begin
|
||||||
(define
|
(define
|
||||||
hs-wait-for
|
hs-wait-for
|
||||||
@@ -102,21 +80,19 @@
|
|||||||
(target event-name timeout-ms)
|
(target event-name timeout-ms)
|
||||||
(perform (list (quote io-wait-event) target event-name timeout-ms)))))
|
(perform (list (quote io-wait-event) target event-name timeout-ms)))))
|
||||||
|
|
||||||
|
;; Toggle between two classes — exactly one is active at a time.
|
||||||
|
(define hs-settle (fn (target) (perform (list (quote io-settle) target))))
|
||||||
|
|
||||||
;; Take a class from siblings — add to target, remove from others.
|
;; Take a class from siblings — add to target, remove from others.
|
||||||
;; (hs-take! target cls) — like radio button class behavior
|
;; (hs-take! target cls) — like radio button class behavior
|
||||||
(define hs-settle (fn (target) (perform (list (quote io-settle) target))))
|
(define
|
||||||
|
hs-toggle-class!
|
||||||
|
(fn (target cls) (host-call (host-get target "classList") "toggle" cls)))
|
||||||
|
|
||||||
;; ── DOM insertion ───────────────────────────────────────────────
|
;; ── DOM insertion ───────────────────────────────────────────────
|
||||||
|
|
||||||
;; Put content at a position relative to a target.
|
;; Put content at a position relative to a target.
|
||||||
;; pos: "into" | "before" | "after"
|
;; pos: "into" | "before" | "after"
|
||||||
(define
|
|
||||||
hs-toggle-class!
|
|
||||||
(fn (target cls) (host-call (host-get target "classList") "toggle" cls)))
|
|
||||||
|
|
||||||
;; ── Navigation / traversal ──────────────────────────────────────
|
|
||||||
|
|
||||||
;; Navigate to a URL.
|
|
||||||
(define
|
(define
|
||||||
hs-toggle-between!
|
hs-toggle-between!
|
||||||
(fn
|
(fn
|
||||||
@@ -126,7 +102,9 @@
|
|||||||
(do (dom-remove-class target cls1) (dom-add-class target cls2))
|
(do (dom-remove-class target cls1) (dom-add-class target cls2))
|
||||||
(do (dom-remove-class target cls2) (dom-add-class target cls1)))))
|
(do (dom-remove-class target cls2) (dom-add-class target cls1)))))
|
||||||
|
|
||||||
;; Find next sibling matching a selector (or any sibling).
|
;; ── Navigation / traversal ──────────────────────────────────────
|
||||||
|
|
||||||
|
;; Navigate to a URL.
|
||||||
(define
|
(define
|
||||||
hs-toggle-style!
|
hs-toggle-style!
|
||||||
(fn
|
(fn
|
||||||
@@ -150,7 +128,7 @@
|
|||||||
(dom-set-style target prop "hidden")
|
(dom-set-style target prop "hidden")
|
||||||
(dom-set-style target prop "")))))))
|
(dom-set-style target prop "")))))))
|
||||||
|
|
||||||
;; Find previous sibling matching a selector.
|
;; Find next sibling matching a selector (or any sibling).
|
||||||
(define
|
(define
|
||||||
hs-toggle-style-between!
|
hs-toggle-style-between!
|
||||||
(fn
|
(fn
|
||||||
@@ -162,7 +140,7 @@
|
|||||||
(dom-set-style target prop val2)
|
(dom-set-style target prop val2)
|
||||||
(dom-set-style target prop val1)))))
|
(dom-set-style target prop val1)))))
|
||||||
|
|
||||||
;; First element matching selector within a scope.
|
;; Find previous sibling matching a selector.
|
||||||
(define
|
(define
|
||||||
hs-toggle-style-cycle!
|
hs-toggle-style-cycle!
|
||||||
(fn
|
(fn
|
||||||
@@ -183,7 +161,7 @@
|
|||||||
(true (find-next (rest remaining))))))
|
(true (find-next (rest remaining))))))
|
||||||
(dom-set-style target prop (find-next vals)))))
|
(dom-set-style target prop (find-next vals)))))
|
||||||
|
|
||||||
;; Last element matching selector.
|
;; First element matching selector within a scope.
|
||||||
(define
|
(define
|
||||||
hs-take!
|
hs-take!
|
||||||
(fn
|
(fn
|
||||||
@@ -206,7 +184,8 @@
|
|||||||
(when with-cls (dom-remove-class target with-cls))))
|
(when with-cls (dom-remove-class target with-cls))))
|
||||||
(let
|
(let
|
||||||
((attr-val (if (> (len extra) 0) (first extra) nil))
|
((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
|
(do
|
||||||
(for-each
|
(for-each
|
||||||
(fn
|
(fn
|
||||||
@@ -223,7 +202,7 @@
|
|||||||
(dom-set-attr target name attr-val)
|
(dom-set-attr target name attr-val)
|
||||||
(dom-set-attr target name ""))))))))
|
(dom-set-attr target name ""))))))))
|
||||||
|
|
||||||
;; First/last within a specific scope.
|
;; Last element matching selector.
|
||||||
(begin
|
(begin
|
||||||
(define
|
(define
|
||||||
hs-element?
|
hs-element?
|
||||||
@@ -335,6 +314,7 @@
|
|||||||
(dom-insert-adjacent-html target "beforeend" value)
|
(dom-insert-adjacent-html target "beforeend" value)
|
||||||
(hs-boot-subtree! target)))))))))
|
(hs-boot-subtree! target)))))))))
|
||||||
|
|
||||||
|
;; First/last within a specific scope.
|
||||||
(define
|
(define
|
||||||
hs-add-to!
|
hs-add-to!
|
||||||
(fn
|
(fn
|
||||||
@@ -347,9 +327,6 @@
|
|||||||
(append target (list value))))
|
(append target (list value))))
|
||||||
(true (do (host-call target "push" value) target)))))
|
(true (do (host-call target "push" value) target)))))
|
||||||
|
|
||||||
;; ── Iteration ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
;; Repeat a thunk N times.
|
|
||||||
(define
|
(define
|
||||||
hs-remove-from!
|
hs-remove-from!
|
||||||
(fn
|
(fn
|
||||||
@@ -357,9 +334,15 @@
|
|||||||
(if
|
(if
|
||||||
(list? target)
|
(list? target)
|
||||||
(filter (fn (x) (not (= x value))) target)
|
(filter (fn (x) (not (= x value))) target)
|
||||||
(host-call target "splice" (host-call target "indexOf" value) 1))))
|
(host-call
|
||||||
|
target
|
||||||
|
"splice"
|
||||||
|
(host-call target "indexOf" value)
|
||||||
|
1))))
|
||||||
|
|
||||||
;; Repeat forever (until break — relies on exception/continuation).
|
;; ── Iteration ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
;; Repeat a thunk N times.
|
||||||
(define
|
(define
|
||||||
hs-splice-at!
|
hs-splice-at!
|
||||||
(fn
|
(fn
|
||||||
@@ -372,7 +355,10 @@
|
|||||||
((i (if (< idx 0) (+ n idx) idx)))
|
((i (if (< idx 0) (+ n idx) idx)))
|
||||||
(cond
|
(cond
|
||||||
((or (< i 0) (>= i n)) target)
|
((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
|
(do
|
||||||
(when
|
(when
|
||||||
target
|
target
|
||||||
@@ -383,10 +369,7 @@
|
|||||||
(host-call target "splice" i 1))))
|
(host-call target "splice" i 1))))
|
||||||
target))))
|
target))))
|
||||||
|
|
||||||
;; ── Fetch ───────────────────────────────────────────────────────
|
;; Repeat forever (until break — relies on exception/continuation).
|
||||||
|
|
||||||
;; Fetch a URL, parse response according to format.
|
|
||||||
;; (hs-fetch url format) — format is "json" | "text" | "html"
|
|
||||||
(define
|
(define
|
||||||
hs-index
|
hs-index
|
||||||
(fn
|
(fn
|
||||||
@@ -398,10 +381,10 @@
|
|||||||
((string? obj) (nth obj key))
|
((string? obj) (nth obj key))
|
||||||
(true (host-get obj key)))))
|
(true (host-get obj key)))))
|
||||||
|
|
||||||
;; ── Type coercion ───────────────────────────────────────────────
|
;; ── Fetch ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
;; Coerce a value to a type by name.
|
;; Fetch a URL, parse response according to format.
|
||||||
;; (hs-coerce value type-name) — type-name is "Int", "Float", "String", etc.
|
;; (hs-fetch url format) — format is "json" | "text" | "html"
|
||||||
(define
|
(define
|
||||||
hs-put-at!
|
hs-put-at!
|
||||||
(fn
|
(fn
|
||||||
@@ -423,10 +406,10 @@
|
|||||||
((= pos "start") (host-call target "unshift" value)))
|
((= pos "start") (host-call target "unshift" value)))
|
||||||
target)))))))
|
target)))))))
|
||||||
|
|
||||||
;; ── Object creation ─────────────────────────────────────────────
|
;; ── Type coercion ───────────────────────────────────────────────
|
||||||
|
|
||||||
;; Make a new object of a given type.
|
;; Coerce a value to a type by name.
|
||||||
;; (hs-make type-name) — creates empty object/collection
|
;; (hs-coerce value type-name) — type-name is "Int", "Float", "String", etc.
|
||||||
(define
|
(define
|
||||||
hs-dict-without
|
hs-dict-without
|
||||||
(fn
|
(fn
|
||||||
@@ -447,27 +430,27 @@
|
|||||||
(host-call (host-global "Reflect") "deleteProperty" out key)
|
(host-call (host-global "Reflect") "deleteProperty" out key)
|
||||||
out)))))
|
out)))))
|
||||||
|
|
||||||
;; ── Behavior installation ───────────────────────────────────────
|
;; ── Object creation ─────────────────────────────────────────────
|
||||||
|
|
||||||
;; Install a behavior on an element.
|
;; Make a new object of a given type.
|
||||||
;; A behavior is a function that takes (me ...params) and sets up features.
|
;; (hs-make type-name) — creates empty object/collection
|
||||||
;; (hs-install behavior-fn me ...args)
|
|
||||||
(define
|
(define
|
||||||
hs-set-on!
|
hs-set-on!
|
||||||
(fn
|
(fn
|
||||||
(props target)
|
(props target)
|
||||||
(for-each (fn (k) (host-set! target k (get props k))) (keys props))))
|
(for-each (fn (k) (host-set! target k (get props k))) (keys props))))
|
||||||
|
|
||||||
|
;; ── Behavior installation ───────────────────────────────────────
|
||||||
|
|
||||||
|
;; 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-navigate! (fn (url) (perform (list (quote io-navigate) url))))
|
||||||
|
|
||||||
;; ── Measurement ─────────────────────────────────────────────────
|
;; ── Measurement ─────────────────────────────────────────────────
|
||||||
|
|
||||||
;; Measure an element's bounding rect, store as local variables.
|
;; Measure an element's bounding rect, store as local variables.
|
||||||
;; Returns a dict with x, y, width, height, top, left, right, bottom.
|
;; Returns a dict with x, y, width, height, top, left, right, bottom.
|
||||||
(define hs-navigate! (fn (url) (perform (list (quote io-navigate) url))))
|
|
||||||
|
|
||||||
;; 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
|
(define
|
||||||
hs-ask
|
hs-ask
|
||||||
(fn
|
(fn
|
||||||
@@ -476,11 +459,10 @@
|
|||||||
((w (host-global "window")))
|
((w (host-global "window")))
|
||||||
(if w (host-call w "prompt" msg) nil))))
|
(if w (host-call w "prompt" msg) nil))))
|
||||||
|
|
||||||
|
;; Return the current text selection as a string. In the browser this is
|
||||||
;; ── Transition ──────────────────────────────────────────────────
|
;; `window.getSelection().toString()`. In the mock test runner, a test
|
||||||
|
;; setup stashes the desired selection text at `window.__test_selection`
|
||||||
;; Transition a CSS property to a value, optionally with duration.
|
;; and the fallback path returns that so tests can assert on the result.
|
||||||
;; (hs-transition target prop value duration)
|
|
||||||
(define
|
(define
|
||||||
hs-answer
|
hs-answer
|
||||||
(fn
|
(fn
|
||||||
@@ -489,6 +471,11 @@
|
|||||||
((w (host-global "window")))
|
((w (host-global "window")))
|
||||||
(if w (if (host-call w "confirm" msg) yes-val no-val) no-val))))
|
(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
|
(define
|
||||||
hs-answer-alert
|
hs-answer-alert
|
||||||
(fn
|
(fn
|
||||||
@@ -643,25 +630,25 @@
|
|||||||
(hs-query-all sel)
|
(hs-query-all sel)
|
||||||
(host-call target "querySelectorAll" sel))))
|
(host-call target "querySelectorAll" sel))))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(define
|
(define
|
||||||
hs-list-set
|
hs-list-set
|
||||||
(fn
|
(fn
|
||||||
(lst idx val)
|
(lst idx val)
|
||||||
(append (take lst idx) (cons val (drop lst (+ idx 1))))))
|
(append (take lst idx) (cons val (drop lst (+ idx 1))))))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(define
|
(define
|
||||||
hs-to-number
|
hs-to-number
|
||||||
(fn (v) (if (number? v) v (or (parse-number (str v)) 0))))
|
(fn (v) (if (number? v) v (or (parse-number (str v)) 0))))
|
||||||
;; ── Sandbox/test runtime additions ──────────────────────────────
|
|
||||||
;; Property access — dot notation and .length
|
|
||||||
(define
|
(define
|
||||||
hs-query-first
|
hs-query-first
|
||||||
(fn (sel) (host-call (host-global "document") "querySelector" sel)))
|
(fn (sel) (host-call (host-global "document") "querySelector" sel)))
|
||||||
;; DOM query stub — sandbox returns empty list
|
;; ── Sandbox/test runtime additions ──────────────────────────────
|
||||||
|
;; Property access — dot notation and .length
|
||||||
(define
|
(define
|
||||||
hs-query-last
|
hs-query-last
|
||||||
(fn
|
(fn
|
||||||
@@ -669,11 +656,9 @@
|
|||||||
(let
|
(let
|
||||||
((all (dom-query-all (dom-body) sel)))
|
((all (dom-query-all (dom-body) sel)))
|
||||||
(if (> (len all) 0) (nth all (- (len all) 1)) nil))))
|
(if (> (len all) 0) (nth all (- (len all) 1)) nil))))
|
||||||
;; Method dispatch — obj.method(args)
|
;; DOM query stub — sandbox returns empty list
|
||||||
(define hs-first (fn (scope sel) (dom-query-all scope sel)))
|
(define hs-first (fn (scope sel) (dom-query-all scope sel)))
|
||||||
|
;; Method dispatch — obj.method(args)
|
||||||
;; ── 0.9.90 features ─────────────────────────────────────────────
|
|
||||||
;; beep! — debug logging, returns value unchanged
|
|
||||||
(define
|
(define
|
||||||
hs-last
|
hs-last
|
||||||
(fn
|
(fn
|
||||||
@@ -681,7 +666,9 @@
|
|||||||
(let
|
(let
|
||||||
((all (dom-query-all scope sel)))
|
((all (dom-query-all scope sel)))
|
||||||
(if (> (len all) 0) (nth all (- (len all) 1)) nil))))
|
(if (> (len all) 0) (nth all (- (len all) 1)) nil))))
|
||||||
;; Property-based is — check obj.key truthiness
|
|
||||||
|
;; ── 0.9.90 features ─────────────────────────────────────────────
|
||||||
|
;; beep! — debug logging, returns value unchanged
|
||||||
(define
|
(define
|
||||||
hs-repeat-times
|
hs-repeat-times
|
||||||
(fn
|
(fn
|
||||||
@@ -699,7 +686,7 @@
|
|||||||
((= signal "hs-continue") (do-repeat (+ i 1)))
|
((= signal "hs-continue") (do-repeat (+ i 1)))
|
||||||
(true (do-repeat (+ i 1))))))))
|
(true (do-repeat (+ i 1))))))))
|
||||||
(do-repeat 0)))
|
(do-repeat 0)))
|
||||||
;; Array slicing (inclusive both ends)
|
;; Property-based is — check obj.key truthiness
|
||||||
(define
|
(define
|
||||||
hs-repeat-forever
|
hs-repeat-forever
|
||||||
(fn
|
(fn
|
||||||
@@ -715,7 +702,7 @@
|
|||||||
((= signal "hs-continue") (do-forever))
|
((= signal "hs-continue") (do-forever))
|
||||||
(true (do-forever))))))
|
(true (do-forever))))))
|
||||||
(do-forever)))
|
(do-forever)))
|
||||||
;; Collection: sorted by
|
;; Array slicing (inclusive both ends)
|
||||||
(define
|
(define
|
||||||
hs-repeat-while
|
hs-repeat-while
|
||||||
(fn
|
(fn
|
||||||
@@ -728,7 +715,7 @@
|
|||||||
((= signal "hs-break") nil)
|
((= signal "hs-break") nil)
|
||||||
((= signal "hs-continue") (hs-repeat-while cond-fn thunk))
|
((= signal "hs-continue") (hs-repeat-while cond-fn thunk))
|
||||||
(true (hs-repeat-while cond-fn thunk)))))))
|
(true (hs-repeat-while cond-fn thunk)))))))
|
||||||
;; Collection: sorted by descending
|
;; Collection: sorted by
|
||||||
(define
|
(define
|
||||||
hs-repeat-until
|
hs-repeat-until
|
||||||
(fn
|
(fn
|
||||||
@@ -740,7 +727,7 @@
|
|||||||
((= signal "hs-continue")
|
((= signal "hs-continue")
|
||||||
(if (cond-fn) nil (hs-repeat-until cond-fn thunk)))
|
(if (cond-fn) nil (hs-repeat-until cond-fn thunk)))
|
||||||
(true (if (cond-fn) nil (hs-repeat-until cond-fn thunk)))))))
|
(true (if (cond-fn) nil (hs-repeat-until cond-fn thunk)))))))
|
||||||
;; Collection: split by
|
;; Collection: sorted by descending
|
||||||
(define
|
(define
|
||||||
hs-for-each
|
hs-for-each
|
||||||
(fn
|
(fn
|
||||||
@@ -760,7 +747,7 @@
|
|||||||
((= signal "hs-continue") (do-loop (rest remaining)))
|
((= signal "hs-continue") (do-loop (rest remaining)))
|
||||||
(true (do-loop (rest remaining))))))))
|
(true (do-loop (rest remaining))))))))
|
||||||
(do-loop items))))
|
(do-loop items))))
|
||||||
;; Collection: joined by
|
;; Collection: split by
|
||||||
(begin
|
(begin
|
||||||
(define
|
(define
|
||||||
hs-append
|
hs-append
|
||||||
@@ -788,7 +775,7 @@
|
|||||||
((hs-element? target)
|
((hs-element? target)
|
||||||
(dom-insert-adjacent-html target "beforeend" (str value)))
|
(dom-insert-adjacent-html target "beforeend" (str value)))
|
||||||
(true nil)))))
|
(true nil)))))
|
||||||
|
;; Collection: joined by
|
||||||
(define
|
(define
|
||||||
hs-sender
|
hs-sender
|
||||||
(fn
|
(fn
|
||||||
@@ -1310,10 +1297,14 @@
|
|||||||
((ch (substring sel i (+ i 1))))
|
((ch (substring sel i (+ i 1))))
|
||||||
(cond
|
(cond
|
||||||
((= ch ".")
|
((= ch ".")
|
||||||
(do (flush!) (set! mode "class") (walk (+ i 1))))
|
(do
|
||||||
|
(flush!)
|
||||||
|
(set! mode "class")
|
||||||
|
(walk (+ i 1))))
|
||||||
((= ch "#")
|
((= ch "#")
|
||||||
(do (flush!) (set! mode "id") (walk (+ i 1))))
|
(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)
|
(walk 0)
|
||||||
(flush!)
|
(flush!)
|
||||||
{:tag tag :classes classes :id id}))))
|
{:tag tag :classes classes :id id}))))
|
||||||
@@ -1398,6 +1389,7 @@
|
|||||||
hs-strict-eq
|
hs-strict-eq
|
||||||
(fn (a b) (and (= (type-of a) (type-of b)) (= a b))))
|
(fn (a b) (and (= (type-of a) (type-of b)) (= a b))))
|
||||||
|
|
||||||
|
|
||||||
(define
|
(define
|
||||||
hs-eq-ignore-case
|
hs-eq-ignore-case
|
||||||
(fn (a b) (= (downcase (str a)) (downcase (str b)))))
|
(fn (a b) (= (downcase (str a)) (downcase (str b)))))
|
||||||
@@ -1438,7 +1430,10 @@
|
|||||||
((and (dict? a) (dict? b))
|
((and (dict? a) (dict? b))
|
||||||
(let
|
(let
|
||||||
((pos (host-call a "compareDocumentPosition" b)))
|
((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))))))
|
(true (< (str a) (str b))))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
@@ -1540,7 +1535,10 @@
|
|||||||
((and (dict? a) (dict? b))
|
((and (dict? a) (dict? b))
|
||||||
(let
|
(let
|
||||||
((pos (host-call a "compareDocumentPosition" b)))
|
((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))))))
|
(true (< (str a) (str b))))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
@@ -1591,7 +1589,9 @@
|
|||||||
|
|
||||||
(define
|
(define
|
||||||
hs-morph-char
|
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
|
(define
|
||||||
hs-morph-index-from
|
hs-morph-index-from
|
||||||
@@ -1619,7 +1619,10 @@
|
|||||||
(q)
|
(q)
|
||||||
(let
|
(let
|
||||||
((c (hs-morph-char s q)))
|
((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))))
|
(let ((e (loop p))) (list (substring s p e) e))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
@@ -1661,7 +1664,9 @@
|
|||||||
(append
|
(append
|
||||||
acc
|
acc
|
||||||
(list
|
(list
|
||||||
(list name (substring s (+ p4 1) close)))))))
|
(list
|
||||||
|
name
|
||||||
|
(substring s (+ p4 1) close)))))))
|
||||||
((= c2 "'")
|
((= c2 "'")
|
||||||
(let
|
(let
|
||||||
((close (hs-morph-index-from s "'" (+ p4 1))))
|
((close (hs-morph-index-from s "'" (+ p4 1))))
|
||||||
@@ -1671,7 +1676,9 @@
|
|||||||
(append
|
(append
|
||||||
acc
|
acc
|
||||||
(list
|
(list
|
||||||
(list name (substring s (+ p4 1) close)))))))
|
(list
|
||||||
|
name
|
||||||
|
(substring s (+ p4 1) close)))))))
|
||||||
(true
|
(true
|
||||||
(let
|
(let
|
||||||
((r2 (hs-morph-read-until s p4 " \t\n/>")))
|
((r2 (hs-morph-read-until s p4 " \t\n/>")))
|
||||||
@@ -1755,7 +1762,9 @@
|
|||||||
(for-each
|
(for-each
|
||||||
(fn
|
(fn
|
||||||
(c)
|
(c)
|
||||||
(when (> (string-length c) 0) (dom-add-class el c)))
|
(when
|
||||||
|
(> (string-length c) 0)
|
||||||
|
(dom-add-class el c)))
|
||||||
(split v " ")))
|
(split v " ")))
|
||||||
((and keep-id (= n "id")) nil)
|
((and keep-id (= n "id")) nil)
|
||||||
(true (dom-set-attr el n v)))))
|
(true (dom-set-attr el n v)))))
|
||||||
@@ -1856,7 +1865,8 @@
|
|||||||
((parts (split resolved ":")))
|
((parts (split resolved ":")))
|
||||||
(let
|
(let
|
||||||
((prop (first parts))
|
((prop (first parts))
|
||||||
(val (if (> (len parts) 1) (nth parts 1) nil)))
|
(val
|
||||||
|
(if (> (len parts) 1) (nth parts 1) nil)))
|
||||||
(cond
|
(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))
|
((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
|
(let
|
||||||
@@ -1895,7 +1905,8 @@
|
|||||||
((parts (split resolved ":")))
|
((parts (split resolved ":")))
|
||||||
(let
|
(let
|
||||||
((prop (first parts))
|
((prop (first parts))
|
||||||
(val (if (> (len parts) 1) (nth parts 1) nil)))
|
(val
|
||||||
|
(if (> (len parts) 1) (nth parts 1) nil)))
|
||||||
(cond
|
(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))
|
((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
|
(let
|
||||||
@@ -1999,10 +2010,14 @@
|
|||||||
(if
|
(if
|
||||||
(= depth 1)
|
(= depth 1)
|
||||||
j
|
j
|
||||||
(find-close (+ j 1) (- depth 1)))
|
(find-close
|
||||||
|
(+ j 1)
|
||||||
|
(- depth 1)))
|
||||||
(if
|
(if
|
||||||
(= (nth raw j) "{")
|
(= (nth raw j) "{")
|
||||||
(find-close (+ j 1) (+ depth 1))
|
(find-close
|
||||||
|
(+ j 1)
|
||||||
|
(+ depth 1))
|
||||||
(find-close (+ j 1) depth))))))
|
(find-close (+ j 1) depth))))))
|
||||||
(let
|
(let
|
||||||
((close (find-close start 1)))
|
((close (find-close start 1)))
|
||||||
@@ -2093,7 +2108,10 @@
|
|||||||
(if
|
(if
|
||||||
(= (len lst) 0)
|
(= (len lst) 0)
|
||||||
-1
|
-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)))
|
(idx-loop obj 0)))
|
||||||
(true nil))))
|
(true nil))))
|
||||||
|
|
||||||
@@ -2179,7 +2197,8 @@
|
|||||||
(cond
|
(cond
|
||||||
((= end "hs-pick-end") n)
|
((= end "hs-pick-end") n)
|
||||||
((= end "hs-pick-start") 0)
|
((= 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))))
|
(true end))))
|
||||||
(cond
|
(cond
|
||||||
((string? col) (slice col s e))
|
((string? col) (slice col s e))
|
||||||
@@ -2466,6 +2485,50 @@
|
|||||||
((nth entry 2) val)))
|
((nth entry 2) val)))
|
||||||
_hs-dom-watchers)))
|
_hs-dom-watchers)))
|
||||||
|
|
||||||
|
(define hs-prolog-hook nil)
|
||||||
|
|
||||||
|
(define hs-set-prolog-hook! (fn (f) (set! hs-prolog-hook f)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
prolog
|
||||||
|
(fn
|
||||||
|
(db goal)
|
||||||
|
(if
|
||||||
|
(nil? hs-prolog-hook)
|
||||||
|
(raise "prolog hook not installed")
|
||||||
|
(hs-prolog-hook db goal))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hs-null-error!
|
||||||
|
(fn (selector) (raise (str "'" selector "' is null"))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hs-named-target
|
||||||
|
(fn (selector value) (if (nil? value) (hs-null-error! selector) value)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hs-named-target-list
|
||||||
|
(fn
|
||||||
|
(selector values)
|
||||||
|
(if (nil? values) (hs-null-error! selector) values)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hs-query-named-all
|
||||||
|
(fn
|
||||||
|
(selector)
|
||||||
|
(let
|
||||||
|
((results (hs-query-all selector)))
|
||||||
|
(if
|
||||||
|
(and
|
||||||
|
(or
|
||||||
|
(nil? results)
|
||||||
|
(and (list? results) (= (len results) 0)))
|
||||||
|
(string? selector)
|
||||||
|
(> (len selector) 0)
|
||||||
|
(= (substring selector 0 1) "#"))
|
||||||
|
(hs-null-error! selector)
|
||||||
|
results))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
hs-dom-is-ancestor?
|
hs-dom-is-ancestor?
|
||||||
(fn
|
(fn
|
||||||
|
|||||||
21
lib/prolog/hs-bridge.sx
Normal file
21
lib/prolog/hs-bridge.sx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
;; lib/prolog/hs-bridge.sx — Prolog ↔ _hyperscript bridge
|
||||||
|
;;
|
||||||
|
;; Installs the prolog hook into the hyperscript runtime so that
|
||||||
|
;; hyperscript scripts can call:
|
||||||
|
;;
|
||||||
|
;; prolog(db, "goal(args)") → true (at least one solution)
|
||||||
|
;; → false (no solution)
|
||||||
|
;;
|
||||||
|
;; Usage:
|
||||||
|
;; (pl-install-hs-hook!) ;; call once at startup, after loading both libs
|
||||||
|
;;
|
||||||
|
;; Depends on:
|
||||||
|
;; lib/hyperscript/runtime.sx — provides hs-set-prolog-hook!
|
||||||
|
;; lib/prolog/runtime.sx — provides pl-query-one (Phase 3+)
|
||||||
|
|
||||||
|
(define
|
||||||
|
pl-install-hs-hook!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(hs-set-prolog-hook!
|
||||||
|
(fn (db goal) (not (= nil (pl-query-one db goal)))))))
|
||||||
145
plans/datalog-on-sx.md
Normal file
145
plans/datalog-on-sx.md
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
# Datalog-on-SX: Datalog on the CEK/VM
|
||||||
|
|
||||||
|
Datalog is a declarative query language: a restricted subset of Prolog with no function
|
||||||
|
symbols, only relations. Programs are sets of facts and rules; queries ask what follows.
|
||||||
|
Evaluation is bottom-up (fixpoint iteration) rather than Prolog's top-down DFS — which
|
||||||
|
means no infinite loops, guaranteed termination, and efficient incremental updates.
|
||||||
|
|
||||||
|
The unique angle: Datalog is a natural companion to the Prolog implementation already in
|
||||||
|
progress (`lib/prolog/`). The parser and term representation can share infrastructure;
|
||||||
|
the evaluator is an entirely different fixpoint engine rather than a DFS solver.
|
||||||
|
|
||||||
|
End-state goal: **full core Datalog** (facts, rules, stratified negation, aggregation,
|
||||||
|
recursion) with a clean SX query API, and a demonstration of Datalog as a query engine
|
||||||
|
for rose-ash data (e.g. federation graph, content relationships).
|
||||||
|
|
||||||
|
## Ground rules
|
||||||
|
|
||||||
|
- **Scope:** only touch `lib/datalog/**` and `plans/datalog-on-sx.md`. Do **not** edit
|
||||||
|
`spec/`, `hosts/`, `shared/`, `lib/prolog/**`, or other `lib/<lang>/`.
|
||||||
|
- **Shared-file issues** go under "Blockers" below with a minimal repro; do not fix here.
|
||||||
|
- **SX files:** use `sx-tree` MCP tools only.
|
||||||
|
- **Architecture:** Datalog source → term AST → fixpoint evaluator. No transpiler to SX AST —
|
||||||
|
the evaluator is written in SX and works directly on term structures.
|
||||||
|
- **Reference:** Ramakrishnan & Ullman "A Survey of Deductive Database Systems";
|
||||||
|
Dalmau "Datalog and Constraint Satisfaction".
|
||||||
|
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick boxes.
|
||||||
|
|
||||||
|
## Architecture sketch
|
||||||
|
|
||||||
|
```
|
||||||
|
Datalog source text
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
lib/datalog/tokenizer.sx — atoms, variables, numbers, strings, punct (?- :- , . ( ) [ ])
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
lib/datalog/parser.sx — facts: atom(args). rules: head :- body. queries: ?- goal.
|
||||||
|
│ No function symbols (only constants and variables in args).
|
||||||
|
▼
|
||||||
|
lib/datalog/db.sx — extensional DB (EDB): ground facts; IDB: derived relations;
|
||||||
|
│ clause index by relation name/arity
|
||||||
|
▼
|
||||||
|
lib/datalog/eval.sx — bottom-up fixpoint: semi-naive evaluation with delta sets;
|
||||||
|
│ stratification for negation; incremental update API
|
||||||
|
▼
|
||||||
|
lib/datalog/query.sx — query API: (datalog-query db goal) → list of substitutions;
|
||||||
|
SX embedding: define facts/rules as SX data directly
|
||||||
|
```
|
||||||
|
|
||||||
|
Key differences from Prolog:
|
||||||
|
- **No function symbols** — args are atoms, numbers, strings, or variables only. No `f(a,b)`.
|
||||||
|
- **No cuts** — no procedural control.
|
||||||
|
- **Bottom-up** — derive all consequences of all rules before answering; no search tree.
|
||||||
|
- **Termination guaranteed** — no infinite derivation chains (no function symbols → finite Herbrand base).
|
||||||
|
- **Stratified negation** — `not(P)` legal iff P does not recursively depend on its own negation.
|
||||||
|
- **Aggregation** — `count`, `sum`, `min`, `max` over derived tuples (Datalog+).
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
### Phase 1 — tokenizer + parser
|
||||||
|
- [ ] Tokenizer: atoms (lowercase/quoted), variables (uppercase/`_`), numbers, strings,
|
||||||
|
operators (`:- `, `?-`, `,`, `.`), comments (`%`, `/* */`)
|
||||||
|
Note: no function symbol syntax (no nested `f(...)` in arg position).
|
||||||
|
- [ ] Parser:
|
||||||
|
- Facts: `parent(tom, bob).` → `{:head (parent tom bob) :body ()}`
|
||||||
|
- Rules: `ancestor(X,Z) :- parent(X,Y), ancestor(Y,Z).`
|
||||||
|
→ `{:head (ancestor X Z) :body ((parent X Y) (ancestor Y Z))}`
|
||||||
|
- Queries: `?- ancestor(tom, X).` → `{:query (ancestor tom X)}`
|
||||||
|
- Negation: `not(parent(X,Y))` in body position → `{:neg (parent X Y)}`
|
||||||
|
- [ ] Tests in `lib/datalog/tests/parse.sx`
|
||||||
|
|
||||||
|
### Phase 2 — unification + substitution
|
||||||
|
- [ ] Share or port unification from `lib/prolog/` — term walk, occurs check off by default
|
||||||
|
- [ ] `dl-unify` `t1` `t2` `subst` → extended subst or nil (no function symbols means simpler)
|
||||||
|
- [ ] `dl-ground?` `term` → bool — all variables bound in substitution
|
||||||
|
- [ ] Tests: atom/atom, var/atom, var/var, list args
|
||||||
|
|
||||||
|
### Phase 3 — extensional DB + naive evaluation
|
||||||
|
- [ ] EDB: `{:relation-name → set-of-ground-tuples}` using SX sets (Phase 18 of primitives)
|
||||||
|
- [ ] `dl-add-fact!` `db` `relation` `args` → add ground tuple
|
||||||
|
- [ ] `dl-add-rule!` `db` `head` `body` → add rule clause
|
||||||
|
- [ ] Naive evaluation: iterate rules until fixpoint
|
||||||
|
For each rule, for each combination of body tuples that unify, derive head tuple.
|
||||||
|
Repeat until no new tuples added.
|
||||||
|
- [ ] `dl-query` `db` `goal` → list of substitutions satisfying goal against derived DB
|
||||||
|
- [ ] Tests: transitive closure (ancestor), sibling, same-generation — classic Datalog programs
|
||||||
|
|
||||||
|
### Phase 4 — semi-naive evaluation (performance)
|
||||||
|
- [ ] Delta sets: track newly derived tuples per iteration
|
||||||
|
- [ ] Semi-naive rule: only join against delta tuples from last iteration, not full relation
|
||||||
|
- [ ] Significant speedup for recursive rules — avoids re-deriving known tuples
|
||||||
|
- [ ] `dl-stratify` `db` → dependency graph + SCC analysis → stratum ordering
|
||||||
|
- [ ] Tests: verify semi-naive produces same results as naive; benchmark on large ancestor chain
|
||||||
|
|
||||||
|
### Phase 5 — stratified negation
|
||||||
|
- [ ] Dependency graph analysis: which relations depend on which (positively or negatively)
|
||||||
|
- [ ] Stratification check: error if negation is in a cycle (non-stratifiable program)
|
||||||
|
- [ ] Evaluation: process strata in order — lower stratum fully computed before using its
|
||||||
|
complement in a higher stratum
|
||||||
|
- [ ] `not(P)` in rule body: at evaluation time, check P is NOT in the derived EDB
|
||||||
|
- [ ] Tests: non-member (`not(member(X,L))`), colored-graph (`not(same-color(X,Y))`),
|
||||||
|
stratification error detection
|
||||||
|
|
||||||
|
### Phase 6 — aggregation (Datalog+)
|
||||||
|
- [ ] `count(X, Goal)` → number of distinct X satisfying Goal
|
||||||
|
- [ ] `sum(X, Goal)` → sum of X values satisfying Goal
|
||||||
|
- [ ] `min(X, Goal)` / `max(X, Goal)` → min/max of X satisfying Goal
|
||||||
|
- [ ] `group-by` semantics: `count(X, sibling(bob, X))` → count of bob's siblings
|
||||||
|
- [ ] Aggregation breaks stratification — evaluate in a separate post-fixpoint pass
|
||||||
|
- [ ] Tests: social network statistics, grade aggregation, inventory sums
|
||||||
|
|
||||||
|
### Phase 7 — SX embedding API
|
||||||
|
- [ ] `(dl-program facts rules)` → database from SX data directly (no parsing required)
|
||||||
|
```
|
||||||
|
(dl-program
|
||||||
|
'((parent tom bob) (parent tom liz) (parent bob ann))
|
||||||
|
'((ancestor X Z :- (parent X Y) (ancestor Y Z))
|
||||||
|
(ancestor X Y :- (parent X Y))))
|
||||||
|
```
|
||||||
|
- [ ] `(dl-query db '(ancestor tom ?X))` → `((ann) (bob) (liz) (pat))`
|
||||||
|
- [ ] `(dl-assert! db '(parent ann pat))` → incremental fact addition + re-derive
|
||||||
|
- [ ] `(dl-retract! db '(parent tom bob))` → fact removal + re-derive from scratch
|
||||||
|
- [ ] Integration demo: federation graph query — `(ancestor actor1 actor2)` over
|
||||||
|
rose-ash ActivityPub follow relationships
|
||||||
|
|
||||||
|
### Phase 8 — Datalog as a query language for rose-ash
|
||||||
|
- [ ] Schema: map SQLAlchemy model relationships to Datalog EDB facts
|
||||||
|
(e.g. `(follows user1 user2)`, `(authored user post)`, `(tagged post tag)`)
|
||||||
|
- [ ] Loader: `dl-load-from-db!` — query PostgreSQL, populate Datalog EDB
|
||||||
|
- [ ] Query examples:
|
||||||
|
- `?- ancestor(me, X), authored(X, Post), tagged(Post, cooking).`
|
||||||
|
→ posts about cooking by people I follow (transitively)
|
||||||
|
- `?- popular(Post) :- tagged(Post, T), count(L, (liked(L, Post))) >= 10.`
|
||||||
|
→ posts with 10+ likes
|
||||||
|
- [ ] Expose as a rose-ash service endpoint: `POST /internal/datalog` with program + query
|
||||||
|
|
||||||
|
## Blockers
|
||||||
|
|
||||||
|
_(none yet)_
|
||||||
|
|
||||||
|
## Progress log
|
||||||
|
|
||||||
|
_Newest first._
|
||||||
|
|
||||||
|
_(awaiting phase 1)_
|
||||||
80
plans/designs/f-breakpoint.md
Normal file
80
plans/designs/f-breakpoint.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# F-Breakpoint — `breakpoint` command (+2)
|
||||||
|
|
||||||
|
**Suite:** `hs-upstream-breakpoint`
|
||||||
|
**Target:** Both tests are `SKIP (untranslated)`.
|
||||||
|
|
||||||
|
## 1. The 2 tests
|
||||||
|
|
||||||
|
- `parses as a top-level command`
|
||||||
|
- `parses inside an event handler`
|
||||||
|
|
||||||
|
Both are untranslated — no test body exists. The test names say "parses" — these are parser tests, not runtime tests.
|
||||||
|
|
||||||
|
## 2. What upstream checks
|
||||||
|
|
||||||
|
From `test/core/breakpoint.js`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
it('parses as a top-level command', () => {
|
||||||
|
expect(() => _hyperscript.evaluate("breakpoint")).not.toThrow();
|
||||||
|
});
|
||||||
|
it('parses inside an event handler', () => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.setAttribute('_', 'on click breakpoint');
|
||||||
|
expect(() => _hyperscript.processNode(el)).not.toThrow();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Both tests verify that `breakpoint` is accepted by the parser without throwing. Neither test checks that the debugger actually fires. `breakpoint` is a no-op command in production builds — it calls `debugger` in JS, which is a no-op when devtools are closed.
|
||||||
|
|
||||||
|
## 3. What's needed
|
||||||
|
|
||||||
|
### Parser (`lib/hyperscript/parser.sx`)
|
||||||
|
|
||||||
|
Add `breakpoint` to the command dispatch — it should parse as a zero-argument command. The parser's command `cond` (wherever `add`, `remove`, `hide` etc. are dispatched) needs a branch:
|
||||||
|
|
||||||
|
```
|
||||||
|
((= val "breakpoint") (hs-parse-breakpoint))
|
||||||
|
```
|
||||||
|
|
||||||
|
`hs-parse-breakpoint` just returns a `{:cmd "breakpoint"}` AST node (or however commands are represented). It consumes no additional tokens.
|
||||||
|
|
||||||
|
### Compiler (`lib/hyperscript/compiler.sx`)
|
||||||
|
|
||||||
|
Add a compiler branch for `breakpoint` AST node. Emits a no-op or a `debugger` statement equivalent. Since we're in SX (not JS), a no-op `(do nil)` is correct.
|
||||||
|
|
||||||
|
### Generator (`tests/playwright/generate-sx-tests.py`)
|
||||||
|
|
||||||
|
The 2 tests are simple — hand-write them:
|
||||||
|
|
||||||
|
```lisp
|
||||||
|
(deftest "parses as a top-level command"
|
||||||
|
(let ((result (guard (e (true false))
|
||||||
|
(hs-compile "breakpoint")
|
||||||
|
true)))
|
||||||
|
(assert result)))
|
||||||
|
|
||||||
|
(deftest "parses inside an event handler"
|
||||||
|
(hs-cleanup!)
|
||||||
|
(let ((el (dom-create-element "div")))
|
||||||
|
(dom-set-attr el "_" "on click breakpoint")
|
||||||
|
(let ((result (guard (e (true false))
|
||||||
|
(hs-activate! el)
|
||||||
|
true)))
|
||||||
|
(assert result))))
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Implementation checklist
|
||||||
|
|
||||||
|
1. `sx_find_all` in `lib/hyperscript/parser.sx` for the command dispatch `cond`.
|
||||||
|
2. Add `breakpoint` branch → `hs-parse-breakpoint` function returning minimal command node.
|
||||||
|
3. `sx_find_all` in `lib/hyperscript/compiler.sx` for command compilation dispatch.
|
||||||
|
4. Add `breakpoint` branch → emit no-op.
|
||||||
|
5. Replace 2 `SKIP` bodies in `spec/tests/test-hyperscript-behavioral.sx` with translated tests above.
|
||||||
|
6. Run `hs_test_run suite="hs-upstream-breakpoint"` — expect 2/2.
|
||||||
|
7. Run smoke 0–195 — no regressions.
|
||||||
|
8. Commit: `HS: breakpoint command — parser + no-op compiler (+2)`
|
||||||
|
|
||||||
|
## 5. Risk
|
||||||
|
|
||||||
|
Very low. Zero-argument no-op command. The only risk is mis-locating the command dispatch branch in the parser.
|
||||||
68
plans/designs/f1-null-safety.md
Normal file
68
plans/designs/f1-null-safety.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# F1 — Null Safety Reporting (+7)
|
||||||
|
|
||||||
|
**Suite:** `hs-upstream-core/runtimeErrors`
|
||||||
|
**Target:** 7 currently-failing tests (decrement, default, increment, put, remove, settle, transition commands)
|
||||||
|
|
||||||
|
## 1. Failing tests
|
||||||
|
|
||||||
|
The suite has 18 tests total; 11 already pass. The 7 failures all share the pattern:
|
||||||
|
|
||||||
|
```
|
||||||
|
Expected '#doesntExist' is null, got
|
||||||
|
```
|
||||||
|
|
||||||
|
The `eval-hs-error` helper already exists (landed in null-safety piece 1). It compiles and runs a HS snippet and returns the error string. The problem is that the listed commands don't guard against null targets before operating, so they produce no error (or a cryptic one) instead of `"'#doesntExist' is null"`.
|
||||||
|
|
||||||
|
| Test | Command | Null target expression |
|
||||||
|
|------|---------|----------------------|
|
||||||
|
| decrement | `decrement #doesntExist's innerHTML` | `#doesntExist` |
|
||||||
|
| default | `default #doesntExist's innerHTML to 'foo'` | `#doesntExist` |
|
||||||
|
| increment | `increment #doesntExist's innerHTML` | `#doesntExist` |
|
||||||
|
| put | `put 'foo' into/before/after/at start of/at end of #doesntExist` | `#doesntExist` |
|
||||||
|
| remove | `remove .foo/.@foo/#doesntExist from #doesntExist` | `#doesntExist` |
|
||||||
|
| settle | `settle #doesntExist` | `#doesntExist` |
|
||||||
|
| transition | `transition #doesntExist's *visibility to 0` | `#doesntExist` |
|
||||||
|
|
||||||
|
Note: add, hide, measure, send, sets, show, toggle, trigger already pass — they already guard.
|
||||||
|
|
||||||
|
## 2. Required error format
|
||||||
|
|
||||||
|
```
|
||||||
|
'#doesntExist' is null
|
||||||
|
```
|
||||||
|
|
||||||
|
The apostrophe-quoted selector string followed by ` is null`. The selector text is the original source text of the element expression (e.g. `#doesntExist`, not a stringified DOM node).
|
||||||
|
|
||||||
|
This is the same format already used by passing commands. The null-safety piece 1 commit added `eval-hs-error` and `hs-null-error` helper — just need to call it at the right point in each missing command.
|
||||||
|
|
||||||
|
## 3. Where to add guards
|
||||||
|
|
||||||
|
All in `lib/hyperscript/runtime.sx`. Pattern for each command:
|
||||||
|
|
||||||
|
```
|
||||||
|
(when (nil? target)
|
||||||
|
(hs-null-error target-source-text))
|
||||||
|
```
|
||||||
|
|
||||||
|
Where `hs-null-error` (or equivalent) raises with the formatted message.
|
||||||
|
|
||||||
|
### Per-command location
|
||||||
|
|
||||||
|
- **decrement / increment** — after resolving the target element, before reading/writing innerHTML
|
||||||
|
- **default** — after resolving target element, before reading current value
|
||||||
|
- **put** — after resolving destination element (covers all put variants: into, before, after, at start, at end)
|
||||||
|
- **remove** — after resolving the `from` target element
|
||||||
|
- **settle** — after resolving target element, before starting transition poll
|
||||||
|
- **transition** — after resolving target element, before reading/setting style
|
||||||
|
|
||||||
|
## 4. Implementation checklist
|
||||||
|
|
||||||
|
1. Find each failing command's runtime function in `lib/hyperscript/runtime.sx` using `sx_find_all`.
|
||||||
|
2. For each: `sx_read_subtree` on the function body, locate where target is resolved, insert null guard calling `hs-null-error` (or the equivalent raise form already used by passing commands).
|
||||||
|
3. After all 7: run `hs_test_run suite="hs-upstream-core/runtimeErrors"` — expect 18/18.
|
||||||
|
4. Run smoke range 0–195 — expect no regressions.
|
||||||
|
5. Commit: `HS: null-safety guards on decrement/default/increment/put/remove/settle/transition (+7)`
|
||||||
|
|
||||||
|
## 5. Risk
|
||||||
|
|
||||||
|
Low. The pattern is established by the 11 already-passing tests. The only risk is finding the correct point in each command where the element is resolved and before it's first used.
|
||||||
166
plans/designs/f13-step-limit-and-meta.md
Normal file
166
plans/designs/f13-step-limit-and-meta.md
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
# F13 — Step Limit + `meta.caller` (+5 → 100%)
|
||||||
|
|
||||||
|
Five tests currently timeout or produce wrong values due to two root causes:
|
||||||
|
step budget exhaustion and a missing `meta` implementation.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
| # | Suite | Test | Failure |
|
||||||
|
|---|-------|------|---------|
|
||||||
|
| 198 | `hs-upstream-core/runtime` | `has proper stack from event handler` | wrong-value: `meta.caller` returns `""` instead of an object with `.meta.feature.type = "onFeature"` |
|
||||||
|
| 200 | `hs-upstream-core/runtime` | `hypertrace is reasonable` | TIMEOUT (15s, step limit) |
|
||||||
|
| 615 | `hs-upstream-expressions/in` | `query template returns values` | TIMEOUT (37s, step limit) |
|
||||||
|
| 1197 | `hs-upstream-repeat` | `repeat forever works` | TIMEOUT (step limit) |
|
||||||
|
| 1198 | `hs-upstream-repeat` | `repeat forever works w/o keyword` | TIMEOUT (step limit) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Root cause A — Step limit (tests 200, 615, 1197, 1198)
|
||||||
|
|
||||||
|
The runner sets `HS_STEP_LIMIT=200000`. Every CEK step consumed by any
|
||||||
|
expression in a test — including the double compilation warm-up guard blocks
|
||||||
|
that appear before the actual DOM test — counts against this shared budget.
|
||||||
|
|
||||||
|
### `repeat forever` (1197, 1198)
|
||||||
|
|
||||||
|
The loop body terminates in exactly **5 iterations** (`if retVal == 5 then return`).
|
||||||
|
This is bounded, not infinite. The step budget is exhausted before the loop
|
||||||
|
runs because two `eval-expr-cek` compilation warm-up calls each consume tens
|
||||||
|
of thousands of steps.
|
||||||
|
|
||||||
|
Fix: each warm-up guard compiles and discards a HS function definition. Those
|
||||||
|
calls are defensive (wrapped in `guard` that swallows errors). We do NOT need
|
||||||
|
to run the compiled code — the warm-up's purpose is just to ensure the
|
||||||
|
compiler doesn't crash, not to consume steps. The step counter should not tick
|
||||||
|
during compilation (compilation is a pure transform, not evaluation). If that's
|
||||||
|
impractical to gate, raise `HS_STEP_LIMIT` to `2000000` (10×).
|
||||||
|
|
||||||
|
### `hypertrace is reasonable` (200)
|
||||||
|
|
||||||
|
Defines `bar()` → calls `baz()` → throws. Simple call chain. The "hypertrace"
|
||||||
|
in the test name implies the HS runtime trace recorder is active during the
|
||||||
|
test. If trace recording is on globally, every CEK step generates a trace entry
|
||||||
|
allocation. Fix: confirm whether trace recording is always-on in the test runner
|
||||||
|
and disable it by default (trace should only be on when explicitly requested).
|
||||||
|
Alternatively raise step limit.
|
||||||
|
|
||||||
|
### `query template returns values` (615)
|
||||||
|
|
||||||
|
Uses `<${"p"}/>` — a CSS query selector built from a template string. Takes 37
|
||||||
|
seconds. Likely the template selector evaluation triggers repeated DOM scanning
|
||||||
|
or expensive string construction per step. Fix: profile with `hs_test_run
|
||||||
|
verbose=true` to identify which step is slow. If it's a regex compilation
|
||||||
|
per-call, cache it. If step limit only, raise to 2M.
|
||||||
|
|
||||||
|
### Unified fix: raise `HS_STEP_LIMIT` to `2000000`
|
||||||
|
|
||||||
|
The simplest fix that unblocks all four timeout tests. In
|
||||||
|
`tests/hs-run-filtered.js`, change the default step limit. Per-test overrides
|
||||||
|
can still be set via `HS_STEP_LIMIT` env var for debugging.
|
||||||
|
|
||||||
|
If the `query template` test is still slow at 2M steps (37s × 10 = 370s, which
|
||||||
|
would be unacceptable), that test needs a separate performance fix — cache the
|
||||||
|
compiled regex/query from the template string rather than rebuilding it on every
|
||||||
|
access.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Root cause B — `meta.caller` not implemented (test 198)
|
||||||
|
|
||||||
|
The HS `meta` object is available inside any function call. It exposes:
|
||||||
|
|
||||||
|
- `meta.caller` — the calling context object
|
||||||
|
- `meta.caller.meta.feature.type` — the HS feature type of the caller
|
||||||
|
(e.g. `"onFeature"` when called from an `on click` handler)
|
||||||
|
|
||||||
|
Test script:
|
||||||
|
```
|
||||||
|
def bar()
|
||||||
|
log meta.caller
|
||||||
|
return meta.caller
|
||||||
|
end
|
||||||
|
```
|
||||||
|
Triggered via `on click put bar().meta.feature.type into my.innerHTML`.
|
||||||
|
Expects `"onFeature"` in innerHTML. Currently gets `""`.
|
||||||
|
|
||||||
|
### What `meta` needs
|
||||||
|
|
||||||
|
`meta` is a dict-like object injected into every function's execution context
|
||||||
|
at call time. Minimum fields for this test:
|
||||||
|
|
||||||
|
```
|
||||||
|
meta = {
|
||||||
|
:caller <the calling context — a dict with its own :meta field>
|
||||||
|
:element <the element the script is attached to>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`meta.caller.meta.feature.type` must return `"onFeature"` when called from an
|
||||||
|
`on` event handler. The feature type string `"onFeature"` is already used
|
||||||
|
internally (event handler features are tagged with this type).
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
|
||||||
|
In `lib/hyperscript/runtime.sx`, at the point where a HS `def` function is
|
||||||
|
called:
|
||||||
|
|
||||||
|
1. Build a `meta` dict:
|
||||||
|
```
|
||||||
|
{:caller calling-context :element current-element}
|
||||||
|
```
|
||||||
|
where `calling-context` is the current runtime context dict (which includes
|
||||||
|
its own `:meta` field with `:feature {:type "onFeature"}` for event handlers).
|
||||||
|
|
||||||
|
2. Bind `meta` in the function's execution env.
|
||||||
|
|
||||||
|
3. Ensure event handler contexts carry `{:meta {:feature {:type "onFeature"}}}`.
|
||||||
|
|
||||||
|
This is an additive change — nothing currently uses `meta`, so no regression
|
||||||
|
risk.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation checklist
|
||||||
|
|
||||||
|
### Step A — Raise step limit
|
||||||
|
1. In `tests/hs-run-filtered.js`, change default `HS_STEP_LIMIT` from `200000`
|
||||||
|
to `2000000`.
|
||||||
|
2. Run tests 1197–1198: `hs_test_run(start=1197, end=1199)` — expect 2/2.
|
||||||
|
3. Run test 615: `hs_test_run(start=615, end=616)` — expect 1/1 or note if
|
||||||
|
still too slow.
|
||||||
|
4. Run test 200: `hs_test_run(start=200, end=201)` — expect 1/1.
|
||||||
|
|
||||||
|
### Step B — `meta.caller` (test 198)
|
||||||
|
5. `sx_find_all` in `lib/hyperscript/runtime.sx` for where `def` functions are
|
||||||
|
called / where event handler contexts are constructed.
|
||||||
|
6. Add `meta` dict construction at call time; bind in function env.
|
||||||
|
7. Ensure `on` handler context carries `{:meta {:feature {:type "onFeature"}}}`.
|
||||||
|
8. Run test 198: `hs_test_run(start=198, end=199)` — expect 1/1.
|
||||||
|
|
||||||
|
### Step C — Query template performance (if still slow after step A)
|
||||||
|
9. Profile `hs_test_run(start=615, end=616, step_limit=2000000, verbose=true)`.
|
||||||
|
10. If the CSS template query `<${"p"}/>` rebuilds on every call, add a memoize
|
||||||
|
cache keyed on the template result string.
|
||||||
|
11. Rerun — expect < 5s.
|
||||||
|
|
||||||
|
### Step D — Full suite verification
|
||||||
|
12. Run all ranges with raised step limit:
|
||||||
|
- `hs_test_run(start=0, end=201, step_limit=2000000)`
|
||||||
|
- `hs_test_run(start=201, end=616, step_limit=2000000)`
|
||||||
|
- `hs_test_run(start=616, end=1200, step_limit=2000000)`
|
||||||
|
- `hs_test_run(start=1200, end=1496, step_limit=2000000)`
|
||||||
|
13. Confirm all previously-passing tests still pass.
|
||||||
|
14. Commit: `HS: raise step limit to 2M + meta.caller for onFeature stack (+5)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risk
|
||||||
|
|
||||||
|
- **Step limit raise:** May make test suite slower overall (more steps to exhaust
|
||||||
|
before timeout). But if tests pass quickly the limit is never reached.
|
||||||
|
The 37s query-template test is the only real concern — if it genuinely needs
|
||||||
|
2M steps × (time per step), it needs a performance fix too.
|
||||||
|
- **`meta.caller`:** Additive binding in function scope. Zero regression risk.
|
||||||
|
The only complexity is constructing the right shape for the calling context
|
||||||
|
chain — but since only one test exercises this and the shape is simple, the
|
||||||
|
risk is low.
|
||||||
81
plans/designs/f2-tell.md
Normal file
81
plans/designs/f2-tell.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# F2 — `tell` Semantics Fix (+3)
|
||||||
|
|
||||||
|
**Suite:** `hs-upstream-tell`
|
||||||
|
**Target:** 3 failing tests out of 10. 7 already pass.
|
||||||
|
|
||||||
|
## 1. Failing tests
|
||||||
|
|
||||||
|
### "attributes refer to the thing being told"
|
||||||
|
```
|
||||||
|
on click tell #d2 then put @foo into me
|
||||||
|
```
|
||||||
|
d2 has attribute `foo="bar"`. After click, d1's text content should be `"bar"`.
|
||||||
|
`@foo` is an attribute ref — it should resolve against the **told element** (d2), not the event target (d1).
|
||||||
|
Currently gets `""` — attribute resolves against d1, which has no `foo` attribute.
|
||||||
|
|
||||||
|
### "your symbol represents the thing being told"
|
||||||
|
```
|
||||||
|
on click tell #d2 then put your innerText into me
|
||||||
|
```
|
||||||
|
d2 has innerText `"foo"`. After click, d1's text content should be `"foo"`.
|
||||||
|
`your` is the possessive of `you` — inside a `tell` block, `you`/`your` should bind to the told element.
|
||||||
|
Currently gets `""`.
|
||||||
|
|
||||||
|
### "does not overwrite the me symbol"
|
||||||
|
```
|
||||||
|
on click add .foo then tell #d2 then add .bar to me
|
||||||
|
```
|
||||||
|
After click: d1 should have both `.foo` and `.bar`; d2 should have neither.
|
||||||
|
`me` inside the `tell` block must still refer to d1 (the original event target).
|
||||||
|
Currently: assertion fails — `.bar` is going to d2 instead of d1.
|
||||||
|
|
||||||
|
## 2. What the 7 passing tests reveal about current behaviour
|
||||||
|
|
||||||
|
The passing tests include:
|
||||||
|
- `you symbol represents the thing being told` — `add .bar to you` adds to d2 ✓
|
||||||
|
- `establishes a proper beingTold symbol` — bare `add .bar` (no target) adds to the told element ✓
|
||||||
|
- `restores a proper implicit me symbol` — after `tell` block ends, bare commands target d1 again ✓
|
||||||
|
- `yourself attribute also works` — `remove yourself` inside tell removes d2 ✓
|
||||||
|
|
||||||
|
So `you`, `yourself`, and bare implicit target all work. The three bugs are:
|
||||||
|
1. Attribute refs (`@foo`) don't resolve against the told element
|
||||||
|
2. `your` (possessive of `you`) doesn't resolve
|
||||||
|
3. `me` is being rebound to the told element instead of kept as d1
|
||||||
|
|
||||||
|
## 3. Root cause analysis
|
||||||
|
|
||||||
|
Inside a `tell X` block, the runtime sets the implicit target to X. The three failures suggest:
|
||||||
|
|
||||||
|
**Bug A — attribute refs:** `@foo` resolves via a property-access path that reads from the *current event target* (`me`/`self`), not from the *implicit tell target*. The tell block sets implicit target but the attribute ref lookup skips it.
|
||||||
|
|
||||||
|
**Bug B — `your`:** `your` is parsed as a possessive modifier expecting `you` to be bound. If `you` is not bound in the tell scope (and only the implicit target is set), `your X` fails to resolve.
|
||||||
|
|
||||||
|
**Bug C — `me` rebinding:** The tell command saves/restores `me` but the save/restore is either not happening or is restoring the wrong value. `me` inside the block should remain d1 while the implicit default target is d2.
|
||||||
|
|
||||||
|
## 4. Fix
|
||||||
|
|
||||||
|
In `lib/hyperscript/runtime.sx`, find the `tell` command handler (search for `hs-tell` or the tell dispatch branch).
|
||||||
|
|
||||||
|
The correct semantics:
|
||||||
|
- Save current `me` value
|
||||||
|
- Set implicit target (used by bare commands like `add .bar`) to the told element
|
||||||
|
- Bind `you` = told element (so `you`, `your`, `yourself` work)
|
||||||
|
- Do **not** rebind `me` — keep it as the original event target
|
||||||
|
- Restore implicit target and unbind `you` after the block
|
||||||
|
|
||||||
|
For attribute refs (`@foo`): resolve against the current *implicit target* (told element), not against `me`. Find where `@attr` expressions are evaluated and ensure they read from the implicit target when inside a tell block.
|
||||||
|
|
||||||
|
## 5. Implementation checklist
|
||||||
|
|
||||||
|
1. `sx_find_all` in `lib/hyperscript/runtime.sx` for tell handler.
|
||||||
|
2. `sx_read_subtree` on the tell handler — verify save/restore of `me` vs implicit target.
|
||||||
|
3. Fix `me` rebinding: save old implicit target, set new one, do NOT touch `me`.
|
||||||
|
4. Bind `you`/`your`/`yourself` to told element in the tell scope env.
|
||||||
|
5. Find attribute ref (`@`) evaluation — ensure it reads from implicit target.
|
||||||
|
6. Run `hs_test_run suite="hs-upstream-tell"` — expect 10/10.
|
||||||
|
7. Run smoke 0–195 — no regressions.
|
||||||
|
8. Commit: `HS: tell — fix me rebinding, your/attribute-ref resolution (+3)`
|
||||||
|
|
||||||
|
## 6. Risk
|
||||||
|
|
||||||
|
Medium. The 7 passing tests constrain what can change — the fix must preserve `you`, `yourself`, bare implicit target, and restore-after-tell semantics. The three bugs are independent enough that they can be fixed one at a time and verified after each.
|
||||||
128
plans/designs/f5-cookies.md
Normal file
128
plans/designs/f5-cookies.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# F5 — Cookie API (+5)
|
||||||
|
|
||||||
|
**Suite:** `hs-upstream-expressions/cookies`
|
||||||
|
**Target:** All 5 tests are `SKIP (untranslated)`.
|
||||||
|
|
||||||
|
## 1. The 5 tests
|
||||||
|
|
||||||
|
From upstream `test/expressions/cookies.js`:
|
||||||
|
|
||||||
|
| Test | What it checks |
|
||||||
|
|------|---------------|
|
||||||
|
| `length is 0 when no cookies are set` | `cookies.length == 0` with no cookies set |
|
||||||
|
| `basic set cookie values work` | `set cookies.name to "value"` then `cookies.name == "value"` |
|
||||||
|
| `update cookie values work` | set, then set again, value updates |
|
||||||
|
| `basic clear cookie values work` | `set cookies.name to "value"` then `clear cookies.name`, then `cookies.name == undefined` |
|
||||||
|
| `iterate cookies values work` | `for name in cookies` iterates cookie names |
|
||||||
|
|
||||||
|
## 2. HyperScript cookie syntax
|
||||||
|
|
||||||
|
`cookies` is a special global expression in HyperScript backed by `document.cookie`. The upstream implementation wraps `document.cookie` in a proxy:
|
||||||
|
|
||||||
|
- `cookies.name` → read cookie by name (returns string or `undefined`)
|
||||||
|
- `set cookies.name to val` → write cookie (sets `document.cookie = "name=val"`)
|
||||||
|
- `clear cookies.name` → delete cookie (sets max-age=-1)
|
||||||
|
- `cookies.length` → number of cookies set
|
||||||
|
- `for name in cookies` → iterate over cookie names
|
||||||
|
|
||||||
|
## 3. Test runner mock
|
||||||
|
|
||||||
|
All 5 tests are untranslated — no SX test bodies exist yet. The generator needs patterns for the cookie expressions, and `hs-run-filtered.js` needs a `document.cookie` mock.
|
||||||
|
|
||||||
|
### Mock in `tests/hs-run-filtered.js`
|
||||||
|
|
||||||
|
Add a simple in-memory cookie store to the `dom` mock:
|
||||||
|
|
||||||
|
```js
|
||||||
|
let _cookieStore = {};
|
||||||
|
Object.defineProperty(global.document, 'cookie', {
|
||||||
|
get() {
|
||||||
|
return Object.entries(_cookieStore)
|
||||||
|
.map(([k,v]) => `${k}=${v}`)
|
||||||
|
.join('; ');
|
||||||
|
},
|
||||||
|
set(str) {
|
||||||
|
const [pair, ...attrs] = str.split(';');
|
||||||
|
const [name, val] = pair.split('=').map(s => s.trim());
|
||||||
|
const maxAge = attrs.find(a => a.trim().startsWith('max-age='));
|
||||||
|
if (maxAge && parseInt(maxAge.split('=')[1]) < 0) {
|
||||||
|
delete _cookieStore[name];
|
||||||
|
} else {
|
||||||
|
_cookieStore[name] = val;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
configurable: true
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `_cookieStore = {}` reset to `hs-cleanup!` equivalent in the runner.
|
||||||
|
|
||||||
|
## 4. SX runtime additions in `lib/hyperscript/runtime.sx`
|
||||||
|
|
||||||
|
HS needs a `cookies` special expression that the compiler resolves. Two approaches:
|
||||||
|
|
||||||
|
**Option A (simpler):** Treat `cookies` as a built-in variable bound to a proxy dict at runtime. When property access `cookies.name` is evaluated, dispatch to cookie read/write helpers.
|
||||||
|
|
||||||
|
**Option B (upstream-faithful):** Parse `cookies` as a special primary expression, emit runtime calls `hs-cookie-get`, `hs-cookie-set`, `hs-cookie-delete`, `hs-cookie-length`, `hs-cookie-names`.
|
||||||
|
|
||||||
|
Option A is less invasive. The runtime env gets a `cookies` binding pointing to a special object; property access and assignment on it dispatch to the cookie helpers, which call `(platform-cookie-get name)` / `(platform-cookie-set name val)` / `(platform-cookie-delete name)`.
|
||||||
|
|
||||||
|
Platform cookie operations map to `document.cookie` reads/writes in JS.
|
||||||
|
|
||||||
|
## 5. Generator patterns (`tests/playwright/generate-sx-tests.py`)
|
||||||
|
|
||||||
|
The upstream tests use patterns like:
|
||||||
|
|
||||||
|
```js
|
||||||
|
await page.evaluate(() => { _hyperscript.evaluate("set cookies.foo to 'bar'") });
|
||||||
|
expect(await page.evaluate(() => _hyperscript.evaluate("cookies.foo"))).toBe("bar");
|
||||||
|
```
|
||||||
|
|
||||||
|
In our SX harness these become direct `eval-hs` calls. Since all 5 tests are untranslated, hand-write them rather than extending the generator (similar to E39).
|
||||||
|
|
||||||
|
## 6. Translated test bodies
|
||||||
|
|
||||||
|
```lisp
|
||||||
|
(deftest "length is 0 when no cookies are set"
|
||||||
|
(hs-cleanup!)
|
||||||
|
(assert= (eval-hs "cookies.length") 0))
|
||||||
|
|
||||||
|
(deftest "basic set cookie values work"
|
||||||
|
(hs-cleanup!)
|
||||||
|
(eval-hs "set cookies.foo to 'bar'")
|
||||||
|
(assert= (eval-hs "cookies.foo") "bar"))
|
||||||
|
|
||||||
|
(deftest "update cookie values work"
|
||||||
|
(hs-cleanup!)
|
||||||
|
(eval-hs "set cookies.foo to 'bar'")
|
||||||
|
(eval-hs "set cookies.foo to 'baz'")
|
||||||
|
(assert= (eval-hs "cookies.foo") "baz"))
|
||||||
|
|
||||||
|
(deftest "basic clear cookie values work"
|
||||||
|
(hs-cleanup!)
|
||||||
|
(eval-hs "set cookies.foo to 'bar'")
|
||||||
|
(eval-hs "clear cookies.foo")
|
||||||
|
(assert= (eval-hs "cookies.foo") nil))
|
||||||
|
|
||||||
|
(deftest "iterate cookies values work"
|
||||||
|
(hs-cleanup!)
|
||||||
|
(eval-hs "set cookies.a to '1'")
|
||||||
|
(eval-hs "set cookies.b to '2'")
|
||||||
|
(let ((names (eval-hs "for name in cookies collect name")))
|
||||||
|
(assert (contains? names "a"))
|
||||||
|
(assert (contains? names "b"))))
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Implementation checklist
|
||||||
|
|
||||||
|
1. Add cookie mock to `tests/hs-run-filtered.js`. Wire reset into test cleanup.
|
||||||
|
2. Add `hs-cookie-get`, `hs-cookie-set`, `hs-cookie-delete`, `hs-cookie-length`, `hs-cookie-names` to `lib/hyperscript/runtime.sx`.
|
||||||
|
3. Add `cookies` as a special expression in the HS parser/evaluator that dispatches to the above.
|
||||||
|
4. Replace 5 `SKIP` bodies in `spec/tests/test-hyperscript-behavioral.sx` with translated test bodies above.
|
||||||
|
5. Run `hs_test_run suite="hs-upstream-expressions/cookies"` — expect 5/5.
|
||||||
|
6. Run smoke 0–195 — no regressions.
|
||||||
|
7. Commit: `HS: cookie API — document.cookie proxy + 5 tests`
|
||||||
|
|
||||||
|
## 8. Risk
|
||||||
|
|
||||||
|
Medium. The mock is simple. The main risk is the `cookies` expression integration in the parser — it needs to hook into property-access and assignment paths that are already well-exercised. Keep the implementation thin: `cookies` is a runtime value with a special type, not a new parse form.
|
||||||
107
plans/designs/f8-eval-statically.md
Normal file
107
plans/designs/f8-eval-statically.md
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
# F8 — evalStatically (+3)
|
||||||
|
|
||||||
|
**Suite:** `hs-upstream-core/evalStatically`
|
||||||
|
**Target:** 3 failing (untranslated) out of 8. 5 already pass.
|
||||||
|
|
||||||
|
## 1. Current state
|
||||||
|
|
||||||
|
5 passing tests use `(eval-hs expr)` and check the return value for literals: booleans, null, numbers, plain strings, time expressions. These call `_hyperscript.evaluate(src)` and return the result.
|
||||||
|
|
||||||
|
3 failing tests are named:
|
||||||
|
- `throws on math expressions`
|
||||||
|
- `throws on symbol references`
|
||||||
|
- `throws on template strings`
|
||||||
|
|
||||||
|
All are `SKIP (untranslated)` — no test body has been generated.
|
||||||
|
|
||||||
|
## 2. What upstream checks
|
||||||
|
|
||||||
|
From `test/core/evalStatically.js`, the `throwErrors` mode:
|
||||||
|
|
||||||
|
```js
|
||||||
|
expect(() => _hyperscript.evaluate("1 + 2")).toThrow();
|
||||||
|
expect(() => _hyperscript.evaluate("x")).toThrow();
|
||||||
|
expect(() => _hyperscript.evaluate(`"hello ${name}"`)).toThrow();
|
||||||
|
```
|
||||||
|
|
||||||
|
`_hyperscript.evaluate(src)` in strict static mode throws when the expression is not a pure literal — math operators, symbol references, and template string interpolation all involve runtime evaluation that can't be statically resolved.
|
||||||
|
|
||||||
|
The "static" constraint: only literals that can be evaluated without any runtime context or side effects are allowed. `1 + 2` is not static (it's a math op). `x` is not static (symbol lookup). `"hello ${name}"` is not static (interpolation).
|
||||||
|
|
||||||
|
## 3. What `eval-hs` currently does
|
||||||
|
|
||||||
|
`eval-hs` in our harness calls `(hs-compile-and-run src)` or equivalent. It does NOT currently have a "static mode" — it runs everything with the full runtime.
|
||||||
|
|
||||||
|
We need a new harness helper `eval-hs-static-error` that:
|
||||||
|
1. Calls `(hs-compile src)` with a flag that makes it throw on non-literal expressions
|
||||||
|
2. Returns the caught error message, or raises if no error was thrown
|
||||||
|
|
||||||
|
## 4. Implementation options
|
||||||
|
|
||||||
|
### Option A — Static analysis pass (accurate)
|
||||||
|
|
||||||
|
Before evaluation, walk the AST and reject any node that isn't a literal:
|
||||||
|
- Number literal ✓
|
||||||
|
- String literal (no interpolation) ✓
|
||||||
|
- Boolean literal ✓
|
||||||
|
- Null literal ✓
|
||||||
|
- Time expression (`200ms`, `2s`) ✓
|
||||||
|
- Everything else → throw `"expression is not static"`
|
||||||
|
|
||||||
|
This is a pre-eval AST check, not a runtime change. Lives in `lib/hyperscript/compiler.sx` as `hs-check-static`.
|
||||||
|
|
||||||
|
### Option B — Generator translation (simpler)
|
||||||
|
|
||||||
|
The 3 tests are untranslated. All three just verify that `_hyperscript.evaluate(expr)` throws. In our SX harness we can test this with a `guard` form:
|
||||||
|
|
||||||
|
```lisp
|
||||||
|
(deftest "throws on math expressions"
|
||||||
|
(let ((result (guard (e (true true))
|
||||||
|
(eval-hs "1 + 2")
|
||||||
|
false)))
|
||||||
|
(assert result)))
|
||||||
|
```
|
||||||
|
|
||||||
|
But this only works if `eval-hs` actually throws on math expressions. Currently it doesn't — `eval-hs "1 + 2"` returns `3`. So we'd need the static analysis anyway to make the test pass.
|
||||||
|
|
||||||
|
### Chosen approach: Option A
|
||||||
|
|
||||||
|
Add `hs-static-check` to the compiler: a fast AST walker that throws on any non-literal node. Wire it as an optional mode. The test harness calls `eval-hs-static` which runs with static-check enabled.
|
||||||
|
|
||||||
|
Actually, reading the upstream more carefully: `_hyperscript.evaluate` already throws in static mode without additional flags — the "evaluate" API is documented as static-only. Our `eval-hs` in the passing tests works because booleans/numbers/strings/time ARE static. `1 + 2`, `x`, and template strings are NOT static and should throw.
|
||||||
|
|
||||||
|
So the fix is: make `hs-compile-and-run` (or whatever backs `eval-hs`) reject non-literal AST nodes. The 5 passing tests will continue to pass (they use literals). The 3 failing tests will get translated using `eval-hs-error` or a guard pattern.
|
||||||
|
|
||||||
|
## 5. Non-literal AST node types to reject
|
||||||
|
|
||||||
|
| Expression | AST node type | Reject? |
|
||||||
|
|-----------|--------------|---------|
|
||||||
|
| `1`, `3.14` | number literal | ✓ allow |
|
||||||
|
| `"hello"`, `'world'` | string literal (no interpolation) | ✓ allow |
|
||||||
|
| `true`, `false` | boolean literal | ✓ allow |
|
||||||
|
| `null` | null literal | ✓ allow |
|
||||||
|
| `200ms`, `2s` | time literal | ✓ allow |
|
||||||
|
| `1 + 2` | math operator | ✗ throw |
|
||||||
|
| `x` | symbol reference | ✗ throw |
|
||||||
|
| `"hello ${name}"` | template string | ✗ throw |
|
||||||
|
|
||||||
|
## 6. Implementation checklist
|
||||||
|
|
||||||
|
1. In `lib/hyperscript/compiler.sx`, add `hs-static?` predicate: returns true only for literal AST node types.
|
||||||
|
2. In the `eval-hs` path (wherever `hs-compile-and-run` is called for the evaluate API), call `hs-static?` on the parsed AST and throw `"expression is not statically evaluable"` if false.
|
||||||
|
3. Replace 3 `SKIP` bodies in `spec/tests/test-hyperscript-behavioral.sx`:
|
||||||
|
```lisp
|
||||||
|
(deftest "throws on math expressions"
|
||||||
|
(assert (string? (eval-hs-error "1 + 2"))))
|
||||||
|
(deftest "throws on symbol references"
|
||||||
|
(assert (string? (eval-hs-error "x"))))
|
||||||
|
(deftest "throws on template strings"
|
||||||
|
(assert (string? (eval-hs-error "\"hello ${name}\""))))
|
||||||
|
```
|
||||||
|
4. Run `hs_test_run suite="hs-upstream-core/evalStatically"` — expect 8/8.
|
||||||
|
5. Run smoke 0–195 — verify the 5 passing tests still pass.
|
||||||
|
6. Commit: `HS: evalStatically — static literal check, 3 tests (+3)`
|
||||||
|
|
||||||
|
## 7. Risk
|
||||||
|
|
||||||
|
Low-medium. The main risk is that `eval-hs` is used in many tests for non-static expressions and adding a static check to the shared path would break them. The fix must be gated — either a separate `eval-hs-static` helper or a flag parameter. The passing tests must not be affected.
|
||||||
341
plans/designs/hs-plugin-system.md
Normal file
341
plans/designs/hs-plugin-system.md
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
# HyperScript Plugin / Extension System
|
||||||
|
|
||||||
|
Post-Bucket-F capability work. No conformance delta on its own — the payoff is
|
||||||
|
clean architecture for language embeds (Lua, Prolog, Worker runtime) and
|
||||||
|
alignment with real `_hyperscript`'s extension model.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Motivation
|
||||||
|
|
||||||
|
### 1a. Real `_hyperscript` has a plugin API
|
||||||
|
|
||||||
|
Stock `_hyperscript` ships a core bundle with feature stubs and a `use(ext)`
|
||||||
|
hook that loads named extensions at runtime. The worker feature is the canonical
|
||||||
|
example: the core parser has a stub that errors helpfully; loading the worker
|
||||||
|
extension replaces the stub with a real implementation.
|
||||||
|
|
||||||
|
We currently have no equivalent. New grammar or compiler targets require editing
|
||||||
|
`parse-feat`'s hardcoded `cond` or `hs-to-sx`'s hardcoded dispatch. This is
|
||||||
|
fine for conformance work but wrong for language embeds.
|
||||||
|
|
||||||
|
### 1b. Ad-hoc hooks are accumulating
|
||||||
|
|
||||||
|
`runtime.sx` already has `hs-prolog-hook` / `hs-set-prolog-hook!` / `prolog`
|
||||||
|
(nodes 140–142) — an informal plugin slot bolted on outside the parser and
|
||||||
|
compiler. This pattern will repeat for Lua, and again for the Worker runtime.
|
||||||
|
A proper registry prevents the drift.
|
||||||
|
|
||||||
|
### 1c. E39 worker stub is a placeholder
|
||||||
|
|
||||||
|
The stub added in E39 (`parse-feat` raises immediately on `"worker"`) was
|
||||||
|
explicitly designed to be replaced by a real plugin at a single site. This plan
|
||||||
|
is where that replacement happens.
|
||||||
|
|
||||||
|
### 1d. Bucket-F Group 10 needs a converter registry
|
||||||
|
|
||||||
|
`as MyType` via registered converter is already in the Bucket-F plan (Group 10).
|
||||||
|
A `hs-register-converter!` registry is the natural home for it — and the plugin
|
||||||
|
system is the right time to add registries generally.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Scope
|
||||||
|
|
||||||
|
**In scope:**
|
||||||
|
- Parser feature registry (`parse-feat` dispatch)
|
||||||
|
- Compiler command registry (`hs-to-sx` dispatch)
|
||||||
|
- `as` converter registry (`hs-coerce` dispatch)
|
||||||
|
- Migration of E39 worker stub to use the parser registry
|
||||||
|
- Migration of `hs-prolog-hook` ad-hoc slot to a proper plugin
|
||||||
|
- Worker full runtime plugin (first real plugin)
|
||||||
|
- Lua embed plugin
|
||||||
|
- Prolog embed plugin
|
||||||
|
|
||||||
|
**Out of scope:**
|
||||||
|
- Changing the test runner or generator
|
||||||
|
- Any conformance delta (this plan doesn't target failing tests)
|
||||||
|
- Third-party plugin loading from external URLs (future)
|
||||||
|
- Hot-reload of plugins (future)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Registry design
|
||||||
|
|
||||||
|
Three registries, all SX dicts. Checked before the hardcoded `cond` in each
|
||||||
|
dispatch. Registration functions defined alongside the registries in their
|
||||||
|
respective files.
|
||||||
|
|
||||||
|
### 3a. Parser feature registry (`lib/hyperscript/parser.sx`)
|
||||||
|
|
||||||
|
```lisp
|
||||||
|
(define _hs-feature-registry (dict))
|
||||||
|
|
||||||
|
(define hs-register-feature!
|
||||||
|
(fn (keyword parse-fn)
|
||||||
|
(set! _hs-feature-registry
|
||||||
|
(dict-set _hs-feature-registry keyword parse-fn))))
|
||||||
|
```
|
||||||
|
|
||||||
|
In `parse-feat`, prepend a registry lookup before the existing `cond`:
|
||||||
|
|
||||||
|
```lisp
|
||||||
|
(let ((registered (dict-get _hs-feature-registry val)))
|
||||||
|
(if registered
|
||||||
|
(registered) ;; call the registered parse-fn (no args; uses closure over adv!/tp-val etc.)
|
||||||
|
(cond ;; existing dispatch unchanged below
|
||||||
|
...)))
|
||||||
|
```
|
||||||
|
|
||||||
|
`parse-fn` is a zero-arg thunk that has access to the parser's internal state
|
||||||
|
via the same closure that the existing `parse-*` helpers use. Since `parse-feat`
|
||||||
|
is itself defined inside the big `let` in `hs-parse`, all the parser helpers
|
||||||
|
(`adv!`, `tp-val`, `tp-typ`, `parse-cmd-list`, etc.) are in scope.
|
||||||
|
|
||||||
|
### 3b. Compiler command registry (`lib/hyperscript/compiler.sx`)
|
||||||
|
|
||||||
|
```lisp
|
||||||
|
(define _hs-compiler-registry (dict))
|
||||||
|
|
||||||
|
(define hs-register-compiler!
|
||||||
|
(fn (head compile-fn)
|
||||||
|
(set! _hs-compiler-registry
|
||||||
|
(dict-set _hs-compiler-registry (str head) compile-fn))))
|
||||||
|
```
|
||||||
|
|
||||||
|
In `hs-to-sx`, before the existing `cond` on `head`, check the registry:
|
||||||
|
|
||||||
|
```lisp
|
||||||
|
(let ((registered (dict-get _hs-compiler-registry (str head))))
|
||||||
|
(if registered
|
||||||
|
(registered ast)
|
||||||
|
(cond ...)))
|
||||||
|
```
|
||||||
|
|
||||||
|
`compile-fn` receives the full AST node and returns an SX expression.
|
||||||
|
|
||||||
|
### 3c. `as` converter registry (`lib/hyperscript/runtime.sx`)
|
||||||
|
|
||||||
|
```lisp
|
||||||
|
(define _hs-converters (dict))
|
||||||
|
|
||||||
|
(define hs-register-converter!
|
||||||
|
(fn (type-name converter-fn)
|
||||||
|
(set! _hs-converters
|
||||||
|
(dict-set _hs-converters type-name converter-fn))))
|
||||||
|
```
|
||||||
|
|
||||||
|
In `hs-coerce`, add a registry lookup as the last `cond` clause before the
|
||||||
|
fallthrough error:
|
||||||
|
|
||||||
|
```lisp
|
||||||
|
((dict-get _hs-converters type-name)
|
||||||
|
((dict-get _hs-converters type-name) value))
|
||||||
|
```
|
||||||
|
|
||||||
|
This is also the hook that Bucket-F Group 10 (`can accept custom conversions`)
|
||||||
|
hangs on — so implementing it here kills two birds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. First-party plugins
|
||||||
|
|
||||||
|
Each plugin is a `.sx` file in `lib/hyperscript/plugins/`. Plugins call the
|
||||||
|
registration functions at load time (top-level `do` forms). The host loads
|
||||||
|
plugins explicitly after the core files.
|
||||||
|
|
||||||
|
### 4a. Worker plugin (`lib/hyperscript/plugins/worker.sx`)
|
||||||
|
|
||||||
|
**Phase 1 — stub migration (immediate):**
|
||||||
|
Remove the inline error branch from `parse-feat` (the E39 stub). Replace with:
|
||||||
|
|
||||||
|
```lisp
|
||||||
|
(hs-register-feature! "worker"
|
||||||
|
(fn ()
|
||||||
|
(error "worker plugin is not installed — see https://hyperscript.org/features/worker")))
|
||||||
|
```
|
||||||
|
|
||||||
|
This is identical behaviour to E39 but routed through the registry. The stub
|
||||||
|
lives in the plugin file, not the core parser. No test regression.
|
||||||
|
|
||||||
|
**Phase 2 — full runtime:**
|
||||||
|
|
||||||
|
Parser: `parse-worker-feat` — consumes `worker <Name> [(<url>*)] <def|js>* end`,
|
||||||
|
returns `(worker Name urls defs)` AST node.
|
||||||
|
|
||||||
|
Compiler: registered under `"worker"` head:
|
||||||
|
- Emits `(hs-worker-define! "Name" urls defs)` call.
|
||||||
|
|
||||||
|
Runtime additions in the plugin file:
|
||||||
|
- `hs-worker-define!` — creates a `{:_hs-worker true :name N :handle H :exports (...)}` record,
|
||||||
|
binds it in the HS top-level env under `Name`.
|
||||||
|
- `hs-method-call` (existing) detects `:_hs-worker` and dispatches via `postMessage`.
|
||||||
|
- Worker script body compiled to a standalone SX bundle posted to a Blob URL.
|
||||||
|
- Return values are promise-wrapped; async-transparent via `perform`/IO suspension.
|
||||||
|
|
||||||
|
Mock env additions for the test runner: `Worker` constructor + synchronous
|
||||||
|
message loop for the 7 sibling `test.skip(...)` upstream tests (the ones
|
||||||
|
deferred in E39).
|
||||||
|
|
||||||
|
### 4b. Prolog plugin (`lib/hyperscript/plugins/prolog.sx`)
|
||||||
|
|
||||||
|
Replaces the ad-hoc `hs-prolog-hook` in `runtime.sx`.
|
||||||
|
|
||||||
|
**Parser:** Register `"prolog"` feature — parses
|
||||||
|
`prolog(<db-expr>, <goal-expr>)` at feature level (alternative: keep as an
|
||||||
|
expression, register a compiler extension only).
|
||||||
|
|
||||||
|
**Compiler:** Registered under `"prolog"` head — emits `(prolog db goal)`.
|
||||||
|
|
||||||
|
**Runtime:** The existing `prolog` function in `runtime.sx` moves here.
|
||||||
|
`hs-prolog-hook` and `hs-set-prolog-hook!` are removed from `runtime.sx` and
|
||||||
|
the hook mechanism is replaced by the plugin loading `lib/prolog/runtime.sx`
|
||||||
|
and wiring the solver directly.
|
||||||
|
|
||||||
|
Remove from `runtime.sx` nodes 140–142 once the plugin is live.
|
||||||
|
|
||||||
|
### 4c. Lua plugin (`lib/hyperscript/plugins/lua.sx`)
|
||||||
|
|
||||||
|
**Parser:** Register `"lua"` feature — parses `lua ... end` block, captures
|
||||||
|
the body as a raw string.
|
||||||
|
|
||||||
|
**Compiler:** Registered under `"lua"` head — emits `(lua-eval <body-string>)`.
|
||||||
|
|
||||||
|
**Runtime:** `lua-eval` calls `lib/lua/runtime.sx`'s eval entry point, returns
|
||||||
|
result as an SX value via `hs-host-to-sx`. Errors surface as HS `catch`-able
|
||||||
|
exceptions.
|
||||||
|
|
||||||
|
This enables inline Lua in HyperScript:
|
||||||
|
|
||||||
|
```
|
||||||
|
on click
|
||||||
|
lua
|
||||||
|
return document.title:upper()
|
||||||
|
end
|
||||||
|
put it into me
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Load order
|
||||||
|
|
||||||
|
```
|
||||||
|
lib/hyperscript/parser.sx ;; defines _hs-feature-registry, hs-register-feature!
|
||||||
|
lib/hyperscript/compiler.sx ;; defines _hs-compiler-registry, hs-register-compiler!
|
||||||
|
lib/hyperscript/runtime.sx ;; defines _hs-converters, hs-register-converter!
|
||||||
|
lib/hyperscript/plugins/worker.sx
|
||||||
|
lib/hyperscript/plugins/prolog.sx
|
||||||
|
lib/hyperscript/plugins/lua.sx
|
||||||
|
```
|
||||||
|
|
||||||
|
The test runner (`tests/hs-run-filtered.js`) loads plugins after core. The
|
||||||
|
browser WASM bundle includes all three by default (plugins are small; no
|
||||||
|
reason to lazy-load them).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Migration checklist
|
||||||
|
|
||||||
|
The work below is ordered to keep main green at every commit. Each step is
|
||||||
|
independently committable.
|
||||||
|
|
||||||
|
### Step 1 — Registries (infrastructure, no behaviour change)
|
||||||
|
|
||||||
|
1. Add `_hs-feature-registry` + `hs-register-feature!` to `parser.sx`.
|
||||||
|
Thread the registry check into `parse-feat`. No entries yet → behaviour
|
||||||
|
unchanged.
|
||||||
|
2. Add `_hs-compiler-registry` + `hs-register-compiler!` to `compiler.sx`.
|
||||||
|
Thread into `hs-to-sx`. No entries yet → behaviour unchanged.
|
||||||
|
3. Add `_hs-converters` + `hs-register-converter!` to `runtime.sx`. Thread
|
||||||
|
into `hs-coerce`. No entries yet → behaviour unchanged.
|
||||||
|
4. `sx_validate` all three files. Run full HS suite — expect zero regressions.
|
||||||
|
5. Commit: `HS: plugin registry infrastructure (parser + compiler + converter)`.
|
||||||
|
|
||||||
|
### Step 2 — Worker stub migration
|
||||||
|
|
||||||
|
6. Create `lib/hyperscript/plugins/worker.sx`. Register the worker stub error.
|
||||||
|
7. Remove the inline `((= val "worker") ...)` branch from `parse-feat` in
|
||||||
|
`parser.sx`.
|
||||||
|
8. Update the test runner to load `worker.sx` after core.
|
||||||
|
9. Run `HS_SUITE=hs-upstream-worker` — expect 1/1. Run full suite — expect no
|
||||||
|
regressions.
|
||||||
|
10. Commit: `HS: migrate E39 worker stub to plugin registry`.
|
||||||
|
|
||||||
|
### Step 3 — Prolog plugin
|
||||||
|
|
||||||
|
11. Create `lib/hyperscript/plugins/prolog.sx`. Wire to `lib/prolog/runtime.sx`.
|
||||||
|
12. Remove `hs-prolog-hook`, `hs-set-prolog-hook!`, `prolog` from `runtime.sx`
|
||||||
|
nodes 140–142.
|
||||||
|
13. Update test runner to load `prolog.sx`.
|
||||||
|
14. Validate and run full suite.
|
||||||
|
15. Commit: `HS: prolog plugin replaces ad-hoc hook`.
|
||||||
|
|
||||||
|
### Step 4 — `as` converter registry (bridges Bucket-F Group 10)
|
||||||
|
|
||||||
|
16. Confirm `hs-register-converter!` satisfies the Group 10 test
|
||||||
|
`can accept custom conversions`. If yes, this step may be pulled into
|
||||||
|
Bucket-F Group 10 instead (no duplication — just move step 3 of §6 there).
|
||||||
|
17. Commit: `HS: as-converter registry wired into hs-coerce`.
|
||||||
|
|
||||||
|
### Step 5 — Lua plugin
|
||||||
|
|
||||||
|
18. Create `lib/hyperscript/plugins/lua.sx`.
|
||||||
|
19. Add `lua-eval` to `runtime.sx` or directly in the plugin file.
|
||||||
|
20. Parser: `parse-lua-feat` consuming `lua … end`.
|
||||||
|
21. Compiler: registered `"lua"` head.
|
||||||
|
22. Write 3–5 tests in `spec/tests/test-hyperscript-lua.sx`:
|
||||||
|
- Lua returns a string → HS uses it.
|
||||||
|
- Lua error → HS catch.
|
||||||
|
- Lua reads a passed argument.
|
||||||
|
23. Commit: `HS: Lua plugin — inline lua...end blocks`.
|
||||||
|
|
||||||
|
### Step 6 — Worker full runtime plugin
|
||||||
|
|
||||||
|
24. Extend `worker.sx`: implement `parse-worker-feat`, compiler entry,
|
||||||
|
`hs-worker-define!`, `hs-method-call` worker branch.
|
||||||
|
25. Extend test runner: `Worker` constructor + synchronous message loop.
|
||||||
|
26. Un-skip the 7 sibling worker tests from upstream.
|
||||||
|
27. Target: 7/7 worker suite.
|
||||||
|
28. Commit: `HS: Worker plugin full runtime (+7 tests)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Risks
|
||||||
|
|
||||||
|
- **`parse-feat` closure scope** — `hs-register-feature!` stores parse-fns
|
||||||
|
that need access to parser-internal helpers (`adv!`, `tp-val`, etc.). These
|
||||||
|
are only in scope inside `hs-parse`'s big `let`. Two options:
|
||||||
|
(a) the registry stores fns that receive a parser-context dict as arg, or
|
||||||
|
(b) the registry is checked *inside* `parse-feat` where helpers are in scope
|
||||||
|
and fns are zero-arg closures captured at registration time.
|
||||||
|
Option (b) is simpler but requires plugins to be loaded while the parser
|
||||||
|
`let` is being evaluated — i.e., plugins must be defined *inside* the parser
|
||||||
|
file or the context dict must be exposed. **Recommended:** expose a
|
||||||
|
`_hs-parser-ctx` dict at the module level that parse-fns receive as their
|
||||||
|
sole argument. This makes the API explicit and plugins independent files.
|
||||||
|
|
||||||
|
- **Worker Blob URL in WASM** — `URL.createObjectURL` is available in browsers
|
||||||
|
but not in the OCaml WASM host. Worker full runtime is browser-only; flag it
|
||||||
|
with a capability check and graceful fallback.
|
||||||
|
|
||||||
|
- **Lua/Prolog mutual recursion** — a Lua block calling back into HS calling
|
||||||
|
back into Lua is theoretically possible via the IO suspension machinery.
|
||||||
|
Don't try to support it initially; raise a clear error if detected.
|
||||||
|
|
||||||
|
- **Plugin load-order sensitivity** — `hs-register-feature!` must be called
|
||||||
|
before any source is parsed. If a plugin is loaded lazily (future), a
|
||||||
|
`worker MyWorker` in the page would hit the stub before the full plugin
|
||||||
|
registers. Acceptable for now; document that plugins must be loaded at boot.
|
||||||
|
|
||||||
|
- **`runtime.sx` cleanup for prolog** — nodes 140–142 are referenced nowhere
|
||||||
|
else in the codebase (grep confirms). Safe to delete once the plugin is live.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Non-goals
|
||||||
|
|
||||||
|
- Runtime `use(ext)` API (JS-style dynamic plugin install) — future.
|
||||||
|
- Plugin namespacing / versioning — future.
|
||||||
|
- Any conformance tests other than the 7 worker tests in step 6.
|
||||||
|
- Changing how the WASM bundle is built or split.
|
||||||
173
plans/elixir-on-sx.md
Normal file
173
plans/elixir-on-sx.md
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# Elixir-on-SX: Elixir on the CEK/VM
|
||||||
|
|
||||||
|
Compile Elixir source to SX AST; the existing CEK evaluator runs it. The natural companion
|
||||||
|
to `lib/erlang/` — Elixir compiles to the BEAM and most of its runtime semantics are
|
||||||
|
Erlang's. The interesting parts are Elixir-specific: the macro system (`quote`/`unquote`),
|
||||||
|
the pipe operator `|>`, `with` expressions, `defmodule`/`def`/`defp`, protocol dispatch,
|
||||||
|
and the `Stream` lazy evaluation library.
|
||||||
|
|
||||||
|
End-state goal: **core Elixir programs running**, including modules, pattern matching, the
|
||||||
|
pipe operator, macros (`quote`/`unquote`/`defmacro`), protocols, and actor-style processes
|
||||||
|
reusing the Erlang runtime foundation.
|
||||||
|
|
||||||
|
## Ground rules
|
||||||
|
|
||||||
|
- **Scope:** only touch `lib/elixir/**` and `plans/elixir-on-sx.md`. Do **not** edit
|
||||||
|
`spec/`, `hosts/`, `shared/`, or other `lib/<lang>/`. Reuse `lib/erlang/` runtime
|
||||||
|
functions where possible — import them, don't duplicate.
|
||||||
|
- **Shared-file issues** go under "Blockers" below with a minimal repro; do not fix here.
|
||||||
|
- **SX files:** use `sx-tree` MCP tools only.
|
||||||
|
- **Architecture:** Elixir source → Elixir AST → SX AST. Reuse Erlang runtime for process/
|
||||||
|
message/pattern primitives; add Elixir-specific surface in `lib/elixir/`.
|
||||||
|
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick boxes.
|
||||||
|
|
||||||
|
## Architecture sketch
|
||||||
|
|
||||||
|
```
|
||||||
|
Elixir source text
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
lib/elixir/tokenizer.sx — atoms (:atom), strings (""), charlists (''), sigils (~r, ~s etc.),
|
||||||
|
│ operators (|>, <>, ++, :::, etc.), do/end blocks
|
||||||
|
▼
|
||||||
|
lib/elixir/parser.sx — Elixir AST: defmodule, def/defp/defmacro, @attribute,
|
||||||
|
│ pattern matching, |> pipe, with, for comprehension, quote/unquote,
|
||||||
|
│ case/cond/if/unless, fn, receive, try/rescue/catch/after
|
||||||
|
▼
|
||||||
|
lib/elixir/transpile.sx — Elixir AST → SX AST
|
||||||
|
│
|
||||||
|
├── lib/erlang/runtime.sx (reused: processes, message passing, pattern match)
|
||||||
|
└── lib/elixir/runtime.sx — Elixir-specific: Kernel, String, Enum, Stream, Map,
|
||||||
|
List, Tuple, IO, protocol dispatch, macro expansion
|
||||||
|
```
|
||||||
|
|
||||||
|
Key semantic mappings (differences from Erlang):
|
||||||
|
- `defmodule M do ... end` → SX `define-library` + module dict `{:module "M" :fns {...}}`
|
||||||
|
- `def f(args) do body end` → named function in module dict, with pattern-match dispatch
|
||||||
|
- `|>` pipe → left-to-right function composition; `a |> f(b)` = `f(a, b)`
|
||||||
|
- `with x <- expr, y <- expr2 do body else patterns end` → chained pattern match with early exit
|
||||||
|
- `for x <- list, filter, do: expr` → list comprehension (SX `map`/`filter`)
|
||||||
|
- `quote do expr end` → returns AST as SX list (homoiconic — Elixir AST IS SX-like)
|
||||||
|
- `unquote(expr)` → evaluate expr and splice into surrounding `quote`
|
||||||
|
- `defmacro` → macro in module; expanded at compile time by calling the SX macro
|
||||||
|
- Protocol → dict of implementations keyed by type name; `defprotocol` defines interface,
|
||||||
|
`defimpl` registers an implementation
|
||||||
|
- `Stream` → lazy sequences using SX promises/coroutines (Phase 9/4 of primitives)
|
||||||
|
- `Agent`/`GenServer` → SX coroutine + message queue (similar to Erlang process model)
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
### Phase 1 — tokenizer + parser
|
||||||
|
- [ ] Tokenizer: atoms (`:atom`, `:"atom with spaces"`), strings (`""`), charlists (`''`),
|
||||||
|
numbers (int, float, hex `0xFF`, octal `0o77`, binary `0b11`), booleans (`true`/`false`/`nil`),
|
||||||
|
operators (`|>`, `<>`, `++`, `--`, `:::`, `&&`, `||`, `!`, `..`, `<-`, `=~`),
|
||||||
|
sigils (`~r/regex/`, `~s"string"`, `~w(word list)`), do/end blocks, keywords as args
|
||||||
|
`f(key: val)`, `@module_attribute`
|
||||||
|
- [ ] Parser:
|
||||||
|
- Module: `defmodule Name do ... end` → module AST with body
|
||||||
|
- Functions: `def f(pat) do body end`, `def f(pat) when guard do body end`,
|
||||||
|
multi-clause `def f(a) do ...; def f(b) do ...` → clause list
|
||||||
|
- `defp` (private), `defmacro`, `defmacrop`
|
||||||
|
- `@doc`, `@moduledoc`, `@spec`, `@type`, `@behaviour` module attributes
|
||||||
|
- `case expr do patterns end`, `cond do clauses end`, `if`/`unless`
|
||||||
|
- `with x <- e, y <- e2, do: body, else: [pattern -> body]`
|
||||||
|
- `for x <- list, filter, into: acc, do: expr` comprehension
|
||||||
|
- `fn pat -> body end` anonymous function; capture `&Module.fun/arity`, `&(&1 + 1)`
|
||||||
|
- `receive do patterns after timeout -> body end`
|
||||||
|
- `try do body rescue e -> ... catch type, val -> ... after ... end`
|
||||||
|
- `quote do ... end`, `unquote(expr)`, `unquote_splicing(list)`
|
||||||
|
- `|>` pipe chain: `a |> f |> g(b)` → `g(f(a), b)`
|
||||||
|
- [ ] Tests in `lib/elixir/tests/parse.sx`
|
||||||
|
|
||||||
|
### Phase 2 — transpile: basic Elixir (no macros, no processes)
|
||||||
|
- [ ] `ex-eval-ast` entry
|
||||||
|
- [ ] Arithmetic, string `<>`, list `++`/`--`, comparison, boolean (`and`/`or`/`not`)
|
||||||
|
- [ ] Pattern matching in `=`, function heads, `case` — reuse Erlang pattern engine
|
||||||
|
- [ ] `def`/`defp` → SX `define` with clause dispatch (like Erlang function clauses)
|
||||||
|
- [ ] Module as a dict of named functions; `ModuleName.function(args)` dispatch
|
||||||
|
- [ ] `|>` pipe: desugar `a |> f(b, c)` → `f(a, b, c)` at transpile time
|
||||||
|
- [ ] `with` expression: chain of `<-` bindings, short-circuit on mismatch to `else`
|
||||||
|
- [ ] `for` comprehension: `for x <- list, filter do body end` → `map`/`filter`
|
||||||
|
- [ ] `fn` anonymous functions, `&` capture forms
|
||||||
|
- [ ] `if`/`unless`/`cond`/`case`
|
||||||
|
- [ ] String interpolation: `"Hello #{name}"` → string concat
|
||||||
|
- [ ] Keyword lists `[key: val]` → SX list of `{:key val}` dicts; maps `%{key: val}` → SX dict
|
||||||
|
- [ ] Tuples `{a, b, c}` → SX list (or vector); `elem/2`, `put_elem/3`
|
||||||
|
- [ ] 40+ eval tests in `lib/elixir/tests/eval.sx`
|
||||||
|
|
||||||
|
### Phase 3 — macro system
|
||||||
|
- [ ] `quote do expr end` → returns Elixir AST as SX list structure
|
||||||
|
(Elixir AST is 3-tuples `{name, meta, args}` — map to SX `(list name meta args)`)
|
||||||
|
- [ ] `unquote(expr)` → evaluate and splice into surrounding `quote`
|
||||||
|
- [ ] `unquote_splicing(list)` → splice list into surrounding `quote`
|
||||||
|
- [ ] `defmacro` → define a macro in the module; macro receives AST args, returns AST
|
||||||
|
- [ ] Macro expansion: expand macros before transpiling (two-pass: collect defs, then expand)
|
||||||
|
- [ ] `use Module` → calls `Module.__using__/1` macro, injects code into caller
|
||||||
|
- [ ] `import Module` → bring functions into scope without prefix
|
||||||
|
- [ ] `alias Module, as: M` → short name for module
|
||||||
|
- [ ] Tests: `defmacro unless`, `defmacro my_if`, `use` injection, `__MODULE__`, `__DIR__`
|
||||||
|
|
||||||
|
### Phase 4 — protocols
|
||||||
|
- [ ] `defprotocol P do @spec f(t) :: result end` → defines protocol dict + dispatch fn
|
||||||
|
- [ ] `defimpl P, for: Type do def f(t) do ... end end` → register implementation
|
||||||
|
- [ ] Protocol dispatch: `P.f(value)` → look up type of value, find implementation, call it
|
||||||
|
- [ ] Built-in protocols: `Enumerable`, `Collectable`, `String.Chars`, `Inspect`
|
||||||
|
- [ ] `Enumerable` implementation for lists, maps, ranges — enables `Enum.*` on custom types
|
||||||
|
- [ ] `derive` — automatic protocol implementation for simple structs
|
||||||
|
- [ ] Tests: custom type implementing `Enumerable`, `String.Chars`, protocol fallback
|
||||||
|
|
||||||
|
### Phase 5 — structs + behaviours
|
||||||
|
- [ ] `defstruct [:field1, field2: default]` → defines `%ModuleName{}` struct type
|
||||||
|
Structs are maps with `__struct__: ModuleName` key + defined fields
|
||||||
|
- [ ] Struct pattern matching: `%User{name: n} = user`
|
||||||
|
- [ ] `@behaviour Module` → declares behaviour callbacks; compile-time check
|
||||||
|
- [ ] `@impl true` / `@impl BehaviourName` → marks function as behaviour implementation
|
||||||
|
- [ ] Built-in behaviours: `GenServer`, `Supervisor`, `Agent`, `Task`
|
||||||
|
- [ ] Tests: struct creation, update syntax `%{struct | field: val}`, behaviour callbacks
|
||||||
|
|
||||||
|
### Phase 6 — processes + OTP patterns (reuses Erlang runtime)
|
||||||
|
- [ ] `spawn(fn -> ... end)` / `spawn(M, f, args)` → SX coroutine on scheduler
|
||||||
|
Reuse `lib/erlang/` process + message queue infrastructure
|
||||||
|
- [ ] `send(pid, msg)` / `receive do patterns end` — already in Erlang runtime
|
||||||
|
- [ ] `GenServer` behaviour: `start_link`, `call`, `cast`, `handle_call`, `handle_cast`,
|
||||||
|
`handle_info`, `init` — implement as SX macros expanding to process + message loop
|
||||||
|
- [ ] `Agent` — simple state wrapper over GenServer; `Agent.start_link`, `get`, `update`
|
||||||
|
- [ ] `Task` — async computation; `Task.async`, `Task.await`
|
||||||
|
- [ ] `Supervisor` — child spec, restart strategy (`one_for_one`, `one_for_all`)
|
||||||
|
- [ ] Tests: counter GenServer, bank account Agent, parallel Task, supervised worker
|
||||||
|
|
||||||
|
### Phase 7 — standard library
|
||||||
|
- [ ] `Enum.*` — `map`, `filter`, `reduce`, `each`, `into`, `flat_map`, `zip`, `sort`,
|
||||||
|
`sort_by`, `min_by`, `max_by`, `group_by`, `frequencies`, `count`, `any?`, `all?`,
|
||||||
|
`find`, `take`, `drop`, `take_while`, `drop_while`, `chunk_every`, `chunk_by`,
|
||||||
|
`flat_map_reduce`, `scan`, `uniq`, `uniq_by`, `member?`, `empty?`, `sum`, `product`
|
||||||
|
- [ ] `Stream.*` — lazy versions of Enum; `Stream.map`, `Stream.filter`, `Stream.take`,
|
||||||
|
`Stream.cycle`, `Stream.iterate`, `Stream.unfold`, `Stream.resource`
|
||||||
|
Uses SX promises (Phase 9) for laziness
|
||||||
|
- [ ] `String.*` — `length`, `upcase`, `downcase`, `trim`, `split`, `replace`, `contains?`,
|
||||||
|
`starts_with?`, `ends_with?`, `slice`, `at`, `graphemes`, `codepoints`, `to_integer`,
|
||||||
|
`to_float`, `pad_leading`, `pad_trailing`, `duplicate`, `match?`
|
||||||
|
- [ ] `Map.*` — `new`, `get`, `put`, `delete`, `update`, `merge`, `keys`, `values`,
|
||||||
|
`to_list`, `from_struct`, `has_key?`, `filter`, `map`, `reject`, `take`, `drop`
|
||||||
|
- [ ] `List.*` — `first`, `last`, `flatten`, `zip`, `unzip`, `keystore`, `keyfind`,
|
||||||
|
`wrap`, `duplicate`, `improper?`, `delete`, `insert_at`, `replace_at`
|
||||||
|
- [ ] `Tuple.*` — `to_list`, `from_list`, `append`, `insert_at`, `delete_at`
|
||||||
|
- [ ] `Integer.*` / `Float.*` — `parse`, `to_string`, `digits`, `pow`, `is_odd?`, `is_even?`
|
||||||
|
- [ ] `IO.*` — `puts`, `gets`, `inspect`, `write`, `read` → SX IO perform
|
||||||
|
- [ ] `Kernel.*` — built-in functions: `is_integer?`, `is_binary?`, `length`, `hd`, `tl`,
|
||||||
|
`elem`, `put_elem`, `apply`, `raise`, `exit`, `inspect`
|
||||||
|
- [ ] `inspect/1` / `IO.inspect/2` — debug printing using `Inspect` protocol
|
||||||
|
|
||||||
|
### Phase 8 — conformance target
|
||||||
|
- [ ] Vendor or hand-build 100+ Elixir program tests in `lib/elixir/tests/programs/`
|
||||||
|
- [ ] Drive scoreboard
|
||||||
|
|
||||||
|
## Blockers
|
||||||
|
|
||||||
|
_(none yet)_
|
||||||
|
|
||||||
|
## Progress log
|
||||||
|
|
||||||
|
_Newest first._
|
||||||
|
|
||||||
|
_(awaiting phase 1)_
|
||||||
131
plans/elm-on-sx.md
Normal file
131
plans/elm-on-sx.md
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
# Elm-on-SX: Elm 0.19 on the CEK/VM
|
||||||
|
|
||||||
|
Compile Elm source to SX AST; the existing CEK evaluator runs it. The unique angle: SX's
|
||||||
|
reactive island system (`defisland`, signals, `provide`/`context`) is a natural host for
|
||||||
|
The Elm Architecture — Model/Update/View maps almost directly onto SX's reactive runtime.
|
||||||
|
This is the only language in the set that targets SX's browser-side reactivity rather than
|
||||||
|
the server-side evaluator.
|
||||||
|
|
||||||
|
End-state goal: **core Elm programs running in the browser via SX islands**, with The Elm
|
||||||
|
Architecture wired to SX signals. Not a full Elm compiler — no exhaustiveness checking, no
|
||||||
|
module system, no type inference — but a faithful runtime that can run Elm programs written
|
||||||
|
in idiomatic style.
|
||||||
|
|
||||||
|
## Ground rules
|
||||||
|
|
||||||
|
- **Scope:** only touch `lib/elm/**` and `plans/elm-on-sx.md`. Do **not** edit `spec/`,
|
||||||
|
`hosts/`, `shared/`, or other `lib/<lang>/`.
|
||||||
|
- **Shared-file issues** go under "Blockers" below with a minimal repro; do not fix here.
|
||||||
|
- **SX files:** use `sx-tree` MCP tools only.
|
||||||
|
- **Architecture:** Elm source → Elm AST → SX AST. No standalone Elm evaluator.
|
||||||
|
- **Type system:** defer. Focus on runtime semantics. Type errors surface at eval time.
|
||||||
|
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick boxes.
|
||||||
|
|
||||||
|
## Architecture sketch
|
||||||
|
|
||||||
|
```
|
||||||
|
Elm source text
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
lib/elm/tokenizer.sx — numbers, strings, idents, operators, indentation-sensitive lexer
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
lib/elm/parser.sx — Elm AST: module, import, type alias, type, let, case, lambda,
|
||||||
|
│ if, list/tuple/record literals, pipe operator |>
|
||||||
|
▼
|
||||||
|
lib/elm/transpile.sx — Elm AST → SX AST
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
lib/elm/runtime.sx — TEA runtime: Program, sandbox, element; Cmd/Sub wrappers;
|
||||||
|
│ Html.* shims; Browser.* shims
|
||||||
|
▼
|
||||||
|
SX island / reactive runtime (browser)
|
||||||
|
```
|
||||||
|
|
||||||
|
Key semantic mappings:
|
||||||
|
- `Model` → SX signal (`make-signal`)
|
||||||
|
- `update : Msg -> Model -> Model` → SX signal updater (called on each message)
|
||||||
|
- `view : Model -> Html Msg` → SX component (re-renders on model signal change)
|
||||||
|
- `Cmd` → SX `perform` IO request
|
||||||
|
- `Sub` → SX event listener registered via `dom-listen`
|
||||||
|
- `Maybe a` → `nil` (Nothing) or value (Just a) — uses ADTs from Phase 6 of primitives
|
||||||
|
- `Result a b` → ADT `(Ok val)` / `(Err err)`
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
### Phase 1 — tokenizer + parser
|
||||||
|
- [ ] Tokenizer: keywords (`module`, `import`, `type`, `alias`, `let`, `in`, `if`, `then`,
|
||||||
|
`else`, `case`, `of`, `port`), indentation tokens (indent/dedent/newline), string
|
||||||
|
literals, number literals, operators (`|>`, `>>`, `<<`, `<|`, `++`, `::`), type vars
|
||||||
|
- [ ] Parser: module declaration, imports, type aliases, union types, function definitions
|
||||||
|
with pattern matching, `let`/`in`, `case`/`of`, `if`/`then`/`else`, lambda `\x -> e`,
|
||||||
|
list literals `[1,2,3]`, tuple literals `(a,b)`, record literals `{x=1, y=2}`,
|
||||||
|
record update `{ r | x = 1 }`, pipe operator `|>`
|
||||||
|
- [ ] Skip for phase 1: ports, subscriptions, effects manager, type annotations
|
||||||
|
- [ ] Tests in `lib/elm/tests/parse.sx`
|
||||||
|
|
||||||
|
### Phase 2 — transpile: expressions + pattern matching
|
||||||
|
- [ ] `elm-eval-ast` entry
|
||||||
|
- [ ] Arithmetic, string `++`, comparison, boolean ops
|
||||||
|
- [ ] Lambda → SX `fn`; function application
|
||||||
|
- [ ] `let`/`in` → SX `let`
|
||||||
|
- [ ] `if`/`then`/`else` → SX `if`
|
||||||
|
- [ ] `case`/`of` with constructor, literal, tuple, list, wildcard patterns → SX `cond`
|
||||||
|
using ADT match (Phase 6 primitives)
|
||||||
|
- [ ] List ops: `List.map`, `List.filter`, `List.foldl`, `List.foldr`
|
||||||
|
- [ ] `Maybe` and `Result` as ADTs
|
||||||
|
- [ ] 30+ eval tests in `lib/elm/tests/eval.sx`
|
||||||
|
|
||||||
|
### Phase 3 — The Elm Architecture runtime
|
||||||
|
- [ ] `Browser.sandbox` — pure TEA loop (no Cmds, no Subs)
|
||||||
|
`{ init : model, update : msg -> model -> model, view : model -> Html msg }`
|
||||||
|
Wires to: SX signal for model, SX component for view, message dispatch on user events
|
||||||
|
- [ ] `Html.*` shims: `div`, `p`, `button`, `input`, `text`, `h1`–`h6`, `ul`, `li`, `a`,
|
||||||
|
`span`, `img` — emit SX component calls
|
||||||
|
- [ ] `Html.Attributes.*`: `class`, `id`, `href`, `src`, `type_`, `placeholder`, `value`
|
||||||
|
- [ ] `Html.Events.*`: `onClick`, `onInput`, `onSubmit`, `onBlur`, `onFocus`
|
||||||
|
- [ ] `Browser.element` — adds `init` returning `(model, Cmd msg)`, `subscriptions`
|
||||||
|
- [ ] Demo: counter app (`init=0`, `update Increment m = m+1`, `view` shows count + button)
|
||||||
|
|
||||||
|
### Phase 4 — Cmds and Subs
|
||||||
|
- [ ] `Cmd` — mapped to SX `perform` IO requests. `Cmd.none`, `Cmd.batch`
|
||||||
|
- [ ] `Http.get`/`Http.post` → SX fetch IO
|
||||||
|
- [ ] `Sub` — mapped to SX `dom-listen`. `Sub.none`, `Sub.batch`
|
||||||
|
- [ ] `Browser.Events.onClick`, `onKeyPress`, `onAnimationFrame`
|
||||||
|
- [ ] `Time.every` — periodic subscription via SX timer IO
|
||||||
|
- [ ] `Task.perform`/`Task.attempt` — single-shot async operations
|
||||||
|
|
||||||
|
### Phase 5 — standard library
|
||||||
|
- [ ] `String.*` — `length`, `append`, `concat`, `split`, `join`, `trim`, `toUpper`, `toLower`,
|
||||||
|
`contains`, `startsWith`, `endsWith`, `replace`, `toInt`, `toFloat`, `fromInt`, `fromFloat`
|
||||||
|
- [ ] `List.*` — `map`, `filter`, `foldl`, `foldr`, `head`, `tail`, `isEmpty`, `length`,
|
||||||
|
`reverse`, `append`, `concat`, `member`, `sort`, `sortBy`, `indexedMap`, `range`
|
||||||
|
- [ ] `Dict.*` — SX immutable dict; `fromList`, `toList`, `get`, `insert`, `remove`, `update`,
|
||||||
|
`member`, `keys`, `values`, `map`, `filter`, `foldl`
|
||||||
|
- [ ] `Set.*` — SX set primitive (Phase 18); `fromList`, `toList`, `member`, `insert`,
|
||||||
|
`remove`, `union`, `intersect`, `diff`
|
||||||
|
- [ ] `Maybe.*` — `withDefault`, `map`, `andThen`, `map2`
|
||||||
|
- [ ] `Result.*` — `withDefault`, `map`, `andThen`, `mapError`, `toMaybe`
|
||||||
|
- [ ] `Tuple.*` — `first`, `second`, `pair`, `mapFirst`, `mapSecond`
|
||||||
|
- [ ] `Basics.*` — `identity`, `always`, `not`, `xor`, `modBy`, `remainderBy`, `clamp`,
|
||||||
|
`min`, `max`, `abs`, `sqrt`, `logBase`, `e`, `pi`, `floor`, `ceiling`, `round`,
|
||||||
|
`truncate`, `toFloat`, `isNaN`, `isInfinite`, `compare`
|
||||||
|
- [ ] `Random.*` — seed-based PRNG via SX IO perform
|
||||||
|
|
||||||
|
### Phase 6 — full browser integration
|
||||||
|
- [ ] `Browser.application` — URL routing, `onUrlChange`, `onUrlRequest`
|
||||||
|
- [ ] `Browser.Navigation.*` — `pushUrl`, `replaceUrl`, `back`, `forward`
|
||||||
|
- [ ] `Url.Parser.*` — path segment parsing
|
||||||
|
- [ ] `Json.Decode.*` — JSON decoder combinators
|
||||||
|
- [ ] `Json.Encode.*` — JSON encoder
|
||||||
|
- [ ] `Ports` — `port` keyword; JS interop via SX `host-call`
|
||||||
|
|
||||||
|
## Blockers
|
||||||
|
|
||||||
|
_(none yet)_
|
||||||
|
|
||||||
|
## Progress log
|
||||||
|
|
||||||
|
_Newest first._
|
||||||
|
|
||||||
|
_(awaiting phase 1)_
|
||||||
145
plans/go-on-sx.md
Normal file
145
plans/go-on-sx.md
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
# Go-on-SX: Go on the CEK/VM
|
||||||
|
|
||||||
|
Compile Go source to SX AST; the existing CEK evaluator runs it. The unique angle: Go's
|
||||||
|
goroutines and channels map cleanly onto SX's IO suspension machinery (`perform`/`cek-resume`)
|
||||||
|
— a goroutine is a `cek-step-loop` running in a cooperative scheduler, a channel send/receive
|
||||||
|
is a `perform` that suspends until the other end is ready.
|
||||||
|
|
||||||
|
End-state goal: **core Go programs running**, including goroutines, channels, defer/panic/recover,
|
||||||
|
interfaces, and structs. Not a full Go compiler — no generics, no CGo, no full stdlib — but
|
||||||
|
a faithful runtime for idiomatic Go concurrent programs.
|
||||||
|
|
||||||
|
## Ground rules
|
||||||
|
|
||||||
|
- **Scope:** only touch `lib/go/**` and `plans/go-on-sx.md`. Do **not** edit `spec/`,
|
||||||
|
`hosts/`, `shared/`, or other `lib/<lang>/`.
|
||||||
|
- **Shared-file issues** go under "Blockers" below with a minimal repro; do not fix here.
|
||||||
|
- **SX files:** use `sx-tree` MCP tools only.
|
||||||
|
- **Architecture:** Go source → Go AST → SX AST. No standalone Go evaluator.
|
||||||
|
- **Concurrency model:** cooperative, not preemptive. Goroutines yield at channel ops and
|
||||||
|
`time.Sleep`. A round-robin scheduler in SX drives them.
|
||||||
|
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick boxes.
|
||||||
|
|
||||||
|
## Architecture sketch
|
||||||
|
|
||||||
|
```
|
||||||
|
Go source text
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
lib/go/tokenizer.sx — Go tokens: keywords, idents, string/rune/number literals,
|
||||||
|
│ operators, semicolon insertion rules
|
||||||
|
▼
|
||||||
|
lib/go/parser.sx — Go AST: package, import, var, const, type, func, struct,
|
||||||
|
│ interface, goroutine, channel ops, defer, select, for range
|
||||||
|
▼
|
||||||
|
lib/go/transpile.sx — Go AST → SX AST
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
lib/go/runtime.sx — goroutine scheduler, channel primitives, defer stack,
|
||||||
|
│ panic/recover, interface dispatch, slice/map ops
|
||||||
|
▼
|
||||||
|
CEK / VM
|
||||||
|
```
|
||||||
|
|
||||||
|
Key semantic mappings:
|
||||||
|
- `go fn()` → spawn new coroutine (SX coroutine primitive, Phase 4 of primitives)
|
||||||
|
- `ch <- v` (send) → `perform` that suspends until receiver ready; scheduler picks next goroutine
|
||||||
|
- `v := <-ch` (receive) → `perform` that suspends until sender ready
|
||||||
|
- `select { case ... }` → scheduler checks all channel readiness, picks first ready
|
||||||
|
- `defer fn()` → push onto a per-goroutine defer stack; run on return/panic
|
||||||
|
- `panic(v)` → `raise` the value; `recover()` catches it in deferred function
|
||||||
|
- `interface{}` → any SX value (duck typed)
|
||||||
|
- `struct { ... }` → SX hash table with field names as keys
|
||||||
|
- `slice` → SX vector with length + capacity metadata
|
||||||
|
- `map[K]V` → SX mutable hash table (Phase 10 of primitives)
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
### Phase 1 — tokenizer + parser
|
||||||
|
- [ ] Tokenizer: keywords (`package`, `import`, `func`, `var`, `const`, `type`, `struct`,
|
||||||
|
`interface`, `go`, `chan`, `select`, `defer`, `return`, `if`, `else`, `for`, `range`,
|
||||||
|
`switch`, `case`, `default`, `break`, `continue`, `goto`, `fallthrough`, `map`,
|
||||||
|
`make`, `new`, `nil`, `true`, `false`), automatic semicolon insertion, string literals
|
||||||
|
(interpreted + raw `` `...` ``), rune literals `'a'`, number literals (int, float, hex,
|
||||||
|
octal, binary, complex), operators, slices `[:]`
|
||||||
|
- [ ] Parser: package clause, imports, top-level `func`/`var`/`const`/`type`; function
|
||||||
|
bodies: short variable decl `:=`, assignments, `if`/`else`, `for`/`range`, `switch`,
|
||||||
|
`return`, struct literals, slice literals, map literals, composite literals, type
|
||||||
|
assertions `v.(T)`, method calls `v.Method(args)`, goroutine `go`, channel ops
|
||||||
|
`<-ch`, `ch <- v`, `defer`, `select`
|
||||||
|
- [ ] Tests in `lib/go/tests/parse.sx`
|
||||||
|
|
||||||
|
### Phase 2 — transpile: basic Go (no goroutines)
|
||||||
|
- [ ] `go-eval-ast` entry
|
||||||
|
- [ ] Arithmetic, string ops, comparison, boolean
|
||||||
|
- [ ] Variables, short decl, assignment, multiple assignment
|
||||||
|
- [ ] `if`/`else if`/`else`
|
||||||
|
- [ ] `for` (C-style), `for range` over slice/map/string
|
||||||
|
- [ ] Functions: named + anonymous, multiple return values (SX multiple values, Phase 8)
|
||||||
|
- [ ] Structs → SX hash tables; field access `.field`; struct literals `T{f: v}`
|
||||||
|
- [ ] Slices → SX vectors; `len`, `cap`, `append`, `copy`, slice expressions `s[a:b]`
|
||||||
|
- [ ] Maps → SX hash tables; `make(map[K]V)`, `m[k]`, `m[k] = v`, `delete(m, k)`,
|
||||||
|
comma-ok `v, ok := m[k]`
|
||||||
|
- [ ] Pointers — modelled as single-element mutable vectors; `&x` creates wrapper, `*p` dereferences
|
||||||
|
- [ ] `fmt.Println`/`fmt.Printf`/`fmt.Sprintf` → SX IO perform (print)
|
||||||
|
- [ ] 40+ eval tests in `lib/go/tests/eval.sx`
|
||||||
|
|
||||||
|
### Phase 3 — defer / panic / recover
|
||||||
|
- [ ] Defer stack per function frame — SX list of thunks, run LIFO on return
|
||||||
|
- [ ] `defer` statement pushes thunk; transpiler wraps function body in try/finally equivalent
|
||||||
|
- [ ] `panic(v)` → `raise` with Go panic wrapper
|
||||||
|
- [ ] `recover()` → catches panic value inside a deferred function; returns nil otherwise
|
||||||
|
- [ ] Panic propagation across call stack until recovered or fatal
|
||||||
|
- [ ] Tests: defer ordering, panic/recover, panic in goroutine without recover
|
||||||
|
|
||||||
|
### Phase 4 — goroutines + channels
|
||||||
|
- [ ] Coroutine-based goroutine type using SX coroutine primitive (Phase 4 of primitives)
|
||||||
|
- [ ] Round-robin scheduler in `lib/go/runtime.sx`: maintains run queue, steps each
|
||||||
|
goroutine one turn at a time, suspends at channel ops
|
||||||
|
- [ ] Unbuffered channels: `make(chan T)` → rendezvous point; send suspends until receive
|
||||||
|
and vice versa. Implemented as a pair of waiting queues + `cek-resume`.
|
||||||
|
- [ ] Buffered channels: `make(chan T, n)` → circular buffer; send only blocks when full,
|
||||||
|
receive only blocks when empty
|
||||||
|
- [ ] `close(ch)` — mark channel closed; receivers drain then get zero value + `false`
|
||||||
|
- [ ] `select` — scheduler inspects all cases, picks a ready one (random if multiple),
|
||||||
|
blocks if none ready until at least one becomes ready
|
||||||
|
- [ ] `go fn(args)` — spawns new goroutine on run queue
|
||||||
|
- [ ] `time.Sleep(d)` — yields current goroutine, re-queues after d milliseconds
|
||||||
|
(simulated with IO perform timer)
|
||||||
|
- [ ] Tests: ping-pong, fan-out, fan-in, select with default, range over channel
|
||||||
|
|
||||||
|
### Phase 5 — interfaces
|
||||||
|
- [ ] Interface type → SX dict `{:type "T" :methods {...}}` dispatch table
|
||||||
|
- [ ] `interface{}` / `any` → any SX value (already implicit)
|
||||||
|
- [ ] Type assertion `v.(T)` → check `:type` field, panic if mismatch
|
||||||
|
- [ ] Type switch `switch v.(type) { case T: ... }` → dispatches on `:type`
|
||||||
|
- [ ] Method sets — structs implement interfaces implicitly if they have the right methods
|
||||||
|
- [ ] Value vs pointer receivers — pointer receiver gets the mutable vector wrapper
|
||||||
|
- [ ] Built-in interfaces: `error` (`Error() string`), `Stringer` (`String() string`)
|
||||||
|
- [ ] Tests: interface satisfaction, type assertion, type switch, error interface
|
||||||
|
|
||||||
|
### Phase 6 — standard library subset
|
||||||
|
- [ ] `fmt` — `Println`, `Printf`, `Sprintf`, `Fprintf`, `Errorf`, `Stringer` dispatch
|
||||||
|
- [ ] `strings` — `Contains`, `HasPrefix`, `HasSuffix`, `Split`, `Join`, `TrimSpace`,
|
||||||
|
`ToUpper`, `ToLower`, `Replace`, `Index`, `Count`, `Repeat`
|
||||||
|
- [ ] `strconv` — `Itoa`, `Atoi`, `FormatFloat`, `ParseFloat`, `ParseInt`, `FormatInt`
|
||||||
|
- [ ] `math` — full surface via SX math primitives (Phase 15)
|
||||||
|
- [ ] `sort` — `sort.Slice`, `sort.Ints`, `sort.Strings`
|
||||||
|
- [ ] `errors` — `errors.New`, `errors.Is`, `errors.As`
|
||||||
|
- [ ] `sync` — `sync.Mutex` (cooperative — just a boolean flag + goroutine queue),
|
||||||
|
`sync.WaitGroup`, `sync.Once`
|
||||||
|
- [ ] `io` — `io.Reader`/`io.Writer` interfaces; `io.ReadAll`; `strings.NewReader`
|
||||||
|
|
||||||
|
### Phase 7 — full conformance target
|
||||||
|
- [ ] Vendor a Go test suite or hand-build 100+ program tests in `lib/go/tests/programs/`
|
||||||
|
- [ ] Drive scoreboard
|
||||||
|
|
||||||
|
## Blockers
|
||||||
|
|
||||||
|
_(none yet)_
|
||||||
|
|
||||||
|
## Progress log
|
||||||
|
|
||||||
|
_Newest first._
|
||||||
|
|
||||||
|
_(awaiting phase 1)_
|
||||||
351
plans/hs-bucket-f.md
Normal file
351
plans/hs-bucket-f.md
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
# HS Conformance — Bucket F Plan
|
||||||
|
|
||||||
|
Based on a full suite run on 2026-04-26. Current score: **~1297/1489 covered** (~87%).
|
||||||
|
Skipped from runs: tests 197–200 (hypertrace, slow), 615 (slow), 1197–1198 (repeat-forever timeouts).
|
||||||
|
|
||||||
|
**⚠ Updated 2026-04-26:** The hs-loop completed significant Bucket D work before being stopped.
|
||||||
|
`hs-f` branches from `loops/hs` HEAD which already includes:
|
||||||
|
- MutationObserver mock + `on mutation` dispatch (+7) → **Group 4 likely done**
|
||||||
|
- Cookie API partial (+3/5) → **Group 5 partially done**
|
||||||
|
- `elsewhere`/`from elsewhere` + count filters (+7) → **Group 3a/3c partially done**
|
||||||
|
- Namespaced `def` (+3) → already done
|
||||||
|
- SourceInfo E38 (+4) + WebWorker E39 (+1) → already merged
|
||||||
|
|
||||||
|
**The Bucket F agent must run `hs_test_run` on each group's suite before implementing,
|
||||||
|
to verify what's actually still failing. Skip any group that already passes.**
|
||||||
|
|
||||||
|
Total remaining failures: ~193. Broken into groups below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Group 0 — Bucket E payoff (~47 tests, will land automatically)
|
||||||
|
|
||||||
|
These are already implemented or in-flight on Bucket E branches. Once merged they close ~47 tests.
|
||||||
|
|
||||||
|
| Suite | Tests | Status |
|
||||||
|
|-------|------:|-------|
|
||||||
|
| `hs-upstream-core/tokenizer` | 17 | E37 in progress |
|
||||||
|
| `hs-upstream-socket` | 16 | E36 in progress |
|
||||||
|
| `hs-upstream-fetch` | 8 | E40 in progress |
|
||||||
|
| `hs-upstream-core/sourceInfo` | 4 | E38 done, not yet merged |
|
||||||
|
| `hs-upstream-worker` | 1 | E39 done, not yet merged |
|
||||||
|
| E37 string interpolation bug | 1 | E37 |
|
||||||
|
|
||||||
|
**Do not plan these — they resolve when Bucket E merges.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Group 1 — Null safety reporting (+7)
|
||||||
|
|
||||||
|
**Suite:** `hs-upstream-core/runtimeErrors`
|
||||||
|
**Failures:** 7 tests, all "Expected `'#doesntExist' is null`, got ``"
|
||||||
|
**What's needed:** When a command like `put`, `increment`, `decrement`, `default`, `remove`, `settle`, `transition` receives a null element (e.g. `#doesntExist`), HS must throw a structured null-safety error with the element reference in the message. The null check + error format is already designed in Bucket D #31 (cluster 31 of `hs-conformance-to-100.md`).
|
||||||
|
|
||||||
|
**Estimate:** +7. Straightforward — null guard at command dispatch entry.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Group 2 — `tell` semantics (+3)
|
||||||
|
|
||||||
|
**Suite:** `hs-upstream-tell`
|
||||||
|
**Failures:**
|
||||||
|
- `attributes refer to the thing being told` — Expected `bar`, got ``
|
||||||
|
- `your symbol represents the thing being told` — Expected `foo`, got ``
|
||||||
|
- `does not overwrite the me symbol` — assertion fail
|
||||||
|
|
||||||
|
**What's needed:** Inside a `tell X` block, `you`/`your` must resolve to X, attribute refs must resolve against X, and `me` must retain its original value (not be rebound to X). Currently `tell` rebinds `me` instead of introducing a separate `you` binding.
|
||||||
|
|
||||||
|
**Estimate:** +3. Scoping fix in the `tell` command handler.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Group 3 — `on` event handler features (+19, skip-list)
|
||||||
|
|
||||||
|
**Suite:** `hs-upstream-on`
|
||||||
|
**34 tests on skip-list.** Prioritise tractable subsets:
|
||||||
|
|
||||||
|
### 3a — Event filtering by count (+6)
|
||||||
|
- `can filter events based on count`
|
||||||
|
- `can filter events based on count range`
|
||||||
|
- `can filter events based on unbounded count range`
|
||||||
|
- `can mix ranges`
|
||||||
|
- `on first click fires only once`
|
||||||
|
- `multiple event handlers at a time are allowed to execute with the every keyword`
|
||||||
|
|
||||||
|
The `on (N)`, `on (N to M)`, `on first`, `every` modifiers. Parser + runtime counter state per handler.
|
||||||
|
|
||||||
|
### 3b — `finally` blocks (+6)
|
||||||
|
- `basic finally blocks work`
|
||||||
|
- `async basic finally blocks work`
|
||||||
|
- `exceptions in finally block don't kill the event queue`
|
||||||
|
- `async exceptions in finally block don't kill the event queue`
|
||||||
|
- `finally blocks work when exception thrown in catch`
|
||||||
|
- `async finally blocks work when exception thrown in catch`
|
||||||
|
|
||||||
|
`on … catch … finally` analogous to JS try/catch/finally. Needs a finally-frame in the CEK machine (similar to dynamic-wind).
|
||||||
|
|
||||||
|
### 3c — `elsewhere` modifier (+2)
|
||||||
|
- `supports "elsewhere" modifier`
|
||||||
|
- `supports "from elsewhere" modifier`
|
||||||
|
|
||||||
|
`on click elsewhere` = click outside the element. Needs a global listener + target exclusion check.
|
||||||
|
|
||||||
|
### 3d — Exception events (+3)
|
||||||
|
- `rethrown exceptions trigger 'exception' event`
|
||||||
|
- `uncaught exceptions trigger 'exception' event`
|
||||||
|
- `can catch exceptions thrown in hyperscript functions`
|
||||||
|
- `can catch exceptions thrown in js functions`
|
||||||
|
|
||||||
|
When an unhandled exception escapes an `on` handler, HS must dispatch an `exception` CustomEvent on the element.
|
||||||
|
|
||||||
|
### 3e — Element removal cleanup (+2)
|
||||||
|
- `listeners on other elements are removed when the registering element is removed`
|
||||||
|
- `listeners on self are not removed when the element is removed`
|
||||||
|
|
||||||
|
Cleanup hook via MutationObserver watching for element removal.
|
||||||
|
|
||||||
|
### Deferred (skip-list, complex):
|
||||||
|
- `can be in a top level script tag` — requires script tag re-initialisation
|
||||||
|
- `can ignore when target doesn't exist` — target null guard
|
||||||
|
- `can handle an or after a from clause` — parser edge case
|
||||||
|
- `each behavior installation has its own event queue` — behavior isolation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Group 4 — MutationObserver / `on mutation` (+10)
|
||||||
|
|
||||||
|
**Suite:** `hs-upstream-on` (mutation subset, skip-list)
|
||||||
|
**Tests:**
|
||||||
|
- `can listen for attribute mutations`
|
||||||
|
- `can listen for attribute mutations on other elements`
|
||||||
|
- `can listen for childList mutations`
|
||||||
|
- `can listen for general mutations`
|
||||||
|
- `can listen for multiple mutations`
|
||||||
|
- `can listen for multiple mutations 2`
|
||||||
|
- `can listen for specific attribute mutations`
|
||||||
|
- `can pick event properties out by name`
|
||||||
|
- `can pick detail fields out by name`
|
||||||
|
- `attribute observers are persistent (not recreated on re-run)` (hs-upstream-when)
|
||||||
|
|
||||||
|
**What's needed:** MutationObserver mock in the test runner (`hs-run-filtered.js`) + `on mutation` command in the parser/runtime. Already prototyped in Bucket D #32.
|
||||||
|
|
||||||
|
**Estimate:** +10.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Group 5 — Cookie API (+5)
|
||||||
|
|
||||||
|
**Suite:** `hs-upstream-expressions/cookies`
|
||||||
|
All 5 tests untranslated. Cookie read/write as an expression: `cookies.name`, `set cookies.name to val`, `cookies.name is undefined`. Needs `document.cookie` mock in runner + cookie-expression parse path.
|
||||||
|
|
||||||
|
**Estimate:** +5. Self-contained.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Group 6 — Block literals (+4)
|
||||||
|
|
||||||
|
**Suite:** `hs-upstream-expressions/blockLiteral`
|
||||||
|
All 4 untranslated. Syntax: `[x | x + 1]` — an inline lambda. Used as a first-class value passable to `map`, `filter` etc.
|
||||||
|
|
||||||
|
- `basic block literals work`
|
||||||
|
- `basic identity works`
|
||||||
|
- `basic two arg identity works`
|
||||||
|
- `can map an array`
|
||||||
|
|
||||||
|
**Estimate:** +4. Parser addition + runtime callable wrapping.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Group 7 — Async logical operators (+5)
|
||||||
|
|
||||||
|
**Suite:** `hs-upstream-expressions/logicalOperator`
|
||||||
|
Promise-aware `and`/`or`:
|
||||||
|
- `and short-circuits when lhs promise resolves to false`
|
||||||
|
- `or short-circuits when lhs promise resolves to true`
|
||||||
|
- `or evaluates rhs when lhs promise resolves to false`
|
||||||
|
- `should short circuit with and expression`
|
||||||
|
- `should short circuit with or expression`
|
||||||
|
|
||||||
|
**What's needed:** `and`/`or` must await promise operands before short-circuiting. Currently they evaluate eagerly without awaiting.
|
||||||
|
|
||||||
|
**Estimate:** +5. Async await integration in logical operator eval.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Group 8 — `evalStatically` (+3)
|
||||||
|
|
||||||
|
**Suite:** `hs-upstream-core/evalStatically`
|
||||||
|
- `throws on math expressions`
|
||||||
|
- `throws on symbol references`
|
||||||
|
- `throws on template strings`
|
||||||
|
|
||||||
|
`_hyperscript.evaluate(src, {}, { throwErrors: true })` must throw synchronously for expressions with side-effects or unresolved symbols. Currently the static evaluator doesn't gate on `throwErrors`.
|
||||||
|
|
||||||
|
**Estimate:** +3. Flag-gated error throw path.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Group 9 — Parse error API (+6)
|
||||||
|
|
||||||
|
**Suite:** `hs-upstream-core/parser` + `hs-upstream-core/bootstrap`
|
||||||
|
- `basic parse error messages work`
|
||||||
|
- `fires hyperscript:parse-error event with all errors`
|
||||||
|
- `parse error at EOF on trailing newline does not crash`
|
||||||
|
- `_hyperscript() evaluate API still throws on first error`
|
||||||
|
- `fires hyperscript:before:init and hyperscript:after:init` (bootstrap)
|
||||||
|
- `hyperscript:before:init can cancel initialization` (bootstrap)
|
||||||
|
|
||||||
|
**What's needed:**
|
||||||
|
- Parser must emit a `hyperscript:parse-error` CustomEvent on `document` when compilation fails, with the error list as detail.
|
||||||
|
- `hyperscript:before:init` / `hyperscript:after:init` lifecycle events dispatched around element initialization.
|
||||||
|
- `before:init` can cancel (return false / `event.preventDefault()`).
|
||||||
|
|
||||||
|
**Estimate:** +6. Event dispatch hooks in the bootstrap/init path.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Group 10 — `as` expression conversions (+8)
|
||||||
|
|
||||||
|
**Suite:** `hs-upstream-expressions/asExpression`
|
||||||
|
Currently 30/42 = 12 failures. Tractable subset:
|
||||||
|
|
||||||
|
- `converts a NodeList into HTML` — NodeList → outerHTML join
|
||||||
|
- `converts strings into fragments` — string → DocumentFragment
|
||||||
|
- `converts elements into fragments` — element → DocumentFragment
|
||||||
|
- `converts arrays into fragments` — array of elements → DocumentFragment
|
||||||
|
- `converts array as Set` — array → Set (dedup)
|
||||||
|
- `converts object as Map` — object → Map
|
||||||
|
- `can accept custom conversions` — `as MyType` via registered converter
|
||||||
|
- `can use the a modifier if you like` — `as a Number` synonym
|
||||||
|
|
||||||
|
Two already-broken non-skip failures:
|
||||||
|
- `converts a complete form into Values` — Expected `dog`, got ``
|
||||||
|
- `converts multiple selects with programmatically changed selections` — Expected `cat`, got `dog`
|
||||||
|
|
||||||
|
**Estimate:** +8 for the tractable subset. Custom converters and Map/Set require runtime additions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Group 11 — Miscellaneous runtime bugs (+12)
|
||||||
|
|
||||||
|
Small scattered failures, each 1–3 tests:
|
||||||
|
|
||||||
|
| Suite | Failure | Likely cause |
|
||||||
|
|-------|---------|-------------|
|
||||||
|
| `hs-upstream-put` | `properly processes hyperscript` ×3 (got 40, expected 42) | Off-by-one in `put ... before/after` reprocessing |
|
||||||
|
| `hs-upstream-put` | `waits on promises` | Promise await missing from put target eval |
|
||||||
|
| `hs-upstream-js` | `can return values to _hyperscript` | JS block return value not threaded back |
|
||||||
|
| `hs-upstream-js` | `can do both of the above` | Same |
|
||||||
|
| `hs-upstream-js` | `handles rejected promises without hanging` | Rejected promise in js block uncaught |
|
||||||
|
| `hs-upstream-set` | `set waits on promises` | Same as put |
|
||||||
|
| `hs-upstream-set` | `can set into indirect style ref 3` | Indirect style ref path bug |
|
||||||
|
| `hs-upstream-hide` | `retain original display` | `none` vs `block` display tracking |
|
||||||
|
| `hs-upstream-toggle` | `toggle for fixed time` | Timed toggle assertion timing |
|
||||||
|
| `hs-upstream-transition` | `initial value` | `initial` keyword not restoring computed value |
|
||||||
|
| `hs-upstream-expressions/arrayLiteral` | `objects with _order` | `_order` internal key leaking into equality check |
|
||||||
|
| `hs-upstream-core/bootstrap` | 4 bugs | Event handler bugs in reinit, cleanup, respond |
|
||||||
|
| `hs-upstream-expressions/closest` | `where clause` | `where` consumed by `closest` instead of outer |
|
||||||
|
| `hs-upstream-core/scoping` | 2 bugs | Pseudo-possessive, built-in variable clash |
|
||||||
|
|
||||||
|
**Estimate:** +12 once individually triaged.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Group 12 — Formerly "hard floor" — now in scope
|
||||||
|
|
||||||
|
Initial assessment was wrong — these are medium difficulty, not genuinely hard. All 16 are worth attempting.
|
||||||
|
|
||||||
|
| Suite | Tests | Actual difficulty | What's needed |
|
||||||
|
|-------|------:|-------------------|---------------|
|
||||||
|
| `hs-upstream-breakpoint` | 2 | **Trivial** | No-op parser command + generator translation. Design: `plans/designs/f-breakpoint.md` |
|
||||||
|
| `hs-upstream-expressions/logicalOperator` (unparenthesized error) | 2 | Low | Parser strictness: `1 + 2 + 3` should throw "ambiguous operator precedence" |
|
||||||
|
| `hs-upstream-core/security` | 1 | Medium | `_hyperscript.config.disableScripting = true` guard at `hs-activate!` time |
|
||||||
|
| `hs-upstream-expressions/asExpression` (Date, custom dynamic) | 3 | Medium | `as a Date` → `new Date(val)`; custom converters via `_hyperscript.addType` registry |
|
||||||
|
| `hs-upstream-on` (remaining skip-list) | ~8 | Medium | Script tag reinit (MutationObserver on `<script>` changes); behavior isolation queue |
|
||||||
|
|
||||||
|
**Breakpoint** — both tests just check that `breakpoint` *parses* without throwing. No devtools. See design doc.
|
||||||
|
|
||||||
|
**Security** — test creates a div with `_="on click add .foo"`, activates it, clicks, asserts `.foo` is NOT added. This is a `disableScripting` config flag: when set, `hs-activate!` skips initialisation. One guard at activation entry.
|
||||||
|
|
||||||
|
**Unparenthesized operator error** — `1 + 2 + 3` in HS is ambiguous (no defined associativity for chained operators). Parser should throw a parse error rather than silently picking left-associativity. Needs a "multiple operators at same precedence level" check after parsing a binary expression.
|
||||||
|
|
||||||
|
**Sequence these last** — after groups 1–11 are done. Breakpoint is a 30-min job and should be pulled into the quick-wins batch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
| Group | Tests | Difficulty | Design doc |
|
||||||
|
|-------|------:|-----------|-----------|
|
||||||
|
| 0 — Bucket E payoff | ~47 | Free | (E branches) |
|
||||||
|
| 1 — Null safety | +7 | Low | `f1-null-safety.md` |
|
||||||
|
| 2 — `tell` semantics | +3 | Low | `f2-tell.md` |
|
||||||
|
| 3 — `on` event features | +19 | Medium | (TBD) |
|
||||||
|
| 4 — MutationObserver | +10 | Medium | (TBD) |
|
||||||
|
| 5 — Cookie API | +5 | Low | `f5-cookies.md` |
|
||||||
|
| 6 — Block literals | +4 | Medium | (TBD) |
|
||||||
|
| 7 — Async logical ops | +5 | Medium | (TBD) |
|
||||||
|
| 8 — evalStatically | +3 | Low | `f8-eval-statically.md` |
|
||||||
|
| 9 — Parse error API | +6 | Medium | (TBD) |
|
||||||
|
| 10 — `as` conversions | +8 | Medium | (TBD) |
|
||||||
|
| 11 — Misc bugs | +12 | Low–Medium | (TBD) |
|
||||||
|
| 12 — Breakpoint | +2 | Trivial | `f-breakpoint.md` |
|
||||||
|
| 12 — Security config | +1 | Medium | (TBD) |
|
||||||
|
| 12 — Unparenthesized op error | +2 | Low | (TBD) |
|
||||||
|
| 12 — `as` Date + custom | +3 | Medium | (TBD) |
|
||||||
|
| 12 — `on` remaining | +8 | Medium | (TBD) |
|
||||||
|
| **Total recoverable** | **~145** | | |
|
||||||
|
|
||||||
|
## Group 13 — Step limit + `meta.caller` (+5 → 100%)
|
||||||
|
|
||||||
|
Design doc: `plans/designs/f13-step-limit-and-meta.md`
|
||||||
|
|
||||||
|
| Test | Failure | Fix |
|
||||||
|
|------|---------|-----|
|
||||||
|
| `repeat forever works` (×2) | Step limit — loop terminates in 5 iterations but two compilation warm-up guards eat the budget first | Raise `HS_STEP_LIMIT` to 2,000,000 in `hs-run-filtered.js` |
|
||||||
|
| `hypertrace is reasonable` | Step limit — trace recorder may be on globally inflating step count | Raise step limit; disable global trace if on |
|
||||||
|
| `query template returns values` | Step limit (37s) — CSS template query `<${"p"}/>` may rebuild on every call | Raise step limit; cache compiled template query if still slow |
|
||||||
|
| `has proper stack from event handler` | Wrong value — `meta.caller.meta.feature.type` returns `""` instead of `"onFeature"` | Implement `meta` dict in `def` function call scope; wire `{:feature {:type "onFeature"}}` into event handler contexts |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
| Group | Tests | Difficulty | Design doc |
|
||||||
|
|-------|------:|-----------|-----------|
|
||||||
|
| 0 — Bucket E payoff | ~47 | Free | (E branches) |
|
||||||
|
| 1 — Null safety | +7 | Low | `f1-null-safety.md` |
|
||||||
|
| 2 — `tell` semantics | +3 | Low | `f2-tell.md` |
|
||||||
|
| 3 — `on` event features | +19 | Medium | (TBD) |
|
||||||
|
| 4 — MutationObserver | +10 | Medium | (TBD) |
|
||||||
|
| 5 — Cookie API | +5 | Low | `f5-cookies.md` |
|
||||||
|
| 6 — Block literals | +4 | Medium | (TBD) |
|
||||||
|
| 7 — Async logical ops | +5 | Medium | (TBD) |
|
||||||
|
| 8 — evalStatically | +3 | Low | `f8-eval-statically.md` |
|
||||||
|
| 9 — Parse error API | +6 | Medium | (TBD) |
|
||||||
|
| 10 — `as` conversions | +8 | Medium | (TBD) |
|
||||||
|
| 11 — Misc bugs | +12 | Low–Medium | (TBD) |
|
||||||
|
| 12 — Breakpoint | +2 | Trivial | `f-breakpoint.md` |
|
||||||
|
| 12 — Security config | +1 | Medium | (TBD) |
|
||||||
|
| 12 — Unparenthesized op error | +2 | Low | (TBD) |
|
||||||
|
| 12 — `as` Date + custom | +3 | Medium | (TBD) |
|
||||||
|
| 12 — `on` remaining | +8 | Medium | (TBD) |
|
||||||
|
| 13 — Step limit + meta.caller | +5 | Low | `f13-step-limit-and-meta.md` |
|
||||||
|
| **Total recoverable** | **~150** | | |
|
||||||
|
|
||||||
|
**Projected ceiling: ~1299 + 47 + 150 = 1496/1496 = 100%**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Suggested sequencing for Bucket F loop
|
||||||
|
|
||||||
|
1. Groups 1, 2, 5, 8 + breakpoint — quick wins, design docs ready, ~20 tests
|
||||||
|
2. Groups 11 misc bugs — isolate and fix one suite at a time
|
||||||
|
3. Group 9 parse error API — hooks into bootstrap, needs care
|
||||||
|
4. Groups 3a, 3b (on-count + finally) — medium, self-contained
|
||||||
|
5. Groups 4 (MutationObserver) + 3c/3d/3e (elsewhere, exceptions, cleanup)
|
||||||
|
6. Groups 6, 7 (block literals, async logical ops) — new syntax
|
||||||
|
7. Group 10 (as conversions) — additive, low regression risk
|
||||||
|
8. Group 12 remainder — security config, unparenthesized op error, as-Date, on remaining
|
||||||
|
|
||||||
|
Each group should get a design doc in `plans/designs/f<N>-<name>.md` before implementation starts.
|
||||||
229
plans/koka-on-sx.md
Normal file
229
plans/koka-on-sx.md
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
# Koka-on-SX: Koka on the CEK/VM
|
||||||
|
|
||||||
|
Implement a Koka interpreter on SX. The unique angle: Koka's algebraic effects and
|
||||||
|
handlers map directly onto SX's `perform`/`cek-resume` machinery — this is the language
|
||||||
|
that will stress-test whether SX's effect system is principled enough, and expose any
|
||||||
|
gaps. Every other language in the set works around effects ad-hoc; Koka makes them the
|
||||||
|
primary abstraction.
|
||||||
|
|
||||||
|
End-state goal: **core Koka programs running on the SX CEK evaluator**, with algebraic
|
||||||
|
effect handlers wired through `perform`/`cek-resume`. Not a full Koka compiler — no type
|
||||||
|
inference, no row-polymorphic effect types, no LLVM backend — but a faithful runtime for
|
||||||
|
idiomatic Koka programs.
|
||||||
|
|
||||||
|
## What Koka adds that nothing else covers
|
||||||
|
|
||||||
|
- **Structured effect declarations**: `effect state<s> { fun get() : s; fun set(s) : () }`
|
||||||
|
— named, typed effect operations, not just untyped `perform` tokens
|
||||||
|
- **Resumable handlers**: `handler { return(x) -> x; get() -> resume(0); set(x) -> resume(()) }`
|
||||||
|
— multi-shot continuations, handlers as first-class values
|
||||||
|
- **Effect polymorphism**: functions declare their effect set (`a -> <state<int>,console> b`)
|
||||||
|
— exposes whether SX can track which effects are in scope
|
||||||
|
- **Tail-resumptive handlers**: most practical handlers resume exactly once, which should
|
||||||
|
be optimisable — tests whether the CEK machine can detect and collapse this
|
||||||
|
- **Algebraic data types as the foundation**: `type maybe<a> { Nothing; Just(value: a) }`
|
||||||
|
— exercises the Phase 6 ADT primitive directly
|
||||||
|
|
||||||
|
## Ground rules
|
||||||
|
|
||||||
|
- **Scope:** only touch `lib/koka/**` and `plans/koka-on-sx.md`. Do **not** edit `spec/`,
|
||||||
|
`hosts/`, `shared/`, or other `lib/<lang>/`.
|
||||||
|
- **Shared-file issues** go under "Blockers" below with a minimal repro; do not fix here.
|
||||||
|
- **SX files:** use `sx-tree` MCP tools only.
|
||||||
|
- **Architecture:** Koka source → Koka AST → interpret directly via CEK. No separate
|
||||||
|
Koka evaluator — host the semantics in SX, run on the existing CEK machine.
|
||||||
|
- **Effect types:** defer type inference entirely. Track effects at runtime only — an
|
||||||
|
unhandled effect at the top level raises a runtime error, not a type error.
|
||||||
|
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick boxes.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Koka source text
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
lib/koka/tokenizer.sx — keywords, operators, indent-sensitivity, type-level syntax
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
lib/koka/parser.sx — Koka AST: fun, val, effect, handler, with, match, resume,
|
||||||
|
│ return clause, ADT definitions, basic type expressions
|
||||||
|
▼
|
||||||
|
lib/koka/eval.sx — Koka AST → CEK evaluation via SX primitives:
|
||||||
|
│ ADT (define-type/match from Phase 6)
|
||||||
|
│ Effects (perform/cek-resume from spec/evaluator.sx)
|
||||||
|
│ Coroutines optional (Phase 4 primitives)
|
||||||
|
▼
|
||||||
|
SX CEK evaluator (both JS and OCaml hosts)
|
||||||
|
```
|
||||||
|
|
||||||
|
Key semantic mappings:
|
||||||
|
|
||||||
|
| Koka construct | SX mapping |
|
||||||
|
|---------------|-----------|
|
||||||
|
| `fun f(x) body` | `(define (f x) body)` |
|
||||||
|
| `val x = expr` | `(let ((x expr)) ...)` |
|
||||||
|
| `effect E { fun op() : t }` | register effect tag `E/op` in effect env |
|
||||||
|
| `op()` inside handler scope | `(perform (list "E" "op" args))` |
|
||||||
|
| `handler { return(x)->e; op()->resume(v) }` | `(guard ...)` + `cek-resume` |
|
||||||
|
| `with handler { body }` | install handler for duration of body |
|
||||||
|
| `match x { Nothing -> e1; Just(v) -> e2 }` | SX `(match x ...)` via Phase 6 ADT |
|
||||||
|
| `type maybe<a> { Nothing; Just(value:a) }` | `(define-type maybe (Nothing) (Just value))` |
|
||||||
|
| `resume(v)` in handler | `(cek-resume k v)` where k is captured continuation |
|
||||||
|
| `return(x) -> expr` | final-value clause when no effect fires |
|
||||||
|
|
||||||
|
## Koka semantics in brief
|
||||||
|
|
||||||
|
### Effects and handlers
|
||||||
|
|
||||||
|
```koka
|
||||||
|
effect console
|
||||||
|
fun println(s : string) : ()
|
||||||
|
|
||||||
|
fun greet(name : string) : <console> ()
|
||||||
|
println("Hello, " ++ name)
|
||||||
|
|
||||||
|
fun main()
|
||||||
|
with handler
|
||||||
|
return(x) -> x
|
||||||
|
println(s) -> { print-string(s ++ "\n"); resume(()) }
|
||||||
|
greet("world")
|
||||||
|
```
|
||||||
|
|
||||||
|
- `effect console` declares an effect with one operation `println`
|
||||||
|
- `greet` uses `console` — any call to `println` inside will look up the nearest
|
||||||
|
enclosing handler
|
||||||
|
- `with handler { ... }` installs a handler; `resume(())` continues the suspended
|
||||||
|
computation
|
||||||
|
|
||||||
|
### Multi-shot resumption
|
||||||
|
|
||||||
|
```koka
|
||||||
|
effect choice
|
||||||
|
fun choose() : bool
|
||||||
|
|
||||||
|
fun xor(p : bool, q : bool) : <choice> bool
|
||||||
|
val a = choose()
|
||||||
|
val b = choose()
|
||||||
|
(a || b) && !(a && b)
|
||||||
|
|
||||||
|
fun all-results()
|
||||||
|
with handler
|
||||||
|
return(x) -> [x]
|
||||||
|
choose() -> resume(True) ++ resume(False)
|
||||||
|
xor(True, False)
|
||||||
|
// => [True, True, False, True]
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the test that exposes whether `cek-resume` supports multi-shot (calling the
|
||||||
|
same continuation twice). SX's delimited continuations do support this — Koka will
|
||||||
|
verify it end-to-end.
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
### Phase 1 — Tokenizer + parser (core expressions)
|
||||||
|
|
||||||
|
- [ ] Tokenizer: keywords (`fun`, `val`, `effect`, `handler`, `with`, `match`, `return`,
|
||||||
|
`resume`, `type`, `alias`, `if`, `then`, `else`, `fn`), operators (`++`, `->`,
|
||||||
|
`|>`, `:`, `<`, `>`, `,`), identifiers, numbers, strings, booleans
|
||||||
|
- [ ] Parser — expressions:
|
||||||
|
- literals: int, float, bool (`True`/`False`), string
|
||||||
|
- `val x = e` bindings
|
||||||
|
- `fun f(x, y) body` definitions
|
||||||
|
- `if c then e1 else e2`
|
||||||
|
- `match x { Pat -> e; ... }`
|
||||||
|
- lambda `fn(x) -> e`
|
||||||
|
- function application `f(x, y)`
|
||||||
|
- infix operators: `++`, `+`, `-`, `*`, `/`, `==`, `!=`, `<`, `>`, `&&`, `||`
|
||||||
|
- pipe `|>`: `x |> f` = `f(x)`
|
||||||
|
- [ ] Tests: `lib/koka/tests/parse.sx` — 40+ parse round-trip tests
|
||||||
|
|
||||||
|
### Phase 2 — ADT definitions + match
|
||||||
|
|
||||||
|
- [ ] Parser: `type name<a> { Ctor1; Ctor2(field: t); ... }` declarations
|
||||||
|
- [ ] Eval: map to SX `define-type` + `match` (requires Phase 6 primitives)
|
||||||
|
- [ ] Built-in: `maybe<a>` (Nothing / Just), `result<a,e>` (Ok / Error), `list<a>` (Nil / Cons)
|
||||||
|
- [ ] Tests: ADT construction, matching, nested patterns — 25+ tests
|
||||||
|
|
||||||
|
### Phase 3 — Core evaluator
|
||||||
|
|
||||||
|
- [ ] `koka-eval` entry: walks Koka AST, evaluates in SX env
|
||||||
|
- [ ] Arithmetic, string `++`, comparison, boolean ops
|
||||||
|
- [ ] `val`/`let` binding
|
||||||
|
- [ ] Function definitions and application (first-class functions)
|
||||||
|
- [ ] `if`/`then`/`else`
|
||||||
|
- [ ] `match` with constructor, literal, variable, wildcard patterns
|
||||||
|
- [ ] Basic list ops: `map`, `filter`, `foldl`, `length`, `head`, `tail`
|
||||||
|
- [ ] Tests: `lib/koka/tests/eval.sx` — 40+ tests, pure expressions only
|
||||||
|
|
||||||
|
### Phase 4 — Effect system
|
||||||
|
|
||||||
|
- [ ] Effect declaration: `(koka-declare-effect! "console" (list "println"))`
|
||||||
|
registers operations in a global effect registry
|
||||||
|
- [ ] Effect operation call: when `println(s)` is evaluated inside a handler scope,
|
||||||
|
emit `(perform (list :effect "console" :op "println" :args (list s)))`
|
||||||
|
- [ ] Handler installation: `with handler { return(x)->e; println(s)->resume(v) }`
|
||||||
|
installs a `guard`-like frame that catches `perform` signals matching the effect,
|
||||||
|
binds arguments, and exposes `resume` as a callable that invokes `cek-resume`
|
||||||
|
- [ ] `resume(v)`: calls `(cek-resume captured-k v)` where `captured-k` is the
|
||||||
|
continuation captured at the `perform` point
|
||||||
|
- [ ] `return(x) -> e` clause: handles the normal return value when no effect fires
|
||||||
|
- [ ] Tests: `lib/koka/tests/effects.sx` — 30+ tests:
|
||||||
|
- basic handler (state, console, exception)
|
||||||
|
- unhandled effect → runtime error
|
||||||
|
- nested handlers (inner shadows outer)
|
||||||
|
- multi-shot resumption (choice effect — the key test)
|
||||||
|
- tail-resumptive handler (resumes exactly once — verify no extra allocation)
|
||||||
|
|
||||||
|
### Phase 5 — Standard effect library
|
||||||
|
|
||||||
|
- [ ] `console` effect: `println`, `print`, `readline` (mock)
|
||||||
|
- [ ] `exn` effect: `throw`, `catch` wrappers
|
||||||
|
- [ ] `state<s>` effect: `get`, `set`, `modify`
|
||||||
|
- [ ] `async` effect: `await` mapped to SX `perform` IO suspension
|
||||||
|
- [ ] Tests: programs using each stdlib effect — 20+ tests
|
||||||
|
|
||||||
|
### Phase 6 — Classic Koka programs as integration tests
|
||||||
|
|
||||||
|
- [ ] `counter.koka` — stateful counter via state effect
|
||||||
|
- [ ] `choice.koka` — multi-shot choice generating all results
|
||||||
|
- [ ] `iterator.koka` — yield-based iteration via a custom effect
|
||||||
|
- [ ] `exception.koka` — structured exception handling
|
||||||
|
- [ ] `coroutine.koka` — producer/consumer via two interleaved effects
|
||||||
|
- [ ] Each as a self-contained test in `lib/koka/tests/programs.sx`
|
||||||
|
|
||||||
|
## Key blockers / dependencies
|
||||||
|
|
||||||
|
- **Phase 6 ADT primitive** (`define-type`/`match`) — required before Phase 2.
|
||||||
|
Track: `plans/agent-briefings/primitives-loop.md` Phase 6.
|
||||||
|
- **Multi-shot continuations** — `cek-resume` must support calling the same
|
||||||
|
continuation multiple times. Verify with: `(let ((k #f)) (perform 'x) ...)` called
|
||||||
|
twice. This should already work given the multi-shot delimited continuation work.
|
||||||
|
- **Effect handler stack** — SX's `guard` is not quite the right primitive for
|
||||||
|
deep-handler semantics. May need `(with-handler effect-tag handler-fn body)` as a
|
||||||
|
new evaluator form, or can be emulated via `guard` + `perform` reshaping.
|
||||||
|
|
||||||
|
## Comparison to other languages in the set
|
||||||
|
|
||||||
|
| Language | Effect model |
|
||||||
|
|----------|-------------|
|
||||||
|
| Lua | none (errors only) |
|
||||||
|
| Prolog | none (cuts only) |
|
||||||
|
| Erlang | message-passing (not algebraic) |
|
||||||
|
| Haskell | IO monad (monadic, not algebraic) |
|
||||||
|
| JS | promise/async-await (one-shot) |
|
||||||
|
| Ruby | exceptions + fibers |
|
||||||
|
| **Koka** | **algebraic effects + multi-shot handlers** |
|
||||||
|
|
||||||
|
Koka is the only language that uses SX's effect system as its *primary* computational
|
||||||
|
model. It will expose whether `perform`/`cek-resume` is sufficient or needs typed effect
|
||||||
|
tagging, scoping rules, and a handler stack distinct from `guard`.
|
||||||
|
|
||||||
|
## Progress log
|
||||||
|
|
||||||
|
_Newest first._
|
||||||
|
|
||||||
|
- _(none yet)_
|
||||||
|
|
||||||
|
## Blockers
|
||||||
|
|
||||||
|
- ADT primitive (Phase 6 of primitives-loop) must land before Phase 2 starts.
|
||||||
138
plans/minikanren-on-sx.md
Normal file
138
plans/minikanren-on-sx.md
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
# miniKanren-on-SX: relational programming on the CEK/VM
|
||||||
|
|
||||||
|
miniKanren is not a language to parse — it is an **embedded DSL** implemented as a library
|
||||||
|
of SX functions. No tokenizer, no transpiler. The entire system is a set of `define` forms
|
||||||
|
in `lib/minikanren/`. Programs are SX expressions using the miniKanren API.
|
||||||
|
|
||||||
|
The unique angle: SX's delimited continuation machinery (`perform`/`cek-resume`, call/cc)
|
||||||
|
maps almost perfectly to the search monad miniKanren needs. Backtracking is cooperative
|
||||||
|
suspension, not a separate trail machine. This is the cleanest possible host for miniKanren.
|
||||||
|
|
||||||
|
End-state goal: **full core miniKanren** (`run`, `fresh`, `==`, `conde`, `condu`, `onceo`,
|
||||||
|
`project`, `matche`) + **core.logic-style relations** (`appendo`, `membero`, `listo`,
|
||||||
|
`numbero`, etc.) + **arithmetic constraints** (`fd` domain, `CLP(FD)` subset).
|
||||||
|
|
||||||
|
## Ground rules
|
||||||
|
|
||||||
|
- **Scope:** only touch `lib/minikanren/**` and `plans/minikanren-on-sx.md`. Do **not**
|
||||||
|
edit `spec/`, `hosts/`, `shared/`, or other `lib/<lang>/`.
|
||||||
|
- **Shared-file issues** go under "Blockers" below with a minimal repro; do not fix here.
|
||||||
|
- **SX files:** use `sx-tree` MCP tools only.
|
||||||
|
- **Architecture:** pure library — no source parser. Programs are written in SX using the API.
|
||||||
|
- **Reference:** *The Reasoned Schemer* (Friedman/Byrd/Kiselyov) + Byrd's dissertation.
|
||||||
|
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick boxes.
|
||||||
|
|
||||||
|
## Architecture sketch
|
||||||
|
|
||||||
|
```
|
||||||
|
SX program using miniKanren API
|
||||||
|
│
|
||||||
|
├── lib/minikanren/unify.sx — terms, variables, walk, unification, occurs check
|
||||||
|
├── lib/minikanren/substitution.sx — substitution as association list / hash table
|
||||||
|
├── lib/minikanren/stream.sx — lazy streams of substitutions (via delay/force)
|
||||||
|
├── lib/minikanren/goals.sx — == / fresh / conde / condu / onceo / project / matche
|
||||||
|
├── lib/minikanren/run.sx — run* / run n — drive the search, extract answers
|
||||||
|
├── lib/minikanren/relations.sx — standard relations: appendo, membero, listo, etc.
|
||||||
|
└── lib/minikanren/clpfd.sx — arithmetic constraints (CLP(FD) subset)
|
||||||
|
```
|
||||||
|
|
||||||
|
Key semantic mappings:
|
||||||
|
- **Logic variable** → SX vector of length 1 (mutable box); `make-var` creates fresh one;
|
||||||
|
`walk` follows the substitution chain
|
||||||
|
- **Substitution** → SX association list (or hash table for performance) mapping var → term
|
||||||
|
- **Stream of substitutions** → lazy list using `delay`/`force` (Phase 9 of primitives)
|
||||||
|
- **Goal** → SX function `substitution → stream-of-substitutions`
|
||||||
|
- **`==`** → unifies two terms, extending substitution or failing (empty stream)
|
||||||
|
- **`fresh`** → introduces new logic variables; `(fresh (x y) goal)` → goal with x, y bound
|
||||||
|
- **`conde`** → interleave streams from multiple goal clauses (depth-first with interleaving)
|
||||||
|
- **`run n`** → drive the stream, collect first n substitutions, reify answers
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
### Phase 1 — variables + unification
|
||||||
|
- [ ] `make-var` → fresh logic variable (unique mutable box)
|
||||||
|
- [ ] `var?` `v` → bool — is this a logic variable?
|
||||||
|
- [ ] `walk` `term` `subst` → follow substitution chain to ground term or unbound var
|
||||||
|
- [ ] `walk*` `term` `subst` → deep walk (recurse into lists/dicts)
|
||||||
|
- [ ] `unify` `u` `v` `subst` → extended substitution or `#f` (failure)
|
||||||
|
Handles: var/var, var/term, term/var, list unification, number/string/symbol equality.
|
||||||
|
No occurs check by default; `unify-check` with occurs check as opt-in.
|
||||||
|
- [ ] Empty substitution `empty-s` = `(list)` (empty assoc list)
|
||||||
|
- [ ] Tests in `lib/minikanren/tests/unify.sx`: ground terms, vars, lists, failure, occurs
|
||||||
|
|
||||||
|
### Phase 2 — streams + goals
|
||||||
|
- [ ] Stream type: `mzero` (empty stream = `nil`), `unit s` (singleton = `(list s)`),
|
||||||
|
`mplus` (interleave two streams), `bind` (apply goal to stream)
|
||||||
|
- [ ] Lazy streams via `delay`/`force` — mature pairs for depth-first, immature for lazy
|
||||||
|
- [ ] `==` goal: `(fn (s) (let ((s2 (unify u v s))) (if s2 (unit s2) mzero)))`
|
||||||
|
- [ ] `succeed` / `fail` — trivial goals
|
||||||
|
- [ ] `fresh` — `(fn (f) (fn (s) ((f (make-var)) s)))` — introduces one var; `fresh*` for many
|
||||||
|
- [ ] `conde` — interleaving disjunction of goal lists
|
||||||
|
- [ ] `condu` — committed choice (soft-cut): only explores first successful clause
|
||||||
|
- [ ] `onceo` — succeeds at most once
|
||||||
|
- [ ] Tests: basic goal composition, backtracking, interleaving
|
||||||
|
|
||||||
|
### Phase 3 — run + reification
|
||||||
|
- [ ] `run*` `goal` → list of all answers (reified)
|
||||||
|
- [ ] `run n` `goal` → list of first n answers
|
||||||
|
- [ ] `reify` `term` `subst` → replace unbound vars with `_0`, `_1`, ... names
|
||||||
|
- [ ] `reify-s` → builds reification substitution for naming unbound vars consistently
|
||||||
|
- [ ] `fresh` with multiple variables: `(fresh (x y z) goal)` sugar
|
||||||
|
- [ ] Query variable conventions: `q` as canonical query variable
|
||||||
|
- [ ] Tests: classic miniKanren programs — `(run* q (== q 1))` → `(1)`,
|
||||||
|
`(run* q (conde ((== q 1)) ((== q 2))))` → `(1 2)`,
|
||||||
|
Peano arithmetic, `appendo` preview
|
||||||
|
|
||||||
|
### Phase 4 — standard relations
|
||||||
|
- [ ] `appendo` `l` `s` `ls` — list append, runs forwards and backwards
|
||||||
|
- [ ] `membero` `x` `l` — x is a member of l
|
||||||
|
- [ ] `listo` `l` — l is a proper list
|
||||||
|
- [ ] `nullo` `l` — l is empty
|
||||||
|
- [ ] `pairo` `p` — p is a pair (cons cell)
|
||||||
|
- [ ] `caro` `p` `a` — car of pair
|
||||||
|
- [ ] `cdro` `p` `d` — cdr of pair
|
||||||
|
- [ ] `conso` `a` `d` `p` — cons
|
||||||
|
- [ ] `firsto` / `resto` — aliases for caro/cdro
|
||||||
|
- [ ] `reverseo` `l` `r` — reverse of list
|
||||||
|
- [ ] `flatteno` `l` `f` — flatten nested lists
|
||||||
|
- [ ] `permuteo` `l` `p` — permutation of list
|
||||||
|
- [ ] `lengtho` `l` `n` — length as a relation (Peano or integer)
|
||||||
|
- [ ] Tests: run each relation forwards and backwards; generate from partial inputs
|
||||||
|
|
||||||
|
### Phase 5 — `project` + `matche` + negation
|
||||||
|
- [ ] `project` `(x ...) body` — access reified values of logic vars inside a goal;
|
||||||
|
escapes to ground values for arithmetic or string ops
|
||||||
|
- [ ] `matche` — pattern matching over logic terms (extension from core.logic)
|
||||||
|
`(matche l ((head . tail) goal) (() goal))`
|
||||||
|
- [ ] `conda` — soft-cut disjunction (like Prolog `->`)
|
||||||
|
- [ ] `condu` — committed choice (already in phase 2; refine semantics here)
|
||||||
|
- [ ] `nafc` — negation as finite failure with constraint
|
||||||
|
- [ ] Tests: Zebra puzzle, N-queens, Sudoku via `project`, family relations via `matche`
|
||||||
|
|
||||||
|
### Phase 6 — arithmetic constraints CLP(FD)
|
||||||
|
- [ ] Finite domain variables: `fd-var` with domain `[lo..hi]`
|
||||||
|
- [ ] `in` `x` `domain` — constrain x to domain
|
||||||
|
- [ ] `fd-eq` `x` `y` — x = y (constraint propagation)
|
||||||
|
- [ ] `fd-neq` `x` `y` — x ≠ y
|
||||||
|
- [ ] `fd-lt` `fd-lte` `fd-gt` `fd-gte` — ordering constraints
|
||||||
|
- [ ] `fd-plus` `x` `y` `z` — x + y = z (constraint)
|
||||||
|
- [ ] `fd-times` `x` `y` `z` — x * y = z
|
||||||
|
- [ ] Arc consistency propagation — when domain narrows, propagate to constrained vars
|
||||||
|
- [ ] Labelling: `fd-run` drives search by splitting domains when propagation stalls
|
||||||
|
- [ ] Tests: send-more-money, N-queens with CLP(FD), map coloring, cryptarithmetic
|
||||||
|
|
||||||
|
### Phase 7 — tabling (memoization of relations)
|
||||||
|
- [ ] `tabled` annotation: memoize calls to a relation using a hash table
|
||||||
|
- [ ] Prevents infinite loops in recursive relations like `patho` on cyclic graphs
|
||||||
|
- [ ] Producer/consumer scheduling for tabled relations (variant of SLG resolution)
|
||||||
|
- [ ] Tests: cyclic graph reachability, mutual recursion, Fibonacci via tabling
|
||||||
|
|
||||||
|
## Blockers
|
||||||
|
|
||||||
|
_(none yet)_
|
||||||
|
|
||||||
|
## Progress log
|
||||||
|
|
||||||
|
_Newest first._
|
||||||
|
|
||||||
|
_(awaiting phase 1)_
|
||||||
315
plans/ocaml-on-sx.md
Normal file
315
plans/ocaml-on-sx.md
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
# OCaml-on-SX: OCaml + ReasonML + Dream on the CEK/VM
|
||||||
|
|
||||||
|
The meta-circular demo: SX's native evaluator is OCaml, so implementing OCaml on top of
|
||||||
|
SX closes the loop — the source language of the host is running inside the host it
|
||||||
|
compiles to. Beyond the elegance, it's practically useful: once OCaml expressions run on
|
||||||
|
the SX CEK/VM you get Dream (a clean OCaml web framework) almost for free, and ReasonML
|
||||||
|
is a syntax variant that shares the same transpiler output.
|
||||||
|
|
||||||
|
End-state goal: **OCaml programs running on the SX CEK/VM**, with enough of the standard
|
||||||
|
library to support Dream's middleware model. Dream-on-SX is the integration target —
|
||||||
|
a `handler`/`middleware`/`router` API that feels idiomatic while running purely in SX.
|
||||||
|
ReasonML (Phase 8) adds an alternative syntax frontend that targets the same transpiler.
|
||||||
|
|
||||||
|
## What this covers that nothing else in the set does
|
||||||
|
|
||||||
|
- **Strict ML semantics** — unlike Haskell, OCaml is call-by-value with explicit `Lazy.t`
|
||||||
|
for laziness. Pattern match is exhaustive. Polymorphic variants. Structural equality.
|
||||||
|
- **First-class modules and functors** — modules as values (phase 4); functors as SX
|
||||||
|
higher-order functions over module records. Unlike Haskell typeclasses, OCaml's module
|
||||||
|
system is explicit and compositional.
|
||||||
|
- **Mutable state without monads** — `ref`, `:=`, `!` are primitives. Arrays. `Hashtbl`.
|
||||||
|
The IO model is direct; `Lwt`/Dream map to `perform`/`cek-resume` for async.
|
||||||
|
- **Dream's composable HTTP model** — `handler = request -> response promise`,
|
||||||
|
`middleware = handler -> handler`. Algebraically clean; `@@` composition maps to SX
|
||||||
|
function composition trivially.
|
||||||
|
- **ReasonML** — same semantics, JS-friendly surface syntax. JSX variant pairs with SX
|
||||||
|
component rendering.
|
||||||
|
|
||||||
|
## Ground rules
|
||||||
|
|
||||||
|
- **Scope:** only touch `lib/ocaml/**`, `lib/dream/**`, `lib/reasonml/**`, and
|
||||||
|
`plans/ocaml-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, or other
|
||||||
|
`lib/<lang>/`.
|
||||||
|
- **Shared-file issues** go under "Blockers" below with a minimal repro; do not fix here.
|
||||||
|
- **SX files:** use `sx-tree` MCP tools only.
|
||||||
|
- **Architecture:** OCaml source → AST → SX AST → CEK. No standalone OCaml evaluator.
|
||||||
|
The OCaml AST is walked by an `ocaml-eval` function in SX that produces SX values.
|
||||||
|
- **Type system:** deferred until Phase 5. Phases 1–4 are intentionally untyped —
|
||||||
|
get the evaluator right first, then layer HM inference on top.
|
||||||
|
- **Dream:** implemented as a library in Phase 7; no separate build step. `Dream.run`
|
||||||
|
wraps SX's existing HTTP server machinery via `perform`/`cek-resume`.
|
||||||
|
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick boxes.
|
||||||
|
|
||||||
|
## Architecture sketch
|
||||||
|
|
||||||
|
```
|
||||||
|
OCaml source text
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
lib/ocaml/tokenizer.sx — keywords, operators, string/char literals, comments
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
lib/ocaml/parser.sx — OCaml AST: let/let rec, fun, match, if, begin/end,
|
||||||
|
│ module/struct/functor, type decls, expressions
|
||||||
|
▼
|
||||||
|
lib/ocaml/desugar.sx — surface → core: tuple patterns, or-patterns,
|
||||||
|
│ sequence (;) → (do), when guards, field punning
|
||||||
|
▼
|
||||||
|
lib/ocaml/transpile.sx — OCaml AST → SX AST
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
lib/ocaml/runtime.sx — ADT constructors, module primitives, ref/array ops,
|
||||||
|
│ Stdlib shims, Dream server (phase 7)
|
||||||
|
▼
|
||||||
|
SX CEK evaluator (both JS and OCaml hosts)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Semantic mappings
|
||||||
|
|
||||||
|
| OCaml construct | SX mapping |
|
||||||
|
|----------------|-----------|
|
||||||
|
| `let x = e` (top-level) | `(define x e)` |
|
||||||
|
| `let f x y = e` | `(define (f x y) e)` |
|
||||||
|
| `let rec f x = e` | `(define (f x) e)` — SX define is already recursive |
|
||||||
|
| `fun x -> e` | `(fn (x) e)` |
|
||||||
|
| `e1 \|> f` | `(f e1)` — pipe desugars to reverse application |
|
||||||
|
| `e1; e2` | `(do e1 e2)` |
|
||||||
|
| `begin e1; e2; e3 end` | `(do e1 e2 e3)` |
|
||||||
|
| `if c then e1 else e2` | `(if c e1 e2)` |
|
||||||
|
| `match x with \| P -> e` | `(match x (P e) ...)` via Phase 6 ADT primitive |
|
||||||
|
| `type t = A \| B of int` | `(define-type t (A) (B v))` |
|
||||||
|
| `module M = struct ... end` | SX dict `{:let-bindings ...}` — module as record |
|
||||||
|
| `functor (M : S) -> ...` | `(fn (M) ...)` — functor as SX lambda over module record |
|
||||||
|
| `open M` | inject M's bindings into scope via `env-merge` |
|
||||||
|
| `M.field` | `(get M :field)` |
|
||||||
|
| `{ r with f = v }` | `(dict-set r :f v)` |
|
||||||
|
| `ref x` | `(make-ref x)` — mutable cell |
|
||||||
|
| `!r` | `(deref-ref r)` |
|
||||||
|
| `r := v` | `(set-ref! r v)` |
|
||||||
|
| `(a, b, c)` | tagged list `(:tuple a b c)` |
|
||||||
|
| `[1; 2; 3]` | `(list 1 2 3)` |
|
||||||
|
| `[| 1; 2; 3 |]` | `(make-array 1 2 3)` (Phase 6) |
|
||||||
|
| `try e with \| Ex -> h` | `(guard (fn (ex) h) e)` via SX exception system |
|
||||||
|
| `raise Ex` | `(perform (:raise Ex))` |
|
||||||
|
| `Printf.printf "%d" x` | `(perform (:print (format "%d" x)))` |
|
||||||
|
|
||||||
|
## Dream semantic mappings (Phase 7)
|
||||||
|
|
||||||
|
| Dream construct | SX mapping |
|
||||||
|
|----------------|-----------|
|
||||||
|
| `handler = request -> response promise` | `(fn (req) (perform (:http-respond ...)))` |
|
||||||
|
| `middleware = handler -> handler` | `(fn (next) (fn (req) ...))` |
|
||||||
|
| `Dream.router [routes]` | `(ocaml-dream-router routes)` — dispatch on method+path |
|
||||||
|
| `Dream.get "/path" h` | route record `{:method "GET" :path "/path" :handler h}` |
|
||||||
|
| `Dream.scope "/p" [ms] [rs]` | prefix mount with middleware chain |
|
||||||
|
| `Dream.param req "name"` | path param extracted during routing |
|
||||||
|
| `m1 @@ m2 @@ handler` | `(m1 (m2 handler))` — left-fold composition |
|
||||||
|
| `Dream.session_field req "k"` | `(perform (:session-get req "k"))` |
|
||||||
|
| `Dream.set_session_field req "k" v` | `(perform (:session-set req "k" v))` |
|
||||||
|
| `Dream.flash req` | `(perform (:flash-get req))` |
|
||||||
|
| `Dream.form req` | `(perform (:form-parse req))` — returns Ok/Error ADT |
|
||||||
|
| `Dream.websocket handler` | `(perform (:websocket handler))` |
|
||||||
|
| `Dream.run handler` | starts SX HTTP server with handler as root |
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
### Phase 1 — Tokenizer + parser
|
||||||
|
|
||||||
|
- [ ] **Tokenizer:** keywords (`let`, `rec`, `in`, `fun`, `function`, `match`, `with`,
|
||||||
|
`type`, `of`, `module`, `struct`, `end`, `functor`, `sig`, `open`, `include`,
|
||||||
|
`if`, `then`, `else`, `begin`, `try`, `exception`, `raise`, `mutable`,
|
||||||
|
`for`, `while`, `do`, `done`, `and`, `as`, `when`), operators (`->`, `|>`,
|
||||||
|
`<|`, `@@`, `@`, `:=`, `!`, `::`, `**`, `:`, `;`, `;;`), identifiers (lower,
|
||||||
|
upper/ctor, labels `~label:`, optional `?label:`), char literals `'c'`,
|
||||||
|
string literals (escaped + heredoc `{|...|}`), int/float literals,
|
||||||
|
line comments `(*` nested block comments `*)`.
|
||||||
|
- [ ] **Parser:** top-level `let`/`let rec`/`type`/`module`/`exception`/`open`/`include`
|
||||||
|
declarations; expressions: literals, identifiers, constructor application,
|
||||||
|
lambda, application (left-assoc), binary ops with precedence table,
|
||||||
|
`if`/`then`/`else`, `match`/`with`, `try`/`with`, `let`/`in`, `begin`/`end`,
|
||||||
|
`fun`/`function`, tuples, list literals, record literals/updates, field access,
|
||||||
|
sequences `;`, unit `()`.
|
||||||
|
- [ ] **Patterns:** constructor, literal, variable, wildcard `_`, tuple, list cons `::`,
|
||||||
|
list literal, record, `as`, or-pattern `P1 | P2`, `when` guard.
|
||||||
|
- [ ] OCaml is **not** indentation-sensitive — no layout algorithm needed.
|
||||||
|
- [ ] Tests in `lib/ocaml/tests/parse.sx` — 50+ round-trip parse tests.
|
||||||
|
|
||||||
|
### Phase 2 — Core evaluator (untyped)
|
||||||
|
|
||||||
|
- [ ] `ocaml-eval` entry: walks OCaml AST, produces SX values.
|
||||||
|
- [ ] `let`/`let rec`/`let ... in` (mutually recursive with `and`).
|
||||||
|
- [ ] Lambda + application (curried by default — auto-curry multi-param defs).
|
||||||
|
- [ ] `fun`/`function` (single-arg lambda with immediate match on arg).
|
||||||
|
- [ ] `if`/`then`/`else`, `begin`/`end`, sequence `;`.
|
||||||
|
- [ ] Arithmetic, comparison, boolean ops, string `^`, `mod`.
|
||||||
|
- [ ] Unit `()` value; `ignore`.
|
||||||
|
- [ ] References: `ref`, `!`, `:=`.
|
||||||
|
- [ ] Mutable record fields.
|
||||||
|
- [ ] `for i = lo to hi do ... done` loop; `while cond do ... done`.
|
||||||
|
- [ ] `try`/`with` — maps to SX `guard`; `raise` via perform.
|
||||||
|
- [ ] Tests in `lib/ocaml/tests/eval.sx` — 50+ tests, pure + imperative.
|
||||||
|
|
||||||
|
### Phase 3 — ADTs + pattern matching
|
||||||
|
|
||||||
|
- [ ] `type` declarations: `type t = A | B of t1 * t2 | C of { x: int }`.
|
||||||
|
- [ ] Constructors as tagged lists: `A` → `(:A)`, `B(1, "x")` → `(:B 1 "x")`.
|
||||||
|
- [ ] `match`/`with`: constructor, literal, variable, wildcard, tuple, list cons/nil,
|
||||||
|
`as` binding, or-patterns, nested patterns, `when` guard.
|
||||||
|
- [ ] Exhaustiveness: runtime error on incomplete match (no compile-time check yet).
|
||||||
|
- [ ] Built-in types: `option` (`None`/`Some`), `result` (`Ok`/`Error`),
|
||||||
|
`list` (nil/cons), `bool`, `unit`, `exn`.
|
||||||
|
- [ ] `exception` declarations; built-in: `Not_found`, `Invalid_argument`,
|
||||||
|
`Failure`, `Match_failure`.
|
||||||
|
- [ ] Polymorphic variants (surface syntax `\`Tag value`; runtime same tagged list).
|
||||||
|
- [ ] Tests in `lib/ocaml/tests/adt.sx` — 40+ tests: ADTs, match, option/result.
|
||||||
|
|
||||||
|
### Phase 4 — Modules + functors
|
||||||
|
|
||||||
|
- [ ] `module M = struct let x = 1 let f y = x + y end` → SX dict `{:x 1 :f <fn>}`.
|
||||||
|
- [ ] `module type S = sig val x : int val f : int -> int end` → interface record
|
||||||
|
(runtime stub; typed checking in Phase 5).
|
||||||
|
- [ ] `module M : S = struct ... end` — coercive sealing (runtime: pass-through).
|
||||||
|
- [ ] `functor (M : S) -> struct ... end` → SX `(fn (M) ...)`.
|
||||||
|
- [ ] `module F = Functor(Base)` — functor application.
|
||||||
|
- [ ] `open M` — merge M's dict into current env (`env-merge`).
|
||||||
|
- [ ] `include M` — same as open at structure level.
|
||||||
|
- [ ] `M.name` — dict get via `:name` key.
|
||||||
|
- [ ] First-class modules (pack/unpack) — deferred to Phase 5.
|
||||||
|
- [ ] Standard module hierarchy: `List`, `Option`, `Result`, `String`, `Char`,
|
||||||
|
`Int`, `Float`, `Bool`, `Unit`, `Printf`, `Format` (stubs, filled in Phase 6).
|
||||||
|
- [ ] Tests in `lib/ocaml/tests/modules.sx` — 30+ tests.
|
||||||
|
|
||||||
|
### Phase 5 — Hindley-Milner type inference
|
||||||
|
|
||||||
|
- [ ] Algorithm W: `gen`/`inst`, `unify`, `infer-expr`, `infer-decl`.
|
||||||
|
- [ ] Type variables: `'a`, `'b`; unification with occur-check.
|
||||||
|
- [ ] Let-polymorphism: generalise at let-bindings.
|
||||||
|
- [ ] ADT types: `type 'a option = None | Some of 'a`.
|
||||||
|
- [ ] Function types, tuple types, record types.
|
||||||
|
- [ ] Type signatures: `val f : int -> int` — verify against inferred type.
|
||||||
|
- [ ] Module type checking: seal against `sig` (Phase 4 stubs become real checks).
|
||||||
|
- [ ] Error reporting: position-tagged errors with expected vs actual types.
|
||||||
|
- [ ] First-class modules: `(module M : S)` pack; `(val m : (module S))` unpack.
|
||||||
|
- [ ] No rank-2 polymorphism, no GADTs (out of scope).
|
||||||
|
- [ ] Tests in `lib/ocaml/tests/types.sx` — 60+ inference tests.
|
||||||
|
|
||||||
|
### Phase 6 — Standard library
|
||||||
|
|
||||||
|
- [ ] `List`: `map`, `filter`, `fold_left`, `fold_right`, `length`, `rev`, `append`,
|
||||||
|
`concat`, `flatten`, `iter`, `iteri`, `mapi`, `for_all`, `exists`, `find`,
|
||||||
|
`find_opt`, `mem`, `assoc`, `assq`, `sort`, `stable_sort`, `nth`, `hd`, `tl`,
|
||||||
|
`init`, `combine`, `split`, `partition`.
|
||||||
|
- [ ] `Option`: `map`, `bind`, `fold`, `get`, `value`, `join`, `iter`, `to_list`,
|
||||||
|
`to_result`, `is_none`, `is_some`.
|
||||||
|
- [ ] `Result`: `map`, `bind`, `fold`, `get_ok`, `get_error`, `map_error`,
|
||||||
|
`to_option`, `is_ok`, `is_error`.
|
||||||
|
- [ ] `String`: `length`, `get`, `sub`, `concat`, `split_on_char`, `trim`,
|
||||||
|
`uppercase_ascii`, `lowercase_ascii`, `contains`, `starts_with`, `ends_with`,
|
||||||
|
`index_opt`, `replace_all` (non-stdlib but needed).
|
||||||
|
- [ ] `Char`: `code`, `chr`, `escaped`, `lowercase_ascii`, `uppercase_ascii`.
|
||||||
|
- [ ] `Int`/`Float`: arithmetic, `to_string`, `of_string_opt`, `min_int`, `max_int`.
|
||||||
|
- [ ] `Hashtbl`: `create`, `add`, `replace`, `find`, `find_opt`, `remove`, `mem`,
|
||||||
|
`iter`, `fold`, `length` — backed by SX mutable dict.
|
||||||
|
- [ ] `Map.Make` functor — balanced BST backed by SX sorted dict.
|
||||||
|
- [ ] `Set.Make` functor.
|
||||||
|
- [ ] `Printf`: `sprintf`, `printf`, `eprintf` — format strings via `(format ...)`.
|
||||||
|
- [ ] `Sys`: `argv`, `getenv_opt`, `getcwd` — via `perform` IO.
|
||||||
|
- [ ] Scoreboard runner: `lib/ocaml/conformance.sh` + `scoreboard.json`.
|
||||||
|
- [ ] Target: 150+ tests across all stdlib modules.
|
||||||
|
|
||||||
|
### Phase 7 — Dream web framework (`lib/dream/`)
|
||||||
|
|
||||||
|
The five types: `request`, `response`, `handler = request -> response`,
|
||||||
|
`middleware = handler -> handler`, `route`. Everything else is a function over these.
|
||||||
|
|
||||||
|
- [ ] **Core types** in `lib/dream/types.sx`: request/response records, route record.
|
||||||
|
- [ ] **Router** in `lib/dream/router.sx`:
|
||||||
|
- `dream-get path handler`, `dream-post path handler`, etc. for all HTTP methods.
|
||||||
|
- `dream-scope prefix middlewares routes` — prefix mount with middleware chain.
|
||||||
|
- `dream-router routes` — dispatch tree, returns handler; no match → 404.
|
||||||
|
- Path param extraction: `:name` segments, `**` wildcard.
|
||||||
|
- `dream-param req name` — retrieve matched path param.
|
||||||
|
- [ ] **Middleware** in `lib/dream/middleware.sx`:
|
||||||
|
- `dream-pipeline middlewares handler` — compose middleware left-to-right.
|
||||||
|
- `dream-no-middleware` — identity.
|
||||||
|
- Logger: `(dream-logger next req)` — logs method, path, status, timing.
|
||||||
|
- Content-type sniffer.
|
||||||
|
- [ ] **Sessions** in `lib/dream/session.sx`:
|
||||||
|
- Cookie-backed session middleware.
|
||||||
|
- `dream-session-field req key`, `dream-set-session-field req key val`.
|
||||||
|
- `dream-invalidate-session req`.
|
||||||
|
- [ ] **Flash messages** in `lib/dream/flash.sx`:
|
||||||
|
- `dream-flash-middleware` — single-request cookie store.
|
||||||
|
- `dream-add-flash-message req category msg`.
|
||||||
|
- `dream-flash-messages req` — returns list of `(category, msg)`.
|
||||||
|
- [ ] **Forms + CSRF** in `lib/dream/form.sx`:
|
||||||
|
- `dream-form req` — returns `(Ok fields)` or `(Err :csrf-token-invalid)`.
|
||||||
|
- `dream-multipart req` — streaming multipart form data.
|
||||||
|
- CSRF middleware: stateless signed tokens, session-scoped.
|
||||||
|
- `dream-csrf-tag req` — returns hidden input fragment for SX templates.
|
||||||
|
- [ ] **WebSockets** in `lib/dream/websocket.sx`:
|
||||||
|
- `dream-websocket handler` — upgrades request; handler `(fn (ws) ...)`.
|
||||||
|
- `dream-send ws msg`, `dream-receive ws`, `dream-close ws`.
|
||||||
|
- [ ] **Static files:** `dream-static root-path` — serves files, ETags, range requests.
|
||||||
|
- [ ] **`dream-run`**: wires root handler into SX's `perform (:http-listen ...)`.
|
||||||
|
- [ ] **Demos** in `lib/dream/demos/`:
|
||||||
|
- `hello.ml` → `lib/dream/demos/hello.sx`: "Hello, World!" route.
|
||||||
|
- `counter.ml` → `lib/dream/demos/counter.sx`: in-memory counter with sessions.
|
||||||
|
- `chat.ml` → `lib/dream/demos/chat.sx`: multi-room WebSocket chat.
|
||||||
|
- `todo.ml` → `lib/dream/demos/todo.sx`: CRUD list with forms + CSRF.
|
||||||
|
- [ ] Tests in `lib/dream/tests/`: routing dispatch, middleware composition,
|
||||||
|
session round-trip, CSRF accept/reject, flash read-after-write — 60+ tests.
|
||||||
|
|
||||||
|
### Phase 8 — ReasonML syntax variant (`lib/reasonml/`)
|
||||||
|
|
||||||
|
ReasonML is OCaml with a JS-friendly surface: semicolons, `let` with `=` everywhere,
|
||||||
|
`=>` for lambdas, `switch` for match, `{j|...|j}` string interpolation. Same semantics —
|
||||||
|
different tokenizer + parser, same `lib/ocaml/transpile.sx` output.
|
||||||
|
|
||||||
|
- [ ] **Tokenizer** in `lib/reasonml/tokenizer.sx`:
|
||||||
|
- `let x = e;` binding syntax (semicolons required).
|
||||||
|
- `(x, y) => e` arrow function syntax.
|
||||||
|
- `switch (x) { | Pat => e | ... }` for match.
|
||||||
|
- JSX: `<Comp prop=val />`, `<div>children</div>`.
|
||||||
|
- String interpolation: `{j|hello $(name)|j}`.
|
||||||
|
- Type annotations: `x : int`, `let f : int => int = x => x + 1`.
|
||||||
|
- [ ] **Parser** in `lib/reasonml/parser.sx`:
|
||||||
|
- Produce same OCaml AST nodes as `lib/ocaml/parser.sx`.
|
||||||
|
- JSX → SX component calls: `<Comp x=1 />` → `(~comp :x 1)`.
|
||||||
|
- Multi-arg functions: `(x, y) => e` → auto-curried pair.
|
||||||
|
- [ ] Shared transpiler: `lib/reasonml/transpile.sx` delegates to
|
||||||
|
`lib/ocaml/transpile.sx` (parse → ReasonML AST → OCaml AST → SX AST).
|
||||||
|
- [ ] Tests in `lib/reasonml/tests/`: tokenizer, parser, eval, JSX — 40+ tests.
|
||||||
|
- [ ] ReasonML Dream demos: translate Phase 7 demos to ReasonML syntax.
|
||||||
|
|
||||||
|
## The meta-circular angle
|
||||||
|
|
||||||
|
SX is bootstrapped to OCaml (`hosts/ocaml/`). Running OCaml inside SX running on OCaml is
|
||||||
|
the "mother tongue" closure: OCaml → SX → OCaml. This means:
|
||||||
|
|
||||||
|
- The OCaml host's native pattern matching and ADTs are exact reference semantics for
|
||||||
|
the SX-level implementation — any mismatch is a bug.
|
||||||
|
- The SX `match` / `define-type` primitives (Phase 6 of the primitives roadmap) were
|
||||||
|
built knowing OCaml was the intended target.
|
||||||
|
- When debugging the transpiler, the OCaml REPL is always available as oracle.
|
||||||
|
- Dream running in SX can serve the sx.rose-ash.com docs site — the framework that
|
||||||
|
describes the runtime it runs on.
|
||||||
|
|
||||||
|
## Key dependencies
|
||||||
|
|
||||||
|
- **Phase 6 ADT primitive** (`define-type`/`match`) — required before Phase 3.
|
||||||
|
- **`perform`/`cek-resume`** IO suspension — required before Phase 7 (Dream async).
|
||||||
|
- **HO forms** and first-class lambdas — already in spec, no blocker.
|
||||||
|
- **Module system** (Phase 4) is independent of type inference (Phase 5) — can overlap.
|
||||||
|
- **ReasonML** (Phase 8) can start once OCaml parser is stable (after Phase 2).
|
||||||
|
|
||||||
|
## Progress log
|
||||||
|
|
||||||
|
_Newest first._
|
||||||
|
|
||||||
|
_(awaiting phase 1)_
|
||||||
|
|
||||||
|
## Blockers
|
||||||
|
|
||||||
|
_(none yet)_
|
||||||
@@ -72,7 +72,7 @@ Representation choices (finalise in phase 1, document here):
|
|||||||
- [ ] String/atom predicates
|
- [ ] String/atom predicates
|
||||||
|
|
||||||
### Phase 5 — Hyperscript integration
|
### Phase 5 — Hyperscript integration
|
||||||
- [ ] `prolog-query` primitive callable from SX/Hyperscript
|
- [x] `prolog-query` primitive callable from SX/Hyperscript — `prolog(db, goal)` hook in runtime
|
||||||
- [ ] Hyperscript DSL: `when allowed(user, :edit) then …`
|
- [ ] Hyperscript DSL: `when allowed(user, :edit) then …`
|
||||||
- [ ] Integration suite
|
- [ ] Integration suite
|
||||||
|
|
||||||
|
|||||||
41
scripts/sx-hs-e-down.sh
Executable file
41
scripts/sx-hs-e-down.sh
Executable file
@@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Stop the sx-hs-e tmux session. Optionally (--clean) remove worktrees.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SESSION="sx-hs-e"
|
||||||
|
WORKTREE_BASE="/root/rose-ash-e"
|
||||||
|
CLEAN=0
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--clean) CLEAN=1 ;;
|
||||||
|
*) echo "Unknown arg: $arg"; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if tmux has-session -t "$SESSION" 2>/dev/null; then
|
||||||
|
WINDOWS=$(tmux list-windows -t "$SESSION" -F '#W')
|
||||||
|
for w in $WINDOWS; do
|
||||||
|
tmux send-keys -t "$SESSION:$w" "/exit" C-m 2>/dev/null || true
|
||||||
|
done
|
||||||
|
echo "Sent /exit to all windows. Waiting 5s..."
|
||||||
|
sleep 5
|
||||||
|
tmux kill-session -t "$SESSION"
|
||||||
|
echo "Killed tmux session '$SESSION'."
|
||||||
|
else
|
||||||
|
echo "No $SESSION tmux session running."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$CLEAN" = "1" ]; then
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
for item in e36 e37 e38 e39 e40; do
|
||||||
|
wt="$WORKTREE_BASE/$item"
|
||||||
|
if [ -d "$wt" ]; then
|
||||||
|
git worktree remove --force "$wt" 2>/dev/null || rm -rf "$wt"
|
||||||
|
echo "Removed worktree: $wt"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
git worktree prune
|
||||||
|
echo "Branches preserved. Remove manually if desired:"
|
||||||
|
echo " git branch -D hs-e36-websocket hs-e37-tokenizer hs-e38-sourceinfo hs-e39-webworker hs-e40-fetch"
|
||||||
|
fi
|
||||||
190
scripts/sx-hs-e-up.sh
Executable file
190
scripts/sx-hs-e-up.sh
Executable file
@@ -0,0 +1,190 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Spawn 5 claude sessions in tmux, one per Bucket-E HS subsystem.
|
||||||
|
# Each runs in its own git worktree rooted at /root/rose-ash-e/<item>,
|
||||||
|
# on branch hs-e<N>, rebased onto loops/hs.
|
||||||
|
#
|
||||||
|
# Usage: ./scripts/sx-hs-e-up.sh [interval]
|
||||||
|
# interval defaults to self-paced (omit to let model decide)
|
||||||
|
#
|
||||||
|
# After the script prints done:
|
||||||
|
# tmux a -t sx-hs-e
|
||||||
|
# Ctrl-B + <0..4> to switch (0=e36 ... 4=e40)
|
||||||
|
# Ctrl-B + d to detach
|
||||||
|
#
|
||||||
|
# Stop: ./scripts/sx-hs-e-down.sh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
SESSION="sx-hs-e"
|
||||||
|
WORKTREE_BASE="/root/rose-ash-e"
|
||||||
|
INTERVAL="${1:-}"
|
||||||
|
BOOT_WAIT=20
|
||||||
|
|
||||||
|
if tmux has-session -t "$SESSION" 2>/dev/null; then
|
||||||
|
echo "Session '$SESSION' already exists."
|
||||||
|
echo " Attach: tmux a -t $SESSION"
|
||||||
|
echo " Kill: ./scripts/sx-hs-e-down.sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
declare -A DESIGN=(
|
||||||
|
[e36]=e36-websocket.md
|
||||||
|
[e37]=e37-tokenizer-api.md
|
||||||
|
[e38]=e38-sourceinfo.md
|
||||||
|
[e39]=e39-webworker.md
|
||||||
|
[e40]=e40-real-fetch.md
|
||||||
|
)
|
||||||
|
|
||||||
|
declare -A BRANCH=(
|
||||||
|
[e36]=hs-e36-websocket
|
||||||
|
[e37]=hs-e37-tokenizer
|
||||||
|
[e38]=hs-e38-sourceinfo
|
||||||
|
[e39]=hs-e39-webworker
|
||||||
|
[e40]=hs-e40-fetch
|
||||||
|
)
|
||||||
|
|
||||||
|
declare -A LABEL=(
|
||||||
|
[e36]="E36 WebSocket (+16)"
|
||||||
|
[e37]="E37 Tokenizer-as-API (+17)"
|
||||||
|
[e38]="E38 SourceInfo (+4)"
|
||||||
|
[e39]="E39 WebWorker (+1)"
|
||||||
|
[e40]="E40 Fetch/non-2xx (+7)"
|
||||||
|
)
|
||||||
|
|
||||||
|
ORDER=(e36 e37 e38 e39 e40)
|
||||||
|
|
||||||
|
write_worktree_settings() {
|
||||||
|
local wt="$1"
|
||||||
|
local settings_dir="$wt/.claude"
|
||||||
|
mkdir -p "$settings_dir"
|
||||||
|
cat > "$settings_dir/settings.local.json" <<'SETTINGS'
|
||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"mcp__sx-tree__sx_summarise",
|
||||||
|
"mcp__sx-tree__sx_read_tree",
|
||||||
|
"mcp__sx-tree__sx_read_subtree",
|
||||||
|
"mcp__sx-tree__sx_get_context",
|
||||||
|
"mcp__sx-tree__sx_find_all",
|
||||||
|
"mcp__sx-tree__sx_find_across",
|
||||||
|
"mcp__sx-tree__sx_get_siblings",
|
||||||
|
"mcp__sx-tree__sx_validate",
|
||||||
|
"mcp__sx-tree__sx_replace_node",
|
||||||
|
"mcp__sx-tree__sx_insert_child",
|
||||||
|
"mcp__sx-tree__sx_insert_near",
|
||||||
|
"mcp__sx-tree__sx_delete_node",
|
||||||
|
"mcp__sx-tree__sx_wrap_node",
|
||||||
|
"mcp__sx-tree__sx_rename_symbol",
|
||||||
|
"mcp__sx-tree__sx_replace_by_pattern",
|
||||||
|
"mcp__sx-tree__sx_rename_across",
|
||||||
|
"mcp__sx-tree__sx_write_file",
|
||||||
|
"mcp__sx-tree__sx_pretty_print",
|
||||||
|
"mcp__sx-tree__sx_eval",
|
||||||
|
"mcp__sx-tree__sx_harness_eval",
|
||||||
|
"mcp__sx-tree__sx_macroexpand",
|
||||||
|
"mcp__sx-tree__sx_trace",
|
||||||
|
"mcp__sx-tree__sx_deps",
|
||||||
|
"mcp__sx-tree__sx_diff",
|
||||||
|
"mcp__sx-tree__sx_diff_branch",
|
||||||
|
"mcp__sx-tree__sx_changed",
|
||||||
|
"mcp__sx-tree__sx_blame",
|
||||||
|
"mcp__sx-tree__sx_build",
|
||||||
|
"mcp__sx-tree__sx_build_manifest",
|
||||||
|
"mcp__sx-tree__sx_build_bytecode",
|
||||||
|
"mcp__sx-tree__sx_test",
|
||||||
|
"mcp__sx-tree__sx_format_check",
|
||||||
|
"mcp__sx-tree__sx_comp_list",
|
||||||
|
"mcp__sx-tree__sx_comp_usage",
|
||||||
|
"mcp__sx-tree__sx_nav",
|
||||||
|
"mcp__sx-tree__sx_env",
|
||||||
|
"mcp__sx-tree__sx_playwright",
|
||||||
|
"mcp__hs-test__hs_test_run",
|
||||||
|
"mcp__hs-test__hs_test_regen",
|
||||||
|
"mcp__hs-test__hs_test_kill",
|
||||||
|
"mcp__hs-test__hs_test_status",
|
||||||
|
"Bash(node *)",
|
||||||
|
"Bash(python3 *)",
|
||||||
|
"Bash(bash *)",
|
||||||
|
"Bash(cp *)",
|
||||||
|
"Bash(git *)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"enabledMcpjsonServers": [
|
||||||
|
"sx-tree",
|
||||||
|
"rose-ash-services",
|
||||||
|
"hs-test"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
SETTINGS
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Preparing Bucket-E worktrees under $WORKTREE_BASE ..."
|
||||||
|
mkdir -p "$WORKTREE_BASE"
|
||||||
|
|
||||||
|
for item in "${ORDER[@]}"; do
|
||||||
|
wt="$WORKTREE_BASE/$item"
|
||||||
|
branch="${BRANCH[$item]}"
|
||||||
|
|
||||||
|
# Create or reset implementation branch from loops/hs
|
||||||
|
if git show-ref --verify --quiet "refs/heads/$branch"; then
|
||||||
|
echo " $item: branch $branch exists"
|
||||||
|
else
|
||||||
|
git branch "$branch" loops/hs
|
||||||
|
echo " $item: created branch $branch from loops/hs"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add worktree
|
||||||
|
if [ -d "$wt/.git" ] || [ -f "$wt/.git" ]; then
|
||||||
|
echo " $item: worktree exists at $wt"
|
||||||
|
else
|
||||||
|
git worktree add "$wt" "$branch"
|
||||||
|
echo " $item: worktree created at $wt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
write_worktree_settings "$wt"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Create tmux session
|
||||||
|
tmux new-session -d -s "$SESSION" -n "${ORDER[0]}" -c "$WORKTREE_BASE/${ORDER[0]}"
|
||||||
|
for item in "${ORDER[@]:1}"; do
|
||||||
|
tmux new-window -t "$SESSION" -n "$item" -c "$WORKTREE_BASE/$item"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Starting ${#ORDER[@]} claude sessions..."
|
||||||
|
for item in "${ORDER[@]}"; do
|
||||||
|
tmux send-keys -t "$SESSION:$item" "claude" C-m
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Waiting ${BOOT_WAIT}s for claude to boot..."
|
||||||
|
sleep "$BOOT_WAIT"
|
||||||
|
|
||||||
|
for item in "${ORDER[@]}"; do
|
||||||
|
design="${DESIGN[$item]}"
|
||||||
|
label="${LABEL[$item]}"
|
||||||
|
branch="${BRANCH[$item]}"
|
||||||
|
|
||||||
|
if [ -n "$INTERVAL" ]; then
|
||||||
|
preamble="/loop $INTERVAL "
|
||||||
|
else
|
||||||
|
preamble="/loop "
|
||||||
|
fi
|
||||||
|
|
||||||
|
cmd="${preamble}You are implementing HS conformance Bucket-E item ${label}. Read plans/designs/${design} carefully — it is your complete spec. Do ONE piece of work per fire: implement the next unimplemented step from the design doc, run the relevant hs_test_run suite to verify, commit with a short factual message, then stop. Scope: lib/hyperscript/**, tests/playwright/generate-sx-tests.py, tests/hs-run-filtered.js only — never touch spec/, hosts/, shared/ kernel, or other lib/<lang>/. Use sx-tree MCP for all .sx edits. You are on branch ${branch} in worktree /root/rose-ash-e/${item}; push commits to origin/${branch} (never main or loops/hs)."
|
||||||
|
|
||||||
|
tmux send-keys -t "$SESSION:$item" "$cmd"
|
||||||
|
sleep 0.5
|
||||||
|
tmux send-keys -t "$SESSION:$item" Enter
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Done. 5 Bucket-E loops started in tmux session '$SESSION'."
|
||||||
|
echo ""
|
||||||
|
echo " Attach: tmux a -t $SESSION"
|
||||||
|
echo " Switch: Ctrl-B <0..4> (0=e36 1=e37 2=e38 3=e39 4=e40)"
|
||||||
|
echo " List: Ctrl-B w"
|
||||||
|
echo " Detach: Ctrl-B d"
|
||||||
|
echo " Stop: ./scripts/sx-hs-e-down.sh"
|
||||||
|
echo ""
|
||||||
|
echo "Worktrees:"
|
||||||
|
git worktree list | grep rose-ash-e || true
|
||||||
@@ -48,6 +48,72 @@ ORDER=(lua prolog forth erlang haskell js hs smalltalk common-lisp apl ruby tcl)
|
|||||||
|
|
||||||
mkdir -p "$WORKTREE_BASE"
|
mkdir -p "$WORKTREE_BASE"
|
||||||
|
|
||||||
|
# Settings written into every worktree so loops never stall on sx-tree permission prompts
|
||||||
|
write_worktree_settings() {
|
||||||
|
local wt="$1"
|
||||||
|
local settings_dir="$wt/.claude"
|
||||||
|
mkdir -p "$settings_dir"
|
||||||
|
cat > "$settings_dir/settings.local.json" <<'SETTINGS'
|
||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"mcp__sx-tree__sx_summarise",
|
||||||
|
"mcp__sx-tree__sx_read_tree",
|
||||||
|
"mcp__sx-tree__sx_read_subtree",
|
||||||
|
"mcp__sx-tree__sx_get_context",
|
||||||
|
"mcp__sx-tree__sx_find_all",
|
||||||
|
"mcp__sx-tree__sx_find_across",
|
||||||
|
"mcp__sx-tree__sx_get_siblings",
|
||||||
|
"mcp__sx-tree__sx_validate",
|
||||||
|
"mcp__sx-tree__sx_replace_node",
|
||||||
|
"mcp__sx-tree__sx_insert_child",
|
||||||
|
"mcp__sx-tree__sx_insert_near",
|
||||||
|
"mcp__sx-tree__sx_delete_node",
|
||||||
|
"mcp__sx-tree__sx_wrap_node",
|
||||||
|
"mcp__sx-tree__sx_rename_symbol",
|
||||||
|
"mcp__sx-tree__sx_replace_by_pattern",
|
||||||
|
"mcp__sx-tree__sx_rename_across",
|
||||||
|
"mcp__sx-tree__sx_write_file",
|
||||||
|
"mcp__sx-tree__sx_pretty_print",
|
||||||
|
"mcp__sx-tree__sx_eval",
|
||||||
|
"mcp__sx-tree__sx_harness_eval",
|
||||||
|
"mcp__sx-tree__sx_macroexpand",
|
||||||
|
"mcp__sx-tree__sx_trace",
|
||||||
|
"mcp__sx-tree__sx_deps",
|
||||||
|
"mcp__sx-tree__sx_diff",
|
||||||
|
"mcp__sx-tree__sx_diff_branch",
|
||||||
|
"mcp__sx-tree__sx_changed",
|
||||||
|
"mcp__sx-tree__sx_blame",
|
||||||
|
"mcp__sx-tree__sx_build",
|
||||||
|
"mcp__sx-tree__sx_build_manifest",
|
||||||
|
"mcp__sx-tree__sx_build_bytecode",
|
||||||
|
"mcp__sx-tree__sx_test",
|
||||||
|
"mcp__sx-tree__sx_format_check",
|
||||||
|
"mcp__sx-tree__sx_comp_list",
|
||||||
|
"mcp__sx-tree__sx_comp_usage",
|
||||||
|
"mcp__sx-tree__sx_nav",
|
||||||
|
"mcp__sx-tree__sx_env",
|
||||||
|
"mcp__sx-tree__sx_playwright",
|
||||||
|
"mcp__hs-test__hs_test_run",
|
||||||
|
"mcp__hs-test__hs_test_regen",
|
||||||
|
"mcp__hs-test__hs_test_kill",
|
||||||
|
"mcp__hs-test__hs_test_status",
|
||||||
|
"Bash(node *)",
|
||||||
|
"Bash(python3 *)",
|
||||||
|
"Bash(bash *)",
|
||||||
|
"Bash(cp *)",
|
||||||
|
"Bash(git *)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"enabledMcpjsonServers": [
|
||||||
|
"sx-tree",
|
||||||
|
"rose-ash-services",
|
||||||
|
"hs-test"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
SETTINGS
|
||||||
|
}
|
||||||
|
|
||||||
echo "Preparing per-language worktrees under $WORKTREE_BASE ..."
|
echo "Preparing per-language worktrees under $WORKTREE_BASE ..."
|
||||||
for lang in "${ORDER[@]}"; do
|
for lang in "${ORDER[@]}"; do
|
||||||
wt="$WORKTREE_BASE/$lang"
|
wt="$WORKTREE_BASE/$lang"
|
||||||
@@ -63,6 +129,7 @@ for lang in "${ORDER[@]}"; do
|
|||||||
fi
|
fi
|
||||||
echo " $lang: worktree created at $wt on $branch"
|
echo " $lang: worktree created at $wt on $branch"
|
||||||
fi
|
fi
|
||||||
|
write_worktree_settings "$wt"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Create tmux session with one window per language, each cwd in its worktree
|
# Create tmux session with one window per language, each cwd in its worktree
|
||||||
|
|||||||
15
scripts/sx-primitives-down.sh
Executable file
15
scripts/sx-primitives-down.sh
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Stop the sx-primitives tmux session.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SESSION="sx-primitives"
|
||||||
|
|
||||||
|
if tmux has-session -t "$SESSION" 2>/dev/null; then
|
||||||
|
tmux send-keys -t "$SESSION:primitives" "/exit" C-m 2>/dev/null || true
|
||||||
|
echo "Sent /exit. Waiting 5s..."
|
||||||
|
sleep 5
|
||||||
|
tmux kill-session -t "$SESSION"
|
||||||
|
echo "Killed tmux session '$SESSION'."
|
||||||
|
else
|
||||||
|
echo "No $SESSION tmux session running."
|
||||||
|
fi
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
||||||
var SX_VERSION = "2026-05-01T18:54:28Z";
|
var SX_VERSION = "2026-05-01T19:10:01Z";
|
||||||
|
|
||||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||||
@@ -186,6 +186,7 @@
|
|||||||
if (x._hash_table) return "hash-table";
|
if (x._hash_table) return "hash-table";
|
||||||
if (x._sxset) return "set";
|
if (x._sxset) return "set";
|
||||||
if (x._regexp) return "regexp";
|
if (x._regexp) return "regexp";
|
||||||
|
if (x._bytevector) return "bytevector";
|
||||||
if (x._rational) return "rational";
|
if (x._rational) return "rational";
|
||||||
if (typeof Node !== "undefined" && x instanceof Node) return "dom-node";
|
if (typeof Node !== "undefined" && x instanceof Node) return "dom-node";
|
||||||
if (Array.isArray(x)) return "list";
|
if (Array.isArray(x)) return "list";
|
||||||
@@ -1213,6 +1214,73 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// stdlib.bytevectors — R7RS bytevector type backed by Uint8Array
|
||||||
|
function SxBytevector(size_or_buf) {
|
||||||
|
if (size_or_buf instanceof Uint8Array) {
|
||||||
|
this.data = size_or_buf;
|
||||||
|
} else {
|
||||||
|
this.data = new Uint8Array(typeof size_or_buf === "number" ? size_or_buf : 0);
|
||||||
|
}
|
||||||
|
this._bytevector = true;
|
||||||
|
}
|
||||||
|
SxBytevector.prototype._type = "bytevector";
|
||||||
|
PRIMITIVES["make-bytevector"] = function(n, fill) {
|
||||||
|
var bv = new SxBytevector(n);
|
||||||
|
if (fill !== undefined) bv.data.fill(fill & 0xff);
|
||||||
|
return bv;
|
||||||
|
};
|
||||||
|
PRIMITIVES["bytevector?"] = function(v) { return v instanceof SxBytevector; };
|
||||||
|
PRIMITIVES["bytevector-length"] = function(bv) { return bv.data.length; };
|
||||||
|
PRIMITIVES["bytevector-u8-ref"] = function(bv, i) { return bv.data[i]; };
|
||||||
|
PRIMITIVES["bytevector-u8-set!"] = function(bv, i, byte) { bv.data[i] = byte & 0xff; return NIL; };
|
||||||
|
PRIMITIVES["bytevector-copy"] = function(bv, start, end_) {
|
||||||
|
var s = start === undefined ? 0 : start;
|
||||||
|
var e = end_ === undefined ? bv.data.length : end_;
|
||||||
|
return new SxBytevector(bv.data.slice(s, e));
|
||||||
|
};
|
||||||
|
PRIMITIVES["bytevector-copy!"] = function(dst, at, src, start, end_) {
|
||||||
|
var s = start === undefined ? 0 : start;
|
||||||
|
var e = end_ === undefined ? src.data.length : end_;
|
||||||
|
dst.data.set(src.data.subarray(s, e), at);
|
||||||
|
return NIL;
|
||||||
|
};
|
||||||
|
PRIMITIVES["bytevector-append"] = function() {
|
||||||
|
var total = 0;
|
||||||
|
for (var i = 0; i < arguments.length; i++) total += arguments[i].data.length;
|
||||||
|
var result = new Uint8Array(total);
|
||||||
|
var pos = 0;
|
||||||
|
for (var i = 0; i < arguments.length; i++) {
|
||||||
|
result.set(arguments[i].data, pos);
|
||||||
|
pos += arguments[i].data.length;
|
||||||
|
}
|
||||||
|
return new SxBytevector(result);
|
||||||
|
};
|
||||||
|
PRIMITIVES["utf8->string"] = function(bv, start, end_) {
|
||||||
|
var s = start === undefined ? 0 : start;
|
||||||
|
var e = end_ === undefined ? bv.data.length : end_;
|
||||||
|
var dec = new TextDecoder("utf-8");
|
||||||
|
return dec.decode(bv.data.subarray(s, e));
|
||||||
|
};
|
||||||
|
PRIMITIVES["string->utf8"] = function(str, start, end_) {
|
||||||
|
var enc = new TextEncoder();
|
||||||
|
var full = enc.encode(str);
|
||||||
|
var s = start === undefined ? 0 : start;
|
||||||
|
var e = end_ === undefined ? full.length : end_;
|
||||||
|
return new SxBytevector(full.slice(s, e));
|
||||||
|
};
|
||||||
|
PRIMITIVES["bytevector->list"] = function(bv) {
|
||||||
|
var out = [];
|
||||||
|
for (var i = 0; i < bv.data.length; i++) out.push(bv.data[i]);
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
PRIMITIVES["list->bytevector"] = function(lst) {
|
||||||
|
if (!Array.isArray(lst)) lst = [];
|
||||||
|
var b = new Uint8Array(lst.length);
|
||||||
|
for (var i = 0; i < lst.length; i++) b[i] = lst[i] & 0xff;
|
||||||
|
return new SxBytevector(b);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
function isPrimitive(name) { return name in PRIMITIVES; }
|
function isPrimitive(name) { return name in PRIMITIVES; }
|
||||||
function getPrimitive(name) { return PRIMITIVES[name]; }
|
function getPrimitive(name) { return PRIMITIVES[name]; }
|
||||||
|
|
||||||
|
|||||||
@@ -274,17 +274,33 @@
|
|||||||
((name (nth ast 1)) (rest-parts (rest (rest ast))))
|
((name (nth ast 1)) (rest-parts (rest (rest ast))))
|
||||||
(cond
|
(cond
|
||||||
((and (= (len ast) 4) (list? (nth ast 2)) (= (first (nth ast 2)) (quote dict)))
|
((and (= (len ast) 4) (list? (nth ast 2)) (= (first (nth ast 2)) (quote dict)))
|
||||||
(list
|
(let
|
||||||
(quote dom-dispatch)
|
((tgt-ast (nth ast 3)))
|
||||||
(hs-to-sx (nth ast 3))
|
(list
|
||||||
name
|
(quote dom-dispatch)
|
||||||
(hs-to-sx (nth ast 2))))
|
(if
|
||||||
|
(and (list? tgt-ast) (= (first tgt-ast) (quote query)))
|
||||||
|
(list
|
||||||
|
(quote hs-named-target)
|
||||||
|
(nth tgt-ast 1)
|
||||||
|
(list (quote hs-query-first) (nth tgt-ast 1)))
|
||||||
|
(hs-to-sx tgt-ast))
|
||||||
|
name
|
||||||
|
(hs-to-sx (nth ast 2)))))
|
||||||
((= (len ast) 3)
|
((= (len ast) 3)
|
||||||
(list
|
(let
|
||||||
(quote dom-dispatch)
|
((tgt-ast (nth ast 2)))
|
||||||
(hs-to-sx (nth ast 2))
|
(list
|
||||||
name
|
(quote dom-dispatch)
|
||||||
(list (quote dict) "sender" (quote me))))
|
(if
|
||||||
|
(and (list? tgt-ast) (= (first tgt-ast) (quote query)))
|
||||||
|
(list
|
||||||
|
(quote hs-named-target)
|
||||||
|
(nth tgt-ast 1)
|
||||||
|
(list (quote hs-query-first) (nth tgt-ast 1)))
|
||||||
|
(hs-to-sx tgt-ast))
|
||||||
|
name
|
||||||
|
(list (quote dict) "sender" (quote me)))))
|
||||||
(true
|
(true
|
||||||
(list
|
(list
|
||||||
(quote dom-dispatch)
|
(quote dom-dispatch)
|
||||||
@@ -1226,12 +1242,21 @@
|
|||||||
(if
|
(if
|
||||||
(and (list? raw-tgt) (= (first raw-tgt) (quote query)))
|
(and (list? raw-tgt) (= (first raw-tgt) (quote query)))
|
||||||
(list
|
(list
|
||||||
(quote for-each)
|
(quote let)
|
||||||
(list
|
(list
|
||||||
(quote fn)
|
(list
|
||||||
(list (quote _el))
|
(quote _tgt)
|
||||||
(list (quote dom-add-class) (quote _el) (nth ast 1)))
|
(list (quote hs-query-named-all) (nth raw-tgt 1))))
|
||||||
(list (quote hs-query-all) (nth raw-tgt 1)))
|
(list
|
||||||
|
(quote for-each)
|
||||||
|
(list
|
||||||
|
(quote fn)
|
||||||
|
(list (quote _el))
|
||||||
|
(list
|
||||||
|
(quote dom-add-class)
|
||||||
|
(quote _el)
|
||||||
|
(nth ast 1)))
|
||||||
|
(quote _tgt)))
|
||||||
(list
|
(list
|
||||||
(quote dom-add-class)
|
(quote dom-add-class)
|
||||||
(hs-to-sx raw-tgt)
|
(hs-to-sx raw-tgt)
|
||||||
@@ -1244,14 +1269,20 @@
|
|||||||
(nth ast 2)))
|
(nth ast 2)))
|
||||||
((= head (quote set-styles))
|
((= head (quote set-styles))
|
||||||
(let
|
(let
|
||||||
((pairs (nth ast 1)) (tgt (hs-to-sx (nth ast 2))))
|
((pairs (nth ast 1)) (tgt-ast (nth ast 2)))
|
||||||
(cons
|
(let
|
||||||
(quote do)
|
((tgt (if (and (list? tgt-ast) (= (first tgt-ast) (quote query))) (list (quote hs-named-target) (nth tgt-ast 1) (list (quote hs-query-first) (nth tgt-ast 1))) (hs-to-sx tgt-ast))))
|
||||||
(map
|
(cons
|
||||||
(fn
|
(quote do)
|
||||||
(p)
|
(map
|
||||||
(list (quote dom-set-style) tgt (first p) (nth p 1)))
|
(fn
|
||||||
pairs))))
|
(p)
|
||||||
|
(list
|
||||||
|
(quote dom-set-style)
|
||||||
|
tgt
|
||||||
|
(first p)
|
||||||
|
(nth p 1)))
|
||||||
|
pairs)))))
|
||||||
((= head (quote multi-add-class))
|
((= head (quote multi-add-class))
|
||||||
(let
|
(let
|
||||||
((target (hs-to-sx (nth ast 1)))
|
((target (hs-to-sx (nth ast 1)))
|
||||||
@@ -1349,15 +1380,21 @@
|
|||||||
(if
|
(if
|
||||||
(and (list? raw-tgt) (= (first raw-tgt) (quote query)))
|
(and (list? raw-tgt) (= (first raw-tgt) (quote query)))
|
||||||
(list
|
(list
|
||||||
(quote for-each)
|
(quote let)
|
||||||
(list
|
(list
|
||||||
(quote fn)
|
|
||||||
(list (quote _el))
|
|
||||||
(list
|
(list
|
||||||
(quote dom-remove-class)
|
(quote _tgt)
|
||||||
(quote _el)
|
(list (quote hs-query-named-all) (nth raw-tgt 1))))
|
||||||
(nth ast 1)))
|
(list
|
||||||
(list (quote hs-query-all) (nth raw-tgt 1)))
|
(quote for-each)
|
||||||
|
(list
|
||||||
|
(quote fn)
|
||||||
|
(list (quote _el))
|
||||||
|
(list
|
||||||
|
(quote dom-remove-class)
|
||||||
|
(quote _el)
|
||||||
|
(nth ast 1)))
|
||||||
|
(quote _tgt)))
|
||||||
(list
|
(list
|
||||||
(quote dom-remove-class)
|
(quote dom-remove-class)
|
||||||
(if (nil? raw-tgt) (quote me) (hs-to-sx raw-tgt))
|
(if (nil? raw-tgt) (quote me) (hs-to-sx raw-tgt))
|
||||||
@@ -1398,10 +1435,18 @@
|
|||||||
(list (quote hs-add-to!) val (hs-to-sx tgt)))))
|
(list (quote hs-add-to!) val (hs-to-sx tgt)))))
|
||||||
((= head (quote add-attr))
|
((= head (quote add-attr))
|
||||||
(let
|
(let
|
||||||
((tgt (nth ast 3)))
|
((tgt-ast tgt))
|
||||||
(list
|
(list
|
||||||
(quote hs-set-attr!)
|
(quote hs-set-attr!)
|
||||||
(hs-to-sx tgt)
|
(if
|
||||||
|
(and
|
||||||
|
(list? tgt-ast)
|
||||||
|
(= (first tgt-ast) (quote query)))
|
||||||
|
(list
|
||||||
|
(quote hs-named-target)
|
||||||
|
(nth tgt-ast 1)
|
||||||
|
(list (quote hs-query-first) (nth tgt-ast 1)))
|
||||||
|
(hs-to-sx tgt-ast))
|
||||||
(nth ast 1)
|
(nth ast 1)
|
||||||
(hs-to-sx (nth ast 2)))))
|
(hs-to-sx (nth ast 2)))))
|
||||||
((= head (quote remove-value))
|
((= head (quote remove-value))
|
||||||
@@ -1452,10 +1497,20 @@
|
|||||||
(fn (p) (list (quote dom-set-style) tgt p ""))
|
(fn (p) (list (quote dom-set-style) tgt p ""))
|
||||||
props))))
|
props))))
|
||||||
((= head (quote toggle-class))
|
((= head (quote toggle-class))
|
||||||
(list
|
(let
|
||||||
(quote hs-toggle-class!)
|
((tgt-ast (nth ast 2)))
|
||||||
(hs-to-sx (nth ast 2))
|
(list
|
||||||
(nth ast 1)))
|
(quote hs-toggle-class!)
|
||||||
|
(if
|
||||||
|
(and
|
||||||
|
(list? tgt-ast)
|
||||||
|
(= (first tgt-ast) (quote query)))
|
||||||
|
(list
|
||||||
|
(quote hs-named-target)
|
||||||
|
(nth tgt-ast 1)
|
||||||
|
(list (quote hs-query-first) (nth tgt-ast 1)))
|
||||||
|
(hs-to-sx tgt-ast))
|
||||||
|
(nth ast 1))))
|
||||||
((= head (quote toggle-class-for))
|
((= head (quote toggle-class-for))
|
||||||
(list
|
(list
|
||||||
(quote do)
|
(quote do)
|
||||||
@@ -1510,11 +1565,21 @@
|
|||||||
(hs-to-sx tgt-ast)
|
(hs-to-sx tgt-ast)
|
||||||
(hs-to-sx val-ast)))))
|
(hs-to-sx val-ast)))))
|
||||||
((= head (quote toggle-between))
|
((= head (quote toggle-between))
|
||||||
(list
|
(let
|
||||||
(quote hs-toggle-between!)
|
((tgt-ast (nth ast 3)))
|
||||||
(hs-to-sx (nth ast 3))
|
(list
|
||||||
(nth ast 1)
|
(quote hs-toggle-between!)
|
||||||
(nth ast 2)))
|
(if
|
||||||
|
(and
|
||||||
|
(list? tgt-ast)
|
||||||
|
(= (first tgt-ast) (quote query)))
|
||||||
|
(list
|
||||||
|
(quote hs-named-target)
|
||||||
|
(nth tgt-ast 1)
|
||||||
|
(list (quote hs-query-first) (nth tgt-ast 1)))
|
||||||
|
(hs-to-sx tgt-ast))
|
||||||
|
(nth ast 1)
|
||||||
|
(nth ast 2))))
|
||||||
((= head (quote toggle-style))
|
((= head (quote toggle-style))
|
||||||
(let
|
(let
|
||||||
((raw-tgt (nth ast 2)))
|
((raw-tgt (nth ast 2)))
|
||||||
@@ -1538,10 +1603,20 @@
|
|||||||
(quote list)
|
(quote list)
|
||||||
(map hs-to-sx (slice ast 3 (len ast))))))
|
(map hs-to-sx (slice ast 3 (len ast))))))
|
||||||
((= head (quote toggle-attr))
|
((= head (quote toggle-attr))
|
||||||
(list
|
(let
|
||||||
(quote hs-toggle-attr!)
|
((tgt-ast (nth ast 2)))
|
||||||
(hs-to-sx (nth ast 2))
|
(list
|
||||||
(nth ast 1)))
|
(quote hs-toggle-attr!)
|
||||||
|
(if
|
||||||
|
(and
|
||||||
|
(list? tgt-ast)
|
||||||
|
(= (first tgt-ast) (quote query)))
|
||||||
|
(list
|
||||||
|
(quote hs-named-target)
|
||||||
|
(nth tgt-ast 1)
|
||||||
|
(list (quote hs-query-first) (nth tgt-ast 1)))
|
||||||
|
(hs-to-sx tgt-ast))
|
||||||
|
(nth ast 1))))
|
||||||
((= head (quote toggle-attr-between))
|
((= head (quote toggle-attr-between))
|
||||||
(list
|
(list
|
||||||
(quote hs-toggle-attr-between!)
|
(quote hs-toggle-attr-between!)
|
||||||
@@ -1575,7 +1650,22 @@
|
|||||||
(emit-set
|
(emit-set
|
||||||
raw-tgt
|
raw-tgt
|
||||||
(list (quote hs-put-at!) val pos (hs-to-sx raw-tgt))))
|
(list (quote hs-put-at!) val pos (hs-to-sx raw-tgt))))
|
||||||
(true (list (quote hs-put!) val pos (hs-to-sx raw-tgt))))))
|
(true
|
||||||
|
(let
|
||||||
|
((tgt-ast raw-tgt))
|
||||||
|
(list
|
||||||
|
(quote hs-put!)
|
||||||
|
val
|
||||||
|
pos
|
||||||
|
(if
|
||||||
|
(and
|
||||||
|
(list? tgt-ast)
|
||||||
|
(= (first tgt-ast) (quote query)))
|
||||||
|
(list
|
||||||
|
(quote hs-named-target)
|
||||||
|
(nth tgt-ast 1)
|
||||||
|
(list (quote hs-query-first) (nth tgt-ast 1)))
|
||||||
|
(hs-to-sx tgt-ast))))))))
|
||||||
((= head (quote if))
|
((= head (quote if))
|
||||||
(if
|
(if
|
||||||
(> (len ast) 3)
|
(> (len ast) 3)
|
||||||
@@ -1651,12 +1741,22 @@
|
|||||||
(detail (if (= (len ast) 4) (nth ast 2) nil)))
|
(detail (if (= (len ast) 4) (nth ast 2) nil)))
|
||||||
(list
|
(list
|
||||||
(quote dom-dispatch)
|
(quote dom-dispatch)
|
||||||
(hs-to-sx tgt)
|
(let
|
||||||
|
((tgt-ast tgt))
|
||||||
|
(if
|
||||||
|
(and
|
||||||
|
(list? tgt-ast)
|
||||||
|
(= (first tgt-ast) (quote query)))
|
||||||
|
(list
|
||||||
|
(quote hs-named-target)
|
||||||
|
(nth tgt-ast 1)
|
||||||
|
(list (quote hs-query-first) (nth tgt-ast 1)))
|
||||||
|
(hs-to-sx tgt-ast)))
|
||||||
name
|
name
|
||||||
(if has-detail (hs-to-sx detail) nil))))
|
(if has-detail (hs-to-sx detail) nil))))
|
||||||
((= head (quote hide))
|
((= head (quote hide))
|
||||||
(let
|
(let
|
||||||
((tgt (let ((raw-tgt (nth ast 1))) (if (and (list? raw-tgt) (= (first raw-tgt) (quote query))) (list (quote hs-query-all) (nth raw-tgt 1)) (hs-to-sx raw-tgt))))
|
((tgt (let ((raw-tgt (nth ast 1))) (if (and (list? raw-tgt) (= (first raw-tgt) (quote query))) (list (quote hs-query-named-all) (nth raw-tgt 1)) (hs-to-sx raw-tgt))))
|
||||||
(strategy (if (> (len ast) 2) (nth ast 2) "display"))
|
(strategy (if (> (len ast) 2) (nth ast 2) "display"))
|
||||||
(when-cond (if (> (len ast) 3) (nth ast 3) nil)))
|
(when-cond (if (> (len ast) 3) (nth ast 3) nil)))
|
||||||
(if
|
(if
|
||||||
@@ -1672,7 +1772,7 @@
|
|||||||
(hs-to-sx when-cond))))))
|
(hs-to-sx when-cond))))))
|
||||||
((= head (quote show))
|
((= head (quote show))
|
||||||
(let
|
(let
|
||||||
((tgt (let ((raw-tgt (nth ast 1))) (if (and (list? raw-tgt) (= (first raw-tgt) (quote query))) (list (quote hs-query-all) (nth raw-tgt 1)) (hs-to-sx raw-tgt))))
|
((tgt (let ((raw-tgt (nth ast 1))) (if (and (list? raw-tgt) (= (first raw-tgt) (quote query))) (list (quote hs-query-named-all) (nth raw-tgt 1)) (hs-to-sx raw-tgt))))
|
||||||
(strategy (if (> (len ast) 2) (nth ast 2) "display"))
|
(strategy (if (> (len ast) 2) (nth ast 2) "display"))
|
||||||
(when-cond (if (> (len ast) 3) (nth ast 3) nil)))
|
(when-cond (if (> (len ast) 3) (nth ast 3) nil)))
|
||||||
(if
|
(if
|
||||||
@@ -1874,7 +1974,11 @@
|
|||||||
((= head (quote install))
|
((= head (quote install))
|
||||||
(cons (quote hs-install) (map hs-to-sx (rest ast))))
|
(cons (quote hs-install) (map hs-to-sx (rest ast))))
|
||||||
((= head (quote measure))
|
((= head (quote measure))
|
||||||
(list (quote hs-measure) (hs-to-sx (nth ast 1))))
|
(let
|
||||||
|
((raw-tgt (nth ast 1)))
|
||||||
|
(let
|
||||||
|
((compiled-tgt (if (and (list? raw-tgt) (= (first raw-tgt) (quote query))) (list (quote hs-named-target) (nth raw-tgt 1) (list (quote hs-query-first) (nth raw-tgt 1))) (hs-to-sx raw-tgt))))
|
||||||
|
(list (quote hs-measure) compiled-tgt))))
|
||||||
((= head (quote increment!))
|
((= head (quote increment!))
|
||||||
(if
|
(if
|
||||||
(= (len ast) 3)
|
(= (len ast) 3)
|
||||||
|
|||||||
@@ -2466,6 +2466,35 @@
|
|||||||
((nth entry 2) val)))
|
((nth entry 2) val)))
|
||||||
_hs-dom-watchers)))
|
_hs-dom-watchers)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hs-null-error!
|
||||||
|
(fn (selector) (raise (str "'" selector "' is null"))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hs-named-target
|
||||||
|
(fn (selector value) (if (nil? value) (hs-null-error! selector) value)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hs-named-target-list
|
||||||
|
(fn
|
||||||
|
(selector values)
|
||||||
|
(if (nil? values) (hs-null-error! selector) values)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hs-query-named-all
|
||||||
|
(fn
|
||||||
|
(selector)
|
||||||
|
(let
|
||||||
|
((results (hs-query-all selector)))
|
||||||
|
(if
|
||||||
|
(and
|
||||||
|
(or (nil? results) (and (list? results) (= (len results) 0)))
|
||||||
|
(string? selector)
|
||||||
|
(> (len selector) 0)
|
||||||
|
(= (substring selector 0 1) "#"))
|
||||||
|
(hs-null-error! selector)
|
||||||
|
results))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
hs-dom-is-ancestor?
|
hs-dom-is-ancestor?
|
||||||
(fn
|
(fn
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1792,7 +1792,7 @@
|
|||||||
blake2_js_for_wasm_create: blake2_js_for_wasm_create};
|
blake2_js_for_wasm_create: blake2_js_for_wasm_create};
|
||||||
}
|
}
|
||||||
(globalThis))
|
(globalThis))
|
||||||
({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["re-9a0de245",[2]],["sx-f783241f",[2,3]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,5]],["dune__exe__Sx_browser-30cab65c",[2,4,6]],["std_exit-10fb8830",[2]],["start-f808dbe1",0]],"generated":(b=>{var
|
({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["re-9a0de245",[2]],["sx-3a32ee22",[2,3]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,5]],["dune__exe__Sx_browser-456e4a54",[2,4,6]],["std_exit-10fb8830",[2]],["start-f808dbe1",0]],"generated":(b=>{var
|
||||||
c=b,a=b?.module?.export||b;return{"env":{"caml_ba_kind_of_typed_array":()=>{throw new
|
c=b,a=b?.module?.export||b;return{"env":{"caml_ba_kind_of_typed_array":()=>{throw new
|
||||||
Error("caml_ba_kind_of_typed_array not implemented")},"caml_exn_with_js_backtrace":()=>{throw new
|
Error("caml_ba_kind_of_typed_array not implemented")},"caml_exn_with_js_backtrace":()=>{throw new
|
||||||
Error("caml_exn_with_js_backtrace not implemented")},"caml_int64_create_lo_mi_hi":()=>{throw new
|
Error("caml_exn_with_js_backtrace not implemented")},"caml_int64_create_lo_mi_hi":()=>{throw new
|
||||||
|
|||||||
@@ -200,14 +200,21 @@ async def eval_sx_url(raw_path: str) -> Any:
|
|||||||
ocaml_ctx = {"_helper_service": "sx"}
|
ocaml_ctx = {"_helper_service": "sx"}
|
||||||
|
|
||||||
if is_htmx_request():
|
if is_htmx_request():
|
||||||
# HTMX: single-pass — OOB wrapper + content in ONE aser_slot
|
# HTMX: render everything to HTML server-side.
|
||||||
|
# aser_slot left content component calls unexpanded (browser
|
||||||
|
# doesn't have their definitions). bridge.render expands all
|
||||||
|
# server-affinity components — ~layouts/doc, content pages,
|
||||||
|
# syntax highlighting — before the response is sent.
|
||||||
|
# handle-html-response in engine.sx processes sx-swap-oob
|
||||||
|
# attributes (filter/aside/root-menu/sx-nav) as OOB swaps,
|
||||||
|
# then puts the remaining #main-panel section in the target.
|
||||||
oob_ast = [
|
oob_ast = [
|
||||||
Symbol("~shared:layout/oob-sx"),
|
Symbol("~shared:layout/oob-sx"),
|
||||||
Keyword("content"), wrapped_ast,
|
Keyword("content"), wrapped_ast,
|
||||||
]
|
]
|
||||||
content_sx = SxExpr(await bridge.aser_slot(
|
html = await bridge.render(serialize(oob_ast), ctx=ocaml_ctx)
|
||||||
serialize(oob_ast), ctx=ocaml_ctx))
|
return await make_response(html, 200,
|
||||||
return sx_response(content_sx)
|
{"Content-Type": "text/html; charset=utf-8"})
|
||||||
else:
|
else:
|
||||||
# Full page: single OCaml call — aser-slot + shell render
|
# Full page: single OCaml call — aser-slot + shell render
|
||||||
full_ast = [
|
full_ast = [
|
||||||
@@ -236,9 +243,18 @@ async def eval_sx_url(raw_path: str) -> Any:
|
|||||||
logger.error("SX URL render failed for %s: %s", raw_path, e, exc_info=True)
|
logger.error("SX URL render failed for %s: %s", raw_path, e, exc_info=True)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Return response (Python path)
|
# Return response (Python path — SX_USE_OCAML=0 only)
|
||||||
if is_htmx_request():
|
if is_htmx_request():
|
||||||
return sx_response(await oob_page_sx(content=content_sx))
|
from shared.sx.async_eval import async_render
|
||||||
|
from shared.sx.jinja_bridge import get_component_env
|
||||||
|
oob_ast = [
|
||||||
|
Symbol("~shared:layout/oob-sx"),
|
||||||
|
Keyword("content"), wrapped_ast,
|
||||||
|
]
|
||||||
|
env2 = dict(get_component_env())
|
||||||
|
html = await async_render(oob_ast, env2, ctx)
|
||||||
|
return await make_response(html, 200,
|
||||||
|
{"Content-Type": "text/html; charset=utf-8"})
|
||||||
else:
|
else:
|
||||||
tctx = await get_template_context()
|
tctx = await get_template_context()
|
||||||
html = await full_page_sx(tctx, header_rows="", content=content_sx)
|
html = await full_page_sx(tctx, header_rows="", content=content_sx)
|
||||||
|
|||||||
Reference in New Issue
Block a user