Commit Graph

124 Commits

Author SHA1 Message Date
Oleksandr Kozachuk 392f2d0136 fix(core): inline loop-free callees first so the loop guard can fire
The guard ran before inlining and only ever saw calls.
2026-08-11 17:23:38 +02:00
Oleksandr Kozachuk b1cc93edc6 fix(core): typed entry only for a self-recursive word
Non-recursive words paid an extra wrapper hop. Adds the WAFER_DUMP_WASM dump hook.
2026-08-11 17:23:25 +02:00
Oleksandr Kozachuk 4f96f8860a release: 0.2.8
CI / check (push) Has been cancelled
Ships the self-guard expansion, and corrects what the benchmark tables claim.
Measured with wafer, gforth and SwiftForth all native on x86-64 -- the macOS
sf64 build runs under Rosetta 2 and flatters us -- Fibonacci is 1.16x rather
than 0.83x, so sf64 still wins it and wafer takes the other four. README and
OPTIMIZATIONS now carry both tables.
v0.2.8
2026-08-10 12:48:33 +02:00
Oleksandr Kozachuk e963e636d3 perf(core): test a recursive word's base case at the call site
CI / check (push) Has been cancelled
A recursive Forth word almost always opens with a guard that returns early,
so every leaf of the recursion costs a call whose whole body is that test.
`Call(self)` now compiles as `<guard> IF <what the guard returns> ELSE
Call(self) THEN`, which is what the callee would have done on entry anyway.
Half of fib's nodes are leaves: Fibonacci(25) 356 -> 237 us, 1.24x sf64 ->
0.83x, so all five benchmarks now beat it.

The guard runs twice along the recursive path, hence the bounds: at most six
effect-free operations, at most four call sites, never a tail call. WS-018.
2026-08-09 18:27:25 +02:00
Oleksandr Kozachuk 645c2dadd7 Merge pull request from ok2/perf/typed-calls
CI / check (push) Has been cancelled
0.2.7: typed calling convention, per-region and BEGIN loop promotion,
and a fix for a promoted loop/IF that reordered the stack.
v0.2.7
2026-08-09 17:53:00 +02:00
Oleksandr Kozachuk 3bb613ece0 release: 0.2.7
Version bump plus a doc sweep: the benchmark tables in README and
docs/OPTIMIZATIONS.md were still from before the typed calling convention,
OPTIMIZATIONS listed BEGIN loop promotion as not started, and the
subroutine-threading section of docs/WAFER.md described the memory ABI as
the only one.
2026-08-09 17:36:52 +02:00
Oleksandr Kozachuk b8dcc021a2 perf(core): promote per region, promote BEGIN loops, keep loops off the memory stack
Promotion was all-or-nothing per word, so one `.` or one host call put the
whole body -- hot loops included -- on the memory data stack, where a
loop-carried add costs 2.2 ns/iteration instead of 0.31. The stack simulator
now runs over each promotable stretch of a word; BEGIN/UNTIL, BEGIN/AGAIN and
BEGIN/WHILE/REPEAT join DO/LOOP as promotable when the construct is provably
stack-neutral; and the inliner no longer moves a loop-bearing callee into a
caller that can never be promoted.

Fixes a bug the BEGIN work uncovered, present since promotion was introduced
and shipped in 0.2.6: the loop fixup and the IF join copied locals one slot at
a time in index order, so a body that permutes the stack lost a value --
`: C 3 4 2 0 DO SWAP LOOP . . ;` printed `4 4` where gforth prints `4 3`.

Four of five benchmarks now beat sf64: Factorial 0.29x, Collatz 0.30x,
NestedLoops 0.27x, GCD 0.67x. Only Fibonacci is behind, at 1.24x. Also scale
GCD, Factorial and NestedLoops, which ran in 14-51 us where scatter and fixed
costs dominated -- that is what exposed GCD as a loss and pointed at BEGIN.
WS-014, WS-015, WS-016, WS-019.
2026-08-09 17:25:21 +02:00
Oleksandr Kozachuk fc34bd9b24 perf(core): typed calling convention for words with a known stack effect
Such a word now compiles to a fast entry (i32 x p) -> (i32 x q) carrying
its stack items as WASM values, plus the usual ( -- ) wrapper that keeps
the table slot, so EXECUTE / interpreter / host words / CATCH see the
unchanged memory ABI. Fib(25) 1035 -> 366 us, 4.3x slower than sf64 ->
1.2x; the default guards-on config 1740 -> 361 us. WS-006.
2026-08-09 09:20:10 +02:00
Oleksandr Kozachuk e6c10a6fa1 Merge pull request #6 from ok2/fix/abort-reporting
CI / check (push) Has been cancelled
fix(core): silent ABORT, and name the compile-only condition
v0.2.6
2026-08-07 13:11:50 +02:00
Oleksandr Kozachuk e110ca9516 fix(core): silent ABORT, and name the compile-only condition
Two reporting bugs found from the browser shell.

