Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Command-line interface

After building the repository, ./x2c links to the development compiler; ./bin/x2c is the bootstrap compiler until make build-install links it to a copy of the development compiler. Every invocation begins with a command: translate, build, run, new, script, env, install, remove, list, or help.

The generated help is the short option reference:

./x2c --help
./x2c help translate
./x2c build --help
./x2c run --help
./x2c script --help

Translate to C

translate writes one matching .c, .h, .xi, and default .d file for each explicit .x input. The .xi file is the unit’s interface, which later translations read in place of its source when it is current:

mkdir -p /tmp/x2c-generated
./x2c translate --out-dir /tmp/x2c-generated \
  src/main.x src/parse/parse.x

Without --out-dir, output goes to the current directory. An explicitly selected directory must already exist. x2c validates all inputs, output stems, and output paths before translating the first unit. Inputs must be regular .x files; two inputs with the same basename would collide in the flat output directory and are rejected.

--out-dir is only the generated-C directory. Native artifacts use build --output or run --output.

Several inputs translate at once, in worker processes that inherit the collected declarations the parent already read. -j, or --jobs, limits the number of workers:

./x2c translate -j 8 --out-dir /tmp/x2c-generated src/*.x

Each unit is translated by exactly one worker and writes only its own files, so the generated .c and .h are byte-identical to a serial run. A .d file may list more prerequisites than the serial run produces, and never fewer. A worker that parses a header records what that parse reads, where a serial run replays the header from its process cache. The default limit is the host’s online processor count, or one job under Make, as for build below; -j 1 translates serially in one process. The inspection and dump modes stay serial; they write one ordered stream to standard output.

A serial translation stops at the first unit that fails. In a parallel translation, a worker stops at the first failing unit of its share of the inputs while the other workers continue. Diagnostics appear in input order, because the command holds each worker’s standard error until the worker ends; one unit’s diagnostics are never split by another’s. A parallel build translates each unit in its own worker, so it reports every failing unit. Stopping the command with SIGTERM or SIGINT also stops its workers.

Compile-time code runs once per translated unit. When several units import the same file, each worker runs that file’s compile-time forms, including any side effects, for its own unit. A serial run may share one import between the units it translates.

The depfile options control that .d output. They belong to translate:

--no-deps               Do not write x2c dependency files
--dep-file <file>       Override the depfile path (one input only)
--dep-target <target>   Override the depfile target (one input only)
--no-phony-deps         Omit phony rules for included files

Symbol collection options apply to translate, build, and run:

--no-cpp                Skip symbol collection and preprocessing
--live-symbols          Collect symbols through the host preprocessor
--cpp-symbols           Use CPP collection for this translation

Source mapping

--source-map adds source locations to generated C for translate, build, and run. It is off by default. Combine it with native debug information to set breakpoints, step through statements, and read backtraces in the original .x files:

./x2c build --source-map -g -O0 --save-temps --output /tmp/example example.x
lldb /tmp/example

With source mapping enabled, C __FILE__ and __LINE__ refer to the original x2c source. Macro expansions use their invocation locations; generated cleanup uses its owning source construct. Compiler scaffolding without a source origin retains a generated-file location. -g alone keeps generated-C locations, and --source-map alone does not add native debug information. Changing the option invalidates reused translation output. Optimized builds may combine or remove statements, so source mapping does not guarantee exact stepping or recovery of optimized-away values.

On macOS, mapped executable builds with effective native debug information also produce <output>.dSYM before removing temporary objects. Move that companion directory with the executable to retain native debug information. Symbol assembly adds time only to these requested debug links. Its failure fails the build and retains intermediates for diagnosis. A later -Xcc -g0 overrides an earlier -g, including one selected by a project profile. Linux keeps its existing debug output; --build-dir and --save-temps remain available for retaining intermediate files on either platform.

Inspection modes print an intermediate result and stop translation:

./x2c translate --dump-tokens source.x
  • --dump-tokens prints source tokens and stops.
  • --dump-ast prints the parsed, bound AST after macro expansion and stops.
  • --dump-transforms prints the transformed AST and stops.
  • Both AST dumps number bindings from 1 in order of first appearance, so a dump does not depend on how many bindings collection issued.
  • --dump-definitions prints each definition’s location and documentation and stops.
  • --dump-code prints unformatted generated code and stops.
  • --dump-symbols prints the source symbol table and stops.
  • --dump-cpp prints host-preprocessed text and stops.
  • --dump-cpp-text is an alias for --dump-cpp.
  • --dump-cpp-tokens prints host-preprocessed tokens and stops.
  • --dump-cpp-symbols prints the CPP symbol table and stops.
  • --dump-cache prints the compiler cache and stops.
  • --dump-conformance prints protocol conformance and stops.

--dump-definitions prints what the unit defines after translation has selected, expanded, and bound it, one Lisp form per line. Documentation generators and repository tools read it instead of scanning source text. Each unit starts with (unit PATH), so one command can inspect several files. A file that opens with a block comment, after any directives, then prints (module TEXT) with the comment as written. Each function, including static functions, foreign aliases, and functions a Unit macro or class produced, prints a function row, and each typedef or class prints a type row:

(function (name "Point_add") (display "Point.add") (line 11)
 (source "point.x" 11)
 (span 193 242) (static 0) (origin source) (doc " Adds. ")
 (text "int Point.add(Point p, int k)")
 (type ((func (("Point") (int))) int)) (params ("p" "k")))
(type (name "Point") (kind struct) (line 6) (span 68 113) (private 0)
 (source "point.x" 6)
 (doc " A point. ") (text "typedef struct Point { int x; int y; } Point"))
  • name is the C name and display the source spelling, such as Point.add.
  • line is the one-based line of a function’s declarator or a type’s statement. A decorated function reports its declarator, and a function a macro produced reports the invocation. This line is in the expanded unit.
  • source holds the authored path and one-based line, resolving module members and preprocessor line markers. It is empty for a definition without an authored source location. Documentation uses this location; line and span retain their positions in the expanded unit.
  • span holds the byte offsets that start and end the top-level form that produced the definition, including decorators and a meta keyword. It is empty for a definition the compiler made itself, such as a protocol adapter, whose line is 1.
  • origin is source for a function written in the file, macro for one a Unit macro, class, or the compiler produced, and alias for a foreign alias, whose line is that of its alias application.
  • doc is the body of the /** comment that ends on the line before the form or on its first line, without its delimiters; it is empty when there is none. In a Unit macro’s template, $name in that comment reads as the hole’s argument as the invocation wrote it, so $type documents $scalar(char, ...) as char.
  • text is a written function’s declarator, or a type’s statement without its ;, with comments removed and each run of whitespace written as one space. It is empty for a function the file does not spell.
  • type and params give a function’s canonical type and parameter names, as its unit interface records them.
  • kind is alias, struct, union, enum, callback, or class, and private is 1 for a type declared with static.

Compile-time-only meta functions are not part of the translated unit and are not listed.

An inspection that does not write generated files does not need --out-dir. Only one inspection mode runs; the last option given wins.

Build native artifacts

build accepts explicit .x, .c, .o, and .a operands. It translates x2c source, compiles generated and native C, then links an executable. One input may include another through a directory, as in #include "lib/inner.x", when both are operands:

./x2c build --output /tmp/foreach examples/foreach.x
/tmp/foreach

translate, build, and run default to the host’s online processor count for translation and native compilation. Detection failure falls back to one job. Under Make (MAKELEVEL greater than zero), the automatic default is one job so the outer make controls parallelism. An explicit -j N or --jobs N overrides either default. The online count does not account for Linux CPU affinity, container quotas, or memory limits; use -j to choose a smaller limit in those environments.

Parallel builds translate each stale unit in its own worker and generated directory. Units with the same basename remain separate, and each unit records its own prerequisites for subsequent build reuse. Parallel translation preserves native header search and link order.

The driver selects the runtime that matches the compiler and supplies that runtime’s platform libraries. It also derives one object and dependency path per C source. --kind static-library uses the selected archiver:

./x2c build --kind static-library \
  --output /tmp/libwidget.a src/widget.x src/helper.c

--kind meta-module builds a native module, whose functions compile-time code can call. The module leaves the x2c runtime unresolved and uses the loading compiler’s copy, which is the whole runtime, and it records that compiler’s content hash. --native-module <file> loads a module for translate, build, run, and repl, and repeats:

./x2c build --kind meta-module --output helpers.so src/helpers.x
./x2c run --native-module helpers.so src/helpers.x src/main.x

Only the compiler that built a module loads it; any other compiler reports that the module was built by another compiler before running any of its code. The compiler loads a module only on request and keeps it loaded until it exits, and a translation binds only the modules its request names. When two of them define one name, the first supplies it and the declaration reports a warning. Modules work on macOS, Linux and WSL.

--extension <dir> links a package’s compile-time part into the executable being built, which must be the compiler, and repeats. The package’s src/*.x and src/*.c become inputs, and a generated unit registers its meta functions under the package’s name. The resulting compiler imports the package without loading its module. From a built compiler checkout, include every compiler unit and both header search roots:

python3 tools/sources.py |
  xargs ./x2c build --extension packages/tally \
    --x-include-dir src --c-include-dir builds/0/src --output /tmp/x2c

Native modules explains what a module contains.

-c, or --compile-only, stops after compilation. One input may name its object with --output. Multiple compile-only inputs require --build-dir, so every object has an unambiguous path.

Without --build-dir, a direct build uses a private temporary directory and removes intermediates after success. --save-temps retains intermediates; --save-temps=<dir> places them in a chosen directory. With a named build directory, later builds can reuse unchanged results.

--compile-commands <file> writes a native compilation database for C tools. It records the actual compiler argument arrays, working directory, source, and object output for every compilation, including reused objects. Manifest builds include the selected target and its dependency targets. The entries refer to generated C for x2c inputs, so the option also retains intermediates.

./x2c build --build-dir .x2c-build \
  --compile-commands .x2c-build/compile_commands.json examples/foreach.x

The destination’s parent directory must exist when the build finishes. A successful build replaces the database atomically; a failed build preserves the previous file. run writes it before starting the program, and -### writes no database. This file describes native C compilation, not x2c syntax for an editor’s C parser.

VS Code diagnostics, definitions, and hover

Install the VSIX built from etc/vsc-extension with Extensions: Install from VSIX. The extension uses the installed compiler directly; no separate worker build is required. Set an absolute compiler path when it is not on PATH:

"x2c.semantic.compilerPath": "/path/to/x2c/bin/x2c"

Selection order is an explicit x2c.semantic.compilerPath, an explicit legacy x2c.semantic.workerPath, executable ./x2c in the trusted workspace, then x2c on PATH. An explicit selection never silently switches to another compiler. A missing executable produces one setup message in the x2c Output channel, and discovery retries when you use the editor again. The extension does not build or install the compiler automatically.

The default configuration discovers x2c.toml and its default target, or analyzes the current file directly when there is no project. Select a target or profile with ordinary compiler arguments:

"x2c.semantic.arguments": ["build", "--target", "app", "--profile", "debug"]

The selected target must include the open source as an input. For a header or macro file outside that input list, open its owning source or configure a translate command with the needed include and package options. Unsaved included files still affect analysis of their owning source.

Diagnostics, definition navigation, and type hover include unsaved source text. Each request runs in a fresh x2c-editor process with snapshots of open files; editing cancels obsolete results without writing buffers to disk. Native CPP symbol modes cannot consume unsaved source snapshots and report that limitation. Macro-generated syntax may lack a physical definition or identifier location.

Semantic execution requires a trusted local workspace because compilation can run macros. Untrusted and virtual workspaces retain syntax highlighting. Set "x2c.semantic.enabled": false to disable semantic execution. Completion, rename, workspace indexing, and general LSP support are separate features. The shipped x2c-editor command serves these providers through x2c editor. Its argument and response formats are not a public compiler API.

Shared libraries are unsupported. Supporting them requires platform-specific position-independent code, visibility, runtime linkage, library naming, and initialization rules.

Build and run

run builds an executable and launches it after the build succeeds:

./x2c run examples/foreach.x -- first -second input.x

Use -q to suppress build progress and receipts while preserving program output. Use --build-dir <dir> to retain generated files and objects for inspection or reuse. Otherwise temporary intermediates are removed after the program finishes.

-- ends build options. Every later value is passed as one program argument, including values that begin with - or end in .x. x2c returns the program’s exit status. -### prints the build and run actions without creating or launching anything. It reads no input file either, so it also prints the actions for an artifact a planned target would produce.

The program inherits standard input, output, and error, so interactive prompts and terminal applications work as they do when launched directly. Shell pipes and redirections also apply to the program; its output is not held until exit.

Run a script

script runs one source file and builds it only when needed. The file name ends in .x, or may be any name when the first line is a shebang:

./x2c script tools/report.x -- first --second

The first run builds an executable into the per-user cache. Later runs start that executable directly, in a few milliseconds, until one of these changes: the script, a file it includes or imports, a header its generated C includes, a package archive it links, the compiler, the runtime archive, the C compiler, the script’s options, or the CPATH, C_INCLUDE_PATH, LIBRARY_PATH, and SDKROOT environment variables. The cache also records the directories the build searched: include directories, -L directories, the C compiler’s header and library search paths, and the directory of each file the build read. Adding or removing a file in one of them rebuilds the script. This covers a header that now shadows an included one, a __has_include whose result changed, and a -l library that now resolves elsewhere. --rebuild builds regardless.

Options come before the script file, and every word after it is passed to the program unchanged, including -- and words that begin with - or @. A response file given before the script supplies options. script accepts the include options -I, --x-include-dir, --c-include-dir, --c-system-dir, and --package-dir; the C compiler and linker options of run; and -j, --source-map, --rebuild, --clean, -v, -###, --plain, --color, --debug, --max-errors, and --diagnostics-file. x2c help script lists them. It does not read a project manifest.

A successful build prints nothing, so the program’s output is all that appears; build diagnostics still print, and a failed build exits with its status. -v shows the build actions and the final run action. The program replaces the x2c process, so it receives signals and terminal input directly and its exit status is the command’s. Its argv[0] is the script’s absolute path. A --source-map -g build on macOS keeps its run.dSYM beside the cached executable.

A script file can start with a shebang line and run directly:

#!/usr/bin/env -S x2c script

That line also makes the file a script unit, which may put its statements at file scope instead of defining main.

A script can split its code into local modules with ordinary includes, such as #include "lib/report.x". x2c script translates, compiles, and links every local .x file the script includes, directly or through another module, and a change to any of them rebuilds the script. Runtime and package modules come from their archives instead.

env -S splits the line into words. Where x2c has a fixed location, the interpreter path works without it: #!/usr/local/bin/x2c script.

Each script builds under scripts/ in the cache root: X2C_CACHE_DIR when set, otherwise $XDG_CACHE_HOME/x2c, otherwise ~/.cache/x2c. x2c env cache_dir prints the root. Concurrent runs of one script share its build and wait for each other. x2c script --clean <file> removes that script’s entry without running it, and every build removes the entries of scripts that no longer exist. Removing the cache is always safe.

Selecting inputs

Shell wildcards work because the shell expands them into explicit operands:

./x2c build --output /tmp/tool src/*.x

x2c does not expand wildcard operand text or recurse through directory operands. Naming a directory does not select the sources under it.

Response files hold long explicit command lines:

build
--output
build/tool
-I
include
src/main.x
src/parse/parse.x

Invoke one as ./x2c @build.rsp. Whitespace separates arguments; single and double quotes preserve whitespace; backslash quotes the next character. A first-non-whitespace # begins a comment. Response files may include other response files, but cycles are rejected. @@name passes the literal operand @name.

Start a project

new invokes the shipped x2c-new command to create a project that builds and runs as written:

x2c new hello
cd hello
x2c run -q            # Hello, world!
x2c run -q -- Gary    # Hello, Gary!

It writes x2c.toml, src/main.x, and .gitignore, which ignores the default build directory .x2c-build/. The manifest has one executable target named after the directory:

[target.hello]
sources = ["src/*.x"]

The directory may be missing or empty; x2c new . fills the current empty directory. new exits with an error for any other existing path and never overwrites a file. The target name is the directory’s last component and may contain letters, digits, _, and -. -q suppresses the x2c: created <dir> line.

Project manifests

When build or run has no explicit input operands, x2c searches the current directory and its parents for the nearest x2c.toml. --manifest-path selects one exact file and disables discovery. Explicit input operands do not use a nearby manifest.

[project]
name = "demo"
default-target = "demo"
build-dir = "build/x2c"

[target.core]
kind = "static-library"
sources = ["src/core/*.x"]
exclude = ["src/core/experimental.x"]
include-dirs = ["include"]

[target.demo]
sources = ["src/main.x", "src/commands/**/*.x"]
dependencies = ["core"]
libraries = ["m"]

