# E39 — WebWorker plugin

Cluster 39 of `plans/hs-conformance-to-100.md`. Goal: +1 test.

## 1. The 1 failing test

Suite `hs-upstream-worker`, test:

> `raises a helpful error when the worker plugin is not installed`

Translated upstream (`test/features/worker.js`) — currently emitted as
`SKIP (untranslated)` in `spec/tests/test-hyperscript-behavioral.sx` path
`(117 2 2)`:

```
(deftest "raises a helpful error when the worker plugin is not installed"
  (error "SKIP (untranslated): raises a helpful ..."))
```

Upstream assertion:

```js
const msg = await error("worker MyWorker def noop() end end")
expect(msg).toContain('worker plugin')
expect(msg).toContain('hyperscript.org/features/worker')
```

Two substring checks on the error produced by compiling the source.

## 2. Upstream `worker` syntax

From `https://hyperscript.org/features/worker/`:

```
worker <name>[(<external-script-url>*)]
  (<def-feature> | <js-block>)+
end
```

- Declared at top level alongside `on`, `init`, `def`, `behavior`.
- Body restricted to `def` functions and `js ... end` blocks; no DOM / `window` access.
- Callers invoke as `WorkerName.fn(args)` from the main thread; calls
  return promises but are async-transparent.
- Stock `_hyperscript` ships only a **stub** in the core bundle — the
  real implementation is in the `worker` ext. Without the ext loaded,
  parsing must fail with a message mentioning both `worker plugin` and
  `hyperscript.org/features/worker`.

## 3. Proposed SX shape

We ship the stub only. Full runtime is out of scope for a 1-test cluster.

### Parser addition (`lib/hyperscript/parser.sx`, `parse-feat`)

Extend the feature-dispatch `cond` (around line 2620) with one branch
**before** the fallthrough to `parse-cmd-list`:

```
((= val "worker") (parse-worker-stub))
```

`parse-worker-stub` raises immediately — it does not consume tokens,
does not need to recognise the body grammar. The error string contains
both required substrings:

```
"worker plugin is not installed — see https://hyperscript.org/features/worker"
```

`(error ...)` at parser scope is the existing failure channel (used by
`Expected 'of' or 'from' at position …` etc.), so this slots in without
changes to the error plumbing.

### Compile output

None. The stub never returns an AST; the error propagates out of
`hs-compile` and through `hs-to-sx-from-source`, surfacing to the test
runner as a caught exception.

## 4. Runtime architecture

For the 1 test: **no runtime.** Parsing fails, so `runtime.sx` never sees
a worker form. No `Worker` class needed in the `hs-run-filtered.js`
mock. Nothing touches the DOM shim.

For a hypothetical full implementation (explicitly out of scope):

- Server would bind `WorkerName` in the HS top-level env to a record
  `{:worker-handle H :exports (list ...)}`.
- `WorkerName.fn(args)` would compile via the existing property-access
  path to `(hs-method-call WorkerName "fn" args)`, which would detect
  the worker handle and dispatch over a `postMessage` channel.
- Mock env would need a `Worker` class constructor + a serialisable
  message loop driving the worker script's own tiny SX interpreter.
  Deferred until the 7 skipped upstream tests become a target cluster.

## 5. Test delta estimate

+1 test. Feasibility: **high**. Two substring checks. Implementation
is a single `cond` branch plus a 2-line error string. Generator patch
to un-skip the test is mechanical.

## 6. Risks

- **Drift from future real plugin** — if we later implement the plugin,
  the stub must be replaced, not shadowed. Mitigation: the stub lives
  inline in `parse-feat`; adding the real plugin means deleting the
  stub branch and replacing it with `(parse-worker-feat)`. Single site.
- **In-browser vs mock divergence** — none in this cluster. The stub
  errors identically in both hosts because it's pure parser logic.
- **Message serialisation** — N/A until full plugin.
- **Error-message phrasing drift** — the upstream test only checks for
  two substrings. We must keep `worker plugin` and
  `hyperscript.org/features/worker` verbatim; change-detector tests
  should reference this design if the wording is ever touched.
- **Generator mis-translation** — the upstream JS uses `await error(src)`
  which the SX generator currently bails on (`return None`), producing
  the `SKIP (untranslated)`. We can either (a) teach the generator to
  translate `await error(src)` + two `toContain` checks, or (b) hand-
  write the test. Recommend (b) for one test: less generator churn.

## 7. Implementation checklist (single commit)

1. `sx_replace_by_pattern` in `lib/hyperscript/parser.sx` — insert the
   `((= val "worker") ...)` branch inside `parse-feat`'s `cond`. Body:
   `(error "worker plugin is not installed — see https://hyperscript.org/features/worker")`.
2. `sx_replace_node` in `spec/tests/test-hyperscript-behavioral.sx`
   path `(117 2)` — replace the `SKIP (untranslated)` body with:
   ```
   (deftest "raises a helpful error when the worker plugin is not installed"
     (let ((result (guard (e (true (if (string? e) e (str e))))
                     (hs-compile "worker MyWorker def noop() end end")
                     "")))
       (assert (contains? result "worker plugin"))
       (assert (contains? result "hyperscript.org/features/worker"))))
   ```
3. `sx_validate` on both files.
4. Run `node tests/hs-run-filtered.js` with `HS_SUITE=hs-upstream-worker`.
   Expect 1/1.
5. Run full suite smoke (0–195) — expect no regressions (pure additive
   branch in `parse-feat`; `"worker"` was previously caught by the
   fallthrough `parse-cmd-list`, which would have errored anyway on
   an unknown identifier — confirm by checking baseline).
6. Commit: `HS: E39 WebWorker plugin stub (+1 test)`.

## Non-goals

- Any worker runtime. Any real `Worker` object. Any message passing.
- The 7 sibling `test.skip(...)` cases upstream — they remain skipped.
- Generator patches — the test is hand-written per §6 above.