An uncaught ABORT printed 'ABORT (throw -1)'. The standard defines ABORT
as 'empty the data stack and perform the function of QUIT', and QUIT
displays no message; gforth and SwiftForth are both silent. It now takes
the same silent path QUIT got in 0.2.5. CATCH still reports -1 and still
restores the stack depth, and ABORT" still prints its text -- different
word, different code (-2).

Compile-only constructs used in interpretation state claimed to be an
'unknown word', which is misleading for a word the system obviously
knows: ABORT", IF, THEN, LOOP, LITERAL, RECURSE and friends. They now
report 'interpreting a compile-only word: <name> (throw -14)', the
standard condition both reference engines give. The check reuses the
existing INTERPRETER_TOKENS table at the point where interpretation has
already failed, so a genuine typo still reports 'unknown word'.

Ships as v0.2.6.
2026-08-07 13:10:42 +02:00
Oleksandr Kozachuk 8e2fd0d7d4 Merge pull request #5 from ok2/feature/quit
CI / check (push) Has been cancelled
feat(core): QUIT
v0.2.5
2026-08-07 12:34:05 +02:00
Oleksandr Kozachuk 69309006a2 feat(core): QUIT
The CORE word was missing. QUIT empties the return stack, enters
interpretation state, restores SOURCE-ID to the user input device and
returns to the interpreter without a message, leaving the data stack
untouched -- that last part is the whole difference to ABORT, which the
standard defines as 'empty the data stack, then QUIT'.

Implemented on the throw plumbing with the standard code -56, so nested
EVALUATE / INCLUDE frames unwind and are abandoned on the way out. Two
places treat -56 specially: CATCH lets it through (QUIT is a return to
the prompt, not an exception) and evaluate() turns it into a silent Ok
after the compile-state wipe it already performs.

Semantics checked against gforth 0.7.3 and SwiftForth sf64, which agree:
the data stack survives, nothing is printed, the rest of the input is
abandoned, and '1 2 ' QUIT CATCH .' prints nothing while leaving 1 2.
Six tests in outer.rs pin it. Deliberately NOT added to the cross-engine
corpus: what QUIT abandons is the input source, and the three engines are
fed differently there, so a comparison would measure the harness.

The gap survived because the Forth 2012 suite skips QUIT by its own
admission, and HELP's coverage lint compares dictionary against docs --
a word missing from both looks complete. docs/wafer-anki.txt had been
documenting QUIT as if it existed.

ABORT itself was already correct: executed while a definition is open it
clears both stacks and returns to interpretation state.

Ships as v0.2.5.
2026-08-07 12:28:11 +02:00
Oleksandr Kozachuk 9b10723a95 Merge pull request #4 from ok2/fix/web-error-messages
CI / check (push) Has been cancelled
fix(web): surface Forth messages from host-word throws
v0.2.4
2026-08-06 20:46:11 +02:00
Oleksandr Kozachuk 15f8005b6d Merge pull request #3 from ok2/fix/release-strip-breaks-proc-macros
fix(build): exempt build scripts and proc-macros from release strip
v0.2.3
2026-08-06 20:46:08 +02:00
Oleksandr Kozachuk 4769987b20 fix(web): surface Forth messages from host-word throws
A host word signals failure by throwing across the JS boundary, and the
browser runtime reported that exception with its Debug form, so an
empty-stack RESIZE surfaced as

    call_func(134) failed: JsValue(Error: Stack underflow ...)

with the engine's JS stack trace glued on. The thrown message IS the
Forth message, so take it verbatim: 'Stack underflow', the same text the
native CLI prints. Exceptions without a message keep the call context --
those are genuine runtime faults, not Forth throws.

CATCH is unaffected: it reads the throw code from its own channel rather
than parsing messages. Verified against a fresh VM in Node (initSync +
WaferRepl): host-word underflow, compiled-guard underflow, THROW,
unknown word and ' RESIZE CATCH . all match the native CLI.

Ships as v0.2.4.
2026-08-06 20:45:27 +02:00
Oleksandr Kozachuk d55a27873e fix(build): exempt build scripts and proc-macros from release strip
`wasm-pack build --release` died with "can't find crate" for rustversion,
then thiserror_impl, then every other proc-macro. Cargo strips debuginfo
from release artifacts by default and on macOS that takes the metadata
proc-macro dylibs need to be loadable with it, so rustc could no longer
open them.

