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:
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.
|
||||
Reference in New Issue
Block a user