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. Native transcript only, no "run in browser" button: unlike the demos above, patbuild genuinely needs real filesystem access it can't get in the WASI sandbox โ€” it reads the manifest file plus every target's own source file from disk by path, and (with --native) invokes a real rustc subprocess per target, neither of which a browser sandbox can provide. This is a capability limit, not a performance one (compare the dynamic-syntax demo below, which CAN run in-browser, just impractically slowly for now).
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