HS parser/compiler/runtime: fix 8 parse errors, add/remove arrays, return guard

Parser:
- `add VALUE to :var` → (add-value) for array append
- `remove VALUE from :var` → (remove-value) for array removal
- `toggle .foo for 10ms` → (toggle-class-for) with duration
- `append VALUE` without `to` → implicit target (it)
- `set {obj} on target` → (set-on) for object property spread
- `repeat in` body: remove spurious nil (body at index 3→2)
- Keywords followed by `(` parsed as function calls (fixes `increment()`)

Compiler:
- Handle add-value, remove-value, toggle-class-for, set-on AST nodes
- Local variables (`set :var`) use `define` instead of `set!`

Runtime:
- hs-add-to!: append value to list
- hs-remove-from!: filter value from list
- hs-set-on!: spread dict properties onto target
- `as String` for lists: comma-join (JS Array.toString compat)

Tests:
- eval-hs/eval-hs-with-me: guard for hs-return exceptions
  (return compiles to raise, needs handler to extract value)

Parse errors: 20→12 (8 fixed). Remaining: 6 embedded HTML quotes
(tokenizer), 6 transition template values `(expr)px`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-18 11:06:46 +00:00
parent ac65666f6f
commit 3ba819d9ae
7 changed files with 767 additions and 941 deletions

View File

