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.
This commit is contained in:
+2
-1
@@ -5,7 +5,7 @@ All notable changes to WAFER are documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [0.2.7] - 2026-08-09
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
@@ -340,6 +340,7 @@ compliance suite, `CONSOLIDATE` whole-program recompilation, `wafer build`
|
|||||||
AOT export (WASM / native / JS loader), browser REPL, SHA-1/256/512 words,
|
AOT export (WASM / native / JS loader), browser REPL, SHA-1/256/512 words,
|
||||||
and cross-engine benchmark lanes against gforth and SwiftForth.
|
and cross-engine benchmark lanes against gforth and SwiftForth.
|
||||||
|
|
||||||
|
[0.2.7]: https://github.com/ok2/wafer/compare/v0.2.6...v0.2.7
|
||||||
[0.2.1]: https://github.com/ok2/wafer/compare/v0.2.0...v0.2.1
|
[0.2.1]: https://github.com/ok2/wafer/compare/v0.2.0...v0.2.1
|
||||||
[0.2.0]: https://github.com/ok2/wafer/compare/v0.1.0...v0.2.0
|
[0.2.0]: https://github.com/ok2/wafer/compare/v0.1.0...v0.2.0
|
||||||
[0.1.0]: https://github.com/ok2/wafer/releases/tag/v0.1.0
|
[0.1.0]: https://github.com/ok2/wafer/releases/tag/v0.1.0
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## What is WAFER?
|
## What is WAFER?
|
||||||
|
|
||||||
WAFER (WebAssembly Forth Engine in Rust) is an optimizing Forth 2012 compiler targeting WebAssembly. Currently a working Forth system with 200+ words, JIT compilation, 12 word sets at 100% compliance, and a full optimization pipeline (peephole, constant folding, inlining, strength reduction, DCE, tail calls, stack-to-local promotion with loop/IF support, self-recursive direct calls, consolidation). Beats gforth on all benchmarks in release mode. Includes a browser-based REPL via wasm-pack.
|
WAFER (WebAssembly Forth Engine in Rust) is an optimizing Forth 2012 compiler targeting WebAssembly. Currently a working Forth system with 200+ words, JIT compilation, 12 word sets at 100% compliance, and a full optimization pipeline (peephole, constant folding, inlining, strength reduction, DCE, tail calls, per-region stack-to-local promotion with DO/BEGIN loop and IF support, self-recursive direct calls, a typed calling convention for words with a known stack effect, consolidation). Beats gforth on all benchmarks in release mode, and SwiftForth `sf64` on four of five. Includes a browser-based REPL via wasm-pack.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ Handle in `interpret_token_immediate()` or `compile_token()` as a special case.
|
|||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
- Run `cargo test --workspace` before committing (currently 562 unit + 1 benchmark + 11 compliance + 9 comparison + 5 crypto)
|
- Run `cargo test --workspace` before committing (currently 601 unit + 1 benchmark + 12 compliance + 9 comparison + 5 crypto)
|
||||||
- Forth 2012 compliance: `cargo test -p wafer-core --test compliance`
|
- Forth 2012 compliance: `cargo test -p wafer-core --test compliance`
|
||||||
- Cross-engine comparison (vs gforth): `cargo test -p wafer-core --test comparison`
|
- Cross-engine comparison (vs gforth): `cargo test -p wafer-core --test comparison`
|
||||||
- Performance benchmarks (release mode): `cargo test -p wafer-core --test comparison -- --nocapture --ignored`
|
- Performance benchmarks (release mode): `cargo test -p wafer-core --test comparison -- --nocapture --ignored`
|
||||||
|
|||||||
Generated
+3
-3
@@ -1589,7 +1589,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wafer"
|
name = "wafer"
|
||||||
version = "0.2.6"
|
version = "0.2.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -1600,7 +1600,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wafer-core"
|
name = "wafer-core"
|
||||||
version = "0.2.6"
|
version = "0.2.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"insta",
|
"insta",
|
||||||
@@ -1615,7 +1615,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wafer-web"
|
name = "wafer-web"
|
||||||
version = "0.2.6"
|
version = "0.2.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ members = ["crates/*"]
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.2.6"
|
version = "0.2.7"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = "https://github.com/ok2/wafer"
|
repository = "https://github.com/ok2/wafer"
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ Call-heavy code is what this pays for -- Fibonacci went from 4.3x slower than `s
|
|||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# All tests (~620 currently passing)
|
# All tests (~628 currently passing)
|
||||||
cargo test --workspace
|
cargo test --workspace
|
||||||
|
|
||||||
# Forth 2012 compliance suite
|
# Forth 2012 compliance suite
|
||||||
@@ -142,7 +142,7 @@ Forth Source -> Outer Interpreter -> IR -> [Optimize] -> WASM Codegen (wasm-enco
|
|||||||
- `WebRuntime` — browser WebAssembly API via js-sys, for the browser REPL
|
- `WebRuntime` — browser WebAssembly API via js-sys, for the browser REPL
|
||||||
- **Subroutine threading** via WASM function tables (`call_indirect` for cross-word, direct `call` for self-recursion)
|
- **Subroutine threading** via WASM function tables (`call_indirect` for cross-word, direct `call` for self-recursion)
|
||||||
- **JIT mode**: each new word compiles to a separate WASM module linked to shared memory/globals/table
|
- **JIT mode**: each new word compiles to a separate WASM module linked to shared memory/globals/table
|
||||||
- **IR-based pipeline** with 6 optimization passes (peephole, constant folding, strength reduction, DCE, tail call detection, inlining) plus stack-to-local promotion (with loop and IF/ELSE support), DO/LOOP index locals, and consolidation
|
- **IR-based pipeline** with 6 optimization passes (peephole, constant folding, strength reduction, DCE, tail call detection, inlining) plus per-region stack-to-local promotion (DO and BEGIN loops, IF/ELSE), DO/LOOP index locals, typed entry points for words with a known stack effect, and consolidation
|
||||||
- **Dictionary**: linked-list word headers in simulated linear memory
|
- **Dictionary**: linked-list word headers in simulated linear memory
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ license.workspace = true
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
wafer-core = { path = "../core", version = "0.2.6" }
|
wafer-core = { path = "../core", version = "0.2.7" }
|
||||||
wasmtime = { workspace = true }
|
wasmtime = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ workspace = true
|
|||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
wafer-core = { path = "../core", version = "0.2.6", default-features = false, features = ["crypto"] }
|
wafer-core = { path = "../core", version = "0.2.7", default-features = false, features = ["crypto"] }
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
js-sys = "0.3"
|
js-sys = "0.3"
|
||||||
send_wrapper = { workspace = true }
|
send_wrapper = { workspace = true }
|
||||||
|
|||||||
+45
-13
@@ -14,7 +14,7 @@ This document describes every optimization that makes sense for WAFER, why it ma
|
|||||||
|
|
||||||
| # | Optimization | Level | Status | Impact |
|
| # | Optimization | Level | Status | Impact |
|
||||||
| -- | -------------------------- | ------------ | ----------- | ------- |
|
| -- | -------------------------- | ------------ | ----------- | ------- |
|
||||||
| 1 | Stack-to-Local Promotion | Codegen | Phase 2 | Highest |
|
| 1 | Stack-to-Local Promotion | Codegen | Phase 4 | Highest |
|
||||||
| 2 | Peephole Optimization | IR pass | Done | High |
|
| 2 | Peephole Optimization | IR pass | Done | High |
|
||||||
| 3 | Constant Folding | IR pass | Done | High |
|
| 3 | Constant Folding | IR pass | Done | High |
|
||||||
| 4 | Inlining | IR pass | Done | High |
|
| 4 | Inlining | IR pass | Done | High |
|
||||||
@@ -29,12 +29,18 @@ This document describes every optimization that makes sense for WAFER, why it ma
|
|||||||
| 13 | Startup Batching | Architecture | Done | Low |
|
| 13 | Startup Batching | Architecture | Done | Low |
|
||||||
| 14 | Self-Recursive Direct Call | Codegen | Done | High |
|
| 14 | Self-Recursive Direct Call | Codegen | Done | High |
|
||||||
| 15 | Float / Double-Cell | Codegen | Not started | Future |
|
| 15 | Float / Double-Cell | Codegen | Not started | Future |
|
||||||
|
| 16 | Typed Calling Convention | Codegen | Done | Highest |
|
||||||
|
|
||||||
## 1. Stack-to-Local Promotion
|
## 1. Stack-to-Local Promotion
|
||||||
|
|
||||||
**Status: Phase 2 done.** Words with straight-line code, DO/LOOP, and IF/ELSE use WASM locals instead of memory stack. Stack manipulation ops (Swap, Rot, Nip, Tuck, Dup, Drop) emit zero WASM instructions. Loop index/limit kept in WASM locals (zero return stack traffic). Switchable via `WaferConfig::codegen.stack_to_local_promotion`.
|
**Status: Phase 4 done.** Straight-line code, DO/LOOP, IF/ELSE and the BEGIN loop family use WASM locals instead of the memory stack, per region rather than per word. Stack manipulation ops (Swap, Rot, Nip, Tuck, Dup, Drop) emit zero WASM instructions. Loop index/limit stay in WASM locals (zero return stack traffic). Switchable via `WaferConfig::codegen.stack_to_local_promotion`.
|
||||||
|
|
||||||
Phase 1 covered straight-line code only. Phase 2 extends to DO/LOOP (with stack-neutrality check) and IF/ELSE/THEN (with equal-branch-effect check). BEGIN loops and BeginDoubleWhileRepeat are not yet promoted.
|
- **Phase 1** — straight-line code.
|
||||||
|
- **Phase 2** — DO/LOOP (stack-neutrality check) and IF/ELSE/THEN (equal-branch-effect check).
|
||||||
|
- **Phase 3** — _per region instead of per word_. Promotion used to be all-or-nothing: one `.`, `CR`, `>R` or host call anywhere in a definition put the entire body on the memory stack, hot loops included, which costs 2.2 ns per loop-carried add instead of 0.31 — the accumulator round-trips through store-to-load forwarding rather than staying in a register. `emit_body` now partitions a body into maximal promotable stretches and runs the simulator over each, loading what a region reads and writing back what it leaves. A region may only use `I` / `J` when the DO loops naming them are inside it, and a straight-line region needs at least three operations to pay for its prologue and epilogue; a loop always does.
|
||||||
|
- **Phase 4** — `BEGIN..UNTIL`, `BEGIN..AGAIN` and `BEGIN..WHILE..REPEAT`, when the construct is provably stack-neutral: UNTIL's body nets +1 (the flag it consumes), AGAIN's nets 0, and for WHILE..REPEAT the test and the body must balance _separately_, because WHILE leaves the loop between them and a net that only added up over the pair would give the two exits different stack shapes. Bodies containing an `EXIT` stay out, the same rule DO/LOOP follows.
|
||||||
|
|
||||||
|
Still not promoted: `BeginDoubleWhileRepeat`, `>R`/`R>`, floats, `{: :}` locals, `SP@`/`DEPTH`/`EXECUTE`, and the flat forward-block IR ops.
|
||||||
|
|
||||||
### The Problem
|
### The Problem
|
||||||
|
|
||||||
@@ -452,33 +458,59 @@ Fibonacci(25) with ~243K recursive calls:
|
|||||||
|
|
||||||
The optimization is implemented in `emit_op` for `IrOp::Call`: when `ctx.self_word_id == Some(word_id)`, emit `call WORD_FUNC` (function index 1 in the word's own module). The `self_word_id` is derived from `CodegenConfig::base_fn_index`.
|
The optimization is implemented in `emit_op` for `IrOp::Call`: when `ctx.self_word_id == Some(word_id)`, emit `call WORD_FUNC` (function index 1 in the word's own module). The `self_word_id` is derived from `CodegenConfig::base_fn_index`.
|
||||||
|
|
||||||
|
The numbers above are the state before section 16: they measure the call instruction, and what dominated turned out to be the calling _convention_ around it. A self-recursive word that is also typed now calls its own fast entry instead, and Fibonacci(25) is 356 microseconds rather than 1.6 ms.
|
||||||
|
|
||||||
## 15. Float and Double-Cell Stack
|
## 15. Float and Double-Cell Stack
|
||||||
|
|
||||||
**Status: Not started.** `PushI64` and `PushF64` exist as IR ops but are stubs in codegen. Float stack operations are currently all host functions.
|
**Status: Not started.** `PushI64` and `PushF64` exist as IR ops but are stubs in codegen. Float stack operations are currently all host functions.
|
||||||
|
|
||||||
The float stack lives in its own memory region (0x2540--0x2D40). Float operations will have the same memory-based overhead as integer operations, but worse: `f64` values are 8 bytes, doubling the memory traffic per push/pop. Stack-to-local promotion (section 1) is even more impactful for floats because WASM has native `f64` locals and operand stack support.
|
The float stack lives in its own memory region (0x2540--0x2D40). Float operations will have the same memory-based overhead as integer operations, but worse: `f64` values are 8 bytes, doubling the memory traffic per push/pop. Stack-to-local promotion (section 1) is even more impactful for floats because WASM has native `f64` locals and operand stack support.
|
||||||
|
|
||||||
|
## 16. Typed Calling Convention
|
||||||
|
|
||||||
|
**Status: Done.** A word whose stack effect is statically known compiles to two entry points: a fast one with signature `(i32 x p) -> (i32 x q)`, carrying its stack items as WASM values, and the usual `( -- )` wrapper that moves those items on and off the memory data stack. The wrapper keeps the function-table slot, so `EXECUTE`, the outer interpreter, host words and `CATCH` see exactly the ABI they saw before; only direct calls inside a module take the fast entry. `WAFER_TYPED_CALLS=0` falls back.
|
||||||
|
|
||||||
|
### The Problem
|
||||||
|
|
||||||
|
This is what the SwiftForth gap was made of. sf64 keeps TOS in `RBX` and the stack pointer in `RBP`, and both survive a `CALL` untouched, so its `FIB` is 16 instructions and about 7 memory touches per node. WAFER kept the whole stack in linear memory and flushed its cached `$dsp` to an imported global before every call: about 36 touches. Section 1's simulator, which already promoted loop and `IF` bodies into locals, refused any body containing a call or an `EXIT` -- exactly the words where the convention cost the most.
|
||||||
|
|
||||||
|
### The Effect Fixpoint
|
||||||
|
|
||||||
|
Self-recursion makes the stack-effect equation circular (`d = k + m*d`), so the effect is solved by iterating a guess until it reproduces itself: `FIB` settles on `(1,1)` in two rounds, while `: F 1 RECURSE ;` never settles and stays untyped. `CONSOLIDATE` extends this across words, since it puts them all in one module: the effects are solved from the leaves outward, and 105 of 187 words in a booted dictionary end up typed.
|
||||||
|
|
||||||
|
### Impact
|
||||||
|
|
||||||
|
Fibonacci(25) went from 1035 to 366 microseconds, 4.3x slower than `sf64` to 1.2x. Stack guards became nearly free as a side effect -- they hang off the memory-stack push/pop choke points, and a typed word barely has any -- so the default guards-on configuration that the REPL and the web build use went from 1631 to 365 microseconds on the same benchmark.
|
||||||
|
|
||||||
|
Untyped by design: anything using `SP@`, `DEPTH`, `EXECUTE`, `>R`/`R>`, floats or locals; anything calling a word that is itself untyped, which in the JIT path means every call except `RECURSE`; mutually recursive words; and words whose effect is not static -- branches that disagree on depth, `EXIT` at the wrong depth, a non-neutral loop body, or a recursion that grows the stack per level.
|
||||||
|
|
||||||
## Current Performance vs Gforth
|
## Current Performance vs Gforth
|
||||||
|
|
||||||
All optimizations enabled, release mode, measured with UTIME:
|
All optimizations enabled, release mode, measured with UTIME:
|
||||||
|
|
||||||
```
|
```
|
||||||
Benchmark WAFER CONSOL gforth WAFER/gf
|
Benchmark WAFER CONSOL gforth sf64 WAFER/gf WAFER/sf
|
||||||
Fibonacci(25) 1629 1535 3422 0.45x
|
Fibonacci(25) 356 361 3389 287 0.11x 1.24x
|
||||||
Factorial(12)x10K 340 339 638 0.53x
|
Factorial(12)x100K 479 495 6249 1650 0.08x 0.29x
|
||||||
GCD-bench(500) 18 15 30 0.50x
|
GCD-bench(20K) 540 559 1801 801 0.30x 0.67x
|
||||||
NestedLoops(50) 84 73 720 0.10x
|
NestedLoops(50)x1K 509 501 7023 1887 0.07x 0.27x
|
||||||
Collatz(2K) 1212 1202 3914 0.31x
|
Collatz(2K) 185 213 3873 610 0.05x 0.30x
|
||||||
```
|
```
|
||||||
|
|
||||||
Times in microseconds. WAFER/gf < 1.0 means WAFER is faster.
|
Times in microseconds. WAFER/gf < 1.0 means WAFER is faster. `sf64` is SwiftForth,
|
||||||
|
which compiles to native code; two caveats on that column. The install here is an
|
||||||
|
x86-64 binary under Rosetta 2 while WAFER and gforth are native arm64, so it is a
|
||||||
|
native-vs-emulated comparison and a native SwiftForth would be faster than these
|
||||||
|
numbers; and sf64 uses 64-bit cells to WAFER's 32-bit. WAFER is ahead on the four
|
||||||
|
loop-heavy benchmarks and behind on Fibonacci, which is one call per node with no
|
||||||
|
loop to promote.
|
||||||
|
|
||||||
## Remaining Opportunities
|
## Remaining Opportunities
|
||||||
|
|
||||||
| Optimization | Status | Potential Impact |
|
| Optimization | Status | Potential Impact |
|
||||||
| -------------------------------- | ------------------- | ----------------------------------------------------- |
|
| -------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| BEGIN loop promotion | Not started | Would speed up GCD-style tight loops further |
|
| Bounded self-inlining | Not started | Measured 1.33x on Fibonacci, the last benchmark behind sf64. Blocked on `EXIT`: the inliner refuses any body containing one, and a recursive Forth word is `... IF EXIT THEN ... RECURSE`. Needs either a scoped exit (compile an inlined `EXIT` as a branch to the end of a block) or guard-only expansion |
|
||||||
| BeginDoubleWhileRepeat promotion | Not started | Rare pattern, low priority |
|
| BeginDoubleWhileRepeat promotion | Not started | Rare pattern, low priority. Its promoted emitter exists but has no loop fixup and is unverified |
|
||||||
| LEAVE as IR primitive | Not started | Would enable fast-path for loops with LEAVE |
|
| LEAVE as IR primitive | Not started | Would enable fast-path for loops with LEAVE |
|
||||||
| Float stack-to-local | Not started | Eliminate float stack memory traffic |
|
| Float stack-to-local | Not started | Eliminate float stack memory traffic |
|
||||||
| WASM tail calls proposal | Waiting on wasmtime | Would eliminate stack growth for tail-recursive words |
|
| WASM tail calls proposal | Waiting on wasmtime | Would eliminate stack growth for tail-recursive words |
|
||||||
|
|||||||
+4
-2
@@ -282,11 +282,13 @@ When the compiler encounters a word reference during compilation, it emits:
|
|||||||
(call_indirect (type $void) (table 0)) ;; indirect call through the table
|
(call_indirect (type $void) (table 0)) ;; indirect call through the table
|
||||||
```
|
```
|
||||||
|
|
||||||
**Self-recursive optimization**: When a word calls itself (RECURSE), the codegen detects this and emits a direct `call` instead of `call_indirect`, eliminating the table lookup and signature check (~3x faster for recursive words like Fibonacci).
|
**Self-recursive optimization**: When a word calls itself (RECURSE), the codegen detects this and emits a direct `call` instead of `call_indirect`, eliminating the table lookup and signature check (~3x faster for recursive words like Fibonacci). When the word is also typed, that direct call goes to its fast entry -- see below.
|
||||||
|
|
||||||
**After CONSOLIDATE**: All `call_indirect` between words in the consolidated module are replaced with direct `call` instructions, giving similar benefits for cross-word calls.
|
**After CONSOLIDATE**: All `call_indirect` between words in the consolidated module are replaced with direct `call` instructions, giving similar benefits for cross-word calls.
|
||||||
|
|
||||||
At runtime, wasmtime resolves the table entry and calls the target function. Because all functions share the same memory, globals, and table, state passes between words through the data stack in linear memory. There are no function parameters or return values at the WASM level -- everything goes through the stack.
|
At runtime, wasmtime resolves the table entry and calls the target function. Because all functions share the same memory, globals, and table, state passes between words through the data stack in linear memory.
|
||||||
|
|
||||||
|
**Typed entry points**: that last sentence is the default, not the whole story. A word whose stack effect is statically known also gets a _fast_ entry with signature `(i32 x p) -> (i32 x q)`, which takes its arguments as WASM values and returns its results the same way, so they stay in registers across the call instead of round-tripping through linear memory. The `( -- )` function above is then a wrapper around it, and it is the wrapper that keeps the table slot -- so `EXECUTE`, the outer interpreter, host words and `CATCH` see the memory ABI unchanged. Only a direct call inside the same module takes the fast entry: `RECURSE` in the JIT path, and every resolvable call after `CONSOLIDATE`. See [OPTIMIZATIONS.md](OPTIMIZATIONS.md) section 16.
|
||||||
|
|
||||||
This is subroutine threading: each word is a subroutine, and calling a word is an indirect function call.
|
This is subroutine threading: each word is a subroutine, and calling a word is an indirect function call.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user