Skip to content

wasm-oj/forge

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

45 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

WASM OJ Forge

CI npm license

WASM OJ Forge is a local-first compiler, deterministic runner, and online-judge library. It is the experimental successor to WASM-OJ's compilet and wark. In the browser deployment, source compilation, linking, execution, diagnostics, and all 45 original problems stay on the device. Compiler and language-runtime packages execute under the Wasmer JavaScript SDK; submitted programs execute under a portable Rust/Wasmer runtime core compiled for both WebAssembly and native server hosts.

All production compatibility is governed by one wasm-oj-forge-v1 contract. It jointly versions compilation, execution, determinism, metering, artifacts, judge specifications, caches, and conformance schemas; there are no separate compiler/determinism/resource/judge counters. See versioning policy.

Judge experience

  • Browse 45 systems-algorithm problems in a topic-grouped, prerequisite-aware learning path; search by number, title, track, or tag in Traditional Chinese or English.
  • Work in C, C++, Rust, Go, Python, JavaScript, or TypeScript with Monaco and multi-file projects.
  • Build once, run the sample, then execute each judge case locally through Wasmer.
  • Compare normalized stdout, surface compile/runtime/time-limit/wrong-answer verdicts, and retain solved progress in browser storage.
  • Stop a running build or submission by terminating and recreating its isolated Worker.

Because the judge is completely local, its test data can be inspected by a determined user. This is an explicit privacy and learning tradeoff: the product is for practice and self-verification, not cheat-resistant competition.

The 45-problem systems track

The canonical collection is published independently at wasm-oj/problems. Its small browser index lists localized titles and integrity-addressed per-problem bundles. Forge loads that index by default, downloads a full bundle only when the learner opens a problem, verifies its exact byte length and SHA-256 digest, then caches it by content hash. The GitHub owner, repository, ref, and index path are user configurable. The local generated TypeScript catalog remains a deterministic development/test fixture; it is not the browser's runtime data source. See the problem catalog contract for discovery, localization, generation, and cumulative scoring behavior.

The learner-facing order starts with five foundations problems, then progresses through metering and resource limits, filesystems and packaging, build scheduling and caching, graph algorithms, and knapsack variants. Stable source manifest IDs remain unchanged so calibration evidence and GitHub API paths do not drift when the pedagogical sequence changes. No original 20-problem fixture or compatibility catalog remains.

Language support

Language Build path Output
C Pinned Clang 22 WebC; direct cc1 plus LLD with frozen WASI P1 arguments Standalone WASI P1 .wasm; wasix changes Forge profile identity
C++ The same Clang 22 module in C++20 mode plus the same LLD Standalone WASI P1 .wasm; wasix changes Forge profile identity
Rust Pinned rustc 1.91.1-dev WebC with its matching standard library under Wasmer Standalone WASI P1 .wasm
Go Pinned standard Go 1.26.5 compiler, linker, and GOOS=wasip1 GOARCH=wasm standard library under Wasmer Standalone WASI P1 .wasm
Python Source-built CPython 3.14.6/WASI P1 byte-compilation Runtime bundle containing bytecode, project modules, and the pinned WASI P1 runtime contract
JavaScript TypeScript 7.0.2/WASI checking and emit CommonJS runtime bundle executed by QuickJS-ng 0.15.1 plus the pinned Forge adapter
TypeScript Native TypeScript 7.0.2 compiler built for WASI CommonJS runtime bundle executed by QuickJS-ng 0.15.1 plus the pinned Forge adapter

For C and C++, target: "wasip1" and target: "wasix" do not select different compiler ABIs. Both use the same wasm32-unknown-wasip1 cc1 configurations and LLD command from one verified manifest. Given the same language, optimization, and sources, the emitted module bytes are the same; wasix changes the artifact, cache, cost-calibration, and runtime-profile identity. The current Forge runner uses the same runtime implementation for both labels and validates the module's actual imports.