@@ -136,9 +136,35 @@
((= pos "end") (dom-insert-adjacent-html target "beforeend" value)))))
;; Find previous sibling matching a selector.
(define hs-navigate! (fn (url) (perform (list (quote io-navigate) url))))
(define
hs-add-to!
(fn
(value target)
(if
(list? target)
(append target (list value))
(host-call target "push" value))))
;; First element matching selector within a scope.
(define
hs-remove-from!
(fn
(value target)
(if
(list? target)
(filter (fn (x) (not (= x value))) target)
(host-call target "splice" (host-call target "indexOf" value) 1))))
;; Last element matching selector.
(define
hs-set-on!
(fn
(props target)
(for-each (fn (k) (host-set! target k (get props k))) (keys props))))
;; First/last within a specific scope.
(define hs-navigate! (fn (url) (perform (list (quote io-navigate) url))))
(define
hs-scroll!
(fn
@@ -151,7 +177,9 @@
((= position "bottom") (dict :block "end"))
(true (dict :block "start")))))))
;; Last element matching selector.
;; ── Iteration ───────────────────────────────────────────────────
;; Repeat a thunk N times.
(define
hs-halt!
(fn
@@ -161,14 +189,19 @@
(host-call event "preventDefault" (list))
(when (= mode "event") (host-call event "stopPropagation" (list))))))
;; First/last within a specific scope.
;; Repeat forever (until break — relies on exception/continuation).
(define hs-select! (fn (target) (host-call target "select" (list))))
;; ── Fetch ───────────────────────────────────────────────────────
;; Fetch a URL, parse response according to format.
;; (hs-fetch url format) — format is "json" | "text" | "html"
(define hs-reset! (fn (target) (host-call target "reset" (list))))
;; ── Iteration ───────────────────────────────────────────────────
;; ── Type coercion ───────────────────────────────────────────────
;; Repeat a thunk N times.
;; Coerce a value to a type by name.
;; (hs-coerce value type-name) — type-name is "Int", "Float", "String", etc.
(define
hs-next
(fn
@@ -188,7 +221,10 @@
(true (find-next (dom-next-sibling el))))))
(find-next sibling)))))
;; Repeat forever (until break — relies on exception/continuation).
;; ── Object creation ─────────────────────────────────────────────
;; Make a new object of a given type.
;; (hs-make type-name) — creates empty object/collection
(define
hs-previous
(fn
@@ -208,26 +244,27 @@
(true (find-prev (dom-get-prop el "previousElementSibling"))))))
(find-prev sibling)))))
;; ── Fetch ───────────────────────────────────────────────────────
;; ── Behavior installation ───────────────────────────────────────
;; Fetch a URL, parse response according to format.
;; (hs-fetch url format) — format is "json" | "text" | "html"
;; 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-query-all
(fn (sel) (host-call (dom-body) "querySelectorAll" sel)))
;; ── Type coercion ───────────────────────────────────────────────
;; ── Measurement ─────────────────────────────────────────────────
;; Coerce a value to a type by name.
;; (hs-coerce value type-name) — type-name is "Int", "Float", "String", etc.
;; Measure an element's bounding rect, store as local variables.
;; Returns a dict with x, y, width, height, top, left, right, bottom.
(define
hs-query-first
(fn (sel) (host-call (host-global "document") "querySelector" sel)))
;; ── Object creation ─────────────────────────────────────────────
;; ── Transition ──────────────────────────────────────────────────
;; Make a new object of a given type.
;; (hs-make type-name) — creates empty object/collection
;; Transition a CSS property to a value, optionally with duration.
;; (hs-transition target prop value duration)
(define
hs-query-last
(fn
@@ -236,17 +273,8 @@
((all (dom-query-all (dom-body) sel)))
(if (> (len all) 0) (nth all (- (len all) 1)) nil))))
;; ── 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-first (fn (scope sel) (dom-query-all scope sel)))
;; ── 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-last
(fn
@@ -255,10 +283,6 @@
((all (dom-query-all scope sel)))
(if (> (len all) 0) (nth all (- (len all) 1)) nil))))
;; ── Transition ──────────────────────────────────────────────────
;; Transition a CSS property to a value, optionally with duration.
;; (hs-transition target prop value duration)
(define
hs-repeat-times
(fn
@@ -296,7 +320,11 @@
((= type-name "Integer") (floor (+ value 0)))
((= type-name "Float") (+ value 0))
((= type-name "Number") (+ value 0))
((= type-name "String") (str value))
((= type-name "String")
(if
(list? value)
(join "," (map (fn (x) (str x)) value))
(str value)))
((= type-name "Bool") (not (hs-falsy? value)))
((= type-name "Boolean") (not (hs-falsy? value)))
((= type-name "Array") (if (list? value) value (list value)))
@@ -395,6 +423,10 @@
(define hs-install (fn (behavior-fn) (behavior-fn me)))
(define
hs-measure
(fn (target) (perform (list (quote io-measure) target))))
@@ -411,7 +443,8 @@
(str prop " " (/ duration 1000) "s")))
(dom-set-style target prop value)
(when duration (hs-settle target))))
;; ── Sandbox/test runtime additions ──────────────────────────────
;; Property access — dot notation and .length
(define
hs-transition-from
(fn
@@ -425,11 +458,7 @@
(str prop " " (/ duration 1000) "s")))
(dom-set-style target prop (str to-val))
(when duration (hs-settle target))))
;; DOM query stub — sandbox returns empty list
(define
hs-type-check
(fn
@@ -449,34 +478,33 @@
(= (host-typeof value) "element")
(= (host-typeof value) "text")))
(true (= (host-typeof value) (downcase type-name)))))))
;; Method dispatch — obj.method(args)
(define
hs-type-check-strict
(fn
(value type-name)
(if (nil? value) false (hs-type-check value type-name))))
;; ── Sandbox/test runtime additions ──────────────────────────────
;; Property access — dot notation and .length
(define
hs-strict-eq
(fn (a b) (and (= (type-of a) (type-of b)) (= a b))))
;; DOM query stub — sandbox returns empty list
(define
hs-eq-ignore-case
(fn (a b) (= (downcase (str a)) (downcase (str b)))))
;; Method dispatch — obj.method(args)
(define
hs-starts-with-ic?
(fn (str prefix) (starts-with? (downcase str) (downcase prefix))))
;; ── 0.9.90 features ─────────────────────────────────────────────
;; beep! — debug logging, returns value unchanged
(define
hs-strict-eq
(fn (a b) (and (= (type-of a) (type-of b)) (= a b))))
;; Property-based is — check obj.key truthiness
(define
hs-eq-ignore-case
(fn (a b) (= (downcase (str a)) (downcase (str b)))))
;; Array slicing (inclusive both ends)
(define
hs-starts-with-ic?
(fn (str prefix) (starts-with? (downcase str) (downcase prefix))))
;; Collection: sorted by
(define
hs-contains-ignore-case?
(fn
(haystack needle)
(contains? (downcase (str haystack)) (downcase (str needle)))))
;; Property-based is — check obj.key truthiness
;; Collection: sorted by descending
(define
hs-falsy?
(fn
@@ -488,7 +516,7 @@
((and (list? v) (= (len v) 0)) true)
((= v 0) true)
(true false))))
;; Array slicing (inclusive both ends)
;; Collection: split by
(define
hs-matches?
(fn
@@ -499,7 +527,7 @@
((= (host-typeof target) "element")
(if (string? pattern) (host-call target "matches" pattern) false))
(true false))))
;; Collection: sorted by
;; Collection: joined by
(define
hs-contains?
(fn
@@ -519,9 +547,9 @@
true
(hs-contains? (rest collection) item)))))
(true false))))
;; Collection: sorted by descending
(define precedes? (fn (a b) (< (str a) (str b))))
;; Collection: split by
(define
hs-empty?
(fn
@@ -532,7 +560,7 @@
((list? v) (= (len v) 0))
((dict? v) (= (len (keys v)) 0))
(true false))))
;; Collection: joined by
(define
hs-empty-target!
(fn