[target.demo.profile.release]
optimization = "O2"
debug = false

The manifest lists the sources of each target and how the targets depend on each other. Relative paths start at the manifest directory. Its *, ?, and bracket patterns stay within a path component; ** recurses. As in a shell, a name that begins with a dot matches only where the pattern has a literal dot. Matches are deduplicated and bytewise sorted. Unmatched patterns, unknown targets, and dependency cycles are errors before any action runs.

A target may also set defines, C flags, library directories, libraries, link flags, package directories, and an output. kind = "meta-module" builds a native module, and a target’s native-modules field lists the module targets its translation loads. Those build first, like dependencies, and a changed module retranslates the targets that load it. Command-line target, profile, kind, output, build directory, and tool options override the corresponding defaults. --target <name> builds the named manifest target and --profile <name> applies the named manifest build profile; both work with build and run. The selected target must define the named profile; a dependency target takes it when it defines one and builds with its own settings otherwise. --manifest-path, --target, and --profile describe a manifest build, so each conflicts with explicit input operands, and -c needs operands rather than a manifest.

Pinned packages

A [dependencies] section pins packages by exact version. Each key is a package name in the index and each value is the version in that index row:

[dependencies]
pcre2 = "10.48"

This is separate from a target’s dependencies field, which lists other targets in the same manifest.