The Rust path uses a source-traceable browser-runnable rustc 1.91.1-dev build, its matching wasm32-wasip1-threads standard library, and a digest-pinned wasm-ld 22 stage. Forge restricts rustc to one codegen thread, emits the crate object plus rustc's allocator-shim bitcode, then links them in a fresh Wasmer command instance. In the browser, builds are serialized through one persistent Rust stage per compiler Worker generation. That stage keeps its verified Runtime, WebC package, rustc/wasm-ld command handles, and Wasmer SDK thread pool warm while giving every build a fresh project Directory and fresh command instances. The server still uses a fresh isolated child per build; no host compiler or linker participates on either host. The resulting shared-memory module is admitted as Forge wasip1; submitted programs do not receive thread-spawn capability. Collections, traits, structs, enums, macros, iterators, and normal std I/O compile normally.

The Go path runs the standard Go 1.26.5 compile and link commands as verified WebC commands under Wasmer. Its deterministic archive contains all 349 packages from the matching GOOS=wasip1 GOARCH=wasm standard library. The published package is reproducibly traced to the official Go source tarball and contains no host compiler binary. Go currently supports wasip1; Forge does not relabel it as WASIX.

C++ projects may opt into a real precompiled header by adding exactly one file named forge.pch.hpp. If its contents exactly equal the exported FORGE_LIBCXX_PCH_HEADER, Forge loads the matching debug/release PCH generated by the pinned Clang WebC, cc1 arguments, and WASI sysroot; both compressed and expanded bytes are manifest-pinned. Other header contents keep the custom local-PCH path. Forge records compiler dependencies and keys source, header, package, PCH, object, and link-result nodes by content. The browser persists a digest-verified graph archive in IndexedDB, so Worker recycling does not discard clean objects and link results. Units that emitted diagnostics are deliberately rebuilt because a cache hit could not faithfully reconstruct those diagnostics.

Python, JavaScript, and TypeScript use a runtime-bundle artifact because these languages require their language runtime. The executable WebAssembly runtime is pinned in the bundle manifest and is always launched by Wasmer. C, C++, Rust, and Go produce raw standalone modules.

The default conformance suite contains every declared language/target profile, deterministic filesystem metadata, multi-file I/O, and write-time VFS quota probes, plus one C/WASIX denied-capability probe. Browser and server compile measurements use this Forge contract suite rather than a parallel benchmark protocol. See the conformance specification and the latest recorded conformance report.

Enforced execution policy

Every submitted module passes through the same wasm-oj-forge-runtime-core before Wasmer instantiates it:

Control Enforcement
Weighted metering The opcode weights are adapted from Binaryen's optimizer cost model. Forge preserves WARK 0.3 compatibility, including its verified 1000-point penalty rule, and injects the meter into every function and the start section; judging uses a versioned empty-program-normalized net score.
Linear memory Every defined or imported memory receives a hard maximum; the native engine also applies limiting Wasmer tunables.
Captured output stdout, stderr, and explicitly collected output files share one byte budget. The result reports each stream's retained byte count.
Writable VFS File growth and inode creation are reserved transactionally against separate live-occupancy limits. Mounted inputs form the baseline; truncation and deletion release headroom.
Logical time Clock reads advance by a configured deterministic step. WASI clock polls and WASIX sleep fast-forward the same virtual clock without waiting for the host; logicalTimeLimitMs terminates as logical-time-limit.
Emergency wall deadline Browser Workers and native child processes are forcibly replaced or killed if the engine itself stops making progress.
Determinism WASI/WASIX clock and random imports, language-level time/random APIs, locale, timezone, and Python hash seed are controlled by an explicit run contract.

RunResult reports termination, baseline-normalized cost, unadjusted rawCost, baselineCost, logicalTimeNs, profile provenance, meter model, peak linear-memory/VFS occupancy, and output byte counts. The requested 10,000,000,000-point instruction budget is a net budget: the runner enforces raw budget = calibrated empty-program baseline + net budget, then reports cost = max(0, rawCost - baselineCost). The profile covers the Forge contract, language, target, optimization, exact compiler/runtime content identity, and meter model; mismatched or uncalibrated artifacts are rejected. This removes fixed CPython/QuickJS/CRT startup without making it free to parse input, import modules, initialize APIs, I/O, allocation, or execute user code. Logical time, memory, captured output, writable VFS occupancy, and the 60-second emergency wall boundary remain hard limits. Contract 1 calibration evidence and exact values are recorded in the calibration experiment.

