wasmpascal — compile Pascal to WebAssembly in your browser
wasmpascal compiles Pascal source to WebAssembly entirely in the browser and runs the freshly compiled wasm on the same page. No install, no server-side toolchain: the compiler itself is a WebAssembly module written in Odin. It supports write/writeln and read/readln console I/O, TextColor and TextBackground, multi-file projects with uses units, heap allocation, and canvas host ABIs for games.
wasmpascal — compile Pascal to WebAssembly in your browser
wasmpascal compiles Pascal source to
WebAssembly entirely inside your browser, then runs the
freshly compiled wasm on the same page. No file system, no native binary,
no server-side toolchain — the compiler itself is a WebAssembly module
written in Odin.
This page needs JavaScript to run the compiler. If you have JavaScript
enabled, the interactive editor and compiler are available above.
What you can do here
Type or paste Pascal source and compile it to WebAssembly in your browser.
Run the compiled wasm on the same page — console output, canvas graphics, or games.
Work with multi-file projects: a root program plus uses units.
Allocate memory with New/Dispose and GetMem/FreeMem.
Download the compiled .wasm, the project source .zip, or a standalone web app — a ready-to-host folder (.zip: runner index.html + runtime js + the compiled wasm) or a single self-contained .html (opens straight from disk) for putting a program on any static web server.
Export the whole project — every source file — as a .zip archive.
Upload .pas files from your computer as a project.
Example programs
hello.pas — the classic hello world.
hello_write.pas — console output via write/writeln.
colors.pas — TextColor/TextBackground plus a canvas gradient.
fibonacci.pas — recursion and loops.
guess.pas — a number-guessing game with readln.
sweep.pas — a minesweeper game (pascaldom ABI, 7 units).
pascaloids.pas, pong.pas, breakout_graphics.pas, sparks.pas — games using the batchiness canvas ABI.
flightleader.pas — FLIGHT LEADER, an arcade space-combat-style game (batchiness ABI, 6 units).
xonix.pas — Xonix (ported from odinix): draw lines across the water to claim land — the fill floods from the balls, so it can never cover one (batchiness ABI).
basic_canvas.pas — pixel-buffer canvas rendering.
wasmpascal
Pascal source
Output / diagnostics
Loading…Theme: Default
wasmpascal: a Pascal compiler that runs entirely in your browser
wasmpascal compiles Pascal source to
WebAssembly entirely inside your browser, then runs the
freshly compiled wasm on the same page. There is no file system, no native
binary, and no server-side toolchain: the compiler itself is a WebAssembly
module, written in the Odin programming language and compiled to js_wasm32. Type Pascal in the editor above, press Run, and the compiled program executes right there — console output, canvas graphics, or a full game.
What you can do
Compile in the browser — the editor, compiler, and runtime are all on this page; nothing is uploaded anywhere.
Multi-file projects — a root program plus uses units, with a file dropdown, add/rename/remove, and autosave to localStorage.
Turbo-Pascal-style console I/O — write, writeln, read, readln, GotoXY, ClrScr, TextColor, and TextBackground render into a fixed character screen.
Heap allocation — New/Dispose and GetMem/FreeMem back a bump allocator with a free list, so linked lists and dynamic records work.
Canvas and game ABIs — programs can drive a pixel-buffer canvas (basic_canvas), self-wire a canvas and event loop (pascaldom), or use the batched-canvas game bridge (batchiness) with Web Audio sound effects.
Editor themes — Default modern dark theme plus classic Turbo Pascal 7 (DOS blue), Borland Pascal (navy), and FreePascal (royal blue) with live hot-swapping.
Download — the Download… toolbar menu saves the compiled .wasm as a standalone module, the whole project (root + every uses unit) as a .zip archive, or a ready-to-host standalone web app — as a folder (.zip: runner index.html + runtime js + bridges + compiled wasm, upload it to any static web server) or as a single self-contained .html (everything inlined; opens straight from disk).
Upload .pas files — pick any number of files from your computer and rebuild a project from them; the file declaring program becomes the root.
How it works
When you press Run, the page spawns a fresh compile worker that loads the compiler wasm module and feeds it your Pascal source through a memory buffer. The compiler runs the full pipeline — preprocess, lex, parse, semantic analysis, code generation, and module emission — and returns compiled wasm bytes. Those bytes are instantiated on the main thread, and the host detects the program's ABI from its exports: console-only programs run in a worker with an inline input line, while canvas and game programs boot straight into the page.
The compiler is a port of the wasmpascal Pascal-to-wasm32
compiler, recompiled to js_wasm32 with its CLI driver replaced
by a memory-buffer browser driver. It shares its lineage with
zigbasic,
which runs a QBASIC interpreter in the browser the same way.
Example programs
Load any of these from the Examples… dropdown in the toolbar:
hello.pas — the classic hello world.
hello_write.pas — console output via write/writeln.
colors.pas — TextColor/TextBackground plus a canvas gradient.
fibonacci.pas — recursion and loops.
guess.pas — a number-guessing game with readln.
sweep.pas — a minesweeper game (pascaldom ABI, seven units).
pascaloids.pas, pong.pas, breakout_graphics.pas, sparks.pas, growable.pas — games using the batchiness canvas ABI.
flightleader.pas — FLIGHT LEADER, an arcade space-combat-style game (batchiness ABI, six units).
xonix.pas — Xonix (ported from odinix): draw lines across the water to claim land — the fill floods from the balls, so it can never cover one (batchiness ABI).
basic_canvas.pas — pixel-buffer canvas rendering.
About the developer
I'm Ewald Horn, a software developer: I build all kinds of things, not just compilers. wasmpascal is one example from a personal collection of small browser projects used to explore systems programming, WebAssembly, and simulation from first principles. I might be available for freelance development work.
A project is a named set of Pascal files with one
root file (the one compiled). Every other file is
registered as a uses unit at run time.
File dropdown (right of the Examples… menu) switches between the project's files; the root is marked (root).
Files… menu (next to the file dropdown): + File adds a new unit file with a starter skeleton; Rename file renames the selected file (updating uses references); Set as root promotes it to the project's root; − File removes it. The per-file options are disabled while the root file is selected.
Rename project renames the project (shown in the Download .wasm and .zip filenames).
Upload files opens any number of .pas files from your computer and replaces the current project with them (confirming if you have unsaved changes). The file whose first Pascal keyword is program becomes the root; if none — or several — do, main.pas wins, then alphabetical order.
Projects autosave to localStorage (wasmpascal:project).
Program structure
program Hello;
uses u_math; // units registered with the hostvar x: Integer;
begin
...
end.
program, library, and unit
headers are supported. A unit has interface /
implementation sections and an optional
begin..end. init body that runs in the module start
function.
uses pulls in a unit's consts/globals/functions
(deduped by name, loaded depth-first, transitive deps first). Units
must be registered with the host (bundled in UNIT_LIBRARY
or added as project files).
exports foo name 'foo' clauses live only in the root
program/library.
Foreign functions: external 'mod' name 'fn' (FPC form)
or external name 'fn' — the import lands in wasm module
mod (default env). Math imports
(sin/cos/pow/sqrt)
resolve to odin_env (JS Math.*).
Types
Integer/LongInt — 32-bit signed (i32); Cardinal/Card/LongWord — unsigned.
Records, static arrays, pointers (^, @, typed indexing p[i]). Multi-dim arrays use the TP sugar: array[1..3, 1..4] of T with a[i, j] indexing (desugars to nested arrays). Record variants work:
case tag: Byte of 0: (r: Single); 1: (w, h: Single); else (x: Single); end
(arms union at the tag's offset; an else arm is the default).
set of <ordinal> — bitmask sets: set of 0..6 = one i32, set of Byte = 8 words. Ops + - * in = <> <= >=, constructors [1, 3..5] / []. Variables must be globals. See examples/set_demo.pas.
String literals are static [len:byte][bytes]; foreign string params pass (ptr,len). String[n] declares a fixed-length buffer (n ≤ 255) that truncates on store — globals are inline buffers; local String[n] vars behave as dynamic (addr,len) pairs (full short-string semantics are roadmap).
Declarations
const RANGE: Cardinal = 100;
var a, b: Integer;
function Add(x, y: Integer): Integer;
procedure Swap(var a, b: Integer); // by-addresstype
TPoint = record x, y: Integer; end;
TGrid = array [0..9] of Integer;
var params are passed by address and work with globals, fields,
indexed elements, pointer derefs, and locals — every function with an
address-taken local gets a per-call stack frame in linear memory
(__frame_ptr), so Swap(a, b) with local
a/b just works. Args must be type-identical lvalues
(no P(1+2)). See examples/var_params_demo.pas.
type sections support records, static arrays,
set of <ordinal> (bitmask sets: set of
0..6 = one i32, set of Byte = 8 words;
ops + - * in = <> <= >= and constructors
[1, 3..5]/[]), and named aliases. Pointers:
^T declares, @x takes an address,
p^ dereferences. Set variables must be globals
(they need an address, like arrays). See
examples/set_demo.pas.
Foreign functions use external (see Program structure).
Control flow
if x > 0 then ... else ...;
case n of 0..4: ...; 11, 15..20: ... else ... end;
if cond then a else b; // ternary: value of a or b (Delphi 13 style)for i := 1 to 10 do ...;
while cond do ...;
repeat ... until cond;
break; continue; exit;
with rect do s := x + y; // bare field names
for loops run the step at the top (counter starts at
from-1, or from+1 for downto) so
continue still advances.
exit leaves the current function (optionally returning a
value: exit(42)). break/continue
apply to the innermost loop. case arms accept
comma-separated value lists, lo..hi ranges (0..4,
11, 15..20), and an else arm. Dense ranges
dispatch via br_table; very wide or sparse spans fall back
to an if-chain.
Ternary expressions (Delphi 13 style, 2026-08-26):
if cond then a else b used where an expression is expected
(an assignment, a call argument, inside a larger expression). The
condition must be Boolean and the two branches must be the same type —
numeric kinds promote (so if c then 1 else 2.5 is Real),
and strings work too (an (addr,len) pair
via a scratch-pair if/else; a String[n] and dynamic String mix
resolves to dynamic). Sets, records and arrays are not allowed. Only
the taken branch runs (short-circuit), so a divide-by-zero or nil-deref
in the untaken branch never executes. Nested ternaries are
dangling-else safe: the inner if binds its own
else. See examples/ternary.pas.
with <record> do opens a member scope: a bare
identifier matching the record's fields resolves to that field
(locals/params still shadow; the innermost with wins for
comma chains like with a, b do, and a with
scope shadows a same-named global). See
examples/with_demo.pas.
Operators & precedence
not, unary -
*/divmodand
+-orxor
=<><><=>=in
Unsigned div/mod need both operands unsigned —
a Cardinal mod <literal> promotes to signed and can go negative.
Use a power-of-two mask (seed and (RANGE-1)) instead.
Set operators (both operands set of): +
union, - difference, * intersection;
=<> equality; <=>= subset/superset; x in s membership.
Builtins
Arithmetic
Inc/Dec (with optional delta),
Sqrt, Sqr, Trunc,
Round, Abs, Frac,
Int, Pi() (must be called with
parentheses; a bare Pi does not resolve).
Trigonometry (radians)
Sin, Cos, Tan,
ArcTan, ArcSin, ArcCos,
ArcTan2(y, x), plus the hyperbolic
Sinh/Cosh/Tanh.
These math builtins are emitted as odin_env
imports (JS Math.*); integer arguments are promoted
to f64. See examples/math.pas.
Memory & pointers
FillChar, Move(src, dst, count) (copies bytes),
pointer()/PByte()/PWord()/
PCardinal()/PLongInt()/PInteger()
casts, nil (the null pointer constant, for
assignment and =/<> checks —
p <> 0 also works), and
Inc(p)/Dec(p, n) on typed pointers
(the step is scaled by element size).
SizeOf(T)/SizeOf(var) is a compile-time
constant — note SizeOf(String) = 8 (the (addr,len)
pair model) and SizeOf(String[n]) = n+1.
String helpers: StrAddr('lit') → byte address,
StrLen('lit') → byte count. F32Bits(f)
reinterprets an f32 as i32 (bit pattern).
Console & timing
Delay(ms) (pause; console programs sleep in a
worker), GotoXY(x, y) (position the console cursor,
1-based), ClrScr (clear the console),
TextColor, TextBackground.
Random numbers:Random (no args) is a
Real in [0, 1); Random(n) is an
Integer in [0, n-1] (n ≤ 0 gives 0). A xorshift
PRNG is compiled into the program; unseeded runs are
deterministic (same sequence every run, TP behavior).
Randomize reseeds it from host entropy
(crypto.getRandomValues).
Keys:KeyPressed (non-blocking poll) and
ReadKey (blocking pop) read the host's key buffer
while a console program runs. Arrow keys arrive TP-style as
the extended pair #0 + scan code (left
#75, right #77); other single-char
keys pass their char code. See
examples/breakout.pas.
Ordinals & characters
Ord(x) (ordinal of an enum/char/bool),
Chr(n), Pred/Succ (±1,
no range check — TP {$R-} semantics),
Odd(n) → Boolean,
UpCase(ch), and Halt (terminate the
program from anywhere). Enumerated types
(type Color = (Red, Green, Blue)) and named
subranges (type Index = 1..10) write as their
ordinals.
Strings
Length(s), Concat(a, b, ...),
Copy(s, i, n), Pos(sub, s) (1-based;
0 if absent), Val(s, v, code),
Str(x, s), StringOfChar(c, n), the
+ concatenation operator, and full string
comparison (= <> < > <= >=, with
prefix ordering). String constants (const G = 'Hi')
work. Str/Val v1 handle Integer
targets only; a single-char literal is a Char, so
use 2+ char literals in string contexts. See
examples/strings.pas.
Console & output
Console I/O
write(x:5); // field width, right-alignedwriteln('hi', x); // newlinereadln(n); // prompts in the browserGotoXY(10, 5); // position the cursor (1-based)ClrScr; // clear the screenifKeyPressedthen ch := ReadKey; // poll keys
write/writeln accept strings, integers,
floats, and x:N field widths (negative width
left-aligns). A single-char literal (write(' '),
Write('#')) prints the character itself, not its ASCII
code (TP7: 'Y' is a Char). read/readln prompt once per
scalar argument (integers or floats) via the browser's inline input
line (or a dialog in fallback mode when the page isn't
cross-origin-isolated).
Keys (ReadKey / KeyPressed) — while a console
program runs, key presses are forwarded to it through the same input
bridge as read/readln. KeyPressed
is a non-blocking poll (safe in a game loop); ReadKey
pops the next key and blocks when none are pending. Arrow keys
arrive Turbo-Pascal-style as the extended pair #0 + scan
code (left #75, right #77, up
#72, down #80), so check
if ch = #0 then ch := ReadKey; — 'ArrowLeft'
alone is not a single code. Other single-char keys pass their char
code, so 'a'/'A'/'d'/'D'
work as alternates. Escape is reserved for the host (Stop). See
examples/breakout.pas, the first game built on this
(its canvas twin is examples/breakout_graphics.pas, which
moves the same game onto the batchiness canvas ABI with self-wired
keydown listeners instead of the console key buffer).
GotoXY(x, y) moves the cursor to column
x and row y (1-based, clamped to the
screen size);
ClrScr clears the screen and homes the cursor. All
console output renders as a fixed character screen (Turbo Pascal
style): 80×25 by default, or the size set by
{$Screen cols rows} (e.g. {$Screen 80 40}).
When output scrolls past the last row the top rows are discarded (no
scrollback), and GotoXY(1,1) addresses the current top
row. See examples/gotoxy.pas.
String escapes: \n\r\t\\\'\xNN\uXXXX\u{...}. Raw UTF-8 passes through. Doubled quotes
'' still work (Pascal).
Colours (TextColor / TextBackground)
TextColor(4); // blue foreground (palette index)TextBackground(1); // red background (palette index)TextColor('ff0000ff'); // opaque red (RRGGBBAA)TextBackground('0000ff80'); // blue bg, 50% alphaTextColorRGB(255, 0, 0); // opaque red (r,g,b)BackgroundRGB(0, 0, 255); // blue background (r,g,b)writeln('hi'); // rendered with the current colours
Set the colour for subsequent console output; it applies until
changed (there is no reset call — set both back to defaults).
Palette indices (0..15, TP's TextColor numbers):
0 black 1 red 2 green 3 yellow/brown
4 blue 5 magenta 6 cyan 7 light gray (default fg)
8 dark gray 9 light red 10 light green 11 light yellow
12 light blue 13 light magenta 14 light cyan 15 white
uses Crt — the built-in Crt
unit provides the 16 TP7 colour constants
(Black, Blue, Red,
LightGreen, White, …) so you can write
TextColor(Red) instead of TextColor(4).
ClrScr/TextColor/TextBackground/
GotoXY/Delay/ReadKey/
UpCase/KeyPressed are compiler builtins, so
the unit only brings the names into scope.
Hex strings — an 8-digit RRGGBBAA
string gives an arbitrary colour with alpha (CSS order, alpha last):
TextColor('ff0000ff') is opaque red,
'0000ff80' is blue at 50% alpha. The value must be exactly
8 hex digits; anything else is a compile error.
RGB components — TextColorRGB(r, g, b)
and BackgroundRGB(r, g, b) (alias
TextBackgroundRGB) take runtime
0..255 components (opaque, alpha 255), so a colour can
be computed in a loop: TextColorRGB(i * 5, 0, 0) ramps
red. See examples/enhanced_colours.pas.
Colour runs render into the output panel as
<span style="color:..;background:.."> and coexist
with a graphics canvas on the same page. See examples/colors.pas.
Host & configuration
Directives
{$mode fpc}, {$inline on},
{$WARN n off}, {$R/$Q/$B/$H} switches, and
// line comments are accepted (some are no-ops).
{$IF expr} / {$FATAL msg} / {$ENDIF}
are evaluated by a mini preprocessor: {$IF} supports
integer consts (from a prescanned const section), dec/hex
literals, and + - * div mod and or xor ( ) < > <> <= >= = shl shr not.
{$M n} sets the program's linear memory size:
{$M 32M} (MiB), {$M 512K} (KiB), or
{$M 1048576} (bytes). Default is 16 MiB; clamped to
64 KiB..4 GiB. The status bar's RAM readout shows the
available total vs the static data used (globals + string literals),
plus · heap when the program allocates.
GetMem/FreeMem/New/
Dispose allocate from a heap after the static data
(bump allocator with a free list; no coalescing). A bare
readln; pauses until Enter is pressed.
{$Screen cols rows} sets the text screen size (default
80×25), e.g. {$Screen 80 40}. Unknown
{$...} directives are silently ignored.
batchiness — exports batchiness_main; batched-canvas game + Web Audio SFX via app_env. Examples: pascaloids.pas, pong.pas, transforms.pas, sparks.pas, growable.pas, flightleader.pas (FLIGHT LEADER — a 6-unit arcade space-combat-style game), xonix.pas (Xonix — the fill floods from the balls, so it can never cover one).
basic_canvas — exports wasm_init/wasm_update/wasm_click/wasm_get_pixels/wasm_get_width/wasm_get_height; the host drives a fixed-step animation loop and blits the RGBA buffer. Example: basic_canvas.pas.
console-only — no entry export; write/writeln/read/readln/TextColor programs via wasmpascal_env imports. Example: hello_write.pas.
The ABI is chosen automatically by peeking the compiled program's
exports. odin_env provides the runtime hooks (write, trap,
abort, Math.*, rand_bytes); wasmpascal_env
is emitted only when the program uses console I/O.
batchiness keyboard — games self-wire their own
keyboard via batch_add_event_listener on
document (the bridge forwards the raw event and sets
batchiness_set_last_event before the callback), then
read evt.key / evt.code with
batch_get_property_str. There is no host key mapping, so
a game can use any key it wants. See pong.pas (and
pascaloids.pas, transforms.pas) for the
pattern: export SetLastEvent name 'batchiness_set_last_event',
wire keydown/keyup on
bGetGlobal('document'), and in the callback map
evt.key to your own key slots.
Editor
Editor shortcuts & toolbar
Syntax highlighting — keywords, numbers, strings, comments, and operators are colored live as you type; unterminated strings/comments get a wavy red underline. A line-number gutter marks the current error line.
Ctrl/Cmd+X — cut the current line (no selection) or the selection.
Ctrl/Cmd+Z — undo; Ctrl/Cmd+Shift+Z (or Ctrl/Cmd+Y) — redo. Tab, auto-close, comment toggle, and cut-line are all undoable.
Enter — auto-indent to the current line's indent.
([{'" — auto-close.
Ctrl+/ — toggle a (* ... *) comment.
A — cycle editor font size; Clear Console (output panel) — clear the console output.
Help… menu / F1 — this reference; Esc — close.
Toolbar: Run, Stop, Download… (compiled .wasm / project .zip / standalone app .zip or .html), Upload files, A (cycle editor font size), Help… (quick reference / lessons), Examples…, Rename project, file dropdown, Files… (+ File/Rename file/Set as root/− File).
Standalone apps — the Download… standalone .zip is a ready-to-host folder: its runner page loads the compiled .wasm file, so host the folder over HTTP (opening index.html from file:// shows a "serve this folder over HTTP" error). The single-file .html has everything inlined and opens straight from disk.
About
Acknowledgements & Credits
wasmpascal is built on open-source foundations:
CodeMirror 6 (by Marijn Haverbeke and contributors, MIT License) — powers the IDE code editor with virtualized DOM rendering, Pascal syntax stream parsing, bracket matching, code folding, active-line highlighting, and search & replace.
a lesson-by-lesson tour of the language and the playground
1Start here
wasmpascal compiles Pascal source to WebAssembly
entirely inside your browser, then runs the freshly compiled wasm on
the same page. You write a program on the left, press
Run, and its output appears on the right: console text
in the output panel, and a canvas for the graphics host ABIs.
Edit and press Run again to see the change — nothing to install,
nothing to rebuild.
This tutorial teaches the whole language from nothing. The
? button beside Learn is the quick reference for
when you already know your way around and just want to look up a
builtin.
Every runnable example below has a Load
this into the editor button. It replaces the editor's contents
(you will be asked first if there is unsaved work), closes this
window, and drops you back at the code so you can press Run and poke
at it.
2Program structure
A program starts with a program/library
header and ends with end. (the final dot). The
library header is what the examples use; either works.
The optional uses clause pulls in units registered with
the host — more on that in lesson 11.
library hello;
procedure Main;
beginwriteln('Hello from wasmpascal!');
end;
begin
Main;
end.
Statements live in begin .. end blocks. A
writeln(...) prints a line of text, which is how this
program talks to you — press Run and the output panel shows what it
says.
3Values and types
Pascal is a typed language: every variable and function result
has a type, declared ahead of use. wasmpascal's types are:
Enumerations: type Color = (Red, Green, Blue)
gives named values with ordinals 0, 1, 2 — usable in
case, array bounds, and for counters.
Named subranges (type Index = 1..10) bound arrays and
variables the same way.
Structured: records, static arrays (including
multi-dim array[1..3, 1..4] of T with
a[i, j] indexing), pointers, and strings (including
fixed String[n]) — their own lessons below.
Floats and integers mix in expressions: integer arguments to
math builtins are promoted to floats automatically.
4Variables and constants
A var is a name you can reassign; a
const is a compile-time value you cannot. Declarations
go in var and const sections (type
sections come later).
library vars;
const
MAX = 100;
var
score: Integer;
ratio: Double;
begin
score := 42;
ratio := 3.5;
writeln('Hi', ' score ', score);
writeln('ratio ', ratio);
writeln('MAX is ', MAX);
end.
:= is the assignment operator (a plain
= is only for comparison). A const given a
value at its declaration is inlined at compile time — you can use it
in array bounds, case labels, and {$IF}
preprocessor expressions. String literals work in
writeln — single-quoted ('text') or
double-quoted ("text", added 2026-08-19) — and are
stored statically at compile time.
writeln accepts as many arguments as you like,
separated by commas, and prints them one after another followed by a
newline.
5Numbers: integer math
The integer operators are +-*div (division) and mod
(remainder). div is the integer division: it
truncates, so 7 div 2 is 3. A single slash
/ is real division and gives a float even from
two integers.
library intmath;
var
total, n: Integer;
begin
total := 0;
for n := 1to10do
total := total + n;
writeln('sum 1..10 = ', total);
writeln('7 div 2 = ', 7div2);
writeln('7 mod 3 = ', 7mod3);
writeln('7 / 2 = ', 7 / 2);
end.
Watch out: unsigned div/mod need
both operands unsigned. Cardinal mod 100 is
signed and can silently go negative. Use a power-of-two mask
(x and (RANGE - 1)) for wrapping, as the examples do.
6Floats
Floats get the usual arithmetic, plus the math builtins:
Sqrt, Sqr, Abs,
Trunc, Round, Frac,
Pi(), trigonometry
(Sin/Cos/Tan/
ArcTan…), and logs
(Ln/Exp/Log10/
Power/Hypot). The trig/log ones are
implemented by the host's Math.*, so they work
everywhere. (Pi() must be called with parentheses;
a bare Pi does not resolve.)
library floats;
var
r, area: Double;
begin
r := 2.5;
area := 3.14159 * Sqr(r); // Sqr is squares; Pi() is a builtinwriteln('area of circle r=2.5 is ', area);
writeln('sqrt(2) = ', Sqrt(2.0));
writeln('trunc(3.7) = ', Trunc(3.7));
writeln('round(3.7) = ', Round(3.7));
end.
Field widths pad the output — write(x:5)
right-aligns x in 5 columns (lesson 7 covers it in
more depth). Prefer Double/Real for
floats you print: Single values print fine too, but
mixing a Single into a writeln with other
values is a codegen edge case, so keep printed floats
Double. Floats print in the host's default format;
very large or very small values use exponent notation.
7Console I/O: write, readln
write prints text without a newline;
writeln adds one. Both take any number of arguments.
A field width x:N right-aligns the value in
N columns (negative N left-aligns), handy
for tables. read/readln read integers or
floats, prompting once per argument.
library console;
var
n: Integer;
f: Double;
beginwrite('Enter an integer: ');
readln(n);
write('Enter a float: ');
readln(f);
writeln('n + f = ', n + f);
writeln('table:');
writeln(1, n:6, f:8);
writeln(2, n * 2:6, f * 2:8);
end.
Console output renders as a fixed character screen (Turbo Pascal
style, 80×25 by default) in the output panel. When text
reaches the last row the screen scrolls up; there is no scrollback.
readln prompts via the browser's inline input (or a
dialog in fallback mode) — this program blocks until you answer.
Strings behave like Turbo Pascal: string constants
(const G = 'Hi'), + concatenation, full
comparison (= <> < >, with prefix
ordering), and the builtins Length,
Concat, Copy, Pos,
Val, Str, and
StringOfChar. A single-char literal is a
Char, so string literals need two or more characters.
Functions can return strings, including via Exit('x').
library strings2;
function greet: string;
begin
greet := 'Hello';
end;
var
s: string;
begin
s := greet() + ', ' + 'World';
writeln(s); // Hello, Worldwriteln(Length(s)); // 12writeln(Copy(s, 1, 5)); // Hellowriteln(Pos('World', s)); // 8if'abc' < 'abd'thenwriteln('lt');
s := StringOfChar('*', 5);
writeln(s); // *****end.
8Control flow
Conditions go in parentheses; the statement after a condition or
loop is the body (use begin..end to group several).
break/continue work in the innermost
loop; exit leaves the current routine (optionally with
a value, exit(42)).
library control;
var
i: Integer;
beginfor i := 1to10dobeginif i mod3 = 0thencontinue; // skip multiples of 3if i > 7thenbreak; // stop at 8write(i, ' ');
end;
writeln;
case42of1, 2, 3: writeln('small');
41, 42, 43: writeln('medium');
elsewriteln('large');
end;
repeatwriteln('Looping under repeat...until');
i := i - 1;
until i < 0;
end.
case takes case expr of with
;-separated arms, each a comma-separated list of
values or lo..hi ranges (0..4) followed by
: and a statement, then end. An
else arm catches everything else. Dense ranges
dispatch via br_table; very wide or sparse spans fall
back to an if-chain, so both styles behave the same. A
for loop's counter runs from to to
inclusive; the step runs at the top, so continue
inside always advances it.
Ternary expressions (Delphi 13 style):
if cond then a else b used where an expression is
expected — an assignment, a call argument, or inside a larger
expression. The condition must be Boolean and the two branches must
be the same type (numeric kinds promote, so
if c then 1 else 2.5 is Real);
strings work too — the picked string becomes the
result; sets, records and arrays are not allowed. Only the taken
branch runs (short-circuit), so a divide-by-zero or nil-deref in the
untaken branch never executes. Nested ternaries are dangling-else
safe: the inner if binds its own else.
library ternary;
function Min(a, b: Integer): Integer;
begin
Min := if a < b then a else b;
end;
procedure Demo;
var
n, score: Integer;
d: Double;
label_text: String;
beginfor n := 1to4dobegin
score := if n mod2 = 0then1else0; // 1 = evenwriteln('n = ', n, ' parity = ', score);
end;
score := 0;
score := score + (if score = 0then10else5);
writeln('score = ', score);
d := if score > 5then1else2.5; // int + Real -> Realwriteln('d = ', d);
writeln('Min(3, 9) = ', Min(3, 9));
label_text := if score = 10then'ten'else'other'; // string ternarywriteln('word = ', label_text);
writeln(if score > 5then'big'else'small');
end;
begin
Demo;
end.
9Procedures and functions
Routines are declared before the begin..end. of the
program (typically above the main block). A function
declares its result type and assigns the result to its own name;
a procedure returns nothing.
library fns;
function Factorial(n: Integer): Integer;
var
i, r: Integer;
begin
r := 1;
for i := 2to n do r := r * i;
Factorial := r;
end;
procedure Announce(n: Integer);
beginwriteln('value is ', n);
end;
begin
Announce(42);
writeln('5! = ', Factorial(5));
end.
var parameters (declared procedure P(var x:
Integer)) are passed by address and can be modified by the
routine. The argument can be a global, an indexed element, a record
field, a pointer deref, or a local — locals live in a per-call
stack frame in linear memory, so they have a real address. Args must
be type-identical lvalues (no P(1+2)). exit
returns early from either kind of routine (optionally with a value:
exit(42)). Strings work Pascal-side too: string
vars/params are (addr,len) pairs — assign with s := 'lit',
print with writeln(s), concatenate with +, pass
by value or var; foreign functions get the pair as two
i32s.
10Records, arrays, and pointers
A record groups fields; a static
array has fixed bounds; pointers (^T)
hold addresses you take with @ and follow with
p^. Records and arrays nest, and indexed fields work
like balls[i].x.
library recs;
type
TPoint = record
x: Integer;
y: Integer;
end;
TGrid = array [0..9] of Integer;
var
p: TPoint;
g: TGrid;
i: Integer;
begin
p.x := 3;
p.y := 4;
writeln('point ', p.x, ',', p.y);
for i := 0to9do g[i] := i * i;
writeln('g[4] = ', g[4]);
end.
Records use FPC natural alignment for layout — relevant only when
you reach past a record's fields into raw memory.
SizeOf(T) / SizeOf(v) is a compile-time
constant (SizeOf(Integer) = 4,
SizeOf(String) = 8 — the (addr,len) pair model).
nil is the null pointer (assign and compare with
=/<>; the TP idiom
p <> 0 also works), and
Inc(p)/Dec(p, n) move a typed pointer by
whole elements — ideal for walking record arrays. A record can also
carry variants: a case tag: T of part whose
arms share storage, with an else arm as the default.
Multi-dim arrays use the TP sugar
array[1..3, 1..4] of T with a[i, j]
indexing (equivalent to nested arrays).
library recs2;
type
TPair = record
a, b: Integer;
end;
PPair = ^TPair;
TShape = recordcase tag: Byte of0: (r: Single);
1: (w, h: Single);
end;
var
arr: array[0..1] of TPair;
p: PPair;
s: TShape;
begin
p := @arr[0];
Inc(p); // +8: one whole record
p.a := 11;
writeln(arr[1].a); // 11writeln(SizeOf(TPair)); // 8
s.tag := 1;
s.w := 2.0;
s.h := 3.0;
writeln(s.w); // 2writeln(SizeOf(s)); // 12end.
For building games, look at the basic_canvas.pas
example: it mixes arrays of records, Byte pixel
buffers, and an RNG.
11with and set of
with <record> do lets you address a record's
fields by their bare names for the duration of one statement (or a
begin..end block) — handy when a record has many
fields. Comma chains (with a, b do) nest, with the
innermost record winning for overlapping names; a
with scope also shadows a same-named global. Locals
and parameters always shadow with fields.
library withset;
type
TPoint = record
x, y: Integer;
end;
var
p: TPoint;
flags: set of0..7;
beginwith p dobegin
x := 3; // p.x := 3
y := 4; // p.y := 4end;
writeln('p = (', p.x, ',', p.y, ')');
flags := [1, 3, 5];
writeln('3 in flags = ', 3in flags);
writeln('2 in flags = ', 2in flags);
flags := flags + [2]; // union
flags := flags - [1]; // differencewriteln('2 in flags = ', 2in flags);
writeln('1 in flags = ', 1in flags);
end.
A set of <ordinal> is a bitmask: a domain that
fits in 32 bits (0..6, 0..31) is a single
integer; set of Byte uses 8 words. Operators:
+ union, - difference, *
intersection, in membership, = /
<> equality, <= /
>= subset/superset. Constructors look like
[1, 3..5] or [] (empty). Set variables
must be globals — like arrays, they need an
address, and the compiler rejects local sets. See
examples/set_demo.pas.
case arms can also use lo..hi ranges:
case x of 0..4: ...; else ... end; groups contiguous
values in one arm (dense ranges use br_table, wide or
sparse ones degrade to an if-chain — same behavior). See
examples/case_ranges_demo.pas.
12Units and multi-file projects
A project is a named set of Pascal files with one
root (the file that gets compiled). Every other file is a
usesunit — a file with a
unit Name; header and interface /
implementation sections. The root pulls them in with
uses.
library use_math;
uses u_math;
beginwriteln('21 doubled is ', Twice(21));
end.
unit u_math;
interfacefunction Twice(x: Integer): Integer;
implementationfunction Twice(x: Integer): Integer;
begin
Twice := x * 2;
end;
end.
The example above is a two-file project: the root
use_math.pasuses u_math; and calls
Twice(21), and u_math.pas is the unit
file that provides it — both are loaded into the project so the
file dropdown shows them. (Note: avoid naming your own routines
Double — the compiler treats that as the 64-bit float
type.) The + File button inserts a starter skeleton
automatically. Units register with the host at run time and are
deduped depth-first, so transitive uses just work.
The sweep.pas example (minesweeper, 7 units) shows a
real multi-file game.
13Host ABIs and the console
wasmpascal detects how to boot a compiled program by peeking its
exports — you never pick the ABI yourself. There are four:
console-only — no entry export; plain
write/writeln/readln/
TextColor programs (all the lessons above).
pascaldom — exports pascaldom_main;
self-wires its canvas + event loop via pascaldom_env
DOM/canvas imports. Example: sweep.pas.
batchiness — exports batchiness_main;
batched-canvas games + Web Audio SFX via app_env.
Games self-wire their own keyboard (see the ?
reference's Host ABIs section).
Examples: pascaloids.pas, pong.pas,
transforms.pas, sparks.pas,
growable.pas, flightleader.pas
(FLIGHT LEADER, a 6-unit space combat game),
xonix.pas (a Xonix clone: draw lines to claim
land — the fill floods from the balls, so it can never cover
one).
basic_canvas — exports
wasm_init/wasm_update/
wasm_click/wasm_get_pixels/
wasm_get_width/wasm_get_height; the host
drives a fixed-step loop and blits your RGBA buffer. Example:
basic_canvas.pas.
The ? quick reference's Host ABIs section has
the full details and examples. When you want to draw pixels or
build a game, load one of those examples, Run it, and study how it
wires its external 'pascaldom_env' ... /
'app_env' imports before writing your own.
14What next
You now know enough to read every example in the
Examples… dropdown. Good next moves:
guess.pas — a number-guessing game using
readln, loops, and an RNG.
fibonacci.pas — recursion + a loop.
shapes.pas — ASCII art via
write/writeln.
gotoxy.pas, colors.pas
— cursor placement and TextColor.
math.pas — every math builtin.
with_demo.pas,
set_demo.pas,
case_ranges_demo.pas — the with
statement, set operators, and range arms.
ternary.pas — Delphi-13-style
if cond then a else b expressions.
var_params_demo.pas — var
parameters by address: Swap on locals, plus var
string/record/array params and @ on a local.
basic_canvas.pas — the pixel-buffer
canvas ABI.
pascalsweep and
pascaloids.pas — full games on the pascaldom /
batchiness ABIs.
And remember: the fastest way to learn anything here is to take
a working program, Load this into the editor, press Run, then
break it and watch the compiler's error messages point you at the
offending line.