build and run resolve the section once the manifest and the selected target are known good, and before planning. A pinned package missing from the x2c home at that version is installed through the index, the same way x2c install <name> does, and --index selects another index. The resolution is then written to x2c.lock beside the manifest, one name version kind platform url sha256 row per package. Keep that file with the manifest so a later build reproduces the same packages. A manifest with no [dependencies] section pins nothing, so a lockfile left beside it is removed.

A build whose lockfile already covers every pinned package, at the pinned version and installed in the home, reads no index and makes no network request. A pin the lockfile covers but the home does not hold is installed from the url and sha256 that lockfile recorded, not from the index, so the archive is the one the lockfile pinned however the index has changed. Only a pin the lockfile does not cover reaches the index. The lockfile is then rewritten to the manifest’s pins. A version the index cannot supply stops the build with an error that includes both versions. A dry run installs nothing and writes no lockfile, and the editor adapter never installs.

Include and package directories

-I <dir> is the shared include option. Repeated paths keep command order, and the first matching path wins in x2c quote-include collection, any requested x2c host preprocessing, and generated C compilation.

The exceptions are:

--x-include-dir <dir>  x2c collection and x2c preprocessing only
--c-include-dir <dir>  C compilation only, ordinary include semantics
--c-system-dir <dir>   C compilation only, system-header semantics