Debug builds are unstripped, which is why the whole test suite stayed
green while the browser REPL could not be built for production at all.

Stripping buys nothing for build scripts and proc-macros, so
[profile.release.build-override] exempts them; release binaries stay
stripped.

Also pins wafer-core to 0.2.3 in wafer-web and wafer-cli — both still
asked for 0.2.1. The caret requirement resolved, so nothing broke.
2026-08-06 20:16:41 +02:00
Oleksandr Kozachuk 645b00d6e8 Merge pull request #2 from ok2/feature/swiftforth-number-conversion
feat(core): SwiftForth input number conversion, DPL and NH
v0.2.2
2026-08-06 19:57:04 +02:00
Oleksandr Kozachuk 9efb92ddc8 docs(core): correct sign handling note in number conversion docs
The doc comments still claimed a leading + binds as a sign. It does not:
sf64 converts +7 as the double 7 with DPL 1, and the code follows that.
Only a leading - is a sign.
2026-08-06 19:25:48 +02:00
Oleksandr Kozachuk 706c73ce2a feat(core): SwiftForth input number conversion, DPL and NH
Punctuation (`,` `.` `+` `/` `:` and an embedded `-`) after the leftmost
digit now forces double-cell conversion, so `12.34`, `1,234`, `12:30:45`
and `2026-08-06` convert as doubles. Only a trailing `.` worked before,
and `1.5` was an "unknown word" error.

The punctuation is a double-cell marker, not a fractional point, so the
scale has to travel separately: DPL carries the digit count right of the
rightmost punctuation character (negative when there was none), which is
what lets `<# #>` place the point back on output. NH carries the high
cell a single-cell conversion drops, so a token that overflows a cell is
still recoverable as a double.

parse_number and parse_double_number duplicated the prefix and sign
handling and could not share a DPL counter, so they collapse into one
parse_numeric_literal that reports which kind it converted.

Verified token-for-token against sf64. One deliberate divergence: WAFER
keeps accepting a sign before a base prefix (`-$FF`), which sf64 rejects.
2026-08-06 19:19:41 +02:00
Oleksandr Kozachuk a89d7ca704 chore(ci): dprint-format changelog; dedupe sha crates; deny skips for wasmtime 47
CI / check (push) Has been cancelled
2026-08-06 16:15:02 +02:00
Oleksandr Kozachuk 20b8754e27 chore(release): v0.2.1 — changelog + version bump
CI / check (push) Has been cancelled
v0.2.1
2026-08-06 16:05:02 +02:00
Oleksandr Kozachuk 17852ed459 fix(core): search order is authoritative; host words validate stack args
- Dictionary::find no longer falls back to the newest entry across all
  wordlists when the search order has no match (Forth 2012 16.3.3;
  gforth and SwiftForth agree). Cross-engine corpus program guards it.
- ~40 argument-taking host words (RND-SEED, ACCEPT, RESIZE, ALLOCATE,
  SEARCH, SUBSTITUTE, ROLL, M*, UM/MOD, SF@/SF!/DF@/DF!, F./FE./FS./F~,
  2R@, ...) popped or read stack cells with no underflow check; on an
  empty stack the pointer silently drifted past its base. New host_need/
  host_fneed/host_fpop checked helpers; class-wide regression test
  drives every word on an empty stack.
2026-08-06 16:04:58 +02:00
Oleksandr Kozachuk e6eabb098d Merge pull request #1 from ok2/usability
CI / check (push) Has been cancelled
v0.2.0 — usability release: guards, SEE/HELP, INCLUDE, error overhaul
v0.2.0
2026-08-06 12:48:00 +02:00
Oleksandr Kozachuk f8da87187f chore(release): v0.2.0 — changelog, version bump, dependency upgrades 2026-08-06 12:44:37 +02:00
Oleksandr Kozachuk 0645734d94 chore(tools): sync bat syntax with current word set
Alternations diffed against live WORDS output (304 words) plus
outer-interpreter tokens. Adds float transcendentals, double-cell
ops, pictured numeric, conditional compilation, string ops,
SEE/SEE-IR/DUMP/BYE/HELP, RP@/RDEPTH/.RS, REMEMBER/EMPTY/GILD,
DECIMAL/HEX, INCLUDE/INCLUDED, WITHIN, DEFER!/DEFER@, C,.
2026-08-06 12:44:36 +02:00
Oleksandr Kozachuk f83c8f25e4 build: add just install recipe
Installs bat Forth syntax, then cargo install --locked the CLI.
CARGO_PROFILE_RELEASE_STRIP=none because Cargo's release default
(strip = "debuginfo") emits dylibs macOS 27 dyld rejects with
"mis-aligned LINKEDIT string pool"; proc macros then fail to load
during the build.
2026-08-06 12:44:34 +02:00
Oleksandr Kozachuk 9b1cc0cace feat(core): INCLUDE, error overhaul, sf64 lane, WORDS ALL, .RS
WS-012 -- INCLUDE/INCLUDED:
- Injected source loader (core stays IO-free: CLI installs a
  filesystem reader, web leaves it unset -> defined error). Recursive
  include_file feeds files line-by-line through evaluate, so compile
  state and SEE capture span lines for free. Cycle detection, depth
  cap 16, paths relative to the including file, SOURCE-ID per nesting
  level, parent input restored on success/error/BYE.
