Skip to content

Tags: tishlang/tish

Tags

v2.8.2

Toggle v2.8.2's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
perf(native): set-optimistic param-numeric fixpoint (#172) — fannkuch…

… 4.06x → 2.51x (#281)

M4 param inference required every use of a param to be syntactically numeric, but bailed on the
common copy-then-compare shape `let r = n; …; r === n`: `r` wasn't seeded numeric (only literal
inits were), so `r === n` couldn't prove `n` — and `n` → `r` → `count` stayed boxed (the fannkuch
keystone left after #170).

- `collect_numeric_locals` is now a monotone fixpoint: seed literal inits, then also add `let x = y`
  where `y` is already numeric (copy-chains propagate).
- `pi_stmt` runs a per-param OPTIMISTIC fixpoint: assume the candidate param numeric, propagate its
  copies (`let r = n` → `r` numeric), then VERIFY every use of the param (`nus_stmt`) AND every
  copy-descendant (a new conservative `lns_*` local-numeric verifier) is numeric-safe. A genuinely
  non-numeric use — string concat, object store, member access, bare call-arg — still bails, so
  `fn label(x){ return "v=" + x }` keeps `x` dynamic. `lns_*` defaults to bail (`_ => !mentions`),
  so an unhandled form can't launder a wrong type.

Soundness rests on M4's existing contract (caller passes a number for any inferred param; non-number
→ NaN-coercion at the boxed edge), widened monotonically and gated by the copy-descendant verifier.