--package-dir <dir> adds a directory of x2c packages. An import statement searches those directories for a package that is not beside the source. The option applies to translate, build, and run, and repeats like -I. After the explicit and manifest directories, every command also searches <home>/packages when that directory exists.

Explicit --package-dir directories, or a target’s package-dirs in x2c.toml, also select ownership when compiling package sources. Sources under <dir>/<name>/src/ compile with the <name>__ public-name prefix. Automatic home search resolves imports; source compilation uses explicit package directories. Compile providers for direct #include as ordinary sources with flat names, and consume package-compiled providers through import.

Packages

install, remove, and list dispatch to the shipped package command. The driver expands response files once. The package command handles their options and help with the legacy CLI parser. The equivalent x2c package install, x2c package remove, and x2c package list forms pass arguments literally, like other external commands.

install, remove, and list manage <home>/packages; see install packages. install takes one operand: a local directory, a local .tar.gz, a URL with --sha256 <hex>, or a name resolved through the index (--index <url-or-path> overrides the default). --force accepts a bundle built by another x2c version. remove takes one installed name; list prints name version kind lines on standard output, and the install and removal receipts go to standard error with the other action output. Refusals, including a package whose sources or install marker are damaged, exit with status 2 and leave the installed set as it was. One install or removal in a home runs at a time; the lock is released once the packages are in place, so a run that installed a dependency does not hold it while the program runs.

