Running Pascal fully client-side — no server, no upload, nothing leaving the page.
This repository holds two things:
packages/pascal-wasm— the publishable package@live-codes/pascal-wasm: the Free Pascalpas2jscompiler compiled to WebAssembly, with a small API for compiling Pascal in a page or a Web Worker.poc/— a playground that uses the package, to prove the whole path works end to end.
npm start # serves the repositoryThen open the printed URL (http://localhost:8080/poc/). The page loads the
compiler in a Web Worker, compiles whatever is in the editor and runs the result
in a sandboxed frame. Ctrl+Enter runs.
poc/classic-worker.html is the same thing in a
non-module worker, consuming the IIFE bundle with importScripts().
npm test # the package's test suiteimport { compile } from '@live-codes/pascal-wasm';
const { js, diagnostics } = await compile('begin Writeln(42) end.');It works in browsers, in module and classic Web Workers and in Node, needs no
build step, and resolves its assets relative to itself so it can be imported
straight from a CDN. A minified IIFE build (dist/pascal-wasm.iife.min.js,
~27 kB) is shipped for non-module workers and plain <script> tags, exposing the
same API on a global named pascalWasm.
See the package README for the full API —
createCompiler, compile, parseDiagnostics, configure, compiler flags,
extra units, sourceMap, hosting the assets yourself, and worker usage.
Because pas2js emits JavaScript rather than wasm, a compiled program is an
ordinary page citizen. Each of these was verified running in a browser (see
poc/examples/):
| Capability | Status | How |
|---|---|---|
| Read / modify the DOM | native | uses web — document.createElement, innerHTML, window.innerWidth |
| Call host-provided JS | native | procedure hostLog(const msg: String); external name 'hostLog'; |
| stdout | native | Write/Writeln reach console.log; SetWriteCallBack redirects them |
| stderr / errors | native | uncaught exceptions carry $classname, fMessage and a JS stack, delivered via rtl.onUncaughtException |
| Exit code | native | ExitCode := 3 is the RTL's rtl.exitcode, returned by rtl.run() |
| stdin | not in the RTL | there is no Read/Readln — input has to be host-supplied |
So none of stdout, errors or the exit code need mocking; only input does,
because the pas2js RTL has no Read/Readln at all. window.prompt covers the
synchronous case; anything non-blocking would need SharedArrayBuffer +
Atomics.wait (and cross-origin isolation) or a Worker, since a synchronous read
cannot await a message.
The compiler emits source maps, which is what turns the JavaScript-level error
stack into a line in the user's source. From the compiler's own -h:
-Jm generate source maps
-Jminclude include the Pascal sources in the map
-Jmbasedir=<dir> write source file names relative to <dir>
-Jmsourceroot=<x> sourceRoot prefix for source file names
-Jmabsolute store absolute filenames
-Jmxssiheader- drop the XSSI ")]}'" prefix (it is on by default)
Two things only show up once you try it, and both are handled for you by
sourceMap: true:
-Jmon its own fails: source names must be expressible relative to the map's folder, and the RTL lives outside it. Hence-Jmbasedir=/.- Maps start with the XSSI guard
)]}', which is not valid JSON. Hence-Jmxssiheader-.
The pas2js compiler is built for wasm32-wasi, so
the package supplies an in-memory filesystem and a WASI implementation
(browser_wasi_shim, vendored)
and runs the compiler as an ordinary WASI program. Compiling a program takes
roughly 20–500 ms once the binary is loaded, and the compiled module is reused
across calls.
Three details are easy to trip over, all reproduced from the official FPC browser compiler demo:
- The compiler resolves
argv[0]to find its own directory and aborts withEFOpenErrorif that path does not exist, so a/bin/pas2jsstub and a/pas2jspath both have to exist. -nis needed so it does not look for apas2js.cfg.- Instantiation must be asynchronous: Chrome refuses a synchronous
new WebAssembly.Instancefor buffers over 8 MB, and this binary is ~9 MB.
LGPL-2.1-or-later, matching pas2js, which is itself LGPL-2.1 — this package redistributes its compiler binary and runtime library. The bundled WASI shim is MIT OR Apache-2.0.
THIRD-PARTY-NOTICES.md records
versions, licenses, the provenance and SHA-256 of the compiler binary, and where
to find the corresponding source.
Note that the bundled pas2js.wasm comes from the deployment of the official
Free Pascal demo rather than a tagged release (the official release archives
contain no WebAssembly at all, and the demo tracks trunk — it reports 3.3.1 while
the latest release is 3.2.0). Building the compiler from source with FPC and
demo/webcompiler is the route to a version-pinned binary.
packages/pascal-wasm/ the published package
src/ ESM glue: assets, compiler, config, public API
src/vendor/ vendored WASI shim
scripts/build.mjs builds the classic-script bundle
dist/ pascal-wasm.iife.min.js (built, committed)
assets/ pas2js.wasm and the Pascal RTL sources
types/ TypeScript declarations
test/ test suite (npm test)
poc/ the playground
index.html, app.js UI, and running the compiled program
compiler-worker.js compiles in a module worker
classic-worker.html the same via the IIFE bundle in a classic worker
examples/ sample programs
tools/serve.mjs static file server for local development
The package is ready for npm publish --access public from
packages/pascal-wasm. Once published it is immediately importable from a CDN:
import { compile } from 'https://cdn.jsdelivr.net/npm/@live-codes/pascal-wasm@0.1.0/src/index.js';Add a repository field to packages/pascal-wasm/package.json first — it was
left out because the remote for this checkout is unknown.