- CLI file mode now runs through the include machinery: `wafer x.fth`
  gets file:line error context and a base dir for nested INCLUDEs.
- Unlocks the REMEMBER+INCLUDE reload loop.

WS-008 -- error reporting remainder:
- Errors inside included files carry `file.fth:12:` context
  (anyhow context chain; CLI prints {e:#}).
- describe_uncaught now returns typed WaferError::UncaughtThrow
  { code, message } -- display text unchanged, THROW code reachable
  via downcast for CLI/web consumers.
- compile_word emits a WASM name section; wasmtime trap backtraces
  name the faulting word and runtime_native prefixes "in <WORD>:".
  Batch/consolidated modules stay unnamed (no name plumbing there;
  boot primitives rarely trap).

WS-003 -- SwiftForth correctness lane:
- compare_all_programs_sf64 runs the program corpus with sf64 as
  oracle; whitespace-token comparison (sf64 prints numbers
  space-prefixed and echoes piped lines). 34/35 parity; dot-quote
  skipped (interpret-mode ." is a SwiftForth no-op). #[ignore]d like
  the gforth lane; `just compare-correctness` runs both.

WS-011 leftovers:
- WORDS ALL: grouped full view -- one section per wordlist (search
  order first), then internal words, each with counts. Backed by
  Dictionary::visible_entries (name, wid, internal); visible_words
  now derives from it.
- .RS / RDEPTH: return-stack introspection in boot.fth over a new
  RP@ primitive (IrOp::RpFetch); BEGIN/WHILE walk so the walk never
  touches the stack it prints. SPACES clamped per 6.1.2230.

549 unit + 11 compliance + 9(+2) comparison + 5 crypto + 1 bench
green; fmt/clippy clean; --no-default-features and wasm32 web checks
pass.
2026-08-06 12:44:33 +02:00
Oleksandr Kozachuk dc6e0d45e1 feat(core): SEE, SEE-IR, HELP introspection trio (WS-010)
Implements plans/01-see-introspection.md, all phases.

- see.rs: feature-free IR pretty-printer (format_ir/format_ir_with),
  exhaustive over IrOp -- a new variant fails the build, not the output.
- SEE-IR <name>: post-optimization IR view with resolved callee names,
  immediate/does> annotations; host-word and interpreter-token stubs.
- SEE <name>: verbatim source capture for colon words (multi-line,
  comments preserved, EVALUATE-nesting safe, error-path wiped, MARKER/
  REMEMBER/EMPTY roll word sources back too). Data definers (VARIABLE/
  CONSTANT/CREATE/BUFFER:/2*/F*/SYNONYM) record synthesized one-liners
  at definition time; VALUE/2VALUE/FVALUE/DEFER synthesize at SEE time
  so current values and IS targets show. Fallback chain ends at IR dump
  or host-word stub -- SEE never dead-ends on a defined word.
- HELP [<name>]: wordhelp.rs doc table with stack effect + one-line
  description for EVERY word in a fresh VM (300+ dictionary words plus
  all outer-interpreter tokens); a coverage test fails the build if a
  word is ever added undocumented. User words echo their leading
  ( ... -- ... ) comment. SEE/SEE-IR prepend the HELP line as a
  \ comment. Bare HELP prints usage.
- boot.fth colon definitions get real sources for free (they flow
  through evaluate); INTERPRETER_TOKENS gained the missing ?DO.

524 unit + 11 compliance + 9 comparison + 5 crypto + 1 bench green;
fmt/clippy clean; core still builds --no-default-features; web
wasm-pack build unchanged.
2026-08-06 12:44:32 +02:00
Oleksandr Kozachuk cda296aab5 feat(codegen): stack under/overflow guards in compiled words (WS-007)
Compiled code could silently move dsp/rsp/fsp out of their stack
regions (e.g. DROP on an empty stack), corrupting later pushes with
no diagnostic -- the addresses stay inside valid linear memory, so
nothing could trap. Host-side checks cannot catch it.

- Guards are emitted at the sp-adjustment choke points (dsp_inc/
  dsp_dec, fsp_inc/fsp_dec, rpush/rpop/rpeek, peek, TwoDup/TwoDrop,
  promoted prologue/epilogue -- DROP never loads its value, so
  guarding pop() alone is not enough). On fault: write the code to
  SYSVAR_FAULT_CODE, call _STACK_FAULT_, which THROWs it -- so
  guards are CATCHable and print standard messages (-3/-4/-5/-6/
  -44/-45).
- The batch/consolidated compile path (all boot primitives) and the
  export path are wired too; a thread-local carries the fault index
  into the shared emission helpers.
- Config: codegen.stack_guards, default ON. `wafer build` output
  defaults OFF (production artifact); WAFER_STACK_GUARDS=0|1
  overrides either. Perf comparison lanes run unguarded.
- Measured overhead in release loops: within noise (never-taken
  branches).
- toolstest.fth baseline 37 -> 38: line 368's bare interpreted `R>`
  used to underflow silently and count as passing; the guard now
  correctly reports -6.
2026-08-05 17:00:22 +02:00
Oleksandr Kozachuk e31407ab58 feat(core): REMEMBER + EMPTY/GILD; marker rollback covers namespace state
- REMEMBER <name>: SwiftForth-style re-runnable marker -- restores
  to just AFTER its own definition and survives execution. The
  edit-reload-test loop: REMEMBER fresh ... fresh ... fresh.
- EMPTY rolls back to the boot dictionary; GILD re-baselines EMPTY
  to the current state. Baseline captured at VM construction.
- MarkerState now also snapshots search order, wid allocation,
  compilation wordlist, REPLACES table, and ABORT" texts; restore
  discards marker entries newer than the snapshot (was: newer than
  the executing marker id only).
- Shared snapshot_marker_state/apply_marker_state used by MARKER,
  REMEMBER, EMPTY, and GILD.

Known ambiguity (standard-conformant): a DEFER defined before a
marker but retargeted at a word defined after it dangles after
rollback; stale function-table slots are name-unreachable and get
overwritten by later definitions.
2026-08-05 16:22:47 +02:00
Oleksandr Kozachuk 2910884b83 fix(repl): multi-line output starts on its own line, inline ok only for single-line 2026-08-05 16:07:10 +02:00
Oleksandr Kozachuk f584066a0a feat(repl): usability batch - errors, WORDS, .S, BYE, DUMP, history
Core:
- Uncaught THROW prints its standard message ("Stack underflow
  (throw -4)"; unknown codes as "Catch = <n>") instead of the
  "forth-throw" sentinel. ABORT" text is carried as a structured
  payload and shown only when the -2 throw goes uncaught -- CATCH
  stays silent and the payload cannot go stale. ABORT throws -1
  through the same path.
- WORDS: optional same-line substring filter (WORDS FDEPTH), skips
  internal words (new INTERNAL header flag, set at create for
  underscore-prefixed names), wraps at 78 columns, prints a count.
  ORDER names wids (FORTH / wid#N) instead of Rust debug output.
- .S honors BASE. New: F.S (float stack), DUMP (hex+ASCII,
  bounds-checked, 4K cap), ? (fetch-and-print, boot.fth). BYE is a
  real word now: sets a VM flag the driver honors (exits REPL,
  stops rest of line/file).

CLI:
- Persistent history (~/.local/state/wafer/history, 0600 perms,
  $WAFER_HISTORY override), Tab completion over the live dictionary
  (snapshot refreshed after each line), Up/Down do prefix history
  search, Ctrl-C clears the line instead of exiting.

Web:
- History survives reloads (localStorage, cap 200, dedup, init-code
  runs excluded), User Words palette populated via new words()
  export, stack bar annotates non-decimal BASE, base() reads the
  real BASE sysvar instead of returning a hardcoded 10.
2026-08-05 14:57:12 +02:00
Oleksandr Kozachuk 4980648982 feat(bench): SwiftForth sf64 lane in cross-engine performance report
CI / check (push) Has been cancelled
sf64 discovery + stdin runner (no -e flag; input lines truncate at
~256 chars, so one statement per line), ucounter-based µs timing —
same wrapper shape as gforth utime. New sf64 + WAFER/sf columns,
informational only (no regression limit). Justfile: bench-compare
target; CARGO_PROFILE_RELEASE_STRIP=none for Darwin 27 dlopen bug.
v0.1.0
2026-08-04 17:07:47 +02:00
Oleksandr Kozachuk 31dc6c6397 fix(core): no SystemTime on wasm32 — fixed boot seed + UTIME Forth error
CI / check (push) Has been cancelled
2026-07-29 16:56:31 +02:00
Oleksandr Kozachuk 35b78193fd feat(boot): add -ROT <= >= gforth extensions
CI / check (push) Has been cancelled
Non-standard but ubiquitous words; absence aborted otherwise-valid
gforth programs with unknown-word errors.
2026-07-18 15:40:39 +02:00
ok2 d5acdc0e7b fix: Rust 1.95 clippy — match guards + map_or
CI / check (push) Has been cancelled
Rust 1.95 promoted collapsible_match and map_unwrap_or; CI runs
-D warnings so they break the build. Collapse nested `if`s into
match guards across codegen/optimizer/export, and swap
map().unwrap_or(..) for map_or / is_ok_and.
2026-04-21 17:00:21 +02:00
ok2 a66435c93c bat syntax: sync with (LOCAL), quotations, structures, hashes
CI / check (push) Has been cancelled
The syntax file landed in bcccdfb, one commit before `(LOCAL)` and
while several other recently-added words were already in the tree but
unhighlighted. Extend it to cover everything currently registered.

Added contexts:
- `locals` — `{:` `:}` `{F:` `TO` `LOCALS|` `END-LOCALS` `(LOCAL)`.
- `structures` — `BEGIN-STRUCTURE` (captures the following name),
  `END-STRUCTURE`, `+FIELD`, `FIELD:`, `CFIELD:`, `FFIELD:`,
  `SFFIELD:`, `DFFIELD:`.
- `hashing` — `SHA1`, `SHA256`, `SHA512`. Comment notes the list
  mirrors `crypto::ALGOS`.

Extended:
- `definitions` — quotations `[:` / `;]` (Core-ext 6.2.0455).
- `parsing` — state-smart `S` (the string parser from d1a7d55).
- `wafer_extras` — `READ-PASSWORD` (web-side prompter from 9150696).

Context order in `main:` keeps `definitions` ahead of `locals`, so
`: foo` still wins over `{:` / `:}`, and `strings` / `arithmetic`
stay ahead of `parsing` so `S"` and `S>D` keep their existing
highlighting despite the new bare-`S` rule.
2026-04-20 12:40:31 +02:00
ok2 bb217714ac Add (LOCAL) per Forth 2012 §13.6.1.0086
Implement `(LOCAL)` as a host primitive that defers its effect to the
outer-interpreter compile state via two new `PendingAction` variants:

  - `DeclareLocal(name)` — a non-sentinel `(LOCAL)` call with `u > 0`
    appends the name to `compiling_locals` as an int local.
  - `DeclareLocalEnd` — the `0 0 (LOCAL)` sentinel emits reverse-order
    `ForthLocalSet` IR for the batch declared since the last sentinel,
    reusing the same IR shape as the `{: ... :}` locals flow.

`local_batch_base` tracks where the current batch started; it is
saved/restored across nested compile frames and cleared on
`finish_colon_def`. Int-only, per spec — float locals remain `{F: :}`.

Also fix `\` per §6.2.2535: parse-and-discard must stop at the next
`\n`, not at `#TIB`. Under line-wrapped `evaluate` calls (common in
test files) the old behaviour consumed the trailing `;` of a multi-line
`:` definition, silently leaving state in compile mode.

Tighten `compliance.rs`: `load_file` now returns a line-failure count,
every prerequisite is asserted against `expected_load_failures(path)`,
and a new `load_file_whole` handles multi-line definitions (`DOES>`
split across lines in `errorreport.fth`) that the per-line loader
cannot stitch. Baselines document known gaps for `core.fr` (nested
`:`, SOURCE/>IN via EVALUATE), `coreexttest.fth` (SAVE-INPUT, `.(`
inside `[...]`), `exceptiontest.fth` (one garbled parse after
CATCH/THROW source stacking), and `toolstest.fth` (37 `\?`-guarded
lines where `SOURCE >IN ! DROP` fails to skip under per-line
`evaluate`). Each entry is a tech-debt ledger item, not an allowlist.

Regression tests: LT32 (the localstest case that silently skipped
before `(LOCAL)` existed), the `0 0 (LOCAL)` sentinel-only no-op, a
multi-line `:` followed by `VARIABLE` after a `\` comment, and a
direct `\` stops-at-newline case.

Incidental: clear two `implicit_clone` clippy lints in the RANDOM
determinism test (`.to_vec()` → `.clone()`).
2026-04-18 17:12:02 +02:00
ok2 67448caa9c chore: clear pre-existing clippy + fmt in crypto tests
Fix rustfmt drift and two clippy lints (`doc_markdown` missing
backticks around `NativeRuntime`) that surfaced after the Rust 1.94
toolchain update. No functional change.
2026-04-18 17:11:28 +02:00
ok2 bcccdfb49d Add bat syntax for WAFER / Forth 2012
Ship tools/editor-support/bat/WAFER.sublime-syntax so any bat user
(including oked, which probes bat first) renders .fth files with
proper keyword colouring, including the WAFER extras CONSOLIDATE,
RANDOM, RND-SEED, and UTIME.

Keyword list derives from register_primitive/register_host_primitive
calls in crates/core/src/outer.rs plus the boot.fth definitions.
Internal underscore-prefixed words are deliberately omitted.

Install with `just install-syntax`.
2026-04-17 11:22:14 +02:00
ok2 be5dff243f fix: locals beat hardcoded tokens in compile_token
compile_token matched hardcoded tokens (S, ." etc) before
checking compiling_locals. Local named `s` got hijacked by
the `S` string shortcut. Forth 2012 §13.3.3.2 — locals
supersede dict names in scope. Move locals check to top of
compile_token for uniform precedence.

Tests: S-hijack repro, get+set round-trip, int-uninit pipe
syntax coverage (`{: | name :}`).
2026-04-17 10:40:19 +02:00
ok2 49582f7e86 docs: rewrite architecture.txt + fix mem offsets
architecture.txt drifted from code: missing HASH_SCRATCH region,
runtime-trait box, wordlists/search-order, codegen locals layout,
F: locals, quotations, crypto. Rewrite from current source.

memory.rs `// 0x...` annotations were the drift source — RETURN
/ FLOAT / HASH / DICT bases printed values disagreeing with the
const arithmetic. Recompute and correct.
2026-04-16 20:51:12 +02:00
ok2 1a8f27b5bd Add F: float locals (gforth/SwiftForth-style)
`{: F: x F: y :}` now declares float-typed locals that live on the float
stack. `x x F* y y F* F+ FSQRT` writes real float code without manual
FSTACK juggling — previously WAFER had a 100%-compliant float wordset
but no way to name intermediate float values.

New IR ops `ForthFLocalGet(n)` / `ForthFLocalSet(n)` alongside the
existing int-local ops. Each kind has its own index namespace so mixed
declarations like `{: n F: f :}` compose cleanly. Codegen allocates f64
WASM locals after the existing f64 scratch pair; the fsp-bridge logic
mirrors the existing FDup/FSwap path.

Outer interpreter tracks a parallel `compiling_local_kinds` alongside
`compiling_locals` (keeps the 18 existing touch-points unchanged) and
extends `{:` to recognize `F:` as a per-next-name type marker. `TO` and
name resolution branch on kind to pick Int vs Float get/set ops.

Four tests: classic hypot, TO round-trip, mixed int/float args, and
uninitialized float via `|`. Inline-inhibit for the new ops added to
optimizer and is_promotable so they don't sneak into contexts that
would collide with the caller's WASM locals.
2026-04-15 21:29:01 +02:00
ok2 6771f5d46b Add quotations [: ... ;] (Forth 2012 Core-ext 6.2.0455)
State-smart anonymous xt builder. Interpret mode leaves the xt on the
data stack; compile mode emits a literal push into the enclosing word,
so `: APPLY EXECUTE ;  [: 1 2 + ;] APPLY` prints 3.

Supported nested inside colon definitions via a new compile-frame stack
(`Vec<CompileFrame>`). Each frame snapshots `compiling_name`,
`compiling_word_id`, `compiling_word_addr`, `compiling_ir`,
`control_stack`, `saw_create_in_def`, `compiling_locals`, and `state`.
The inner [: ... ;] compiles its body as an anonymous word; on ;] the
outer frame pops back and the xt is either pushed to the data stack
(interpret mode) or compiled as a literal (compile mode).

Also fixes a latent bug: `finish_colon_def` used to reveal `latest`,
which breaks when intermediate dict entries (now including quotations)
move `latest`. Each definition now tracks its own `compiling_word_addr`
and uses `reveal_at`, matching the existing DOES> pattern.

Five tests cover interpret, compile, inside-a-colon-def, two-level
nesting, and the control-stack-travels-with-frame regression (outer
IF/ELSE/THEN must still match around an inner [: ;]).
2026-04-15 21:18:02 +02:00
ok2 64f4b1e857 boot: add structure words (Facility-ext 10.6.2.0935)
BEGIN-STRUCTURE, END-STRUCTURE, +FIELD, FIELD:, CFIELD:, FFIELD:,
SFFIELD:, DFFIELD: — the Forth 2012 structure-definition family plus
the float-typed variants for symmetry with WAFER's float wordset.

Each defining word carries its own inline CREATE .. DOES> — factoring
through a shared +FIELD helper doesn't work in WAFER, because DOES>-
defining words only dispatch at the outer interpreter, not from compiled
IR. So FIELD: can't call +FIELD and have the DOES> action fire; each
FIELD:/CFIELD:/... repeats the pattern directly.

Three tests cover size computation, field offsets, and mixed cell + char
fields with alignment.
2026-04-15 20:50:29 +02:00
ok2 f1752ededa Add RANDOM / RND-SEED — xorshift64 PRNG
Non-standard but ubiquitous in gforth/SwiftForth/VFX. Adds a shared
rng_state on ForthVM, seeded from nanosecond wall-clock at boot.
`RANDOM ( -- u )` returns a 32-bit pseudo-random cell; `RND-SEED ( u -- )`
reseeds, with 0 forced to a nonzero constant to avoid xorshift's fixed
point.

Three tests cover determinism after seeding, distinct-value spread
across 1000 pulls, and the zero-seed safeguard.
2026-04-15 20:31:48 +02:00
ok2 d1a7d55051 boot: fix S interpret-mode — copy string out of TIB
`S name` in interpret mode used to leave (c-addr u) pointing into the
input buffer, so the next REFILL clobbered the bytes. Typing `s test`
then `type` on a fresh line printed "pest" because the new input
overwrote the first chars of the old TIB content.

Move `S` from boot.fth to the Rust outer interpreter alongside `S"` /
`C"`: both interpret and compile modes now copy the token to HERE-space
(stable across REFILL). Compile-mode output is still bit-identical to
writing `S" name"` inline.

Adds `test_s_interpret_survives_refill` regression.
2026-04-15 19:49:51 +02:00
ok2 1b8f4835d6 boot: add S — state-smart parse-next-token-as-string
`S name` is the string analogue of `[CHAR] x` and `['] name`: parses the
next whitespace-delimited token, state-smart.

  Interpret: leaves ( c-addr u ) pointing into the input buffer.
  Compile:   appends run-time push of the copied bytes (identical code
             to writing S" name" inline).

One line in boot.fth, leverages the existing PARSE-NAME + SLITERAL.
Zero runtime overhead inside : definitions.
2026-04-15 19:28:26 +02:00
ok2 9150696807 wafer-web: add set_prompter for a JS-backed READ-PASSWORD
Browser consumers (kelvar) need a host-provided password prompt so the
master never appears on the command line. Exposes a single method:

    WaferRepl::set_prompter(js_sys::Function) -> Result<(), JsError>

Given a JS function `(prompt: string) => string`, registers it as the
Forth word `READ-PASSWORD` with stack effect

    ( prompt-addr prompt-u -- pw-addr pw-u )

The returned bytes land in WAFER's PAD region. Enforces PAD_SIZE-1 as
a hard upper bound — a silent truncation would cause a derived password
to mismatch the one used during setup, which is exactly the failure
mode we are trying to avoid.

`js_sys::Function` is !Send/!Sync but `HostFn` requires both. In a
browser WASM build there is only ever one thread, so wrap it in
`send_wrapper::SendWrapper`, which panics if accessed off-thread — an
honest guard rather than a lie.
2026-04-15 13:30:12 +02:00
ok2 55caf38ab5 Add extensible hash primitives: SHA1, SHA256, SHA512
Introduces a `crypto` feature (on by default) that wires the RustCrypto
sha1/sha2 crates into a small `HashAlgo` registry. `register_primitives`
iterates `crypto::ALGOS` and installs one Forth host word per algorithm,
each with the stack effect

    ( c-addr u -- c-addr2 u2 )

reading `u` bytes from `c-addr` and writing the digest into a shared
`HASH_SCRATCH` region in linear memory (carved out between the float
stack and the dictionary).

Adding a new hash is a one-line entry in `ALGOS`. `register_host_primitive`
is now `pub` so downstream crates can extend the VM with their own I/O
host words without forking WAFER — kelvar (a deterministic password
manager on WAFER) is the first consumer.

- 4 unit tests (lib-level sha1/256/512 + registry sanity)
- 5 integration tests (in-VM `SHA1`/`SHA256`/`SHA512` against RFC-3174,
  FIPS-180, and the first-round S/KEY seed used by `hel`)
- All 437 existing lib tests still pass; `wafer-web` still builds for
  `wasm32-unknown-unknown` with the feature enabled
2026-04-14 22:08:04 +02:00