Files
rose-ash/plans/designs/f8-eval-statically.md
giles 985671cd76 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>
2026-05-06 09:19:56 +00:00

5.2 KiB
Raw Blame History

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:

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:

(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:
    (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 0195 — 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.