Library

toolchain/build

6 files lib/toolchain/build/context.vader, lib/toolchain/build/drive.vader, lib/toolchain/build/generate.vader, lib/toolchain/build/manifest.vader, lib/toolchain/build/message.vader, lib/toolchain/build/plugin.vader 32 exports

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[]
No description.
PhaseMessage :: struct

The pipeline crossed a stage boundary.

Fields

phase: Phase
No description.
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
No description.
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

argsstring[] 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

ctxBuildContext the context the driver configured.

Returns

DriverMode

add_entry

fn context.vader:251
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

ctxBuildContext the context being configured.
namestring what the binary is called, and what --entry selects it by.
pathstring the entry .vader file, relative to project_root.

Errors

  • H6012 when name cannot 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

ctxBuildContext the context being configured.
assetstring 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

ctxBuildContext the context accumulating the report.
spanSpan smallest source range the user can act on.
messagestring 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

ctxBuildContext 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

ctxBuildContext the configured context. Without an entry, or when it already holds an error, the stream yields only a failed CompleteMessage.

Returns

Iterator<Message> a lazy stream of messages, in pipeline order.

Errors

  • H6003 when the driver added no entry ; H6009 when --entry names none of them ; H6011 when --out names 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 on ctx.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

codei32 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

ctxBuildContext 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

ctxBuildContext 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

ctxBuildContext the context accumulating the emissions.
namestring module name, as the import reaching it will spell it. Must be a relative /-separated path, see generated_name_problem.
sourcestring imports and declarations, WITHOUT a module header.

Errors

  • H6007 when name is 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

ctxBuildContext 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

namestring 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.
contentstring 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

namestring module name to declare.
sourcestring 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

ctxBuildContext 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

ctxBuildContext 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

dirstring 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

requestDriverRequest what the command asks; its project_root is the folder holding the vader.json.
manifestManifest that file, read and found without problems.

Returns

i32 0 when every entry built, or checked, cleanly; 1 otherwise, after printing why.
display_phase :: fn(p: Phase) -> string

Render a phase as the name a driver would print in a log line.

Parameters

pPhase No description.

Returns

string
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

ctxBuildContext the context being configured.
pluginBuildPlugin 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.

Written by vader doc. Icons: Codicons, CC BY 4.0. Fonts: Atkinson Hyperlegible, JetBrains Mono, OFL.