Skip to content

[Goal] Add Rust/WASM acceleration coverage for core functions #349

Description

@mrjf

Goal

Add Rust/WASM acceleration coverage for every eligible tsb core function while keeping the existing TypeScript implementations as the default-compatible fallback.

Current Status

This goal is not complete unless the deterministic gate below passes on the exact branch/commit being marked complete.

As of the 2026-07-02 audit:

Completion Contract

The Goal workflow should add goal-completed and remove goal only when all of the following are true:

  • The repository has a Rust/WASM implementation layer that builds to WebAssembly and can be loaded from a Node/Bun environment.
  • Existing TypeScript core implementations remain present and usable as fallbacks; the public tsb API remains backward compatible for current callers.
  • Every public value export from src/core/index.ts is represented in a machine-checkable Rust/WASM coverage manifest.
  • Every top-level value export from src/index.ts whose source is under src/core/** is represented in that manifest, including direct top-level core exports that bypass src/core/index.ts.
  • Every public class/prototype method on exported core classes is represented individually as Class.method or through a machine-checkable per-class methods[] inventory. A parent class entry alone never covers its methods.
  • The coverage manifest classifies each export or method as either:
    • rust-wasm: implemented in Rust, exported through WASM, wired into the Node/Bun-accessible accelerated path, covered by parity tests, and covered by benchmarks or a documented benchmark group; or
    • ts-only-ineligible: explicitly not suitable for Rust/WASM with a concrete reason, such as arbitrary JS callback semantics, JS object identity behavior, JS runtime mutation, external I/O, JS Date/Intl dependence, or another specific incompatibility.
  • ts-only-ineligible is not a catch-all for work that is merely unported. CPU-heavy pure operations such as numeric reductions, search/sort helpers, align/reindex/index operations, and vectorized scalar transforms should be treated as eligible unless a specific method-level JS semantic blocker is documented.
  • There are no todo, unknown, unclassified, placeholder, or missing entries in the coverage manifest.
  • Every rust-wasm entry has parity tests showing the Rust/WASM path returns the same observable result as the existing TypeScript path for representative normal, edge, missing-value, and dtype cases.
  • The accelerated path can be selected from Node/Bun without replacing the TypeScript implementation. A caller should be able to use the TypeScript path and the Rust/WASM path in the same environment for comparison and fallback.
  • Benchmark coverage is added for every rust-wasm entry or for a documented benchmark group that covers that entry, and the benchmark output compares TypeScript tsb against Rust/WASM tsb.
  • Benchmark results are written to a stable JSON artifact, and any Rust/WASM case that is slower than the TypeScript baseline is explicitly identified in the output or in the coverage report with a short explanation.
  • The TypeScript test suite, strict typecheck, lint, Rust tests, WASM build, WASM parity tests, Rust/WASM coverage check, Rust/WASM benchmark command, and the deterministic completion gate below all pass on the same commit.

Deterministic Completion Gate

A run must not add goal-completed unless this gate passes from the repository root on the exact branch/commit being marked complete. The completion comment must paste this gate's output. If this gate fails, the issue remains active even if bun run wasm:coverage, tests, benchmarks, or the older evidence snippet pass.

python3 - <<'PY'
import json
import re
from pathlib import Path

ROOT = Path.cwd()
MANIFEST = ROOT / "wasm-coverage.json"
CORE_INDEX = ROOT / "src/core/index.ts"
TOP_INDEX = ROOT / "src/index.ts"

manifest = json.loads(MANIFEST.read_text())
entries = manifest.get("entries")
if not isinstance(entries, list) or not entries:
    raise SystemExit("wasm-coverage.json must contain a non-empty entries array")

by_name = {}
for entry in entries:
    name = entry.get("name")
    status = entry.get("status")
    if not isinstance(name, str) or not name:
        raise SystemExit("manifest entry has invalid name: %r" % (entry,))
    if name in by_name:
        raise SystemExit("duplicate manifest entry: %s" % name)
    if status not in {"rust-wasm", "ts-only-ineligible"}:
        raise SystemExit("%s: invalid status %r" % (name, status))
    if status == "rust-wasm":
        for key in ("wasm_function", "parity_test", "benchmark_group"):
            value = entry.get(key)
            if not isinstance(value, str) or not value.strip():
                raise SystemExit("%s: rust-wasm entry missing %s" % (name, key))
    if status == "ts-only-ineligible":
        reason = entry.get("reason")
        if not isinstance(reason, str) or len(reason.strip()) < 30:
            raise SystemExit("%s: ts-only-ineligible entry needs a concrete reason" % name)
        lowered = reason.lower()
        banned = ("todo", "unknown", "unclassified", "not yet", "not ported", "future work", "same as series", "same as dataframe")
        if any(token in lowered for token in banned):
            raise SystemExit("%s: reason looks like a placeholder/catch-all: %r" % (name, reason))
    by_name[name] = entry

summary = manifest.get("summary", {})
if summary.get("unclassified") != 0:
    raise SystemExit("summary.unclassified must be 0")
if summary.get("eligible_missing") != 0:
    raise SystemExit("summary.eligible_missing must be 0")
if summary.get("total_core_entries") != len(entries):
    raise SystemExit("summary.total_core_entries must equal len(entries)")
if summary.get("rust_wasm") != sum(1 for e in entries if e.get("status") == "rust-wasm"):
    raise SystemExit("summary.rust_wasm count is wrong")
if summary.get("ts_only_ineligible") != sum(1 for e in entries if e.get("status") == "ts-only-ineligible"):
    raise SystemExit("summary.ts_only_ineligible count is wrong")

export_block_re = re.compile(r'^export\s+(type\s+)?\{([\s\S]*?)\}\s*from\s*["\']([^"\']+)["\']', re.M)

def exported_name(token):
    token = re.sub(r'//.*$', '', token).strip()
    if not token:
        return None
    parts = re.split(r'\s+as\s+', token)
    return parts[-1].strip()

def value_exports(source, source_filter=None):
    out = {}
    for match in export_block_re.finditer(source):
        if match.group(1):
            continue
        from_path = match.group(3)
        if source_filter and not source_filter(from_path):
            continue
        for raw in match.group(2).split(','):
            name = exported_name(raw)
            if name:
                out[name] = from_path
    return out

core_exports = value_exports(CORE_INDEX.read_text())
top_core_exports = value_exports(TOP_INDEX.read_text(), lambda p: p.startswith("./core/"))
required_exports = set(core_exports) | set(top_core_exports)
missing_exports = sorted(required_exports - set(by_name))
if missing_exports:
    raise SystemExit("missing manifest export entries: " + ", ".join(missing_exports[:50]))

# Prefer concrete implementation source from src/core/index.ts for duplicate names.
export_sources = dict(top_core_exports)
export_sources.update(core_exports)

def resolve_source(from_path, base_dir):
    if from_path.startswith("./core/"):
        return ROOT / "src" / from_path[2:]
    if from_path.startswith("./"):
        return base_dir / from_path[2:]
    return None

def find_class_file(name, from_path, base_dir, seen=None):
    if seen is None:
        seen = set()
    path = resolve_source(from_path, base_dir)
    if path is None or not path.is_file() or path in seen:
        return None
    seen.add(path)
    text = path.read_text()
    if re.search(r'(^|\n)export\s+(?:abstract\s+)?class\s+' + re.escape(name) + r'\b', text) or re.search(r'(^|\n)(?:abstract\s+)?class\s+' + re.escape(name) + r'\b', text):
        return path
    for exported, nested_from in value_exports(text).items():
        if exported == name:
            found = find_class_file(name, nested_from, path.parent, seen)
            if found is not None:
                return found
    return None

class_file = {}
for name, from_path in export_sources.items():
    base_dir = ROOT / "src/core" if not from_path.startswith("./core/") else ROOT / "src"
    found = find_class_file(name, from_path, base_dir)
    if found is not None:
        class_file[name] = found

method_head_re = re.compile(r'^\s{2}(?:public\s+)?(?:async\s+)?(?:get\s+|set\s+)?([A-Za-z_$][\w$]*)\s*(?:<[^>{}]+>)?\s*\(', re.M)
privateish = {"constructor"}

def class_body(text, class_name):
    match = re.search(r'(?:export\s+)?(?:abstract\s+)?class\s+' + re.escape(class_name) + r'\b[^{}]*\{', text)
    if not match:
        return ""
    start = match.end()
    depth = 1
    i = start
    while i < len(text):
        ch = text[i]
        if ch == "{":
            depth += 1
        elif ch == "}":
            depth -= 1
            if depth == 0:
                return text[start:i]
        i += 1
    raise SystemExit("could not parse class body for %s" % class_name)

required_methods = []
for class_name, path in sorted(class_file.items()):
    body = class_body(path.read_text(), class_name)
    methods = sorted(set(m.group(1) for m in method_head_re.finditer(body)) - privateish)
    for method in methods:
        required_methods.append("%s.%s" % (class_name, method))

def method_covered(method_name):
    if method_name in by_name:
        return True
    class_name, method = method_name.split('.', 1)
    parent = by_name.get(class_name, {})
    methods = parent.get("methods")
    if isinstance(methods, list):
        for item in methods:
            if not isinstance(item, dict) or item.get("name") != method:
                continue
            status = item.get("status")
            if status not in {"rust-wasm", "ts-only-ineligible"}:
                raise SystemExit("%s: invalid method status %r" % (method_name, status))
            if status == "rust-wasm" and not item.get("wasm_function"):
                raise SystemExit("%s: rust-wasm method missing wasm_function" % method_name)
            if status == "ts-only-ineligible":
                reason = item.get("reason")
                if not isinstance(reason, str) or len(reason.strip()) < 30:
                    raise SystemExit("%s: ts-only-ineligible method needs a concrete reason" % method_name)
            return True
    return False

missing_methods = [name for name in required_methods if not method_covered(name)]
if missing_methods:
    raise SystemExit(
        "missing public method coverage entries (use Class.method entries or a per-class methods[] inventory): "
        + ", ".join(missing_methods[:80])
    )

priority_methods = [
    "Series.add", "Series.sub", "Series.mul", "Series.div", "Series.sum", "Series.mean", "Series.min", "Series.max", "Series.std", "Series.var", "Series.median", "Series.sortValues",
    "DataFrame.sum", "DataFrame.mean", "DataFrame.min", "DataFrame.max", "DataFrame.std", "DataFrame.corr", "DataFrame.cov", "DataFrame.sortValues",
    "DataFrameRolling.mean", "DataFrameRolling.sum", "DataFrameRolling.std", "DataFrameRolling.var", "DataFrameRolling.min", "DataFrameRolling.max", "DataFrameRolling.median",
    "DataFrameExpanding.mean", "DataFrameExpanding.sum", "DataFrameExpanding.std", "DataFrameExpanding.var", "DataFrameExpanding.min", "DataFrameExpanding.max", "DataFrameExpanding.median",
]
missing_priority = [name for name in priority_methods if name in required_methods and not method_covered(name)]
if missing_priority:
    raise SystemExit("missing priority CPU-heavy method entries: " + ", ".join(missing_priority))

print("deterministic Rust/WASM completion gate: PASS")
print("manifest entries: %d" % len(entries))
print("live value exports covered: %d" % len(required_exports))
print("exported core classes audited for public methods: %d" % len(class_file))
print("public methods covered: %d" % len(required_methods))
print("rust-wasm entries: %d" % sum(1 for e in entries if e.get("status") == "rust-wasm"))
print("ts-only-ineligible entries: %d" % sum(1 for e in entries if e.get("status") == "ts-only-ineligible"))
PY

Minimum completion interpretation:

  • If the gate reports any missing Class.method coverage, the goal is incomplete.
  • If Series, DataFrame, DataFrameRolling, DataFrameExpanding, or similar high-level classes are marked ts-only-ineligible, their public methods still need individual entries or a machine-checkable per-class methods[] inventory.
  • CPU-heavy pure methods must not be dismissed by a parent class blocker. Each one must be rust-wasm or have its own concrete method-level incompatibility reason.
  • Completion requires this gate plus the full Bun/Rust/WASM evidence script below to pass on the same commit.

Evidence / Verification

At completion, this script must pass from the repository root on the same commit as the deterministic gate:

set -euo pipefail

bun install
bun run typecheck
bun run lint
bun test ./tests/core/

bun run wasm:build
bun run wasm:test
bun run wasm:coverage
BENCHMARK_WORKERS=2 BENCHMARK_TIMEOUT=60 bun run bench:wasm-core

python3 - <<'PY'
import json
from pathlib import Path

coverage_path = Path("benchmarks/results-wasm-core.json")
if not coverage_path.is_file():
    raise SystemExit("benchmarks/results-wasm-core.json was not written")

results = json.loads(coverage_path.read_text())
benchmarks = results.get("benchmarks")
if not isinstance(benchmarks, list) or not benchmarks:
    raise SystemExit("Rust/WASM benchmark output has no benchmark entries")

missing = [b for b in benchmarks if "tsb" not in b or "tsb_wasm" not in b]
if missing:
    raise SystemExit("Benchmark entries missing tsb/tsb_wasm comparison: %r" % (missing[:5],))

coverage_summary = results.get("coverage")
if not isinstance(coverage_summary, dict):
    raise SystemExit("Benchmark output must include a coverage summary")
if coverage_summary.get("unclassified", 1) != 0:
    raise SystemExit("Rust/WASM coverage still has unclassified entries")
if coverage_summary.get("eligible_missing", 1) != 0:
    raise SystemExit("Rust/WASM coverage still has eligible functions without implementations")
if coverage_summary.get("total_core_entries", 0) <= 0:
    raise SystemExit("Rust/WASM coverage summary did not count core entries")
PY

Completion also requires the final PR body or issue comment to include:

  • The path to the coverage manifest.
  • The number of total core entries, rust-wasm entries, and ts-only-ineligible entries.
  • The number of public methods audited and covered by the deterministic gate.
  • The Rust/WASM benchmark artifact path.
  • A short summary of fastest wins and any slower-than-TypeScript cases.

Scope and Constraints

The workflow may change:

  • Cargo.toml, Cargo.lock, Rust crate files, and Rust/WASM build configuration.
  • New Rust/WASM source and generated bindings under a clearly named path such as rust/, wasm/, src/wasm/, or src/core/wasm/.
  • TypeScript glue code needed to expose the accelerated path from Node/Bun while preserving the current TypeScript behavior.
  • src/core/** only where needed for optional dispatch/fallback glue.
  • tests/core/** and new Rust/WASM parity tests.
  • benchmarks/run_benchmarks.sh, new benchmarks/tsb-wasm/** files, and benchmark result/runner files needed for Rust/WASM comparisons.
  • package.json scripts needed for wasm:build, wasm:test, wasm:coverage, and bench:wasm-core.
  • Documentation or machine-readable manifests that explain Rust/WASM eligibility and coverage.

The workflow must not change:

  • README.md.
  • .autoloop/programs/**.
  • The public tsb API in a breaking way.
  • Existing TypeScript implementations by deleting or replacing them outright.
  • Core runtime JS dependencies. Any new JS dependency must be build/test tooling only and justified.
  • Strict TypeScript rules: no any, no as casts, no @ts-ignore, and no escape hatches.

Context To Read First

  • AGENTS.md
  • package.json
  • src/core/index.ts
  • src/index.ts
  • src/core/**
  • tests/core/**
  • benchmarks/run_benchmarks.sh
  • benchmarks/tsb/**
  • Existing benchmark result shape in benchmarks/results.json

Iteration Policy

Work in small checkpoints. This goal must advance piecemeal across multiple Goal runs.

The next checkpoint should make completion measurable again:

  • Add the deterministic gate as a checked-in script, or update scripts/wasm-coverage-check.ts so it enforces the same public-method inventory rules.
  • Update wasm-coverage.json to include Class.method entries or per-class methods[] inventories.
  • Run the deterministic gate and report its output.

After the method inventory is measurable, port one coherent eligible group per run, prioritizing CPU-heavy core operations such as numeric reductions, search/sort helpers, align/reindex/index operations, and vectorized scalar transforms.

For every run:

  • Keep the change set small enough to review as a single checkpoint.
  • Execute the narrowest relevant parity tests and benchmark subset before broadening to the full evidence script.
  • Report what changed, what was verified, what remains, and whether any functions/methods were marked ts-only-ineligible.
  • Do not mark the goal complete from intent, partial coverage, manifest self-consistency, or class-level ineligibility.

Blocked Stop Condition

Stop substantive work and comment instead of guessing if:

  • The Rust/WASM toolchain cannot be installed or used in the GitHub Actions environment.
  • The workflow cannot load the generated WASM from Node/Bun without adding supported build/test tooling only.
  • A core function or method's eligibility cannot be decided from the code and tests alone.
  • Meeting the coverage contract would require deleting the TypeScript implementation or breaking the public API.
  • The benchmark harness cannot produce stable TypeScript vs Rust/WASM comparisons after reasonable retries.

The blocked comment should include the exact command output, the functions/methods or files affected, what is known, and the smallest maintainer decision or environment change needed to continue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    goal-completedAgentic goal workflow completed this issue

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions