sx: step 9 — parser feature registry

Add `_hs-feature-registry` dict and `hs-register-feature!` to
`lib/hyperscript/parser.sx`. Replace `parse-feat`'s hardcoded `cond`
on feature names with a registry lookup; the paren-open and
default-expression branches remain as fallthroughs.

Each parse-fn receives a `ctx` dict (built per call by `parse-feat-ctx`)
exposing parser internals (`:adv!`, `:tp-val`, `:tp-type`, `:at-end?`,
`:parse-cmd-list`, `:parse-expr`) and the per-feature handlers
(`:parse-on-feat` … `:parse-socket-feat`). All nine builtins
(`on`, `init`, `def`, `behavior`, `live`, `when`, `worker`, `bind`,
`socket`) are registered at file load time, so plugins added later via
`hs-register-feature!` persist across `hs-parse` calls.

Worker stub still raises identically. Mirror `shared/static/wasm/sx/hs-parser.sx`
copied byte-identical. OCaml: 4545/1339, JS: 2591/2465 — both match
baseline, zero regressions.

First piece of plans/designs/hs-plugin-system.md (Steps 10/11 follow).
This commit is contained in:
2026-05-07 00:39:25 +00:00
parent 621e99e456
commit 986d6411d0
3 changed files with 204 additions and 68 deletions

View File

@@ -3,6 +3,17 @@
;; Input: list of {:type T :value V :pos P} tokens from hs-tokenize ;; Input: list of {:type T :value V :pos P} tokens from hs-tokenize
;; Output: SX AST forms that map to runtime primitives ;; Output: SX AST forms that map to runtime primitives
;; ── Feature plugin registry ───────────────────────────────────────
;; Plugins call (hs-register-feature! "name" parse-fn) at load time.
;; parse-fn is (fn (ctx) ...) where ctx is a dict exposing parser
;; helpers (:adv! :tp-val :tp-type :parse-cmd-list ...) and the
;; built-in parse-X-feat dispatch fns.
(begin
(define _hs-feature-registry {})
(define
hs-register-feature!
(fn (name parse-fn) (dict-set! _hs-feature-registry name parse-fn))))
;; ── Parser entry point ──────────────────────────────────────────── ;; ── Parser entry point ────────────────────────────────────────────
(define (define
hs-parse hs-parse
@@ -3231,6 +3242,24 @@
(do (do
(match-kw "end") (match-kw "end")
(list (quote socket) name-path url timeout on-message)))))))))) (list (quote socket) name-path url timeout on-message))))))))))
(define
parse-feat-ctx
(fn
()
{:adv! adv!
:tp-val tp-val
:tp-type tp-type
:at-end? at-end?
:parse-cmd-list parse-cmd-list
:parse-expr parse-expr
:parse-on-feat parse-on-feat
:parse-init-feat parse-init-feat
:parse-def-feat parse-def-feat
:parse-behavior-feat parse-behavior-feat
:parse-live-feat parse-live-feat
:parse-when-feat parse-when-feat
:parse-bind-feat parse-bind-feat
:parse-socket-feat parse-socket-feat}))
(define (define
parse-feat parse-feat
(fn (fn
@@ -3261,18 +3290,12 @@
((unit (tp-val))) ((unit (tp-val)))
(do (adv!) (list (quote string-postfix) inner unit))) (do (adv!) (list (quote string-postfix) inner unit)))
inner)))) inner))))
((= val "on") (do (adv!) (parse-on-feat)))
((= val "init") (do (adv!) (parse-init-feat)))
((= val "def") (do (adv!) (parse-def-feat)))
((= val "behavior") (do (adv!) (parse-behavior-feat)))
((= val "live") (do (adv!) (parse-live-feat)))
((= val "when") (do (adv!) (parse-when-feat)))
((= val "worker")
(error
"worker plugin is not installed — see https://hyperscript.org/features/worker"))
((= val "bind") (do (adv!) (parse-bind-feat)))
((= val "socket") (do (adv!) (parse-socket-feat)))
(true (true
(let
((reg-fn (dict-get _hs-feature-registry val)))
(if
reg-fn
(reg-fn (parse-feat-ctx))
(if (if
(= (tp-type) "keyword") (= (tp-type) "keyword")
(parse-cmd-list) (parse-cmd-list)
@@ -3283,7 +3306,7 @@
(if (if
(and expr (at-end?)) (and expr (at-end?))
expr expr
(do (set! p saved-p) (parse-cmd-list))))))))))) (do (set! p saved-p) (parse-cmd-list)))))))))))))
(define (define
coll-feats coll-feats
(fn (fn
@@ -3326,3 +3349,36 @@
(let (let
((result (hs-parse (hs-tokenize src) src))) ((result (hs-parse (hs-tokenize src) src)))
(do (set! hs-span-mode false) result))))) (do (set! hs-span-mode false) result)))))
;; ── Built-in feature registrations ────────────────────────────────
;; These mirror the original parse-feat cond branches. Registering at
;; load time means plugins can override or extend; ctx exposes the
;; parser internals each fn needs.
(begin
(hs-register-feature!
"on"
(fn (ctx) (begin ((dict-get ctx :adv!)) ((dict-get ctx :parse-on-feat)))))
(hs-register-feature!
"init"
(fn (ctx) (begin ((dict-get ctx :adv!)) ((dict-get ctx :parse-init-feat)))))
(hs-register-feature!
"def"
(fn (ctx) (begin ((dict-get ctx :adv!)) ((dict-get ctx :parse-def-feat)))))
(hs-register-feature!
"behavior"
(fn (ctx) (begin ((dict-get ctx :adv!)) ((dict-get ctx :parse-behavior-feat)))))
(hs-register-feature!
"live"
(fn (ctx) (begin ((dict-get ctx :adv!)) ((dict-get ctx :parse-live-feat)))))
(hs-register-feature!
"when"
(fn (ctx) (begin ((dict-get ctx :adv!)) ((dict-get ctx :parse-when-feat)))))
(hs-register-feature!
"worker"
(fn (ctx) (error "worker plugin is not installed — see https://hyperscript.org/features/worker")))
(hs-register-feature!
"bind"
(fn (ctx) (begin ((dict-get ctx :adv!)) ((dict-get ctx :parse-bind-feat)))))
(hs-register-feature!
"socket"
(fn (ctx) (begin ((dict-get ctx :adv!)) ((dict-get ctx :parse-socket-feat))))))

