Hyperscript conformance: 222 test fixtures from _hyperscript 0.9.14

Extract pure expression tests from the official _hyperscript test suite
and implement parser/compiler/runtime extensions to pass them.

Test infrastructure:
- 222 fixtures extracted from evalHyperScript calls (no DOM dependency)
- SX data format with eval-hs bridge and run-hs-fixture runner
- 24 suites covering expressions, comparisons, coercion, logic, etc.

Parser extensions (parser.sx):
- mod as infix arithmetic operator
- English comparison phrases (is less than, is greater than or equal to)
- is a/an Type typecheck syntax
- === / !== strict equality operators
- I as me synonym, am as is for comparisons
- does not exist/match/contain postfix
- some/every ... with quantifier expressions
- undefined keyword → nil

Compiler updates (compiler.sx):
- + emits hs-add (type-dispatching: string concat or numeric add)
- no emits hs-falsy? (HS truthiness: empty string is falsy)
- matches? emits hs-matches? (string regex in non-DOM context)
- New cases: not-in?, in?, type-check, strict-eq, some, every

Runtime additions (runtime.sx):
- hs-coerce: Int/Integer truncation via floor
- hs-add: string concat when either operand is string
- hs-falsy?: HS-compatible truthiness (nil, false, "" are falsy)
- hs-matches?: string pattern matching
- hs-type-check/hs-type-check!: lenient/strict type checking
- hs-strict-eq: type + value equality

Tokenizer (tokenizer.sx):
- Added keywords: I, am, does, some, mod, equal, equals, really,
  include, includes, contain, undefined, exist

Scorecard: 47/112 test groups passing. 0 non-HS regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 18:53:50 +00:00
parent 71d1ac9ce4
commit 2278443182
6 changed files with 1250 additions and 70 deletions

View File

