PatLang portfolio

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.

All paradigms in one program

Recursion, control flow, lists, events (when/emit), logic (fact/query), OO (new/send/get), and functional map/filter via apply.

PatLang source
# 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]

Self-timing benchmark

fib(27), a 2-million iteration loop, and a 200k-element vector build, each reporting its own elapsed milliseconds.

PatLang source
# 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

Live playground: the compiler, in your browser

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.

(not run yet)

Point of sale (events + OO + logic)

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.

PatLang source
# =============================================================================
# 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

Test framework: unit, integration, Gherkin

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.

PatLang source
# =============================================================================
# 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

Design by contract: require / ensure / assert

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.

PatLang source
# 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))

Project report writer (goal-oriented)

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.

PatLang source
# 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.

patbuild: a goal-oriented build system

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_demo
Targets 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.

PatLang source
# =============================================================================
# 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

flowgraph: control-flow graphs from the compiler IR

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.

PatLang source
# =============================================================================
# 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> &mdash; " + 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, " &middot; ")
  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

Maze solver: logic + goal-oriented + events

IDE

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.

Browser GUI generator

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.

PatLang source
# =============================================================================
# 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, "&amp;")
    else
      if c == 60 then
        sb_push(b, "&lt;")
      else
        if c == 62 then
          sb_push(b, "&gt;")
        else
          if c == 34 then
            sb_push(b, "&quot;")
          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 &rarr; 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 compiler, measured on itself

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.