A project manifest can pin packages instead of installing them by hand; see pinned packages.

Environment

x2c env prints the resolved home, executable, include directory, runtime archive, prelude interface, package roots, C compiler, archiver, and script cache root as name = value lines. It also reports libexec, the directory for external commands, and identity, the compiler executable’s identity. The prelude is the runtime x2c.xi interface the compiler replays for the runtime declarations. Only an interface the same compiler wrote qualifies; an empty value means the compiler reads lib/x2c.x from source once per process. x2c env <name> prints one value; --package-dir, --cc, and --ar show their effect on the report.

x2c env
x2c env runtime_lib

The home is X2C_HOME when set. Otherwise the compiler walks up from its executable, then from the current directory, to the nearest directory holding include/ and etc/compiler-sdk.xlisp. Either way the home is reported and compared as an absolute path with symbolic links resolved, so a relative or symlinked X2C_HOME selects the same stage, prelude, and runtime archive as its real path. A source checkout and an installed prefix are both homes. A stage compiler under <home>/builds/<n>/ links that stage’s runtime archive. Any other compiler links <home>/lib/libx2c.a when it exists, and otherwise <home>/builds/0/libx2c.a, the stage 0 runtime whose headers a source checkout’s include/x2c names.

There is no lowercase -i. -D and -U reach requested x2c preprocessing and C compilation. Optimization, debug, and -Xcc are compile-only; -L, -l, --rpath, -Wl,, -Xlinker, and -framework are link-only. -pthread reaches both the compile and the link. Unknown options are rejected. --rpath <dir> records <dir> in the program as a place to find shared libraries when it runs; a package bundle may include it. -framework <name> links a native framework on macOS.