fannkuch 580ms → 359ms (4.06x → 2.51x vs node; cumulative 8.2x → 2.51x with #170) — `n`/`r` → f64,
`count` → Vec<f64>, the outer `count[r-1]=r` loop native, `r === n` → `r == n`. Still FAIL (does the
same work as node now, just less optimized). Validated: all 21 gauntlet checksums hold
(typed==boxed==node, fannkuch 7319600038), no regression; cargo test --workspace green incl. the
cross-backend mutation.tish/objects.tish differential; +2 param_infer tests (copy-then-compare infers
number; copy-then-string-concat stays dynamic).

v2.8.1

Toggle v2.8.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
perf(native): persist proven array-element types onto the node (#170,…

… fannkuch 2x) (#280)

`si_block` (the type-aware second inference pass) re-derives an unannotated local's type once
array/struct types are known — e.g. `let k = perm[0]` / `let temp = perm[i]` after `perm: number[]`
is inferred — but it only updated the inference `ctx`; the `VarDecl` node kept `type_ann: None`.
Codegen reads the NODE, not `ctx`, so the local stayed a boxed `Value` (with `ops::sub` / type
checks per inner-loop iteration) despite being provably `f64`. Persist a proven `number` type back
onto the node in that second pass.

Soundness: only writes when the node was unannotated and the init proves `number` (not string/etc),
and the codegen demote-gate (`collect_demoted_numeric_locals`) re-boxes any such local whose later
reassignment can escape `number` — so it can never miscompile. This is roadmap #170.

fannkuch 1184ms → 580ms (8.2x → 4.06x vs node) — the inner reversal loop (`k`/`k2`/`temp`) is now
native f64. Also improved object_sum (0.67x→0.33x), nsieve (0.82x→0.69x), matmul (1.0x→0.82x): the
write-back helps any `let x = typedArray[i]`. Not yet a flip — the outer `count[r-1]=r` loop is
still boxed via the param-`n` cascade (a separate keystone fix, FIX 2). Validated: all 21 gauntlet
checksums hold (typed==boxed==node), no regression; cargo test --workspace 370 passed; +2 infer tests.

v2.8.0

Toggle v2.8.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
feat(wasm): --target wasm-gpu — build the WebGPU runtime + start(chun…

…k, host) loader (#277) (#279)

`tish build --target wasm` hardcoded `--features browser` and a `run(chunk)` loader, so the
reflection-based WebGPU bridge that already ships in tishlang_wasm_runtime/src/gpu.rs (behind
`--features gpu`, with a `start(chunk, host)` entry that installs the GPU FFI globals and drives
an rAF loop) was built-but-unreachable from the CLI — a WebGPU project had to hand-roll its own
`cargo build --features gpu` + custom loader.

Add `--target wasm-gpu`: same pipeline as `wasm`, but builds tishlang_wasm_runtime with
`--features gpu` and emits an HTML loader that bootstraps WebGPU (requestAdapter → device, canvas
→ context.configure with the preferred format) into a `host` env object
{ gpu, adapter, device, queue, context, format, canvas, assets } and calls `start(chunk, host)`.
The loader is a working, editable default — its `buildHost()` is the documented host-env contract
(issue option 2), so apps customize the canvas/assets without reverse-engineering gpu.rs. Plain
`--target wasm` is byte-for-byte unchanged.

Threads a `gpu: bool` through compile_to_wasm / compile_program_to_wasm / emit_wasm_from_chunk;
extracts the loader into a pure `loader_html(stem, chunk_b64, gpu)` with unit tests asserting the
gpu loader uses `start`+WebGPU bootstrap and the plain loader uses `run`. Help + unknown-target
error updated. The `gpu` feature builds clean on wasm32-unknown-unknown.

v2.7.6

Toggle v2.7.6's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
perf(native): i32-register loop accumulators — fnv_hash 2.5x faster (…

…to node parity) (#278)

A `number` local proven by the #174 int-range lattice to always hold an integer, and
written ONLY by bitwise/shift reassignments inside a following for-loop, is now kept in
an `i32` register across the loop instead of round-tripping `f64`↔`i32` (`to_int32`, with
its is_finite branch) on every op. Bun/JSC-style: a proven-integer hash accumulator lives
in an integer register, with a single mandatory f64 excursion for the one multiply that
exceeds 2^53 (matching V8's round-then-ToUint32). fnv_hash: 277ms → 108ms vs node ~106ms —
a 2.5x win landing at parity (it does the same f64 multiply V8 does, so parity is the
honest ceiling; the strict typed<=node verdict flaps ±1-2ms run-to-run at the tie).

Mechanism (crates/tish_compile):
- types.rs: new `RustType::I32` (the JS ToInt32 signed bit-pattern view); only the codegen
  loop-var lowering produces it, never `from_annotation`. Boxes as `Value::Number(x as f64)`.
- codegen.rs: `collect_i32_loop_vars` detects the pattern under a STRICT all-or-nothing gate —
  lattice-proven integer (a), not refcell-captured (b), every loop reassignment a fully
  int32-lowerable bitwise chain (c), a forward signedness pass admitting numeric reads only at
  int32-valued points (d, since `^/&/|/<</>>` yield signed int32 but `>>>` yields uint32), and
  no writer outside the loop. Bails to the existing f64 path otherwise (purely additive).
  `emit_int32_operand` reads an I32 leaf as the raw register (no `to_int32`); a non-bitwise
  binop operand coerces `(h as f64)`. A bound-proven leaf (`|x| < 2^62`, finite — proven by a
  conservative `f64_abs_bound`) drops the is_finite guard via an inline `to_int_unchecked::<i64>`.

Among the 21 gauntlet benchmarks only fnv's hash accumulator qualifies (mandelbrot/fannkuch/
queens loop counters use `+`/`++`, not bitwise, so they're untouched). Validated: full gauntlet
all 21 hold typed==boxed==node (fnv checksum 87326928, no regression on the other 20);
cargo test --workspace 368 passed.

v2.7.5

Toggle v2.7.5's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(interp): JSON.parse delegates to the shared parser — string-aware…

… nesting + key order (#276)

The interpreter had its own hand-rolled JSON.parse (eval.rs json_parse_str/_object/_array/_one/
_string). json_parse_one located the end of a nested object/array value by depth-counting `{}`/`[]`
brackets WITHOUT skipping string contents, so any nested value whose string held a bracket —
e.g. `JSON.parse('{"a":{"s":"}"},"b":2}')` — mis-sliced and the entire parse failed to `null`,
diverging from Node and the VM/native/cranelift/wasi backends (which use tishlang_core::json_parse).
It was also O(n^2) (re-scan + re-parse per nested value) and only decoded a subset of escapes
(errored on \u, \/, \b, \f).

Replace the whole chain with a single delegation to tishlang_core::json_parse + core_to_eval — one
spec-correct, string-aware, insertion-ordered source of truth shared with every other backend.
core_to_eval is total for all JSON-producible values, and invalid input still yields null (prior
behavior). Net -180 lines.

Locked in by tests/core/parity_json_key_order (flat/descending/many/nested key order, bracket-in-
string values, unicode escapes, JSON.stringify of the parsed value), verified identical across
interp/vm/rust/cranelift/wasi/node.

v2.7.4

Toggle v2.7.4's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
perf(json): ~2x faster JSON parse/stringify; fix number-format + key-…

…order parity (#180) (#275)

json_roundtrip went 317ms -> 159ms (2.31x -> 1.18x vs node), entirely in the shared
tish_core JSON path (so all backends benefit; codegen flags are irrelevant here). The
verified-sound changes:

- Number stringify: integer fast-path (|n| < 2^53 -> i64 via itoa) with js_number_to_string
  for the rest. Empirically byte-identical to node JSON.stringify (incl -0 -> "0", 1e+21,
  1e-7) — Rust's `{}` Display, used before, diverges. Added js_number_to_string_into so the
  number formatter appends with no per-number String alloc.
- parse_string: escape-free fast-path byte-scans to the closing quote and slices the input in
  one allocation instead of decoding char-by-char (the common case for keys/values).
- parse_object: build the insertion-ordered PropMap directly. Drops a throwaway AHashMap +
  re-collect per object AND fixes a real bug — the AHashMap intermediate scrambled JSON.parse
  key order non-deterministically (its RandomState reseeds per process).
- Per-call key interning: repeated object keys (the shape of an array of records) share one
  Arc<str> instead of re-allocating ~2M times over the run.
- parse_number: integer fast-path (<=15 digits -> i64, exact in f64), preserving -0.0.

Correctness fix (beyond perf): JSON.stringify of -0/1e21/1e-7 and JSON.parse key order now
match node on interp/vm/native/cranelift/js. The interpreter's separate JSON path was aligned
to the shared write_json_number helper. Locked in by the new tests/core/parity_json probe.

Does not fully flip (still 1.18x): the residual is allocation-bound (per-object Rc + per-string
Arc in the parsed tree), which needs a structural change (arena / Value-repr), out of scope here.

v2.7.3

Toggle v2.7.3's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix: -0 ToString is "0" across all backends; inspect keeps "-0" (#247) (

#274)

`(-0).toString()`, `String(-0)`, `"" + (-0)`, and `${-0}` returned "-0" on
interp/vm/native/cranelift, diverging from the JS target (Node), where
ECMAScript Number::toString drops negative zero's sign. The console *inspect*
form correctly keeps "-0" (`console.log(-0)` → `-0`), so the two paths are now
split rather than sharing one formatter:

- tish_core js_number_to_string → "0" for ±0 (the spec ToString), with the
  sign preserved only in Value::to_display_string (inspect); to_js_string gets
  an explicit Number arm so ToString never inherits the inspect form.
- tish_eval Value mirrors the split (Display = inspect "-0", to_js_string = "0").
- tish_opt keeps its duplicated js_number_to_string byte-for-byte in sync so a
  constant-folded `"" + (-0)` matches the runtime ("0").

Locks the behavior into the parity_builtins probe (last open #247 row) + regen
the CI bundle. interp/vm/native/cranelift/js all agree with Node.

v2.7.2

Toggle v2.7.2's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(parser): function span ends at its body's closing brace (#158 fol…

…low-up) (#273)

Companion to the block-span fix: `parse_fun_decl` computed the declaration's
end as `max(peek().start, body.span().end)`. With the body block span now
correct (ends at `}`, #158), the `max` with `peek().start` — the NEXT token
after the function — only ever overran the function span onto the following
line, inflating its folding range and document-symbol range. Use the body's
end directly; a function now spans `fn`/`async` … `}` exactly.

Span-metadata only (no execution change). New parser test pins both the
function span and its body block span to the `}` line. All parser/resolver/
lsp/lint/fmt tests pass.

v2.7.1

Toggle v2.7.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(parser): block span ends at its closing brace, not the next token (

…#158) (#272)

`parse_block` advanced past the closing `}`/`Dedent`, then computed the block
end from `self.peek()` — which by then pointed at the NEXT token. So a block's
span overran onto the following statement. The visible symptom: completion on
the line right after a function's `}` offered the function's params and inner
locals (`collect_block_locals` saw the cursor as still inside the overrun body
span), inserting names that don't resolve.

Fix at the source: capture the closing `}`/`Dedent` token's end BEFORE
advancing and use it as the block end (falling back to the last inner
statement's end for an unterminated block at EOF). A block now spans exactly
`{`..`}`, which is also correct for every other span consumer (folding ranges,
document symbols, hover) — all were getting a too-large range.

Metadata-only: execution is unchanged (verified interp+vm), and the full
cross-backend integration suite, parser/resolver/lint/lsp/fmt/vm/eval unit
tests all pass. New resolver test asserts completion no longer leaks a
function's params/locals onto the line after its closing brace.

v2.7.0

Toggle v2.7.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
feat(lint): add --deny-warnings to gate CI on lint findings (#154) (#271

)

The standalone `tish-lint` could never exit non-zero on an actual lint
finding: both rules emit Severity::Warning and the exit code was gated only on
error_count (which only parse errors reach), so CI couldn't gate on lint
warnings without scraping output. Add `--deny-warnings`: when set, warnings
count toward the non-zero exit alongside errors. Default behavior is unchanged
(warnings print, exit 0). Verified: clean file exits 0; a duplicate-key file
exits 0 by default and 1 with --deny-warnings.