Library API

pnpm add @wasm-oj/forge

The package root, @wasm-oj/forge, is the supported environment-neutral boundary. @wasm-oj/forge/browser adds the Worker-based browser host and @wasm-oj/forge/server adds the Node/Wasmer host. ForgeEngine composes a ForgeCompiler, ForgeRunner, and optional ForgeArtifactStore into compile, run, judge, and execute operations. For a downstream-OJ walkthrough covering browser/server setup, submission lifecycle, stable errors and observations, dependencies, and browser runtime plug-ins, see the Forge integration guide. Maintainers should follow the release contract; a tag is accepted only when it matches package.json, all CI gates pass, and the exact npm tarball is attached to the GitHub Release with its SHA-256 digest.

A downstream language implements ForgeCompiler, including a stable cacheIdentity(project) bound to every compiler and toolchain input that can change output, then registers its language with ForgeCompilerRegistry. The registry routes both cache identity and builds, owns shared compiler lifecycle once, and seals registration on first use. Standalone modules use the existing runtime path; a new runtime-bundle format also needs a RuntimeDriver. The browser Forge convenience class contains only built-in compilers, so custom compiler composition uses ForgeEngine. Browser runtime-driver injection has an additional Worker boundary: runtimeDriverPlugins accepts only trusted, same-origin, SHA-256-pinned, self-contained ESM modules, which are verified and constructed inside the runner Worker. It never transfers arbitrary functions through postMessage. The full deployment and authority contract is described in the library contract.

Forge is the browser host. It computes content-addressed build keys and optionally persists artifacts in IndexedDB. Both C/C++ target labels use the same compiler-stage-bounded WASI P1 Worker pipeline: immutable toolchain state and verified translation-unit objects remain warm until the next build could exceed the eight-stage Clang 22 safety budget. Rust uses a persistent serialized stage; Forge conservatively charges two output-ready stages per Rust build (rustc plus wasm-ld), caps a Worker generation at four, and recycles the complete Worker tree before the third build. Go uses a separate persistent serialized stage that retains its verified toolchain and standard-library bytes while runtime-core creates and drops each two-step compile/link pipeline. A graceful boundary asks every active language stage to release its resources, waits for its shutdown acknowledgement, then waits for the outer compiler Worker to acknowledge quiescence before starting the next generation. Python's compiler stage is disposable, and the outer Wasmer Runtime is initialized lazily only for C, C++, JavaScript, or TypeScript. Cancellation, restart, timeout, cache clearing, disposal, and infrastructure failure remain hard-termination boundaries. precompile() provides safe compile-ahead for edit–test loops: a matching foreground compile joins the exact in-flight request or loads its content-addressed artifact, while supersedePrecompile() cancels stale speculative work without cancelling a foreground build.

import { Forge } from "@wasm-oj/forge/browser";

const forge = await Forge.create({
  assetBaseUrl: "/toolchains/",
  artifactCache: true,
});

try {
  const input = {
    language: "typescript" as const,
    entry: "src/main.ts",
    files: {
      "src/main.ts": 'import * as std from "std";\nstd.out.puts("42\\n");\n',
    },
  };
  // Optional: call after an editor idle debounce.
  void forge.precompile(input);

  const build = await forge.compile(input);

  if (!build.success || !build.artifact) {
    console.table(build.diagnostics);
    throw new Error("Compilation failed.");
  }

  const result = await forge.run(build.artifact, {
    stdin: "7 35\n",
    determinism: {
      randomSeed: 42,
      realtimeEpochMs: Date.UTC(2000, 0, 1),
      clockStepNs: 1_000_000,
    },
  });

  console.log(result.stdout);
} finally {
  forge.dispose();
}