The C compiler selection order is --cc, X2C_CC, CC, the installed toolchain record, then cc. The archiver follows --ar, X2C_AR, AR, the installed record, then ar. Project meta code runs inside the compiler’s host, so it is built by --meta-cc, X2C_META_CC, META_CC, the installed record’s META_CC, then cc, never by the target --cc; translate accepts --meta-cc too. X2C_META_TIMEOUT limits one $ call in the project meta helper to that many seconds, 60 by default; 0 removes the limit. A call past the limit stops the helper and is reported at the call. Omitted optimization, debug, define, and undefine options preserve host defaults. CFLAGS and LDFLAGS are not shell-split or implicitly consumed. For every C compilation it runs, x2c selects signed plain char. The runtime requires an eight-bit signed char, even on hosts whose C compiler defaults to unsigned plain char.

Commands

The built-in commands are listed by x2c help. External commands are separate programs named x2c-<name> in the directory printed by x2c env libexec. For the stage 0 checkout compiler this is builds/0/libexec. A development install in bin/ and an installed prefix use <home>/libexec/x2c. The driver does not search PATH for them. x2c help lists their summaries from that directory’s commands.txt.

The driver passes arguments after an external command name directly to its program, including arguments beginning with @. The external command owns its options and help, so x2c help <name> and x2c <name> --help run the program’s --help. Built-in commands retain response-file expansion.

x2c lint reports an unreadable input’s path and system error on standard error and exits 1, while checking the other readable inputs. This also applies when checking formatting or showing formatting differences. The lint command treats - as an ordinary file name in these modes.

Compiler diagnostics

Translation reports each diagnostic on standard error as file:line:column: code: message, followed by the source line, a caret, and any notes. A unit stops after 20 errors. --max-errors <count> changes that bound for translate, build, run, and script, and --max-errors 0 removes it. When the bound is greater than one, a unit that reaches it ends with limit: too many errors, stopping. With --max-errors 1, the unit’s output ends after its first error. The diagnostics section of the language reference describes which errors are reported together.

Warnings

Ordinary translation reports warnings without stopping the build. Warnings do not count against --max-errors. The internal self-host stage option --fatal-warnings makes warnings fail those builds; it is not a public per-warning suppression or escalation control. A warning reports code that may be deliberate; leave the code unchanged when it is.

Eight codes are reported as warnings:

  • region says a value allocated inside a region can still be reached after the region ends, and names how it leaves. Its note gives the line that opened the region.
  • after-free says a local was read after Scope.free, Scope.realloc, or Array.list_free consumed it.
  • bad-free says Scope.free or Scope.realloc was given a literal, a local’s address, or other storage no Scope allocator returned.
  • literal says a bare word inside a macro is a Symbol where a value was probably meant, and shows the unquoted spelling that inserts the value.
  • macro carries what a macro reported through x2c.diagnostic.warn, at the invocation, with the notes the macro supplied.
  • conversion says a cast or explicit converter call spells a conversion its destination already performs.
  • shadow says a protocol binder shadows a visible type name.
  • native says a native meta prototype names a function that both the compiler and a native module define, or that more than one native module defines.

