Compiler pipeline and driver
How the driver carries source through imports, semantics, AIF, LLVM, object emission and linking — and why a program can pass every earlier command and fail at build.
Last verified
Five different prismio subcommands run overlapping amounts of the same compiler. check and dump-ast stop before any machine code exists; build and run go all the way to a linked executable. If those stages secretly ran different logic, a program that passed check could still fail build for a reason check had no way to catch — and one command's bug fix could silently miss the others. src/driver/compile.psm exists so that does not happen: one pipeline function per command, sharing the same imports, the same semantic analysis, and the same Adaptive Inference Framework (AIF) allocation model, and diverging only at the point each command is defined to stop.
Pipeline
entry source
→ recursive import loading and flattening
→ parsing and semantic analysis
→ AIF fact collection and tier selection
→ LLVM module construction and verification
→ optional IR emission
→ optimized object generation
→ runtime and native input linkingThe frontend must reject an invalid program before intermediate representation (IR) generation ever runs. AIF is allowed to pick a more conservative allocation tier than the fastest legal one, but it must never make an invalid source program valid. LLVM module verification, further down, checks backend structure — not language semantics.
See where a build spends its time
import std.io
fn main() -> Int {
println("hello from prismio")
return 0
}PRISMIO_BUILD_TRACE=1 prismio build hello.psm -o hello[build trace] library bitcode merge 10.3 ms
[build trace] program -O3 (whole program) 128.1 ms
Built helloAn ordinary program build prints exactly those two lines. A per-source timing line appears for each C source a target's native block names -- the compiler's own build has eight -- because those are compiled, where the installed runtime is precompiled bitcode.
check and dump-ast run the same frontend and stop before any of this. dump-ast serializes the resolved AST (abstract syntax tree) as JSON for tooling and the AIF oracle to read:
prismio dump-ast hello.psm{"format":"aif-ast","version":1,"source":"hello.psm","compiler":"0.1.0","files":[{"id":0,"path":"hello.psm"},{"id":1,"path":"~/prismio/.prismio/build/stdlib/io.plib"}],"decls":[{"k":"EXTERN_FUNCTION","s1":"prismio_rt_print_float", ...check prints nothing at all on success — no output means the program is accepted.
A worked example: rejected only at the stage that checks it
Some legality is only visible to AIF, and AIF only runs as part of aif and build — never check. A pin(T) annotation tells the compiler "this value must land in tier T, or refuse to compile"; whether that assertion is true isn't knowable until AIF has actually converged on an answer. Given a value the analysis derives as T2 while the source pins it to T1:
import std.io
import std.string
fn escapes() -> String {
let pin(T1) s = "ab".concat("cd")
return s
}
fn main() -> Int {
let got = escapes()
return 0
}check runs the full frontend and passes — a refuted pin is not a semantic-analysis question:
prismio check pin_refuted.psmNo output, exit status 0.
prismio aif also completes, and does not enforce the pin either — it reports the plan aif describes, it does not gate a build with it:
prismio aif pin_refuted.psmAIF analysis
Source pin_refuted.psm
Result converged in 5 rounds
Sites 2 potential allocation site(s), not runtime allocation counts
Storage plan
Stack 0
Arena 0
Scoped heap 0
Unique heap 2
Shared heap 0
Cycle-managed heap 0
Cross-thread heap 0
Your code
No allocation sites found.
Imported modules
2 potential site(s) found in imported source.
Imported sites are listed when the program can call them, whether or not they run.
ID location type storage reason
1 std.string:2626:35 String unique heap returned to the caller
2 std.string:2627:19 String unique heap returned to the caller
Use `--why=<ID>` for one decision or `--manifest` for compiler/CI details.(exit status 0.) Only build rejects it, because only compileSource — the function behind build and run — calls aifReportPins() before generating code:
prismio build pin_refuted.psm -o pin_refuted.llerror[P5002]: pin(T1) cannot hold: this value is T2
--> ~/prismio/.prismio/build/stdlib/string.plib:2126:35
|
2126 | if (total <= 12) { return __builtin_string_concat_inline2(self, other) }
| ^
note: inference converged, so this is a proven-false assertion rather than a limit of the analysis
error[P5002]: pin(T1) cannot hold: this value is T2
--> ~/prismio/.prismio/build/stdlib/string.plib:2127:19
|
2127 | let out = str_with_capacity(total)
| ^
note: inference converged, so this is a proven-false assertion rather than a limit of the analysis
error: aborting due to 2 previous errorsThe location it names is inside std.string's own .plib, not the caller's file — the pin's refutation happened where the value was actually produced, in a fast-path branch of concat itself. This is not a bug in the pipeline; it is what "AIF is allowed to reject a program, but only build (and run) enforce that" means once you hit it instead of read it. check and dump-ast are for frontend feedback loops precisely because they are cheap and never run AIF's pin gate — reach for aif or build when the question is about storage, not names and types.
Command-dependent exits
check runs the complete frontend without creating LLVM IR or invoking the linker. dump-ast serializes frontend output for tooling and the AIF oracle. An output path ending in .ll stops after module generation. Native build, run, and test continue through object generation and linking. A compiler run from a checkout that has the pinned LLVM links with that clang; on macOS it is given -isysroot from xcrun --show-sdk-path (unless SDKROOT is set or a --target names its own sysroot), because without an SDK it stamps the executable with the minimum deployment version and macOS gives such a binary the older malloc (see telling a difference from noise).
Native compilation
For a native artifact, the driver merges the generated program module with the bitcode of every imported standard-library module and every runtime module, compiles the result, and links it with any user-declared native inputs. The merge is not optional and has no fallback: a missing or unreadable module fails the build naming the exact file, rather than silently producing a slower program. PRISMIO_BUILD_TRACE=1 is the supported way to attribute a compile-time regression to frontend work, the merge, program optimization, or linking, rather than guessing from wall-clock time alone.
Change discipline
New behavior should enter at its earliest semantic owner: keep command-line parsing in src/main.psm, project behavior in src/project, compilation orchestration in the driver, and target-specific mechanics behind the runtime/backend bridge.
If you are changing the driver
Driver function reference
One function backs each command below, including compileSource's own JIT (just-in-time) execution path:
| Function | Inputs and result | Role in the pipeline |
|---|---|---|
analyzedModule(path) | source path → merged AST | Reads source, lexes/parses it, resolves imports, indexes declarations, desugars enums, expands monomorphization work, and runs semantic analysis |
dumpAstCommand(path) | source path → exit status | Stops after analysis and serializes the resolved AST as JSON |
checkCommand(path) | source path → exit status | Runs the full frontend without producing IR or a native artifact |
aifCommand(...) | path plus report flags → exit status | Runs the analyzed module through AIF and selects human, summary, ownership, theta-field, explanation, or manifest output — but does not call aifReportPins |
compileSource(...) | path, output, and a CompileOptions (run, verify, debug info, optimization, JIT, overflow checks, installed runtime, run directory) → exit status | Owns target setup, AIF profile passes, module generation, output paths, native build, optional execution, and cleanup — this is the one that calls aifReportPins before codegen |
checkRuntimeFreshness() | no input | Compares the hash recorded in the installed runtime library with the hash of the runtime sources on disk, when both exist, and stops with P1004 before linking a stale library |
reportForcedLayouts() | no input | Reports command-line AIF layout overrides after analysis |
src/main.psm parses the public command-line interface (CLI). main dispatches to cliCheck, cliDumpAst, cliAif, the project commands, or the build/run path. It reads process arguments only through cli_arg_count and cli_arg, so platform-specific argc/argv handling stays confined to runtime/program_support.c.
For an ordinary build, compileSource configures compiler_set_debug_info, compiler_set_verify_mode, the selected target/sysroot, overflow checks, and LLVM optimization. It asks compiler_temp_ir_path for a private intermediate, calls generateModule, and writes IR; an .ll output ends there. A native output continues through compiler_build_executable, which compiles the target's native sources through the object cache and links them beside the program. A target with runtime = "none" (installedRuntime: false), as the compiler's own is, links no installed runtime, so its runtime C comes from the checkout. run --jit calls ir_jit_run_main for JIT execution; ordinary run executes the linked temporary with compiler_run_executable.
Workload-guided AIF is a deliberate two-pass path, in src/driver/workload.psm: runWorkloadProfile enables workload mode, generates an instrumented executable, runs it with compiler_run_workload, and publishes the profile only on success. The final analysis then consumes that profile. discardWorkloadProfile and endWorkloadPass reset mode and delete temporary state so one compile cannot contaminate the next compiler invocation.
When adding a pipeline phase: define whether it requires parsed, imported, semantically resolved, or AIF-annotated input; whether it mutates the AST or writes side tables; how it reports failure; which temporary files it owns; and whether it must run in check, dump-ast, workload, JIT, and native-build modes. Add order assertions — a phase that happens to work after code generation on one fixture may still violate the compiler's data contract, the way aif's pin check silently not running was a contract question rather than a crash.