submit() is the production OJ boundary: each FIFO submission owns its ID, state, abort signal, result, cancellation, and structured event stream; onObservation() provides the same sequence-correlated events at engine scope. execute() remains available for one-shot use. onProgress() and onStream() are lower-level direct-operation signals; precompile(), supersedePrecompile(), cancel(), restart(), clearCache(), and dispose() manage scheduling and lifecycle. Public asynchronous infrastructure failures are ForgeError records; compiler diagnostics, guest termination, and judge verdicts remain result data. A production browser host must provide Cross-Origin-Opener-Policy: same-origin, Cross-Origin-Embedder-Policy: require-corp, and Cross-Origin-Resource-Policy: same-origin. Forge uses same-origin blob: bootstraps for its module Workers, so a Content Security Policy must include worker-src 'self' blob:. The packed compiler, runner, Python-stage, Rust-stage, Go-stage, and Wasmer secondary-worker assets must all remain deployed at their emitted reachable URLs. Toolchains may use another base URL only when that host provides CORS and Cross-Origin-Resource-Policy headers compatible with the page's COEP policy.

The package exposes a strict public export map for npm, workspace, and packed- tarball consumers. pnpm run library:build emits the three entries, declarations, browser Worker assets, and isolated server stages; pnpm run library:verify validates the official Wasmer SDK integrity, package boundary, licenses, packed contents, and NodeNext consumer types. A browser host must serve the files in public/toolchains/ at its configured assetBaseUrl (the default is /toolchains/). The package also exports @wasm-oj/forge/toolchain-cache-sw.js; copy that file unchanged to a same-origin public URL whose service-worker scope covers assetBaseUrl, then register that exact URL through the browser entrypoint:

import { registerToolchainCache } from "@wasm-oj/forge/browser";

await registerToolchainCache({
  scriptUrl: "/toolchain-cache-sw.js",
  scope: "/",
});

The default options match a host that places the exported file at /toolchain-cache-sw.js. The registration helper waits for that registration's own worker to activate; it does not wait on an unrelated global scope. Size-limited static deployments may additionally pass chunkManifestUrl: "/toolchains/forge-sites-chunks.json" after generating that canonical manifest and its content-addressed parts. This mode is explicit: npm consumers serving the canonical monolithic assets do not probe for, or silently fall back through, a second transport. The service worker verifies each chunk's declared length and SHA-256 before streaming it, and the compiler worker verifies the reconstructed compressed asset against its pinned SHA-256 before decompression or use. prepack performs both build and verification, including real registration of the service-worker asset from the packed tarball.

judgeProject() compiles once and evaluates a versioned, data-oriented JudgeSpec. Named input providers and matchers are registries, so downstream applications can add remote-data adapters, digest-verified fixtures, token/float/property checkers, or domain-specific verdict logic without changing the engine. Custom matcher configuration must remain serialization-safe when a host persists or transfers the specification. See Library contract.

Batch cases may mount multiple input files and collect explicit output paths. Built-in matchers cover text, SHA-256, tokens, floating-point tolerance, sets/multisets, and exact output-file sets. wasmCheckerMatcher() executes a compiled standalone Wasm checker through the same ForgeRunner sandbox. Interactive cases run contestant and interactor concurrently with full-duplex pipes, independent resource budgets, deterministic clocks/randomness, and secret files mounted only on the interactor side.

createDefaultDependencyManager() installs real native-lockfile adapters for Cargo crates, npm packages, PyPI distributions, Go modules, and C/C++ libraries. They consume Cargo.lock v3/v4 checksums, package-lock v2/v3 SRI, exact hash-locked requirements, go.mod plus Go's official h1 ZIP hash, or an explicit Forge C/C++ lock. resolveDependencies() produces the canonical lock; prepareDependencies() verifies and extracts it into an archive-independent DependencyBuildBundle. Passing that bundle as CompileInput.dependencies really mounts and compiles the admitted source/package tree, and binds the lock and every file-tree digest into object/link/artifact cache identity and replay provenance. Forge does not pretend to be a second semver solver: roots must be exact, and build scripts, proc macros, native extensions, npm ESM/lifecycle packages, Python sdists, cgo/assembly, and prebuilt C/C++ objects fail closed. Browser IndexedDB, server filesystem, and offline bundle transports implement the same integrity contract.