@@ -45,10 +45,10 @@
((slen (len val)))
(cond
((and (>= slen 3) (= (substring val (- slen 2) slen) "ms"))
(string->number (substring val 0 (- slen 2))))
(parse-number (substring val 0 (- slen 2))))
((and (>= slen 2) (= (nth val (- slen 1)) "s"))
(* 1000 (string->number (substring val 0 (- slen 1)))))
(true (string->number val))))))
(* 1000 (parse-number (substring val 0 (- slen 1)))))
(true (parse-number val))))))
(define
parse-poss-tail
(fn
@@ -58,6 +58,7 @@
(cond
((or (= typ "ident") (= typ "keyword"))
(do (adv!) (parse-prop-chain (list (quote .) owner val))))
((= typ "attr") (do (adv!) (list (quote attr) val owner)))
((= typ "class")
(let
((prop (get (adv!) "value")))
@@ -112,6 +113,8 @@
((and (= typ "keyword") (= val "false")) (do (adv!) false))
((and (= typ "keyword") (or (= val "null") (= val "nil")))
(do (adv!) nil))
((and (= typ "keyword") (= val "undefined"))
(do (adv!) nil))
((and (= typ "keyword") (= val "not"))
(do (adv!) (list (quote not) (parse-expr))))
((and (= typ "keyword") (= val "no"))
@@ -127,6 +130,8 @@
(do (adv!) (parse-the-expr)))
((and (= typ "keyword") (= val "me"))
(do (adv!) (list (quote me))))
((and (= typ "keyword") (= val "I"))
(do (adv!) (list (quote me))))
((and (= typ "keyword") (or (= val "it") (= val "result")))
(do (adv!) (list (quote it))))
((and (= typ "keyword") (= val "event"))
@@ -179,6 +184,40 @@
(list (quote -) 0 operand))))
((= typ "component")
(do (adv!) (list (quote component) val)))
((and (= typ "keyword") (= val "some"))
(do
(adv!)
(let
((var-name (tp-val)))
(do
(adv!)
(match-kw "in")
(let
((collection (parse-expr)))
(do
(match-kw "with")
(list
(quote some)
var-name
collection
(parse-expr))))))))
((and (= typ "keyword") (= val "every"))
(do
(adv!)
(let
((var-name (tp-val)))
(do
(adv!)
(match-kw "in")
(let
((collection (parse-expr)))
(do
(match-kw "with")
(list
(quote every)
var-name
collection
(parse-expr))))))))
(true nil)))))
(define
parse-poss
@@ -196,23 +235,122 @@
(let
((typ (tp-type)) (val (tp-val)))
(cond
((and (= typ "op") (or (= val "==") (= val "!=") (= val "<") (= val ">") (= val "<=") (= val ">=")))
((and (= typ "op") (or (= val "==") (= val "!=") (= val "<") (= val ">") (= val "<=") (= val ">=") (= val "===") (= val "!==")))
(do
(adv!)
(let
((right (parse-expr)))
(list (if (= val "==") (quote =) val) left right))))
(cond
((= val "==") (list (quote =) left right))
((= val "===") (list (quote strict-eq) left right))
((= val "!==")
(list
(quote not)
(list (quote strict-eq) left right)))
(true (list val left right))))))
((and (= typ "keyword") (= val "is"))
(do
(adv!)
(cond
((match-kw "not")
(if
(match-kw "empty")
(list (quote not) (list (quote empty?) left))
(let
((right (parse-expr)))
(list (quote not) (list (quote =) left right)))))
(cond
((match-kw "empty")
(list (quote not) (list (quote empty?) left)))
((match-kw "in")
(list (quote not-in?) left (parse-expr)))
((match-kw "really")
(do
(match-kw "equal")
(match-kw "to")
(list
(quote not)
(list (quote strict-eq) left (parse-expr)))))
((match-kw "equal")
(do
(match-kw "to")
(list
(quote not)
(list (quote =) left (parse-expr)))))
((or (and (or (= (tp-val) "a") (= (tp-val) "an")) (do (adv!) true)))
(let
((type-name (tp-val)))
(do
(adv!)
(let
((strict (if (string-ends-with? type-name "!") (string-slice type-name 0 (- (len type-name) 1)) nil)))
(if
strict
(list
(quote not)
(list (quote type-check!) left strict))
(list
(quote not)
(list (quote type-check) left type-name)))))))
(true
(let
((right (parse-expr)))
(list (quote not) (list (quote =) left right))))))
((match-kw "empty") (list (quote empty?) left))
((match-kw "less")
(do
(match-kw "than")
(if
(match-kw "or")
(do
(match-kw "equal")
(match-kw "to")
(list (quote <=) left (parse-expr)))
(list (quote <) left (parse-expr)))))
((match-kw "greater")
(do
(match-kw "than")
(if
(match-kw "or")
(do
(match-kw "equal")
(match-kw "to")
(list (quote >=) left (parse-expr)))
(list (quote >) left (parse-expr)))))
((match-kw "in") (list (quote in?) left (parse-expr)))
((match-kw "really")
(do
(match-kw "equal")
(match-kw "to")
(list (quote strict-eq) left (parse-expr))))
((match-kw "equal")
(do
(match-kw "to")
(list (quote =) left (parse-expr))))
((or (and (or (= (tp-val) "a") (= (tp-val) "an")) (do (adv!) true)))
(let
((type-name (tp-val)))
(do
(adv!)
(let
((strict (if (string-ends-with? type-name "!") (string-slice type-name 0 (- (len type-name) 1)) nil)))
(if
strict
(list (quote type-check!) left strict)
(list (quote type-check) left type-name))))))
(true
(let
((right (parse-expr)))
(list (quote =) left right))))))
((and (= typ "keyword") (= val "am"))
(do
(adv!)
(cond
((match-kw "not")
(cond
((match-kw "in")
(list (quote not-in?) left (parse-expr)))
((match-kw "empty")
(list (quote not) (list (quote empty?) left)))
(true
(let
((right (parse-expr)))
(list (quote not) (list (quote =) left right))))))
((match-kw "in") (list (quote in?) left (parse-expr)))
((match-kw "empty") (list (quote empty?) left))
(true
(let
@@ -246,6 +384,35 @@
(list (quote of) left target)))))
((and (= typ "keyword") (= val "in"))
(do (adv!) (list (quote in?) left (parse-expr))))
((and (= typ "keyword") (= val "does"))
(do
(adv!)
(match-kw "not")
(cond
((match-kw "exist")
(list (quote not) (list (quote exists?) left)))
((match-kw "match")
(list
(quote not)
(list (quote matches?) left (parse-expr))))
((or (match-kw "contain") (match-kw "contains"))
(list
(quote not)
(list (quote contains?) left (parse-expr))))
((or (match-kw "include") (match-kw "includes"))
(list
(quote not)
(list (quote contains?) left (parse-expr))))
(true left))))
((and (= typ "keyword") (= val "equals"))
(do (adv!) (list (quote =) left (parse-expr))))
((and (= typ "keyword") (= val "really"))
(do
(adv!)
(match-kw "equals")
(list (quote strict-eq) left (parse-expr))))
((and (= typ "keyword") (or (= val "contain") (= val "include") (= val "includes")))
(do (adv!) (list (quote contains?) left (parse-expr))))
(true left)))))
(define
parse-expr
@@ -414,8 +581,24 @@
((tgt (parse-tgt-kw "on" (list (quote me)))))
(list (quote trigger) name tgt)))))
(define parse-log-cmd (fn () (list (quote log) (parse-expr))))
(define parse-inc-cmd (fn () (list (quote increment!) (parse-expr))))
(define parse-dec-cmd (fn () (list (quote decrement!) (parse-expr))))
(define
parse-inc-cmd
(fn
()
(let
((expr (parse-expr)))
(let
((tgt (parse-tgt-kw "on" (list (quote me)))))
(list (quote increment!) expr tgt)))))
(define
parse-dec-cmd
(fn
()
(let
((expr (parse-expr)))
(let
((tgt (parse-tgt-kw "on" (list (quote me)))))
(list (quote decrement!) expr tgt)))))
(define
parse-hide-cmd
(fn
@@ -519,18 +702,20 @@
(let
((typ (tp-type)) (val (tp-val)))
(if
(and
(= typ "op")
(or
(= val "+")
(= val "-")
(= val "*")
(= val "/")
(= val "%")))
(or
(and
(= typ "op")
(or
(= val "+")
(= val "-")
(= val "*")
(= val "/")
(= val "%")))
(and (= typ "keyword") (= val "mod")))
(do
(adv!)
(let
((op (cond ((= val "+") (quote +)) ((= val "-") (quote -)) ((= val "*") (quote *)) ((= val "/") (quote /)) ((= val "%") (make-symbol "%")))))
((op (cond ((= val "+") (quote +)) ((= val "-") (quote -)) ((= val "*") (quote *)) ((= val "/") (quote /)) ((or (= val "%") (= val "mod")) (make-symbol "%")))))
(let
((right (let ((a (parse-atom))) (if (nil? a) a (parse-poss a)))))
(parse-arith (list op left right)))))