View File

@@ -215,7 +215,7 @@ these when operands are known numbers/lists.
| 6 — JS AdtValue + define-type + match | [x] | fc8a3916 | | 6 — JS AdtValue + define-type + match | [x] | fc8a3916 |
| 7 — nested patterns | [x] | 0679edf5 | | 7 — nested patterns | [x] | 0679edf5 |
| 8 — exhaustiveness warnings | [x] | 6d391119 | | 8 — exhaustiveness warnings | [x] | 6d391119 |
| 9 — parser feature registry | [ ] | | | 9 — parser feature registry | [x] | PENDING |
| 10 — compiler + as converter registry | [ ] | — | | 10 — compiler + as converter registry | [ ] | — |
| 11 — plugin migration + worker | [ ] | — | | 11 — plugin migration + worker | [ ] | — |
| 12 — frame records | [ ] | — | | 12 — frame records | [ ] | — |

View File

@@ -3,6 +3,17 @@
;; Input: list of {:type T :value V :pos P} tokens from hs-tokenize ;; Input: list of {:type T :value V :pos P} tokens from hs-tokenize
;; Output: SX AST forms that map to runtime primitives ;; Output: SX AST forms that map to runtime primitives
;; ── Feature plugin registry ───────────────────────────────────────
;; Plugins call (hs-register-feature! "name" parse-fn) at load time.
;; parse-fn is (fn (ctx) ...) where ctx is a dict exposing parser
;; helpers (:adv! :tp-val :tp-type :parse-cmd-list ...) and the
;; built-in parse-X-feat dispatch fns.
(begin
(define _hs-feature-registry {})
(define
hs-register-feature!
(fn (name parse-fn) (dict-set! _hs-feature-registry name parse-fn))))
;; ── Parser entry point ──────────────────────────────────────────── ;; ── Parser entry point ────────────────────────────────────────────
(define (define
hs-parse hs-parse
@@ -3015,7 +3026,7 @@
(fn (fn
() ()
(let (let
((every? (match-kw "every")) (first? (match-kw "first"))) ((first? (match-kw "first")))
(let (let
((event-name (parse-compound-event-name))) ((event-name (parse-compound-event-name)))
(let (let
@@ -3028,7 +3039,27 @@
((flt (if (= (tp-type) "bracket-open") (do (adv!) (let ((f (parse-expr))) (if (= (tp-type) "bracket-close") (adv!) nil) f)) nil))) ((flt (if (= (tp-type) "bracket-open") (do (adv!) (let ((f (parse-expr))) (if (= (tp-type) "bracket-close") (adv!) nil) f)) nil)))
(let (let
((elsewhere? (cond ((match-kw "elsewhere") true) ((and (= (tp-type) "keyword") (= (tp-val) "from") (let ((nxt (if (< (+ p 1) tok-len) (nth tokens (+ p 1)) nil))) (and nxt (= (get nxt "type") "keyword") (= (get nxt "value") "elsewhere")))) (do (adv!) (adv!) true)) (true false))) ((elsewhere? (cond ((match-kw "elsewhere") true) ((and (= (tp-type) "keyword") (= (tp-val) "from") (let ((nxt (if (< (+ p 1) tok-len) (nth tokens (+ p 1)) nil))) (and nxt (= (get nxt "type") "keyword") (= (get nxt "value") "elsewhere")))) (do (adv!) (adv!) true)) (true false)))
(source (if (match-kw "from") (parse-expr) nil))) (source
(if
(match-kw "from")
(parse-collection
(parse-cmp
(parse-arith (parse-poss (parse-atom)))))
nil)))
(define
collect-ors!
(fn
(acc)
(if
(match-kw "or")
(let
((or-evt (parse-compound-event-name))
(or-src
(if (match-kw "from") (parse-expr) nil)))
(collect-ors!
(append acc (list (list or-evt or-src)))))
acc)))
(define or-sources (collect-ors! (list)))
(let (let
((h-margin nil) (h-threshold nil)) ((h-margin nil) (h-threshold nil))
(define (define
@@ -3058,6 +3089,8 @@
(= (tp-type) "keyword") (= (tp-type) "keyword")
(= (tp-val) "queue")) (= (tp-val) "queue"))
(do (adv!) (adv!))) (do (adv!) (adv!)))
(let
((every? (match-kw "every")))
(let (let
((having (if (or h-margin h-threshold) (dict "margin" h-margin "threshold" h-threshold) nil))) ((having (if (or h-margin h-threshold) (dict "margin" h-margin "threshold" h-threshold) nil)))
(let (let
@@ -3080,6 +3113,8 @@
((parts (if elsewhere? (append parts (list :elsewhere true)) parts))) ((parts (if elsewhere? (append parts (list :elsewhere true)) parts)))
(let (let
((parts (if source (append parts (list :from source)) parts))) ((parts (if source (append parts (list :from source)) parts)))
(let
((parts (if (> (len or-sources) 0) (append parts (list :or-sources or-sources)) parts)))
(let (let
((parts (if count-filter (append parts (list :count-filter count-filter)) parts))) ((parts (if count-filter (append parts (list :count-filter count-filter)) parts)))
(let (let
@@ -3092,7 +3127,7 @@
((parts (if finally-clause (append parts (list :finally finally-clause)) parts))) ((parts (if finally-clause (append parts (list :finally finally-clause)) parts)))
(let (let
((parts (append parts (list (if (> (len event-vars) 0) (cons (quote do) (append (map (fn (nm) (list (quote ref) nm)) event-vars) (if (and (list? body) (= (first body) (quote do))) (rest body) (list body)))) body))))) ((parts (append parts (list (if (> (len event-vars) 0) (cons (quote do) (append (map (fn (nm) (list (quote ref) nm)) event-vars) (if (and (list? body) (= (first body) (quote do))) (rest body) (list body)))) body)))))
parts)))))))))))))))))))))))) parts))))))))))))))))))))))))))
(define (define
parse-init-feat parse-init-feat
(fn (fn
@@ -3207,6 +3242,24 @@
(do (do
(match-kw "end") (match-kw "end")
(list (quote socket) name-path url timeout on-message)))))))))) (list (quote socket) name-path url timeout on-message))))))))))
(define
parse-feat-ctx
(fn
()
{:adv! adv!
:tp-val tp-val
:tp-type tp-type
:at-end? at-end?
:parse-cmd-list parse-cmd-list
:parse-expr parse-expr
:parse-on-feat parse-on-feat
:parse-init-feat parse-init-feat
:parse-def-feat parse-def-feat
:parse-behavior-feat parse-behavior-feat
:parse-live-feat parse-live-feat
:parse-when-feat parse-when-feat
:parse-bind-feat parse-bind-feat
:parse-socket-feat parse-socket-feat}))
(define (define
parse-feat parse-feat
(fn (fn
@@ -3237,18 +3290,12 @@
((unit (tp-val))) ((unit (tp-val)))
(do (adv!) (list (quote string-postfix) inner unit))) (do (adv!) (list (quote string-postfix) inner unit)))
inner)))) inner))))
((= val "on") (do (adv!) (parse-on-feat)))
((= val "init") (do (adv!) (parse-init-feat)))
((= val "def") (do (adv!) (parse-def-feat)))
((= val "behavior") (do (adv!) (parse-behavior-feat)))
((= val "live") (do (adv!) (parse-live-feat)))
((= val "when") (do (adv!) (parse-when-feat)))
((= val "worker")
(error
"worker plugin is not installed — see https://hyperscript.org/features/worker"))
((= val "bind") (do (adv!) (parse-bind-feat)))
((= val "socket") (do (adv!) (parse-socket-feat)))
(true (true
(let
((reg-fn (dict-get _hs-feature-registry val)))
(if
reg-fn
(reg-fn (parse-feat-ctx))
(if (if
(= (tp-type) "keyword") (= (tp-type) "keyword")
(parse-cmd-list) (parse-cmd-list)
@@ -3259,7 +3306,7 @@
(if (if
(and expr (at-end?)) (and expr (at-end?))
expr expr
(do (set! p saved-p) (parse-cmd-list))))))))))) (do (set! p saved-p) (parse-cmd-list)))))))))))))
(define (define
coll-feats coll-feats
(fn (fn
@@ -3302,3 +3349,36 @@
(let (let
((result (hs-parse (hs-tokenize src) src))) ((result (hs-parse (hs-tokenize src) src)))
(do (set! hs-span-mode false) result))))) (do (set! hs-span-mode false) result)))))
;; ── Built-in feature registrations ────────────────────────────────
;; These mirror the original parse-feat cond branches. Registering at
;; load time means plugins can override or extend; ctx exposes the
;; parser internals each fn needs.
(begin
(hs-register-feature!
"on"
(fn (ctx) (begin ((dict-get ctx :adv!)) ((dict-get ctx :parse-on-feat)))))
(hs-register-feature!
"init"
(fn (ctx) (begin ((dict-get ctx :adv!)) ((dict-get ctx :parse-init-feat)))))
(hs-register-feature!
"def"
(fn (ctx) (begin ((dict-get ctx :adv!)) ((dict-get ctx :parse-def-feat)))))
(hs-register-feature!
"behavior"
(fn (ctx) (begin ((dict-get ctx :adv!)) ((dict-get ctx :parse-behavior-feat)))))
(hs-register-feature!
"live"
(fn (ctx) (begin ((dict-get ctx :adv!)) ((dict-get ctx :parse-live-feat)))))
(hs-register-feature!
"when"
(fn (ctx) (begin ((dict-get ctx :adv!)) ((dict-get ctx :parse-when-feat)))))
(hs-register-feature!
"worker"
(fn (ctx) (error "worker plugin is not installed — see https://hyperscript.org/features/worker")))
(hs-register-feature!
"bind"
(fn (ctx) (begin ((dict-get ctx :adv!)) ((dict-get ctx :parse-bind-feat)))))
(hs-register-feature!
"socket"
(fn (ctx) (begin ((dict-get ctx :adv!)) ((dict-get ctx :parse-socket-feat))))))