ForgeReplayBundle is the portable replay boundary. createForgeReplayBundle() normalizes ephemeral project/artifact timestamps, embeds source, artifact, optional offline dependencies, the exact run or self-contained judge contract, and its deterministic transcript. encodeForgeReplayBundle() emits a canonical FORGRPL1 binary envelope with sorted, deduplicated SHA-256 blobs; decode rejects corruption, non-canonical JSON, missing or unused blobs, trailing bytes, and contract drift. forge.replay() recompiles by default, compares the stable artifact digest, reruns or re-judges, and reports field-level deterministic mismatches. Provider-backed judge inputs must be materialized inline; built-in matchers, Wasm checkers, and interactors remain portable because their artifacts are embedded.

The server host resolves and verifies the provisioned package distribution, constructs compiler, runner, artifact/dependency stores, and returns the same ready high-level engine in one call:

import { createServerForge } from "@wasm-oj/forge/server";

const forge = await createServerForge();

const { build, run } = await forge.execute({
  language: "typescript",
  entry: "src/main.ts",
  files: { "src/main.ts": 'import * as std from "std";\nstd.out.puts("42\\n");' },
});

createServerForge({ runtimeDirectory, toolchainDirectory, cacheDirectory }) accepts explicit immutable deployment paths. Startup verifies real executable files and every pinned toolchain SHA-256, and never downloads or builds a fallback. The lower-level ServerForgeCompiler, ServerForgeRunner, FileSystemArtifactStore, and FileSystemDependencyCache remain exported for hosts that need custom composition.

ServerForgeCompiler starts a fresh isolated Node/Wasmer child for each uncached build, so independent library instances do not share scheduler state and cancellation can kill the compiler. Server runtime-package preparation is also a one-shot Node/Wasmer child; the parent keeps only verified bytes and can kill a stalled preparation at its 120-second control deadline. After preparation, ServerForgeRunner starts a fresh native runtime-core process for each run, which supplies the separate guest wall boundary. Call cancelAndWait() before manually clearing runtime storage; ForgeEngine already enforces that ordering.

Forge intentionally does not ship an opaque platform binary and does not run a compiler during package installation. The publish tarball instead contains the exact runtime-core Rust source, Cargo.lock, the pinned rust-toolchain.toml, and the two audited local Cargo patches needed by that lockfile. Provision one native compiler/runner pair explicitly before starting a server:

# Forge source checkout; outputs forge-compiler and forge-runner under target/release.
pnpm run runtime:build-native

# Installed package; keep generated artifacts outside node_modules.
pnpm --dir node_modules/@wasm-oj/forge run runtime:build-native \
  --target-dir /srv/wasm-oj-forge-runtime

The second command produces both executables under /srv/wasm-oj-forge-runtime/release, which can be passed as createServerForge({ runtimeDirectory }). Both commands use the package-root toolchain pin and cargo build --locked; registry dependencies are fetched only during this explicit server provisioning step. The packed-package verifier builds this source contract and then runs a TypeScript program through the packed ServerForgeCompiler and ServerForgeRunner, so a missing source, patch, stage, or executable boundary fails publication.

Deterministic execution contract

Every run requires a validated random seed, realtime epoch, and virtual-clock step. Defaults are seed 0x5eed1234, 2000-01-01T00:00:00Z, and one millisecond per observation. Given the same artifact, stdin, args, environment, and determinism configuration, the supported single-threaded language surface produces the same exit code, stdout, and stderr.

  • The runtime core replaces WASI and WASIX random_get, clock_time_get, clock_res_get, and poll_oneoff imports before instantiation, and virtualizes WASIX thread_sleep. C and C++ link only an entropy adapter; their wasi-libc clock and sleep calls, plus the unchanged Rust and Go standard-library calls, reach this shared host clock directly.
  • Python starts through a bundled bootstrap that controls random, Random(), os.urandom, secrets, UUID entropy, timezone, and locale. CPython's native time, datetime, and sleep implementations reach the shared host clock; interpreter hash randomization is fixed independently.
  • JavaScript and TypeScript install deterministic Math.random, performance, crypto.getRandomValues, and crypto.randomUUID before loading user modules. Native QuickJS Date reaches the shared host clock, and performance.now() derives from it relative to the configured epoch.
  • Each run receives a fresh Wasmer instance and seekable ephemeral filesystem. Supported socket, network, process-spawn, and thread-spawn imports are replaced with signature-preserving traps before instantiation; unknown import namespaces or functions are rejected. Deterministic WASIX helpers such as thread identity, reported parallelism, and logical sleep remain available but cannot create guest concurrency.

