Each example below is written in PatLang, compiled by the self-hosted PatLang compiler (lexer, parser, lowerer, and code generator all written in PatLang), and embedded here as WebAssembly. Press Run to execute it in your browser; programs time themselves with now_ms(), so you can compare the in-browser numbers with the native transcript captured on the build machine.
Recursion, control flow, lists, events (when/emit), logic (fact/query), OO (new/send/get), and functional map/filter via apply.
# Feature demo in the Stage 1 self-hosted language.
# Exercises: functions + recursion, control flow, lists, member access,
# events (when/emit), logic (fact/query), OO (new/send/get), and
# functional style (map/filter over named functions via apply).
make a function called fib takes n returns r
if n < 2 then
return n
else
return fib(n - 1) + fib(n - 2)
end
end
make a function called sum_list takes xs returns total
let total = 0
let i = 0
while i < xs.length do
let total = total + xs[i]
let i = i + 1
end
return total
end
when greeting do
print("event received: " + event_data)
end
let nums = [1, 2, 3, 4]
print("fib(10) = " + fib(10))
print("sum = " + sum_list(nums))
if sum_list(nums) == 10 then
print("sum ok")
else
print("sum wrong")
end
emit("greeting", "hello events")
fact("parent", "alice", "bob")
fact("parent", "alice", "carol")
print("alice children: " + query("parent", "alice", 0))
new("Person", "kim")
send("kim", "set", "age", 42)
print("kim age: " + get("kim", "age"))
# functional style: higher-order map/filter over named functions
make a function called double takes x returns r
return x * 2
end
make a function called is_even takes x returns r
return x % 2 == 0
end
make a function called map_list takes xs, fname returns out
let out = []
let i = 0
while i < xs.length do
let out = list_push(out, apply(fname, xs[i]))
let i = i + 1
end
return out
end
make a function called filter_list takes xs, fname returns out
let out = []
let i = 0
while i < xs.length do
if apply(fname, xs[i]) then
let out = list_push(out, xs[i])
end
let i = i + 1
end
return out
end
print("doubled: " + map_list(nums, "double"))
print("evens: " + filter_list(nums, "is_even"))
(not run yet)
Native run on the build machine:
fib(10) = 55 sum = 10 sum ok event received: hello events alice children: 2 kim age: 42 doubled: [2, 4, 6, 8] evens: [2, 4]
fib(27), a 2-million iteration loop, and a 200k-element vector build, each reporting its own elapsed milliseconds.
# Self-timing benchmark in the Stage 1 self-hosted language.
# Reports its own elapsed time via now_ms(), so the same program measures
# itself natively, on WASM, and under the interpreter.
make a function called fib takes n returns r
if n < 2 then
return n
else
return fib(n - 1) + fib(n - 2)
end
end
make a function called sum_to takes n returns total
let total = 0
let i = 1
while i <= n do
let total = total + i
let i = i + 1
end
return total
end
let t0 = now_ms()
let f = fib(27)
let t1 = now_ms()
print("fib(27) = " + f + " [" + (t1 - t0) + " ms]")
let t2 = now_ms()
let s = sum_to(2000000)
let t3 = now_ms()
print("sum 1..2000000 = " + s + " [" + (t3 - t2) + " ms]")
let t4 = now_ms()
let xs = vec_new()
let k = 0
while k < 200000 do
vec_push(xs, k * 2)
let k = k + 1
end
let t5 = now_ms()
print("built vector of " + vec_len(xs) + " [" + (t5 - t4) + " ms]")
print("total: " + (t5 - t0) + " ms")
(not run yet)
Native run on the build machine:
fib(27) = 196418 [201 ms] sum 1..2000000 = 2000001000000 [615 ms] built vector of 200000 [66 ms] total: 882 ms
The self-hosted PatLang compiler (lexer, parser, lowerer) compiled to WebAssembly. Edit the program, press Run: your source is tokenized, parsed, and lowered by PatLang code running in this page, then executed on the runtime VM. No server, no rustc.
# Edit me, then press Run. This compiles and executes in your browser.
make a function called fib takes n returns r
if n < 2 then
return n
else
return fib(n - 1) + fib(n - 2)
end
end
when greeting do
print("event says: " + event_data)
end
let xs = [3, 1, 4, 1, 5, 9, 2, 6]
let total = 0
let i = 0
while i < xs.length do
let total = total + xs[i]
let i = i + 1
end
print("sum of " + xs + " is " + total)
print("fib(20) = " + fib(20))
emit("greeting", "hello from the browser")
(not run yet)
A checkout session as a stream of scan/pay events: the product catalog lives in the object store, and a dairy fact drives the discount rule.
# =============================================================================
# Point-of-sale library (Stage 1 dialect): event-driven checkout combining
# events (scan/pay), OO (product catalog in the object store), and logic
# (dairy facts drive a discount rule). Prices are integer pence.
# =============================================================================
make a function called pos_setup returns done
new("Product", "apple")
send("apple", "set", "price", 30)
new("Product", "banana")
send("banana", "set", "price", 25)
new("Product", "milk")
send("milk", "set", "price", 120)
fact("dairy", "milk", "yes")
fact("dairy", "cheese", "yes")
set_var("total", 0)
set_var("items", 0)
set_var("receipt", "")
goal("serve_customer", "till-1")
return true
end
make a function called pos_scan takes item returns price
let price = get(item, "price")
if not price then
print(" unknown item: " + item)
return 0
end
if query("dairy", item, 0) > 0 then
# dairy discount: 10% off
let price = price * 9 / 10
end
set_var("total", get("__vars", "total") + price)
set_var("items", get("__vars", "items") + 1)
set_var("receipt", get("__vars", "receipt") + " " + item + " " + price + "p\n")
return price
end
make a function called pos_total returns t
return get("__vars", "total")
end
make a function called pos_pay takes tendered returns change
let total = get("__vars", "total")
let change = tendered - total
print("--- RECEIPT ---")
print(get("__vars", "receipt") + " items: " + get("__vars", "items"))
print(" total: " + total + "p, tendered: " + tendered + "p, change: " + change + "p")
return change
end
when scan do
pos_scan(event_data)
end
when pay do
pos_pay(event_data)
end
# Point-of-sale demo driver (concatenate after lib/pos.patlang).
# A checkout session as a stream of events.
pos_setup()
print("scanning...")
emit("scan", "apple")
emit("scan", "apple")
emit("scan", "banana")
emit("scan", "milk")
emit("scan", "unobtainium")
emit("pay", 500)
print("till session complete")
(not run yet)
Native run on the build machine:
scanning... unknown item: unobtainium --- RECEIPT --- apple 30p apple 30p banana 25p milk 108p items: 4 total: 193p, tendered: 500p, change: 307p till session complete
A PatLang test framework running against the point of sale: unit assertions, event-driven integration checks, and a Gherkin feature whose steps dispatch through apply.
# =============================================================================
# Test framework (Stage 1 dialect): unit assertions plus a Gherkin-style
# feature runner. Step definitions are registered in the object store keyed
# by their text; features are plain text dispatched line by line, so the
# same framework covers unit, integration, and behaviour tests.
# =============================================================================
make a function called t_init returns done
set_var("t_pass", 0)
set_var("t_fail", 0)
set_var("t_tagfilter", "")
set_var("t_pending_tags", "")
set_var("t_skipping", 0)
return true
end
make a function called contains_text takes hay, needle returns r
if needle.length > hay.length then
return false
end
let i = 0
while i <= hay.length - needle.length do
if substr(hay, i, needle.length) == needle then
return true
end
let i = i + 1
end
return false
end
make a function called check takes label, actual, expected returns ok
if actual == expected then
set_var("t_pass", get("__vars", "t_pass") + 1)
print(" ok: " + label)
return true
else
set_var("t_fail", get("__vars", "t_fail") + 1)
print(" FAIL: " + label + " (got " + actual + ", want " + expected + ")")
return false
end
end
make a function called t_report returns ok
let p = get("__vars", "t_pass")
let f = get("__vars", "t_fail")
print("tests: " + p + " passed, " + f + " failed")
if f == 0 then
print("ALL TESTS PASSED")
return true
else
print("TESTS FAILED")
return false
end
end
# ---- Gherkin runner ----
# Register a step: step("a fresh till", "st_fresh_till")
make a function called step takes text, fname returns done
new("Step", text)
send(text, "set", "fn", fname)
return true
end
make a function called starts_with takes s, prefix returns r
if s.length < prefix.length then
return false
end
return substr(s, 0, prefix.length) == prefix
end
make a function called trim_left takes s returns out
let i = 0
let scanning = true
while (i < s.length) and scanning do
let c = char_code(s, i)
if (c == 32) or (c == 9) then
let i = i + 1
else
let scanning = false
end
end
return substr(s, i, s.length - i)
end
# Strip a Gherkin keyword; returns the step text or "" if not a step line
make a function called step_text takes line returns out
if starts_with(line, "Given ") then
return substr(line, 6, line.length - 6)
end
if starts_with(line, "When ") then
return substr(line, 5, line.length - 5)
end
if starts_with(line, "Then ") then
return substr(line, 5, line.length - 5)
end
if starts_with(line, "And ") then
return substr(line, 4, line.length - 4)
end
return ""
end
# Run only scenarios whose preceding @tag line contains `tag` ("" = all)
make a function called run_feature_tagged takes feature, tag returns ok
set_var("t_tagfilter", tag)
return run_feature(feature)
end
make a function called run_feature_file takes path returns ok
return run_feature(read_file(path))
end
make a function called run_feature takes feature returns ok
let h = str_intern(feature)
let n = sc_len(h)
let i = 0
let line = sb_new()
while i <= n do
let c = sc_code(h, i)
if (c == 10) or (c == -1) then
let raw = trim_left(sb_str(line))
let line = sb_new()
if starts_with(raw, "@") then
set_var("t_pending_tags", raw)
end
if starts_with(raw, "Feature:") then
print(raw)
end
if starts_with(raw, "Scenario:") then
let filter = get("__vars", "t_tagfilter")
let tags = get("__vars", "t_pending_tags")
set_var("t_pending_tags", "")
if filter then
if tags then
if contains_text(tags, filter) then
set_var("t_skipping", 0)
print(raw + " [" + tags + "]")
else
set_var("t_skipping", 1)
print(raw + " [skipped: needs " + filter + "]")
end
else
set_var("t_skipping", 1)
print(raw + " [skipped: needs " + filter + "]")
end
else
set_var("t_skipping", 0)
print(raw)
end
else
let text = step_text(raw)
if (text != "") and (get("__vars", "t_skipping") != 1) then
let fname = get(text, "fn")
if fname then
apply(fname)
else
set_var("t_fail", get("__vars", "t_fail") + 1)
print(" FAIL: undefined step: " + text)
end
end
end
let i = i + 1
else
if c == 13 then
let i = i + 1
else
sb_push(line, sc_char(h, i))
let i = i + 1
end
end
end
return true
end
# =============================================================================
# Point-of-sale library (Stage 1 dialect): event-driven checkout combining
# events (scan/pay), OO (product catalog in the object store), and logic
# (dairy facts drive a discount rule). Prices are integer pence.
# =============================================================================
make a function called pos_setup returns done
new("Product", "apple")
send("apple", "set", "price", 30)
new("Product", "banana")
send("banana", "set", "price", 25)
new("Product", "milk")
send("milk", "set", "price", 120)
fact("dairy", "milk", "yes")
fact("dairy", "cheese", "yes")
set_var("total", 0)
set_var("items", 0)
set_var("receipt", "")
goal("serve_customer", "till-1")
return true
end
make a function called pos_scan takes item returns price
let price = get(item, "price")
if not price then
print(" unknown item: " + item)
return 0
end
if query("dairy", item, 0) > 0 then
# dairy discount: 10% off
let price = price * 9 / 10
end
set_var("total", get("__vars", "total") + price)
set_var("items", get("__vars", "items") + 1)
set_var("receipt", get("__vars", "receipt") + " " + item + " " + price + "p\n")
return price
end
make a function called pos_total returns t
return get("__vars", "total")
end
make a function called pos_pay takes tendered returns change
let total = get("__vars", "total")
let change = tendered - total
print("--- RECEIPT ---")
print(get("__vars", "receipt") + " items: " + get("__vars", "items"))
print(" total: " + total + "p, tendered: " + tendered + "p, change: " + change + "p")
return change
end
when scan do
pos_scan(event_data)
end
when pay do
pos_pay(event_data)
end
# Test suite for the point-of-sale library (concatenate after lib/test.patlang
# and lib/pos.patlang). Shows unit tests, event-driven integration tests, and
# a Gherkin feature exercising the same code.
t_init()
# ---- unit tests ----
print("unit: catalog and pricing")
pos_setup()
check("apple price", get("apple", "price"), 30)
check("milk is dairy", query("dairy", "milk", 0), 1)
check("banana is not dairy", query("dairy", "banana", 0), 0)
check("scan returns discounted dairy price", pos_scan("milk"), 108)
check("scan returns full price", pos_scan("banana"), 25)
check("running total", pos_total(), 133)
# ---- integration test: the same flow through events ----
print("integration: checkout via events")
pos_setup()
emit("scan", "apple")
emit("scan", "apple")
emit("scan", "milk")
check("event-driven total", pos_total(), 168)
check("change from 2 pounds", pos_pay(200), 32)
# ---- Gherkin feature ----
make a function called st_fresh_till returns done
pos_setup()
return true
end
make a function called st_scan_two_apples returns done
emit("scan", "apple")
emit("scan", "apple")
return true
end
make a function called st_scan_milk returns done
emit("scan", "milk")
return true
end
make a function called st_total_is_168 returns done
return check("the total is 168p", pos_total(), 168)
end
make a function called st_dairy_discount_applied returns done
return check("dairy discount applied", get("milk", "price") - 12, 108)
end
step("a fresh till", "st_fresh_till")
step("the customer buys two apples", "st_scan_two_apples")
step("the customer buys a bottle of milk", "st_scan_milk")
step("the total is 168 pence", "st_total_is_168")
step("the dairy discount was applied", "st_dairy_discount_applied")
let feature = "Feature: checkout
@smoke @till
Scenario: apples and discounted milk
Given a fresh till
When the customer buys two apples
And the customer buys a bottle of milk
Then the total is 168 pence
And the dairy discount was applied
@audit
Scenario: nightly stock audit
Given a fresh till
Then the total is 168 pence
"
# run only @smoke scenarios: the @audit scenario (which would fail) is skipped
run_feature_tagged(feature, "@smoke")
t_report()
(not run yet)
Native run on the build machine:
unit: catalog and pricing ok: apple price ok: milk is dairy ok: banana is not dairy ok: scan returns discounted dairy price ok: scan returns full price ok: running total integration: checkout via events ok: event-driven total --- RECEIPT --- apple 30p apple 30p milk 108p items: 3 total: 168p, tendered: 200p, change: 32p ok: change from 2 pounds Feature: checkout Scenario: apples and discounted milk [@smoke @till] ok: the total is 168p ok: dairy discount applied Scenario: nightly stock audit [skipped: needs @smoke] tests: 10 passed, 0 failed ALL TESTS PASSED true
require checks a precondition, ensure a postcondition, and assert is the same primitive used standalone — all three lower to one contract_check host call, enforced identically whether interpreted, run here via WASM (no rustc at all), or compiled natively. The last line deliberately violates a precondition so you can see enforcement fire.
# Design-by-contract demo in the Stage 1 self-hosted language.
# `require` checks a precondition, `ensure` a postcondition (checked wherever
# it's written — usually right before the value it names is returned), and
# `assert` is the same primitive used standalone. All three lower to the
# same contract_check(func, kind, text, ok) host call, so enforcement is
# identical whether interpreted, run via run_ir (no rustc), or compiled
# natively. The last line deliberately violates a precondition, to show
# what enforcement actually looks like.
make a function called safe_divide takes a, b returns r
require b != 0
let r = a / b
ensure (r * b) == a
return r
end
make a function called clamp takes x, lo, hi returns r
require lo <= hi
if x < lo then
let r = lo
else
if x > hi then
let r = hi
else
let r = x
end
end
ensure (r >= lo) and (r <= hi)
return r
end
make a function called factorial takes n returns r
require n >= 0
if n < 2 then
let r = 1
else
let r = n * factorial(n - 1)
end
ensure r >= 1
return r
end
print("safe_divide(10, 2) = " + safe_divide(10, 2))
print("clamp(15, 0, 10) = " + clamp(15, 0, 10))
print("clamp(-5, 0, 10) = " + clamp(-5, 0, 10))
print("factorial(6) = " + factorial(6))
assert (1 + 1) == 2
print("standalone assert passed")
print("--- now deliberately violating a precondition ---")
print("safe_divide(1, 0) = " + safe_divide(1, 0))
print("(unreachable: the line above aborts the program)")
(not run yet)
Native run on the build machine:
safe_divide(10, 2) = 5 clamp(15, 0, 10) = 10 clamp(-5, 0, 10) = 0 factorial(6) = 720 standalone assert passed --- now deliberately violating a precondition --- IR runtime error: contract violation: precondition failed in safe_divide(): (Var(b) != Num(0))
Writing the abstract requires every other section to exist first, and several sections have real sub-tasks of their own with their own delegated workers, gated by a QA review stage — a dependency DAG resolved goal by goal, not a linear chain. Real projects aren't linear either: a review can fail and blame an earlier stage, reopening it and everything downstream that depended on it (design sent back to methodology, experiments back to implementation, data analysis requiring experiments to be re-run, and so on). Watch it run as an animated timeline, with failed reviews and rework shown in red.
# Project report simulator: goal-oriented dependency resolution, with each
# task delegated to a worker role (OO), gated by a QA review stage, and
# timestamped on a virtual clock (a plain counter, not real wall-clock time)
# so a browser can replay the schedule as an animated timeline. Real
# projects aren't linear: a review can fail and blame an earlier stage,
# which reopens it AND everything downstream that depended on it - design
# might get sent back to methodology, experiments might get sent back to
# implementation or methodology, data analysis might require experiments to
# be re-run, and so on. Each task can only trigger this once itself
# (attempts < 1), so the whole simulation is guaranteed to converge no
# matter how the dice land.
# Tiny LCG (same constants as lib/maze.patlang's, duplicated rather than
# shared since the two demos are otherwise unrelated) for the QA dice-roll.
make a function called rand_seed takes seed returns done
let s = seed % 32768
if s < 0 then
let s = s + 32768
end
set_var("rp_rand", s)
return true
end
make a function called rand_next returns r
let s = get("__vars", "rp_rand")
let s2 = ((s * 1103) + 12345) % 32768
set_var("rp_rand", s2)
return s2
end
make a function called rand_below takes n returns r
return rand_next() % n
end
make a function called registry_init returns done
new("Registry", "registry")
send("registry", "set", "names", [])
return true
end
make a function called task takes name, worker, duration, requires, rework_targets returns done
new("Task", name)
send(name, "set", "worker", worker)
send(name, "set", "duration", duration)
send(name, "set", "requires", requires)
send(name, "set", "rework_targets", rework_targets)
send(name, "set", "attempts", 0)
send(name, "set", "status", "pending")
send("registry", "set", "names", list_push(get("registry", "names"), name))
return true
end
make a function called vclock returns t
return get("__vars", "vt")
end
make a function called advance takes ms returns done
set_var("vt", get("__vars", "vt") + ms)
return true
end
make a function called review_time takes duration returns rt
let rt = duration / 4
if rt < 150 then
let rt = 150
end
return rt
end
make a function called log_event takes phase, name, worker returns done
let line = vclock() + "," + phase + "," + name + "," + worker
let cur = get("__vars", "report_log")
if cur == "" then
set_var("report_log", line)
else
set_var("report_log", cur + ";" + line)
end
return true
end
# Marks a task (and, transitively, every task that required it, directly or
# indirectly) as no longer approved, since their prior work assumed an input
# that has just changed. A fixed-point relaxation over the whole registry -
# cheap at this scale, and doesn't need a precomputed reverse-dependency map.
make a function called reopen_cascade takes name returns done
send(name, "set", "status", "pending")
let names = get("registry", "names")
let changed = true
while changed do
let changed = false
let i = 0
while i < names.length do
let t = names[i]
if get(t, "status") != "pending" then
let reqs = get(t, "requires")
let j = 0
let hit = false
while j < reqs.length do
if get(reqs[j], "status") == "pending" then
let hit = true
end
let j = j + 1
end
if hit then
send(t, "set", "status", "pending")
let changed = true
end
end
let i = i + 1
end
end
return true
end
# Achieving a goal: make sure every requirement is already approved,
# recursing into whichever aren't; delegate the work to its assigned
# worker; then gate it behind a QA review, which - once per task, at
# most - might fail and blame an earlier stage instead of approving it.
make a function called achieve takes name returns done
if get(name, "status") == "approved" then
return true
end
goal("complete", name)
let reqs = get(name, "requires")
let i = 0
while i < reqs.length do
achieve(reqs[i])
let i = i + 1
end
let worker = get(name, "worker")
let duration = get(name, "duration")
send(name, "set", "status", "in_progress")
log_event("start", name, worker)
advance(duration)
log_event("work_done", name, worker)
send(name, "set", "status", "in_review")
log_event("review_start", name, "qa")
advance(review_time(duration))
let attempts = get(name, "attempts")
let targets = get(name, "rework_targets")
let should_fail = false
if (attempts < 1) and (targets.length > 0) then
if rand_below(100) < 30 then
let should_fail = true
end
end
if should_fail then
let blame = targets[rand_below(targets.length)]
log_event("rework:" + blame, name, "qa")
send(name, "set", "attempts", attempts + 1)
reopen_cascade(blame)
emit("task_reworked", name + ":" + blame)
return achieve(name)
end
log_event("review_done", name, "qa")
send(name, "set", "status", "approved")
fact("approved", name, "1")
emit("task_done", name)
return true
end
make a function called report_reset returns done
set_var("vt", 0)
set_var("report_log", "")
registry_init()
rand_seed(7)
return true
end
# Project report writer (concatenate after lib/report.patlang). Writing the
# abstract requires every other section to exist first, and several of
# those have real sub-tasks of their own with their own workers. Real
# projects aren't linear: a QA review can fail and blame an earlier stage -
# design might get sent back to methodology, experiments to implementation
# or methodology, data analysis might require experiments to be re-run -
# reopening it and everything downstream that depended on it.
report_reset()
task("background", "researcher", 400, [], [])
task("intro", "writer", 300, ["background"], [])
task("lit_review", "researcher", 600, ["background"], [])
task("methodology", "architect", 400, ["intro", "lit_review"], ["lit_review"])
task("design", "architect", 500, ["methodology"], ["methodology"])
task("implementation", "developer", 900, ["design"], ["design", "methodology"])
task("experiments", "researcher", 700, ["implementation"], ["implementation", "methodology"])
task("data_gathering", "researcher", 500, ["experiments"], ["experiments"])
task("data_analysis", "analyst", 800, ["data_gathering"], ["data_gathering", "experiments"])
task("results", "analyst", 300, ["data_analysis"], ["data_analysis"])
task("discussion", "writer", 600, ["results", "lit_review"], ["results"])
task("limitations", "writer", 300, ["discussion"], [])
task("conclusion", "writer", 300, ["discussion", "limitations"], [])
task("abstract", "writer", 400, ["background", "intro", "lit_review", "methodology", "design", "implementation", "experiments", "data_gathering", "data_analysis", "results", "discussion", "limitations", "conclusion"], [])
let all_tasks = ["background", "intro", "lit_review", "methodology", "design", "implementation", "experiments", "data_gathering", "data_analysis", "results", "discussion", "limitations", "conclusion", "abstract"]
when task_reworked do
print("[" + vclock() + "ms] REWORK: " + event_data + " (qa review failed, sending it back)")
end
when task_done do
print("[" + vclock() + "ms] approved: " + event_data + " (" + get(event_data, "worker") + ", reviewed by qa)")
end
print("Goal: complete the report (the abstract can only be written once everything it summarizes is approved).")
goal("report", "abstract")
achieve("abstract")
print("")
print(all_tasks.length + " tasks approved across 5 worker roles + qa review, " + vclock() + "ms of simulated (not real) time.")
Native run on the build machine:
Goal: complete the report (the abstract can only be written once everything it summarizes is approved). [550ms] approved: background (researcher, reviewed by qa) [1000ms] approved: intro (writer, reviewed by qa) [1750ms] approved: lit_review (researcher, reviewed by qa) [2300ms] approved: methodology (architect, reviewed by qa) [2950ms] approved: design (architect, reviewed by qa) [4075ms] approved: implementation (developer, reviewed by qa) [4950ms] approved: experiments (researcher, reviewed by qa) [5600ms] REWORK: data_gathering:experiments (qa review failed, sending it back) [6475ms] approved: experiments (researcher, reviewed by qa) [7125ms] approved: data_gathering (researcher, reviewed by qa) [8125ms] REWORK: data_analysis:data_gathering (qa review failed, sending it back) [8775ms] approved: data_gathering (researcher, reviewed by qa) [9775ms] approved: data_analysis (analyst, reviewed by qa) [10225ms] approved: results (analyst, reviewed by qa) [10975ms] approved: discussion (writer, reviewed by qa) [11425ms] approved: limitations (writer, reviewed by qa) [11875ms] approved: conclusion (writer, reviewed by qa) [12425ms] approved: abstract (writer, reviewed by qa) 14 tasks approved across 5 worker roles + qa review, 12425ms of simulated (not real) time.
Driven by a manifest file (self_hosting/patbuild.manifest):
# patbuild manifest: name <source or -> [deps...] fib_bench self_hosting/examples/fib_bench.patlang echo_server self_hosting/examples/echo_server.patlang contracts_demo self_hosting/examples/contracts_demo.patlang pos_demo self_hosting/lib/pos.patlang+self_hosting/examples/pos_demo.patlang all - fib_bench echo_server contracts_demo pos_demoTargets are objects, dependencies are walked recursively, finished builds become
facts, progress is reported via events. By default each target is only lowered to IR and checked (no rustc at all, matching the in-browser playground); pass --native to also compile every target to a real executable via rustc — the transcript below was captured that way.
# =============================================================================
# patbuild: a goal-oriented build system in PatLang (concatenate after the
# compiler libs). Targets are OO objects, the dependency graph is walked
# recursively, completed builds are recorded as facts, and progress is
# reported through events. By default each target is lowered and executed
# via run_ir (no rustc, matching how the in-browser playground works); pass
# --native to compile targets to real .exe artifacts via rustc_build instead.
# =============================================================================
make a function called target takes name, src, deps returns done
new("Target", name)
send(name, "set", "src", src)
send(name, "set", "deps", deps)
send(name, "set", "built", "no")
return true
end
make a function called split_ws takes s returns parts
let parts = []
let cur = sb_new()
let i = 0
while i <= s.length do
let c = char_code(s, i)
if (c == 32) or (c == -1) then
let word = sb_str(cur)
if word != "" then
let parts = list_push(parts, word)
end
let cur = sb_new()
else
sb_push(cur, s[i])
end
let i = i + 1
end
return parts
end
# Splits a source spec on '+' so a target can bundle a library with its
# driver without a module system, e.g. "lib/pos.patlang+examples/pos_demo.patlang"
make a function called split_plus takes s returns parts
let parts = []
let cur = sb_new()
let i = 0
while i <= s.length do
let c = char_code(s, i)
if (c == 43) or (c == -1) then
let piece = sb_str(cur)
let cur = sb_new()
if piece != "" then
let parts = list_push(parts, piece)
end
else
sb_push(cur, s[i])
end
let i = i + 1
end
return parts
end
make a function called read_bundle takes src returns source
let files = split_plus(src)
let b = sb_new()
let i = 0
while i < files.length do
if i > 0 then
sb_push(b, chr(10))
end
sb_push(b, read_file(files[i]))
let i = i + 1
end
return sb_str(b)
end
make a function called build takes name returns ok
if get(name, "built") == "yes" then
return true
end
let deps = split_ws(get(name, "deps"))
let i = 0
while i < deps.length do
build(deps[i])
let i = i + 1
end
let src = get(name, "src")
if src != "" then
let source = read_bundle(src)
let toks = tokenize(source)
let ast = parse_program(toks)
let ir = lower_program(ast)
if get("__vars", "native_mode") == 1 then
let rs = emit_program_rs(ir)
let exe = rustc_build(rs, "portfolio/build/" + name + ".exe")
fact("built", name, exe)
else
# Default: verify the self-hosted front end compiles this target to IR,
# with no rustc involved. Deliberately does not execute the IR here -
# some targets (e.g. echo_server) block on a real network connection
# and are meant to be run directly via `pat --ir-run`, not driven
# unattended by a build tool.
print("[patbuild] compiled " + name + " to IR (no rustc)")
fact("built", name, "(ir-only)")
end
end
send(name, "set", "built", "yes")
emit("built", name)
return true
end
when built do
print("[patbuild] built: " + event_data)
end
# ---- manifest ----
# Format, one target per line: name <source-path or -> [dep ...]
make a function called load_manifest takes path returns count
let count = 0
let text = read_file(path)
let h = str_intern(text)
let n = sc_len(h)
let i = 0
let line = sb_new()
while i <= n do
let c = sc_code(h, i)
if (c == 10) or (c == -1) then
let raw = sb_str(line)
let line = sb_new()
let parts = split_ws(raw)
if parts.length >= 2 then
if char_code(raw, 0) != 35 then
let src = parts[1]
if src == "-" then
let src = ""
end
let deps = sb_new()
let k = 2
while k < parts.length do
if k > 2 then
sb_push(deps, " ")
end
sb_push(deps, parts[k])
let k = k + 1
end
target(parts[0], src, sb_str(deps))
let count = count + 1
end
end
let i = i + 1
else
if c != 13 then
sb_push(line, sc_char(h, i))
end
let i = i + 1
end
end
return count
end
set_var("native_mode", 0)
let raw_args = argv()
let positional = []
let i = 0
while i < raw_args.length do
if raw_args[i] == "--native" then
set_var("native_mode", 1)
else
let positional = list_push(positional, raw_args[i])
end
let i = i + 1
end
let manifest = "self_hosting/patbuild.manifest"
let goal_target = "all"
if positional.length >= 1 then
let manifest = positional[0]
end
if positional.length >= 2 then
let goal_target = positional[1]
end
let loaded = load_manifest(manifest)
goal("ship", goal_target)
if get("__vars", "native_mode") == 1 then
print("[patbuild] manifest: " + manifest + " (" + loaded + " targets), goal: " + goal_target + " [native]")
else
print("[patbuild] manifest: " + manifest + " (" + loaded + " targets), goal: " + goal_target + " [interpreted]")
end
build(goal_target)
print("[patbuild] targets built: " + (query("built", "fib_bench", 0) + query("built", "echo_server", 0) + query("built", "contracts_demo", 0) + query("built", "pos_demo", 0)))
Native run on the build machine:
[patbuild] manifest: self_hosting/patbuild.manifest (5 targets), goal: all [native] [patbuild] built: fib_bench [patbuild] built: echo_server [patbuild] built: contracts_demo [patbuild] built: pos_demo [patbuild] built: all [patbuild] targets built: 4
A CLI tool (flowgraph <output.html> [inputs...]) that lowers programs with the self-hosted compiler, recovers basic blocks and jump edges from the IR, and renders them as SVG — one section per program. With no arguments it renders a curated set spanning the benchmark, the contracts demo, the point-of-sale bundle, and the all-paradigms demo: open the generated flow graphs.
# =============================================================================
# flowgraph: control-flow graph visualizer (concatenate after the compiler
# libs + lib/html.patlang). Lowers a PatLang program with the self-hosted
# compiler, splits each function's IR into basic blocks, and renders the
# blocks and jump edges as SVG in an HTML page.
# =============================================================================
make a function called contains_num takes xs, v returns r
let i = 0
while i < xs.length do
if xs[i] == v then
return true
end
let i = i + 1
end
return false
end
make a function called instr_text takes ins returns s
let s = ins[0]
if ins.length > 1 then
let s = s + " " + ins[1]
end
if ins.length > 2 then
let s = s + " " + ins[2]
end
return s
end
make a function called is_jump takes tag returns r
return (tag == "Jump") or (tag == "JumpIfFalse") or (tag == "Return")
end
# Render one function's CFG as SVG appended to builder b; returns block count
make a function called render_func takes b, f returns blocks
let instrs = f[3]
let n = instrs.length
# block starts: 0, every jump target, and every instr after a jump
let starts = [0]
let i = 0
while i < n do
let ins = instrs[i]
let tag = ins[0]
if (tag == "Jump") or (tag == "JumpIfFalse") then
if not contains_num(starts, ins[1]) then
let starts = list_push(starts, ins[1])
end
if not contains_num(starts, i + 1) then
let starts = list_push(starts, i + 1)
end
end
let i = i + 1
end
# block index for an instruction = count of starts <= idx that we pass
# build blocks in order by scanning
let rowh = 26
let y = 10
sb_push(b, "<h3>" + html_escape(f[1]) + "</h3>" + nl())
let blockTops = []
let blockIds = []
let i = 0
let bod = sb_new()
let edges = sb_new()
let blocks = 0
while i < n do
if contains_num(starts, i) then
let blocks = blocks + 1
let blockTops = list_push(blockTops, y)
let blockIds = list_push(blockIds, i)
sb_push(bod, "<text x='16' y='" + (y + 16) + "' class='bh'>B@" + i + "</text>" + nl())
let y = y + rowh
end
let ins = instrs[i]
sb_push(bod, "<text x='28' y='" + (y + 14) + "'>" + html_escape(instr_text(ins)) + "</text>" + nl())
let tag = ins[0]
if (tag == "Jump") or (tag == "JumpIfFalse") then
# edge from this row to the target block header (drawn after layout)
let edges = edges
sb_push(edges, "" + (y + 8) + " " + ins[1] + " " + tag + "\n")
end
let y = y + rowh
let i = i + 1
end
let height = y + 20
sb_push(b, "<svg viewBox='0 0 560 " + height + "' width='560' height='" + height + "' xmlns='http://www.w3.org/2000/svg'>" + nl())
# draw jump edges as curves on the right margin
let elist = sb_str(edges)
let eh = str_intern(elist)
let elen = sc_len(eh)
let k = 0
let word = sb_new()
let fields = []
while k <= elen do
let c = sc_code(eh, k)
if (c == 32) or (c == 10) or (c == -1) then
let w = sb_str(word)
let word = sb_new()
if w != "" then
let fields = list_push(fields, w)
end
if (c == 10) and (fields.length == 3) then
# fields: fromY targetInstr tag
let fromY = to_num(fields[0])
let tIdx = to_num(fields[1])
# find target block top
let ti = 0
let tY = 10
while ti < blockIds.length do
if blockIds[ti] == tIdx then
let tY = blockTops[ti]
end
let ti = ti + 1
end
let color = "#d64"
if fields[2] == "Jump" then
let color = "#48c"
end
sb_push(b, "<path d='M 330 " + fromY + " C 520 " + fromY + ", 520 " + (tY + 10) + ", 335 " + (tY + 10) + "' fill='none' stroke='" + color + "' marker-end='url(#arr)'/>" + nl())
let fields = []
end
else
sb_push(word, sc_char(eh, k))
end
let k = k + 1
end
sb_push(b, "<defs><marker id='arr' viewBox='0 0 8 8' refX='7' refY='4' markerWidth='6' markerHeight='6' orient='auto'><path d='M 0 0 L 8 4 L 0 8 z' fill='#888'/></marker></defs>" + nl())
sb_push(b, sb_str(bod))
sb_push(b, "</svg>" + nl())
return blocks
end
# Reads a source spec, supporting "lib.patlang+demo.patlang" bundling for
# programs that need a library concatenated ahead of their driver (mirrors
# patbuild's manifest convention, kept independent to avoid file coupling).
make a function called read_maybe_bundle takes spec returns source
let b = sb_new()
let cur = sb_new()
let i = 0
let first = true
while i <= spec.length do
let c = char_code(spec, i)
if (c == 43) or (c == -1) then
let piece = sb_str(cur)
let cur = sb_new()
if piece != "" then
if not first then
sb_push(b, chr(10))
end
sb_push(b, read_file(piece))
let first = false
end
else
sb_push(cur, spec[i])
end
let i = i + 1
end
return sb_str(b)
end
# Renders one program's functions as a labelled section; returns block count.
make a function called render_program_section takes b, label, spec returns blocks
let source = read_maybe_bundle(spec)
let toks = tokenize(source)
let ast = parse_program(toks)
let ir = lower_program(ast)
let funcs = ir[2]
sb_push(b, "<h2 id='" + label + "'>" + html_escape(label) + "</h2>" + nl())
sb_push(b, "<p class='label'><code>" + html_escape(spec) + "</code> — " + funcs.length + " function(s)</p>" + nl())
let i = 0
let total = 0
while i < funcs.length do
let total = total + render_func(b, funcs[i])
let i = i + 1
end
return total
end
# ---- driver ----
# Usage: flowgraph <output.html> [input1] [input2] ...
# With no inputs, renders a curated set of the portfolio's example programs
# into one navigable page (this is how the portfolio card invokes it).
let args = argv()
let output = "portfolio/flowgraph.html"
let specs = []
if args.length >= 1 then
let output = args[0]
end
if args.length >= 2 then
let k = 1
while k < args.length do
let specs = list_push(specs, args[k])
let k = k + 1
end
end
if specs.length == 0 then
let specs = [
"self_hosting/examples/fib_bench.patlang",
"self_hosting/examples/contracts_demo.patlang",
"self_hosting/lib/pos.patlang+self_hosting/examples/pos_demo.patlang",
"self_hosting/examples/feature_demo.patlang"
]
end
let nav = sb_new()
sb_push(nav, "<p>")
let i = 0
while i < specs.length do
if i > 0 then
sb_push(nav, " · ")
end
sb_push(nav, "<a href='#" + specs[i] + "'>" + html_escape(specs[i]) + "</a>")
let i = i + 1
end
sb_push(nav, "</p>" + nl())
let body = sb_new()
sb_push(body, "<h1>PatLang control-flow graphs</h1>" + nl())
sb_push(body, "<p>Basic blocks and jump edges recovered from the self-hosted compiler's own IR, one section per program. Red curves are conditional (JumpIfFalse), blue are unconditional.</p>" + nl())
sb_push(body, "<style>svg text { font: 12px monospace; fill: currentColor; } svg .bh { font-weight: bold; }</style>" + nl())
sb_push(body, sb_str(nav))
let total_funcs = 0
let total_blocks = 0
let i = 0
while i < specs.length do
let total_blocks = total_blocks + render_program_section(body, specs[i], specs[i])
let i = i + 1
end
let page = html_page("PatLang flow graphs", sb_str(body), "")
write_file(output, page)
print("[flowgraph] " + specs.length + " program(s), " + total_blocks + " basic blocks -> " + output)
Native run on the build machine:
[flowgraph] 4 program(s), 63 basic blocks -> portfolio/flowgraph.html
A full-page editor around the same in-browser compiler as the playground below, with Tokens and AST views alongside Run, Ctrl+Enter to run, and your code kept in this browser's local storage between visits: open the IDE.
This program computes data natively and generates an interactive HTML+JS page (the browser as PatLang's cross-platform GUI). It uses write_file, so it runs natively rather than in the sandboxed WASM here.
# =============================================================================
# HTML/JS page builder (Stage 1 compilable subset).
# Produces well-formed HTML5 documents with embedded JavaScript, for using
# the browser as PatLang's cross-platform GUI. Write pages with write_file
# or serve them with the tcp_* hosts.
#
# Conventions: use single quotes inside embedded JS/CSS/attributes so the
# escape-free Stage 1 dialect can express them directly; q() gives a double
# quote when one is unavoidable.
# =============================================================================
# Self-contained helpers (duplicated from lib/codegen.patlang so GUI programs
# need not pull in the compiler libraries)
make a function called q returns s
return chr(34)
end
make a function called nl returns s
return chr(10)
end
make a function called html_escape takes s returns out
let b = sb_new()
let i = 0
while i < s.length do
let c = char_code(s, i)
if c == 38 then
sb_push(b, "&")
else
if c == 60 then
sb_push(b, "<")
else
if c == 62 then
sb_push(b, ">")
else
if c == 34 then
sb_push(b, """)
else
sb_push(b, s[i])
end
end
end
end
let i = i + 1
end
return sb_str(b)
end
# Standard page chrome: dark-and-light friendly minimal styling
make a function called html_default_style returns s
let b = sb_new()
sb_push(b, ":root { color-scheme: light dark; }" + nl())
sb_push(b, "body { font-family: system-ui, sans-serif; max-width: 40rem; margin: 2rem auto; padding: 0 1rem; line-height: 1.5; }" + nl())
sb_push(b, "h1 { font-size: 1.4rem; }" + nl())
sb_push(b, "button, input { font: inherit; padding: 0.3rem 0.7rem; }" + nl())
sb_push(b, "ul { padding-left: 1.2rem; }" + nl())
sb_push(b, ".card { border: 1px solid color-mix(in srgb, currentColor 25%, transparent); border-radius: 8px; padding: 1rem; margin: 1rem 0; }" + nl())
sb_push(b, "pre { overflow-x: auto; background: color-mix(in srgb, currentColor 8%, transparent); border-radius: 6px; padding: 0.6rem; font-size: 0.85rem; }" + nl())
sb_push(b, "pre.src { max-height: 22rem; }" + nl())
sb_push(b, "pre.out { min-height: 1.5rem; }" + nl())
sb_push(b, ".label { margin-bottom: 0.2rem; opacity: 0.75; font-size: 0.9rem; }" + nl())
sb_push(b, ".ms { opacity: 0.75; font-size: 0.9rem; }" + nl())
sb_push(b, "summary { cursor: pointer; }" + nl())
return sb_str(b)
end
# Well-formed HTML5 document: html_page(title, body_html, script_js)
make a function called html_page takes title, body, script returns out
let b = sb_new()
sb_push(b, "<!DOCTYPE html>" + nl())
sb_push(b, "<html lang='en'>" + nl())
sb_push(b, "<head>" + nl())
sb_push(b, "<meta charset='utf-8'>" + nl())
sb_push(b, "<meta name='viewport' content='width=device-width, initial-scale=1'>" + nl())
sb_push(b, "<title>" + html_escape(title) + "</title>" + nl())
sb_push(b, "<style>" + nl() + html_default_style() + "</style>" + nl())
sb_push(b, "</head>" + nl())
sb_push(b, "<body>" + nl())
sb_push(b, body + nl())
sb_push(b, "<script>" + nl())
sb_push(b, "'use strict';" + nl())
sb_push(b, script + nl())
sb_push(b, "</script>" + nl())
sb_push(b, "</body>" + nl())
sb_push(b, "</html>" + nl())
return sb_str(b)
end
# HTTP response wrapper for serving pages via the tcp_* hosts
make a function called http_ok takes content_type, body returns out
let sep = chr(13) + chr(10)
let b = sb_new()
sb_push(b, "HTTP/1.1 200 OK" + sep)
sb_push(b, "Content-Type: " + content_type + "; charset=utf-8" + sep)
sb_push(b, "Content-Length: " + body.length + sep)
sb_push(b, "Connection: close" + sep)
sb_push(b, sep)
sb_push(b, body)
return sb_str(b)
end
# GUI demo in the Stage 1 self-hosted language.
# PatLang computes data (fibonacci table), then generates a well-formed
# interactive HTML+JS page: the browser is the cross-platform GUI.
# Compiled natively (or to WASM) via the PatLang front-end; writes
# gui_demo.html next to the working directory.
make a function called fib takes n returns r
if n < 2 then
return n
else
return fib(n - 1) + fib(n - 2)
end
end
make a function called fib_json takes count returns out
let b = sb_new()
sb_push(b, "[")
let i = 0
while i < count do
if i > 0 then
sb_push(b, ", ")
end
sb_push(b, "" + fib(i))
let i = i + 1
end
sb_push(b, "]")
return sb_str(b)
end
make a function called build_body returns out
let b = sb_new()
sb_push(b, "<h1>PatLang → Browser GUI</h1>" + nl())
sb_push(b, "<p>The data below was computed by native PatLang code at build time; the interactivity is generated JavaScript.</p>" + nl())
sb_push(b, "<div class='card'>" + nl())
sb_push(b, "<label>Show values above: <input type='number' id='threshold' value='0' min='0'></label>" + nl())
sb_push(b, "<button id='refresh'>Filter</button>" + nl())
sb_push(b, "<ul id='out'></ul>" + nl())
sb_push(b, "</div>" + nl())
return sb_str(b)
end
make a function called build_script takes data returns out
let b = sb_new()
sb_push(b, "const fib = " + data + ";" + nl())
sb_push(b, "function render() {" + nl())
sb_push(b, " const min = Number(document.getElementById('threshold').value) || 0;" + nl())
sb_push(b, " const out = document.getElementById('out');" + nl())
sb_push(b, " out.innerHTML = '';" + nl())
sb_push(b, " fib.forEach((v, i) => {" + nl())
sb_push(b, " if (v >= min) {" + nl())
sb_push(b, " const li = document.createElement('li');" + nl())
sb_push(b, " li.textContent = 'fib(' + i + ') = ' + v;" + nl())
sb_push(b, " out.appendChild(li);" + nl())
sb_push(b, " }" + nl())
sb_push(b, " });" + nl())
sb_push(b, "}" + nl())
sb_push(b, "document.getElementById('refresh').addEventListener('click', render);" + nl())
sb_push(b, "document.getElementById('threshold').addEventListener('input', render);" + nl())
sb_push(b, "render();" + nl())
return sb_str(b)
end
let data = fib_json(16)
let page = html_page("PatLang GUI Demo", build_body(), build_script(data))
write_file("gui_demo.html", page)
print("WROTE: gui_demo.html (" + page.length + " chars)")
Native run on the build machine:
WROTE: gui_demo.html (1880 chars)
The self-hosted compiler front+middle end processing its own complete source (186314 chars), run under the Stage 0 interpreter while building this page:
The natively compiled compiler (patc1) runs the same front end roughly two orders of magnitude faster; rustc then dominates end-to-end compile time.