Files
rose-ash/lib/hyperscript/runtime.sx
giles 1f7f47b4c1 Fix hyperscript conformance: 54/112 passing (was 31/81 baseline)
Runtime visibility fix:
- eval-hs now injects runtime helpers (hs-add, hs-falsy?, hs-strict-eq,
  hs-type-check, hs-matches?, hs-contains?, hs-coerce) via outer let
  binding so the tree-walker evaluator can resolve them

Parser fixes:
- null/undefined: return (null-literal) AST node instead of bare nil
  (nil was indistinguishable from "no parse result" sentinel)
- === / !== tokenized as single 3-char operators
- mod operator: emit (modulo) instead of (%) — modulo is a real primitive

Compiler fixes:
- null-literal → nil
- % → modulo
- contains? → hs-contains? (avoids tree-walker primitive arity conflict)

Runtime additions:
- hs-contains?: wraps list membership + string containment

Tokenizer:
- Added keywords: a, an (removed — broke all tokenization), exist
- Triple operators: === and !== now tokenized correctly

Scorecard: 54/112 test groups passing, +23 from baseline.
Unlocked: really-equals, english comparisons, is-in, null is empty,
null exists, type checks, strict equality, mod.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 19:46:42 +00:00

316 lines
9.9 KiB
Plaintext