durationMs, build duration, artifact IDs, and creation timestamps are host observations and are outside the deterministic transcript. The clock is logical: every supported clock observation advances it by clockStepNs; language sleep and relative or absolute clock polling advance it by the requested virtual duration without waiting. logicalTimeLimitMs bounds this deterministic elapsed time independently of the emergency wall deadline.

Run locally

Requirements: Node.js 22.13 or newer and a modern browser with SharedArrayBuffer support. Building the optional native server runner also requires rustup/Cargo; the package pins Rust 1.97.1.

corepack enable
pnpm install --frozen-lockfile
pnpm run dev

Open the printed local URL. The development and production servers send COOP, COEP, and CORP headers because the Wasmer SDK uses SharedArrayBuffer and host-side Workers. Forge supplies the SDK's official workerUrl protocol with a custom secondary worker: it validates the SDK initialization envelope, disables registry access, calls initSync with a page-aligned 1 MiB secondary stack, then installs the validated sdkUrl in the same order as the official SDK worker. The packed browser build emits the SDK facade and Wasm modules as external content-hashed assets rather than data URLs. These secondary workers belong to the host compiler/runtime implementation and do not expose guest thread-spawn capability. The production launcher serves emitted .wasm assets as application/wasm, preserving streaming WebAssembly compilation.

Useful checks:

pnpm run typecheck
pnpm test
pnpm run contract:verify
pnpm run library:build
pnpm run library:verify
pnpm run runtime:test
pnpm run runtime:check-web
pnpm run cost-baseline:transform <primary-raw-record.json>
pnpm run build

Local-first storage and network policy

  • Per-problem language drafts and build artifacts are stored in IndexedDB. ForgeStorageCoordinator applies one cross-tab Web Locks admission policy across artifacts, dependency payloads, the incremental build graph, runtime files, and toolchains. It enforces logical and browser-quota headroom, then evicts by retention class and LRU; toolchains have the highest retention priority.
  • Solved-problem progress is stored in localStorage.
  • Browser compiler packages, runtime modules, and standard libraries are fetched as digest-pinned static files from the configured assetBaseUrl (same-origin /toolchains/ by default). Each same-origin request carries the expected SHA-256 as its cache key; the bundled service worker verifies both cache hits and network responses, removes stale same-path content, and treats persistence failure as an optimization failure rather than a failed verified fetch. A cross-origin asset host controls its own HTTP caching. Generated runtime filesystem archives use a caller-selected cache directory on the server.
  • The versioned TypeScript and QuickJS-ng WASI assets are built reproducibly from pinned upstream revisions. Their .wasm.gz.bin suffix keeps artifact compression distinct from HTTP Content-Encoding.
  • Rust verifies one source-traceable WebC and its provenance manifest by SHA-256, then runs the package's rustc and wasm-ld commands under Wasmer. The compiler, matching standard library, pinned linker, and linker resources stay inside that package; there is no host compiler, linker, or registry resolution.
  • C/C++ verifies the decompressed Clang WebC, WASI P1 sysroot, single pinned cc1/wasm-ld manifest, and two reproducible toolchain-admitted libc++ PCH profiles by SHA-256. Within a bounded Worker generation, unchanged translation units reuse dependency-validated objects; compressed PCH/toolchain responses remain in Cache Storage across generations. The real C++ PCH conformance case compiles and executes below the ten-second target on the native Wasmer host.
  • Compile and run paths perform no runtime registry resolution. Asset fetches are static GET requests and contain no source, stdin, environment, diagnostics, or artifact data. Guest network imports are denied by the runtime policy rather than routed to a host network implementation.
  • Clearing caches is available under Build settings.

See Architecture for the worker protocol, compilation pipelines, artifact model, and trust boundaries. The latest recorded browser/server parity and efficiency measurements are in the conformance report.

About

A local-first compiler and deterministic runner running fully in the browser or on native servers. Supports C, C++, Rust, Go, Python, TypeScript, and JavaScript.

Topics

Resources

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages