toolchain/build
Types
BuildContext :: struct Everything a build driver configures, plus the entries it adds.
A driver mutates this in place. It describes in code what a vader.json
declares: the entries, the module search roots, the assets. What the command
asked (a build, a check or a run, the profile, --entry) comes from args
and is not the driver's to change.
Fields
project_root: string The project folder: the one holding build.vader, or the vader.json
whose driver runs in the compiler's own process, as the compiler passed
it. The front end searches it for modules and anchors the generated tree
under it, as for any project (project_of).
NOT guaranteed absolute: the stdlib exposes no cwd(), so this is
whatever the compiler passed, which may be relative to the invocation
directory. Join onto it rather than assuming a prefix.
include_paths: string[] Module search roots, highest precedence first, as includePaths in a
vader.json: each relative to project_root, or under the toolchain's
folder when it starts with $VADER_HOME, searched after the project
folder. Left empty by new_build_context.
cc: string C compiler for the link step.
ldflags: string Raw linker flags, whitespace-split and appended verbatim.
target: BuildTarget What draining messages() produces. .Native by default: a build
system builds; a driver that only observes leaves the loop at
BeforeEmit rather than setting this.
BuildTarget :: enum(u8) What a drained messages() leaves behind.
Variants
Native Link a binary per entry.
C Emit C only, collected by emit_c.
None Produce nothing: for a driver that drains the stream purely to observe.
GeneratedModule :: struct One module a driver asked the compiler to generate.
Fields
name: string Module name, spelled as the import that will reach it: "gen/serde".
source: string Imports and declarations. The module header is NOT part of this;
emit_module writes it, so the name and the file's location cannot
disagree.
Message :: TypecheckedMessage | PhaseMessage | CompleteMessage One event the compiler hands a build driver.
TypecheckedMessage :: struct Top-level declarations of one module, after typecheck.
The declarations are the compiler's own nodes, not copies: reading a field
costs nothing, and a span taken from one anchors a diagnostic directly in the
user's source. This is why a driver runs natively rather than through the
comptime VM; see the plan referenced in context.vader.
Each declaration is delivered exactly ONCE per entry. A module the driver generated into is therefore delivered a second time carrying only the generated declarations, never the ones already seen, so a rule applied here fires once however many generation rounds that entry takes.
Fields
module_name: string The module's declared name: "std/io", "vader/front/lexer". This is what a
driver filters on; the loader's own key is an absolute filesystem path,
which no rule can usefully match against.
declarations: AST.Decl[] PhaseMessage :: struct The pipeline crossed a stage boundary.
Fields
phase: Phase CompleteMessage :: struct The build finished; no further message follows.
Carries the exit code, since a stream has no return value: a driver that drains to the end reads its build's outcome here.
Fields
code: i32 Phase :: enum(u8) Where the pipeline is, ordered as the stages run.
Every variant has an emission site: draining messages() walks the whole
build, so there is no reserved half of this enum. BeforeEmit is the one to
know about: it is the last point at which leaving the loop produces nothing,
which is how a driver that only wants to observe declines to build.
Variants
Typechecked Every module parsed, collected, resolved and typechecked.
Evaluated @comptime values evaluated, generic instances harvested.
BeforeEmit Last point before output. break here writes nothing; break after
leaves a partial binary.
Lowered The lowered tree exists: desugared, before CFG and bytecode.
OutputWritten The output file has been written.
BuildPlugin :: trait Behaviour plugged into a build, told each message of the stream in order. Every member does nothing unless the plugin overrides it.
Methods
on_module :: fn(self, ctx: BuildContext, msg: TypecheckedMessage) -> void The declarations of one module, each delivered once per entry.
on_phase :: fn(self, ctx: BuildContext, phase: Phase) -> void The pipeline crossed phase.
on_complete :: fn(self, ctx: BuildContext, code: i32) -> void The build is over with code. A report made here still fails it.
Functions
new_build_context :: fn(args: string[]) -> BuildContext Build a context carrying the defaults a driver would otherwise restate.
The project root comes from args, not from the process working directory:
the stdlib exposes no cwd() (std/env has only get_env, and the C runtime
no getcwd), so threading it is the only way a driver can know where it is.
The compiler passes the root it resolved in the request; invoked bare, the
root is left empty rather than guessed.
Parameters
| args | string[] | the driver's argv, as the compiler passes it: the request parse_driver_request reads. A driver invoked by hand may supply less; each missing part falls back, the whole to a debug build of every entry. |
Returns
BuildContext a context with project_root set (empty when args carried none), no entry and no include paths. Returned MUTABLE: the whole contract is that the driver fills it in.mode :: fn(ctx: BuildContext) -> DriverMode What the command running the driver asks of it. Only .Build writes
outputs: vader check (.Check), run, dump, lint and graph
(.Prepare) and vader info or the language server (.Describe) run the
driver and link nothing, emit_native returning 0 all the same. A step
after the build, installing or packaging, checks ctx.mode() == .Build.
Parameters
| ctx | BuildContext | the context the driver configured. |
Returns
DriverMode add_entry :: fn(ctx: BuildContext, name: string, path: string) -> void Add a program for the build to compile, as a key of entries does in a
vader.json.
Order is preserved and load-bearing: it fixes the order modules are read, and therefore symbol-id assignment, which is what keeps a build byte-identical across runs.
Parameters
| ctx | BuildContext | the context being configured. |
| name | string | what the binary is called, and what --entry selects it by. |
| path | string | the entry .vader file, relative to project_root. |
Errors
- H6012 when
namecannot name a binary on every platform (entry_name_problem). - H6005 when an entry already has that name, in any case, since the two binaries would be one file where case is ignored.
add_asset :: fn(ctx: BuildContext, asset: string) -> void Add a file or folder for a build to copy beside the binaries, as assets
does in a vader.json. It lands under the same relative path, and a folder
is copied with everything it holds.
Parameters
| ctx | BuildContext | the context being configured. |
| asset | string | the file or folder, relative to project_root. |
Errors
- H6010 when it is absolute or climbs out of the project.
report :: fn(ctx: BuildContext, span: Span, message: string) -> void Report a problem from the driver, anchored in the user's source.
Accumulates; the end of the stream or the driver's exit prints, and an error
among the reports makes the build's code 1. The driver describes the problem and the
registry owns the code (H6004): a project's lint rules should never have
to allocate diagnostic ids, and every driver report stays greppable under one
code.
Pass the tightest span the user can act on: a decl's name_span rather than
its span, a field rather than the struct holding it.
Parameters
| ctx | BuildContext | the context accumulating the report. |
| span | Span | smallest source range the user can act on. |
| message | string | what is wrong, lowercase and without a trailing period: backtick identifiers, as the registry style requires. |
reports_of :: fn(ctx: BuildContext) -> Diagnostic[] Reports accumulated so far, in order.
Parameters
| ctx | BuildContext | No description. |
Returns
Diagnostic[] the driver's diagnostics. The end of the stream and the driver's exit put them on the wire.messages :: fn(ctx: BuildContext) -> Iterator<Message> Drive the compilation and yield what the pipeline produces.
Nothing compiles until the first message is pulled, so a driver configures
the context first and iterates second. Each entry is run through
the front end (load → resolve + typecheck → comptime), yielding one
TypecheckedMessage per module, then a PhaseMessage, and finally one
CompleteMessage for the whole run.
Every typed module is yielded, the stdlib included: a driver that only
cares about its own code filters on module_name. Reporting the full set is
the honest default: which modules are "the project" is the driver's policy,
not this function's.
Every declaration is delivered exactly ONCE per entry, however many
generation rounds that entry takes, see TypecheckedMessage. A module with
nothing new is not yielded at all. The "once" is per ENTRY, not per build: two
entries sharing a module both report it, which is already true of the stdlib.
Batched per file rather than streamed per declaration: Vader is single-threaded here, so the compiler fills a phase and hands it over. The loop shape is the same either way, and the order is specified rather than dependent on scheduling, which is what keeps a driven build reproducible.
A driver that generates code makes the stream LOOP: the phases from
Typechecked to BeforeEmit repeat once per generation round, and the back
end runs only after the last one. Declarations are still delivered once each
(see emit_module), so an observing driver cannot tell the difference.
What the command asked shapes the stream: --entry narrows the entries to
one; a check lowers after the last generation round and links nothing; the
preparation of a run writes the project for the CLI and stops after the last
generation round. A build writes each binary to build/<profile>-<target>/<name>,
or to --out for its one entry, copies the assets beside them, and stops at
the first entry that fails. Every mode writes the generated modules.
The plugins use received hear each message first, in that order (see
BuildPlugin). The end of the stream prints the diagnostics not printed yet,
the driver's reports among them, and an error among them makes the build's
code 1 even when every entry built.
Parameters
| ctx | BuildContext | the configured context. Without an entry, or when it already holds an error, the stream yields only a failed CompleteMessage. |
Errors
- H6003 when the driver added no entry ; H6009 when
--entrynames none of them ; H6011 when--outnames one output for several entries ; H6006 when generation never settles ; H6008 when a generated module cannot be written ; H6013 when a plugin pulls it. All land onctx.diagnostics.
finish_driver :: fn(code: i32) -> i32 The exit code of a driver whose build returned code, once every context
it made has printed what it still holds.
The entry vader build writes beside build.vader calls it; a driver does
not. It is what prints the reports, and fails the build on an error among
them, when the driver left messages() before its end or reported once the
build was over.
Parameters
| code | i32 | what build returned. |
Returns
i32 code, or 1 when a context holds an error.Errors
- H6014 for a context whose stream ran its rounds and still holds generated modules: emitted after the last
BeforeEmit, or before the driver left the loop there, no round wrote them.
emit_native :: fn(ctx: BuildContext) -> i32 Compile the entries into what the command asks: native binaries unless
--emit names their C or their text bytecode.
This is what makes a driver a build system rather than an observer. Each
entry is compiled to build/<profile>-<target>/<name> under the project, or
to --out when one entry is built.
Front-end and lowering diagnostics are accumulated onto the context, so they interleave with the driver's own reports in the order they happened, and the end of the stream prints them all at once.
Parameters
| ctx | BuildContext | the configured context ; add entries with add_entry. |
Returns
i32 0 when every entry built and nothing reported an error, 1 otherwise. A driver normally returns this from build.emit_c :: fn(ctx: BuildContext) -> string[] Compile the entries to C, without linking.
The counterpart of emit_native for a driver that wants the source: to
post-process it, to hand it to another toolchain, or just to look. Mirrors
--target=c.
Parameters
| ctx | BuildContext | the configured context ; add entries with add_entry. |
Returns
string[] one C source per entry, in the order they were added. Empty when a front end rejected an entry: the reports say why.emit_module :: fn(ctx: BuildContext, name: string, source: string) -> void Queue a module for the compiler to generate.
The driver writes text; the compiler owns the file. Call it as many times as
you like on the same name: a module is a folder, so the emissions stack
into it, and re-emitting identical text is a no-op. What you must NOT do is
emit two different declarations of one symbol: that is T3053, reported
against the generated files like any other duplicate.
Nothing is written until the round ends: the driver observes, emits, and the
compiler flushes once the stream reaches BeforeEmit. The next round's front
end then sees the new modules, and their declarations come back through
messages(), which is what makes transitive generation work without a
mechanism of its own.
Parameters
| ctx | BuildContext | the context accumulating the emissions. |
| name | string | module name, as the import reaching it will spell it. Must be a relative /-separated path, see generated_name_problem. |
| source | string | imports and declarations, WITHOUT a module header. |
Errors
- H6007 when
nameis not a usable module path. The emission is dropped; the build carries on and reports.
generated_files_of :: fn(ctx: BuildContext) -> string[] Files written for the generated modules so far, in the order they landed.
The set is authoritative for the build that produced it: the sweep runs first, so every path listed here was written by this run.
Parameters
| ctx | BuildContext | No description. |
Returns
string[] the generated files' paths, relative or absolute as project_root is.generated_file_name :: fn(name: string, content: string) -> string File name for a generated module: <leaf>_<content hash>.vader.
Content-addressed on purpose, see this file's header. The hash is over the FINAL file text, banner and header included, so "same file, same path" holds without qualification.
Parameters
| name | string | the module name, supplying the readable half: its last segment, so a diagnostic in generated code names its module without the reader decoding a hash. |
| content | string | the rendered file, supplying the hash. |
Returns
string render_generated_module :: fn(name: string, source: string) -> string Render a generated module's file: banner, module header, then the driver's
text.
The compiler writes the header rather than trusting the driver's, so the declared module and the folder it lives in cannot drift apart: a mismatch would surface as R2020 against machine-written code.
Parameters
| name | string | module name to declare. |
| source | string | the driver's imports and declarations. |
Returns
string the complete file text, newline-terminated.generated_root_of :: fn(ctx: BuildContext) -> string Root the generated tree lives under, as a path.
Parameters
| ctx | BuildContext | the configured context. |
Returns
string flush_generated :: fn(ctx: BuildContext) -> string[] Write every queued emission, and report which modules gained a file.
The return value is the round loop's fixed-point test: an empty list means this round changed nothing on disk, so running the front end again would see exactly the same files. A path that already exists is skipped: it holds the same content by construction, the name being its hash.
Parameters
| ctx | BuildContext | the context holding the queue ; it is emptied. |
Returns
string[] the module names that gained a file this round, without duplicates. Empty when nothing changed.Errors
- H6008 when the directory or the file cannot be written. The module is skipped rather than the build aborted, so one unwritable path reports once instead of cascading.
sweep_generated :: fn(dir: string) -> i64 Delete the generated files of an earlier build, before this one starts.
Mandatory rather than tidy: file names are content hashes, so a module whose text CHANGED leaves its previous file in place: in the same folder, where it still compiles, and its declarations collide with the new ones. Sweeping first is also what keeps a renamed or withdrawn module from resolving forever on the machine that generated it while failing on a fresh clone.
Only files starting with GENERATED_BANNER are removed. That is the whole
safety argument for a recursive delete driven by a driver-supplied path: a
generated_root aimed at real sources destroys nothing. Directories are left
behind: the stdlib has no remove_dir, and an empty folder declares no
module, so nothing resolves through it.
Parameters
| dir | string | the generated root ; a path that does not exist is not an error. |
Returns
i64 how many files were removed.build_from_manifest :: fn(request: DriverRequest, manifest: Manifest) -> i32 Run the driver a vader.json stands for, as the CLI does for a project
folder described by one: the counterpart of a build.vader's build.
It declares what vader eject writes into a build.vader, search roots
included, so that the two describe the project alike. The build itself does
not depend on the roots it declares: the loader reads them from the
vader.json, which it consults first.
Parameters
| request | DriverRequest | what the command asks; its project_root is the folder holding the vader.json. |
| manifest | Manifest | that file, read and found without problems. |
Returns
i32 0 when every entry built, or checked, cleanly; 1 otherwise, after printing why.use :: fn(ctx: BuildContext, plugin: BuildPlugin) -> void Plug plugin into the build, after the plugins plugged before it.
Read when the stream starts, as the rest of the configuration is: a plugin
plugged once messages() has been pulled waits for the next build.
Parameters
| ctx | BuildContext | the context being configured. |
| plugin | BuildPlugin | the behaviour to plug. |
Constants
DEFAULT_GENERATED_ROOT: string :: GENERATED_ROOT_DIR Where generated modules land, relative to the project root.
The resolver's own constant, restated here so a driver reading this contract
does not have to reach into vader/front/resolver for it, and so there is exactly
one string. Gitignored by convention (build/ already is): the generated tree
is a build artefact, reproducible from the driver plus the sources it reads.
GENERATED_BANNER: string :: "// Generated by `vader build`: do not edit." First line of every generated file, and the sweep's whole safety argument.
sweep_generated deletes a .vader file only when it starts with this, so a
generated_root mistakenly pointed at hand-written sources destroys nothing.
MAX_GENERATION_ROUNDS: i32 :: 8 Rounds of generation allowed before the build gives up.
A round is one full front end, so this is a real cost, not a formality: a driver that keeps emitting new text every round is looping, and the alternative to a cap is a build that never ends and says nothing. Eight is far above any legitimate depth: transitive derivation settles in two or three.