The Region Model explains what the first three cover and what they do not detect. Scopes and Lifetime shows the patterns that avoid them.

--diagnostics-file <file> writes the same diagnostics to <file> as JSON Lines instead of standard error:

./x2c build --diagnostics-file /tmp/app.jsonl --output /tmp/app app.x

x2c creates or truncates the file when the command starts, so a command with no diagnostics leaves it empty. Each line is one JSON object:

{"code":"type","message":"operator 'is' requires Var on the left","severity":"error","file":"app.x","line":9,"column":16,"length":2,"position":120,"notes":["operand type: (int)"]}
  • code is the diagnostic category, such as parse, type, macro, protocol, xform, region, warning, or limit.
  • severity is error, warning, or note. It records how the report was submitted, so each entry in a category with both severities, such as region, literal, or macro, has its own severity. The limit notice is a note.
  • file is relative to the working directory for a source inside it and absolute otherwise. A pseudo-source such as <stdin> keeps its name.
  • line and column are one-based, length is the token width in bytes, and position is its zero-based byte offset. All five location fields are null for a diagnostic without a location.
  • notes holds the note line that standard error shows, the text notes joined with spaces, or nothing when the diagnostic has no notes.

Compile-time Lisp and macros may print to standard output and standard error, so neither stream contains JSON. Parallel translation workers share the file; each diagnostic is one appended write, so lines never interleave. A diagnostic is written when it is reported, and the file is complete when the command exits, including after a failed unit. Command-line, host preprocessor, C compiler, and linker failures are not compiler diagnostics and remain on standard error; the exit status reports the failure.

Progress and receipts

Commands report completed translation, C compilation, archive, and link phases on standard error. A final build receipt identifies the artifact, elapsed time, generated C and header size, host compiler and job count, intermediate directory, and output size. Manifest builds name each target. Warm builds mark reused translation, compilation, and static archives as up to date. Executables always relink so changed libraries and native linker inputs take effect.

On a capable terminal, work lasting at least 125 ms may also use one transient progress line. x2c does not enter raw mode, switch screens, or read terminal input, and Make recipes show it too. Redirected standard error also uses stable newline-delimited receipts and no automatic color.

--plain selects stable receipts without terminal rendering or color. -q and --quiet suppress successful progress and receipts without suppressing diagnostics or a run program’s output. --color=auto|always|never controls color; NO_COLOR disables automatic color. --debug enables compiler debug logging; translate, build, and run accept it.

--verbose prints each command as it runs. -### prints the same commands without running them. Use them first when checking runtime selection, include order, or which phase a flag reached. Neither prints completion receipts into that output, and the inspection modes keep progress out of their stdout data.

Dependencies and external builds

Translation depfiles contain the primary source, transitive quote-includes, and the runtime, macro, Lisp, and embedded-text files that unit read. The native driver adds -MMD -MP -MF -MT per object and rejects conflicting dependency flags passed through -Xcc.

Persistent direct and manifest builds use x2c depfiles for translation reuse. Before reusing a native object, the selected C compiler preprocesses its source with the current native flags and environment. Changed header search results and __has_include conditions therefore rebuild the object; unchanged native input reuses it. C depfiles remain available to external build tools. Missing artifacts and missing, corrupt, or old private state are cache misses rather than project errors. Manifest comments and whitespace do not invalidate reuse; changed effective settings still rebuild the affected actions.

External Make builds are supported. They can call translate --out-dir, include its .d files, and set their own C compiler and linker flags. The A project can run its own native build instead of using the driver.

For example, from the repository directory you can translate an example and compile its C output yourself:

mkdir -p /tmp/x2c-example
./x2c translate --out-dir /tmp/x2c-example examples/foreach.x
cc -iquote include/x2c /tmp/x2c-example/foreach.c \
  builds/0/libx2c.a -lm -o /tmp/x2c-example/foreach
/tmp/x2c-example/foreach

Help, version, and status

--help and --version write to standard output and return 0. Source, host-tool, and link failures return 1. Invalid commands, options, response files, manifests, and preflight state return 2. After a successful run, the program’s status is returned. Diagnostics and action output use standard error; requested dumps and program output use standard output.