;; _hyperscript runtime shims
;;
;; Thin wrappers over web/lib/dom.sx and web/lib/browser.sx primitives
;; that implement hyperscript-specific semantics (async transparency,
;; class toggling, event waiting, iteration, type coercion).
;;
;; These are the functions that hs-to-sx (compiler.sx) emits calls to.
;; Each is a pure define — no platform dependency beyond the DOM/browser
;; primitives already available in the SX web platform.
;; ── Event handling ──────────────────────────────────────────────
;; Register an event listener. Returns unlisten function.
;; (hs-on target event-name handler) → unlisten-fn
(define
hs-on
(fn (target event-name handler) (dom-listen target event-name handler)))
;; Register for every occurrence (no queuing — each fires independently).
;; Stock hyperscript queues by default; "every" disables queuing.
(define
hs-on-every
(fn (target event-name handler) (dom-listen target event-name handler)))
;; Run an initializer function immediately.
;; (hs-init thunk) — called at element boot time
(define hs-init (fn (thunk) (thunk)))
;; ── Async / timing ──────────────────────────────────────────────
;; Wait for a duration in milliseconds.
;; In hyperscript, wait is async-transparent — execution pauses.
;; Here we use perform/IO suspension for true pause semantics.
(define hs-wait (fn (ms) (perform (list (quote io-sleep) ms))))
;; Wait for a DOM event on a target.
;; (hs-wait-for target event-name) — suspends until event fires
(define
hs-wait-for
(fn
(target event-name)
(perform (list (quote io-wait-event) target event-name))))
;; Wait for CSS transitions/animations to settle on an element.
(define hs-settle (fn (target) (perform (list (quote io-settle) target))))
;; ── Class manipulation ──────────────────────────────────────────
;; Toggle a single class on an element.
(define
hs-toggle-class!
(fn
(target cls)
(if
(dom-has-class? target cls)
(dom-remove-class target cls)
(dom-add-class target cls))))
;; Toggle between two classes — exactly one is active at a time.
(define
hs-toggle-between!
(fn
(target cls1 cls2)
(if
(dom-has-class? target cls1)
(do (dom-remove-class target cls1) (dom-add-class target cls2))
(do (dom-remove-class target cls2) (dom-add-class target cls1)))))
;; Take a class from siblings — add to target, remove from others.
;; (hs-take! target cls) — like radio button class behavior
(define
hs-take!
(fn
(target cls)
(let
((parent (dom-parent target)))
(when
parent
(for-each
(fn (child) (dom-remove-class child cls))
(dom-child-list parent)))
(dom-add-class target cls))))
;; ── DOM insertion ───────────────────────────────────────────────
;; Put content at a position relative to a target.
;; pos: "into" | "before" | "after"
(define
hs-put!
(fn
(value pos target)
(cond
((= pos "into") (dom-set-inner-html target value))
((= pos "before")
(dom-insert-adjacent-html target "beforebegin" value))
((= pos "after") (dom-insert-adjacent-html target "afterend" value)))))
;; ── Navigation / traversal ──────────────────────────────────────
;; Navigate to a URL.
(define hs-navigate! (fn (url) (perform (list (quote io-navigate) url))))
;; Find next sibling matching a selector (or any sibling).
(define
hs-next
(fn
(target sel)
(if
(= sel "*")
(dom-next-sibling target)
(let
((sibling (dom-next-sibling target)))
(define
find-next
(fn
(el)
(cond
((nil? el) nil)
((dom-matches? el sel) el)
(true (find-next (dom-next-sibling el))))))
(find-next sibling)))))
;; Find previous sibling matching a selector.
(define
hs-previous
(fn
(target sel)
(if
(= sel "*")
(dom-get-prop target "previousElementSibling")
(let
((sibling (dom-get-prop target "previousElementSibling")))
(define
find-prev
(fn
(el)
(cond
((nil? el) nil)
((dom-matches? el sel) el)
(true (find-prev (dom-get-prop el "previousElementSibling"))))))
(find-prev sibling)))))
;; First element matching selector within a scope.
(define hs-query-first (fn (sel) (dom-query sel)))
;; Last element matching selector.
(define
hs-query-last
(fn
(sel)
(let
((all (dom-query-all (dom-body) sel)))
(if (> (len all) 0) (nth all (- (len all) 1)) nil))))
;; First/last within a specific scope.
(define hs-first (fn (scope sel) (dom-query-all scope sel)))
(define
hs-last
(fn
(scope sel)
(let
((all (dom-query-all scope sel)))
(if (> (len all) 0) (nth all (- (len all) 1)) nil))))
;; ── Iteration ───────────────────────────────────────────────────
;; Repeat a thunk N times.
(define
hs-repeat-times
(fn
(n thunk)
(define
do-repeat
(fn (i) (when (< i n) (thunk) (do-repeat (+ i 1)))))
(do-repeat 0)))
;; Repeat forever (until break — relies on exception/continuation).
(define
hs-repeat-forever
(fn
(thunk)
(define do-forever (fn () (thunk) (do-forever)))
(do-forever)))
;; ── Fetch ───────────────────────────────────────────────────────
;; Fetch a URL, parse response according to format.
;; (hs-fetch url format) — format is "json" | "text" | "html"
(define
hs-fetch
(fn
(url format)
(let
((response (perform (list (quote io-fetch) url))))
(cond
((= format "json") (perform (list (quote io-parse-json) response)))
((= format "text") (perform (list (quote io-parse-text) response)))
((= format "html") (perform (list (quote io-parse-html) response)))
(true response)))))
;; ── Type coercion ───────────────────────────────────────────────
;; Coerce a value to a type by name.
;; (hs-coerce value type-name) — type-name is "Int", "Float", "String", etc.
(define
hs-coerce
(fn
(value type-name)
(cond
((= type-name "Int") (floor (+ value 0)))
((= type-name "Integer") (floor (+ value 0)))
((= type-name "Float") (+ value 0))
((= type-name "Number") (+ value 0))
((= type-name "String") (str value))
((= type-name "Boolean") (if value true false))
((= type-name "Array") (if (list? value) value (list value)))
(true value))))
;; ── Object creation ─────────────────────────────────────────────
;; Make a new object of a given type.
;; (hs-make type-name) — creates empty object/collection
(define
hs-add
(fn (a b) (if (or (string? a) (string? b)) (str a b) (+ a b))))
;; ── 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-make
(fn
(type-name)
(cond
((= type-name "Object") (dict))
((= type-name "Array") (list))
((= type-name "Set") (list))
((= type-name "Map") (dict))
(true (dict)))))
;; ── Measurement ─────────────────────────────────────────────────
;; Measure an element's bounding rect, store as local variables.
;; Returns a dict with x, y, width, height, top, left, right, bottom.
(define hs-install (fn (behavior-fn) (behavior-fn me)))
;; ── Transition ──────────────────────────────────────────────────
;; Transition a CSS property to a value, optionally with duration.
;; (hs-transition target prop value duration)
(define
hs-measure
(fn (target) (perform (list (quote io-measure) target))))
(define
hs-transition
(fn
(target prop value duration)
(when
duration
(dom-set-style
target
"transition"
(str prop " " (/ duration 1000) "s")))
(dom-set-style target prop value)
(when duration (hs-settle target))))
(define
hs-type-check
(fn
(value type-name)
(if
(nil? value)
true
(cond
((= type-name "Number") (number? value))
((= type-name "String") (string? value))
((= type-name "Boolean") (or (= value true) (= value false)))
((= type-name "Array") (list? value))
((= type-name "Object") (dict? value))
(true true)))))
(define
hs-type-check!
(fn
(value type-name)
(if (nil? value) false (hs-type-check value type-name))))
(define
hs-strict-eq
(fn (a b) (and (= (type-of a) (type-of b)) (= a b))))
(define
hs-falsy?
(fn (v) (or (nil? v) (= v false) (and (string? v) (= v "")))))
(define
hs-matches?
(fn
(target pattern)
(if
(string? target)
(if (= pattern ".*") true (string-contains? target pattern))
false)))
(define
hs-contains?
(fn
(collection item)
(cond
((list? collection) (some (fn (x) (= x item)) collection))
((string? collection) (string-contains? collection item))
(true false))))