Ashes Compiler CLI Specification
This document is the authoritative reference for every command, flag, and argument supported by the Ashes compiler CLI (ashes).
Source of truth: The CLI entry point and all argument-parsing logic live in
src/Ashes.Cli/Program.cs. Update that file—and this document—together whenever CLI behaviour changes.
Overview
The Ashes CLI is invoked as:
ashes <command> [options] [arguments]Running ashes with no arguments (or an unrecognised command) prints the help text and exits with code 2.
Running ashes --help (or ashes -h) prints the same help text and exits with code 0. Running ashes <command> --help (or ashes <command> -h) also prints the CLI help text and exits with code 0.
Command List
| Command | Short description |
|---|---|
ashes compile | Compile an Ashes source file/expression to a native executable |
ashes run | Compile and immediately execute an Ashes program |
ashes repl | Start an interactive read-eval-print loop |
ashes test | Run .ash test files and compare against // expect: comments |
ashes fmt | Format .ash source files |
ashes init | Create a new Ashes project in the current directory |
ashes add | Add a dependency to the project manifest (--path, --dev) |
ashes remove | Remove a dependency from the project manifest |
ashes restore | Resolve dependencies (path + registry); --frozen, --offline |
ashes tree | Render the resolved dependency tree |
ashes why | Show why a package is in the dependency graph |
ashes login | Store an API token for a registry |
ashes publish | Package the current project and publish it to a registry |
ashes yank | Yank (or --undo) a published version |
ashes search | Search a registry for packages |
ashes info | Show a package's versions, owners, and capabilities |
Registry commands
These talk to an Ashes package registry (see the registry API reference). Each accepts --registry <name-or-url>, resolving a name from ~/.ashes/config.json (the default entry falls back to the canonical public instance) or using a literal http(s):// URL as-is. Credentials are stored per registry base URL in ~/.ashes/credentials.json.
| Command | Synopsis | Notes |
|---|---|---|
ashes login | ashes login [--registry <r>] (--as <account> | --token <t>) | --as mints a token via the registry; --token stores a provided one. |
ashes publish | ashes publish [--registry <r>] [--project <ashes.json>] [--version <v>] | Packages the project's .ash sources (plus a portable ashes.json and README/LICENSE), computes the ash1: content hash, and uploads. Rejects object-valued runtime dependencies; strips overrides and devDependencies. Namespace derives from namespace or the PascalCase package name; version from --version or the manifest. Requires a prior login. |
ashes yank | ashes yank <namespace> <version> [--undo] [--registry <r>] | Marks a version un-resolvable for new builds (existing locks still resolve); --undo reverses it. Owner-only. |
ashes search | ashes search <query> [--registry <r>] | Prints a ranked list (namespace, latest, description). |
ashes info | ashes info <namespace>[@<version>] [--registry <r>] | Shows owners and, for the selected (or latest) version, its capability row and dependencies. |
The capability row shown by ashes info is the registry's server-computed audit — the capabilities the package's public API needs, inferred by the compiler at publish time, not a heuristic scan. Built-in ambient markers such as FileRead, ProcessSpawn, and UnsafeFfi are included in this metadata exactly like declared capability rows.
Common Options
The following help flags are accepted at the top level and for each command:
| Option | Value type | Default | Repeatable | Description |
|---|---|---|---|---|
--help / -h | — | — | No | Print CLI help text and exit successfully. |
The following options are accepted by compile, run, repl, and test:
| Option | Value type | Default | Repeatable | Description |
|---|---|---|---|---|
--target <id> | enum | OS-dependent (see below) | No | Select the code-generation back end. |
--target-cpu <cpu> | string | generic / x86-64 | No | Target a specific CPU microarchitecture (e.g. skylake, znver3, apple-m1). Use native to auto-detect the host CPU only when the host and target architectures match; cross-compilation requires an explicit CPU or the generic default. |
--parallel-stack-size <size> | size | 1M | No | Per-worker stack size for structured parallelism (Ashes.Task.Parallel). Accepts a byte count or a K/M/G suffix (e.g. 2M, 1048576); must be positive. On linux this sets the mmap'd worker-stack length; on win-x64 it is passed to CreateThread (unset leaves the OS default). |
--parallel-workers <n> | int | core count | No | The executable's compiled maximum / hard ceiling for concurrent structured-parallelism workers. Must be positive. When unset, the compiled program detects the machine's core count once at first fork (linux: sched_getaffinity popcount, so taskset/cgroup masks are respected; win-x64: GetSystemInfo), falling back to 8 if detection fails. A program can request fewer workers for a scoped computation with Ashes.Task.Parallel.withWorkers (effective count is min(override, this maximum)); it can never exceed this ceiling. |
-O0|-O1|-O2|-O3 | enum | -O2 | No | Select LLVM optimization level. |
The following option is accepted by compile and run only:
| Option | Value type | Default | Repeatable | Description |
|---|---|---|---|---|
--debug / -g | bool (flag) | false | No | Emit DWARF debug info into the output binary. See Debug Mode below. |
Optimization level values:
| Flag | Meaning |
|---|---|
-O0 | No optimization |
-O1 | Basic optimizations |
-O2 | Standard optimizations (default) |
-O3 | Aggressive optimizations |
The following option is accepted by compile, run, and test:
| Option | Value type | Default | Repeatable | Description |
|---|---|---|---|---|
--explain <kind>[:<selector>] | enum | — | Yes | Report compiler decisions to stderr. See Compiler Reports below. |
The following option is accepted by compile and run:
| Option | Value type | Default | Repeatable | Description |
|---|---|---|---|---|
--emit-ir <stage>[:<selector>] | enum | — | Yes | Dump semantic IR to stderr. See IR Dumps below. |
--project is accepted by project-scoped commands: compile, run, test, add, remove, restore, tree, why, and publish. It is not accepted by ashes repl.
| Option | Value type | Default | Repeatable | Description |
|---|---|---|---|---|
--project <path> | file path | — | No | Load a project manifest (ashes.json). Mutually exclusive with an inline file argument and --expr. |
--target values:
| Value | Platform |
|---|---|
linux-x64 | Linux x86-64 — emits a native ELF64 binary |
linux-arm64 | Linux AArch64 — emits a native ELF64 binary |
win-x64 | Windows x86-64 — emits a native PE32+ binary |
win-arm64 | Windows on ARM64 — emits a native PE32+ (AArch64) binary, and is also a host RID (a released compiler runs on Windows-on-ARM). Neither the emitted image nor the WoA compiler executes on an x64 host — both run only on native ARM64 Windows |
Any other value is rejected with an error message and exit code 1.
Command Reference
ashes compile
Compile an Ashes program to a native executable on disk.
Synopsis
ashes compile [--target <id>] [--target-cpu <cpu>] [--parallel-stack-size <size>] [--parallel-workers <n>] [-O0|-O1|-O2|-O3] [--debug|-g] [--explain <kind>] [-o <output>] <input.ash>
ashes compile [--target <id>] [--target-cpu <cpu>] [--parallel-stack-size <size>] [--parallel-workers <n>] [-O0|-O1|-O2|-O3] [--debug|-g] [--explain <kind>] [-o <output>] --expr "<source>"
ashes compile [--target <id>] [--target-cpu <cpu>] [--parallel-stack-size <size>] [--parallel-workers <n>] [-O0|-O1|-O2|-O3] [--debug|-g] [--explain <kind>] [-o <output>] --project <ashes.json>
ashes compile [--target <id>] [--target-cpu <cpu>] [--parallel-stack-size <size>] [--parallel-workers <n>] [-O0|-O1|-O2|-O3] [--debug|-g] [--explain <kind>] [-o <output>] # discovers ashes.json upwardArguments
| Name | Type | Required | Description |
|---|---|---|---|
<input.ash> | file path | No† | Path to the .ash source file to compile. |
† Exactly one input source must be provided: a positional file, --expr, or a project (explicit or auto-discovered).
Options
| Option | Long form | Value type | Default | Repeatable | Description |
|---|---|---|---|---|---|
-o | --out | file path | Derived from input name (see below) | No | Path for the compiled output binary. |
--expr | string | — | No | Inline Ashes source to compile instead of reading a file. | |
--target | enum | OS default | No | Target back end (linux-x64, linux-arm64, win-x64, or win-arm64). | |
--target-cpu | string | generic / x86-64 | No | Target CPU microarchitecture (e.g. skylake, native). | |
--parallel-stack-size | size | 1M | No | Per-worker stack size for Ashes.Task.Parallel (byte count or K/M/G suffix). | |
--project | file path | — | No | Path to an ashes.json project file. | |
-O0|-O1|-O2|-O3 | enum | -O2 | No | Select LLVM optimization level. | |
--debug | -g | bool (flag) | false | No | Emit DWARF debug info (see Debug Mode). |
Default output path rules (when -o is not given):
- Single file
examples/hello.ash→examples/hello(Linux) orexamples/hello.exe(Windows). --exprwithout a project →out(Linux) orout.exe(Windows).- Project compile →
<outDir>/<name>(fromashes.json), appending.exeon Windows.
Defaults
| Property | Default value |
|---|---|
--target | linux-x64 on Linux x86-64, linux-arm64 on Linux ARM64, win-x64 on Windows |
--target-cpu | x86-64 for x86-64 targets, generic for ARM64 |
-o / --out | Derived from input (see above) |
-O0..-O3 | -O2 (standard optimizations) |
Exit Codes
| Code | Meaning |
|---|---|
0 | Compilation succeeded; binary written to disk. |
1 | Compilation error (parse, type, or code-generation failure), or user/input error such as missing input, file not found, wrong extension, or I/O failure. |
2 | Usage error (bad flag or conflicting options). |
Output (stdout / stderr)
Successful status messages are written to stdout via Spectre.Console. On success, a confirmation line is printed:
OK Wrote <size> to <output>
Target: <target>
Debug: yes (only when --debug / -g is set)
Time: <elapsed><size> is human-readable (e.g. 8.1 KB). <elapsed> is the compilation wall-clock time (e.g. 159ms or 1.23s).
Compiler diagnostics and command errors are written to stderr. No machine-readable output format is currently supported.
Examples
# Compile a file (output: examples/hello on Linux)
ashes compile examples/hello.ash
# Compile to an explicit path
ashes compile examples/hello.ash -o build/hello
# Compile an inline expression
ashes compile --expr 'Ashes.IO.print(40 + 2)' -o out
# Cross-compile to Windows PE
ashes compile examples/hello.ash --target win-x64 -o hello.exe
# Compile the project rooted at ashes.json
ashes compile --project path/to/ashes.json
# Auto-discover ashes.json (searches upward from cwd)
ashes compileSet the ASHES_TIMING environment variable to any value to report how long each compile phase took on stderr, one timing: <phase> <milliseconds> ms line per phase: lower (semantic lowering with lifetime placement), optimize (the IR optimizer), and backend with its backend.emit-module, backend.verify, backend.llvm-passes, backend.bitcode-link, backend.object-code, and backend.link parts (a split program reports backend.parallel-objects with one backend.partition<N>.llvm-passes and backend.partition<N>.object-code pair per partition instead). lower is itself split into lower.deriving, lower.register-traits, lower.register-inlinable, lower.move-analysis (with its calls, handlers, reach, provenance, inspect-only, and summaries parts), lower.body, lower.entry-inference, lower.placement.entry, and lower.placement.functions; a program that needs the inferred-trait discovery pass reports the first four twice, once for that pass. optimize is split into optimize.compile-time-eval, optimize.entry, optimize.functions, optimize.closures (with its captured, returned, currying, scalarize, and returned-again parts), optimize.non-allocating, optimize.brackets, and optimize.concat. The generated code is unaffected.
The compiler runs with the server garbage collector, which keeps collection pauses off the single-threaded lowering and optimizer phases at the cost of a somewhat larger heap.
A large Linux program (1024 lifted functions or more) is optimized and turned into object code in parallel partitions that are merged into one relocatable object before linking; see Parallel code generation. Set ASHES_LLVM_JOBS to the number of partitions to use, or to 1 to keep a single module. The partition count is derived from the program's size by default, so the same source produces the same executable on every machine.
ashes run
Compile and immediately execute an Ashes program. The compiled binary is written to a uniquely named executable under the system temporary directory (for example, <temp>/ashes/) and is not automatically deleted by the CLI.
Synopsis
ashes run [--target <id>] [--target-cpu <cpu>] [--parallel-stack-size <size>] [--parallel-workers <n>] [-O0|-O1|-O2|-O3] [--debug|-g] [--explain <kind>] <input.ash> [-- <args...>]
ashes run [--target <id>] [--target-cpu <cpu>] [--parallel-stack-size <size>] [--parallel-workers <n>] [-O0|-O1|-O2|-O3] [--debug|-g] [--explain <kind>] --expr "<source>" [-- <args...>]
ashes run [--target <id>] [--target-cpu <cpu>] [--parallel-stack-size <size>] [--parallel-workers <n>] [-O0|-O1|-O2|-O3] [--debug|-g] [--explain <kind>] --project <ashes.json> [-- <args...>]
ashes run [--target <id>] [--target-cpu <cpu>] [--parallel-stack-size <size>] [--parallel-workers <n>] [-O0|-O1|-O2|-O3] [--debug|-g] [-- <args...>] # auto-discovers ashes.jsonArguments
| Name | Type | Required | Description |
|---|---|---|---|
<input.ash> | file path | No† | Path to the .ash source file to run. |
-- <args...> | string list | No | Arguments forwarded to the compiled program (args in Ashes source). The -- separator is required. |
† Same rule as compile: one of file, --expr, or project.
Options
| Option | Value type | Default | Repeatable | Description |
|---|---|---|---|---|
--expr | string | — | No | Inline Ashes source to compile and run. |
--target | enum | OS default | No | Target back end. |
--target-cpu | string | generic / x86-64 | No | Target CPU microarchitecture (e.g. skylake, native). |
--parallel-stack-size | size | 1M | No | Per-worker stack size for Ashes.Task.Parallel (byte count or K/M/G suffix). |
--project | file path | — | No | Path to an ashes.json project file. |
-O0|-O1|-O2|-O3 | enum | -O2 | No | Select LLVM optimization level. |
--debug / -g | bool (flag) | false | No | Emit DWARF debug info (see Debug Mode). |
Exit Codes
| Code | Meaning |
|---|---|
0 | Program compiled and exited with code 0. |
| Non-zero (from program) | The compiled program's own exit code is propagated as-is. |
1 | Compilation error or user/input failure (before the program starts). |
2 | Usage error. |
Output (stdout / stderr)
The compiled program's stdout and stderr flow directly to the terminal without interception. No extra diagnostic lines (timing, size, etc.) are printed, so ashes run output is safe to pipe. Compiler diagnostics are printed to the CLI's stderr before the program runs.
Examples
# Run a file
ashes run examples/hello.ash
# Run with arguments passed to the program
ashes run examples/hello.ash -- hello world
# Run an inline expression
ashes run --expr 'Ashes.IO.print(40 + 2)'
# Run the auto-discovered project
ashes run
# Run a named project
ashes run --project examples/project/ashes.jsonashes repl
Start an interactive read-eval-print loop. Each expression is compiled to a temporary binary, executed, and its stdout is displayed.
Synopsis
ashes repl [--target <id>] [--target-cpu <cpu>] [-O0|-O1|-O2|-O3]Options
| Option | Value type | Default | Repeatable | Description |
|---|---|---|---|---|
--target | enum | OS default | No | Target back end used for all REPL evaluations. |
--target-cpu | string | generic / x86-64 | No | Target CPU microarchitecture used for all REPL evaluations. |
-O0|-O1|-O2|-O3 | enum | -O2 | No | Select LLVM optimization level used for all REPL evaluations. |
REPL Commands (typed at the prompt)
| Command | Aliases | Description |
|---|---|---|
:help | :h | Show REPL help. |
:quit | :q, :exit | Exit the REPL (exit code 0). |
:target | Show the current target. | |
:target linux-x64|linux-arm64|win-x64|win-arm64 | Change the active target for subsequent expressions. |
Multi-line input is supported: if the parser detects an incomplete expression (unbalanced parentheses or an expected-token error), the REPL shows a ...> continuation prompt.
Exit Codes
| Code | Meaning |
|---|---|
0 | User exited cleanly (:quit / :exit / :q). |
2 | Usage error (unknown flag). |
Output (stdout / stderr)
All REPL output (prompts, results, errors) is written to stdout. Compiled-program stderr is relayed and highlighted in red.
Examples
ashes repl
# ashes> Ashes.IO.print(40 + 2)
# 42
# ashes> :target linux-x64
# Target set to linux-x64
# ashes> :quitashes test
Discover and execute .ash test files. A test file must contain a leading // expect: comment; the runner compiles the file, executes it, and compares stdout against the expected value.
Synopsis
ashes test [--target <id>] [--target-cpu <cpu>] [-O0|-O1|-O2|-O3] [--pipeline <optimized|lowered|both>] [--project <ashes.json>] [--explain <kind>] [paths...]Arguments
| Name | Type | Required | Description |
|---|---|---|---|
[paths...] | file/directory path list | No | One or more .ash files or directories to search for tests. Repeatable. When --project is supplied and no paths are given, the default search root is <projectDir>/tests if that directory exists, otherwise it is the project directory itself. |
Options
| Option | Value type | Default | Repeatable | Description |
|---|---|---|---|---|
--target | enum | OS default | No | Target back end for test compilation. |
--target-cpu | string | generic / x86-64 | No | Target CPU microarchitecture for test compilation. |
--project | file path | — | No | Load a project manifest. When no explicit [paths...] are given, test discovery uses <projectDir>/tests if that directory exists; otherwise it falls back to the project directory itself. |
-O0|-O1|-O2|-O3 | enum | -O2 | No | Select LLVM optimization level for test compilation. |
--pipeline | optimized|lowered|both | optimized | No | Select whether tests use the normal Ashes semantic optimizer, bypass it, or run through both semantic pipelines. This is independent of the LLVM -O level. |
Test File Conventions
A test file is a regular .ash source file with one or more leading comment directives:
// expect: <expected stdout>
// exit: <expected exit code> (optional; defaults to 0)- Only the first
// expect:line is used. // exit:must appear before// expect:.- Files without
// expect:are reported as SKIP (treated as pass).
Special expected value:
| Value | Meaning |
|---|---|
empty | Expect empty stdout. |
Example:
// exit: 1
// expect: empty
panic("empty")Exit Codes
| Code | Meaning |
|---|---|
0 | All tests with // expect: passed (or none found). |
1 | One or more tests failed. |
2 | Usage error. |
Output (stdout / stderr)
Results are printed as a table to stdout:
Test Result Time
──────────────────────────────
hello.ash PASS 5ms
failing.ash FAIL 12msThe per-test Time value measures only the launched native process, excluding parsing, lowering, LLVM code generation, linking, executable-file setup, and test-fixture setup. Sub-millisecond executions are reported in microseconds. With --pipeline both, the column labels the two process times as opt=<time> lowered=<time>. A compile-error test displays — because it does not launch a process.
The default optimized pipeline matches ashes compile and ashes run: it lowers the program, runs the Ashes semantic optimizer, and passes the resulting IR to LLVM. lowered passes the raw lowered IR directly to LLVM, which is useful for exposing correctness bugs hidden by dead-code elimination or compile-time evaluation. both requires every runnable test to satisfy the same directives in both pipelines. Compile-error tests run once because compilation stops before the semantic optimizer boundary. The selected -O0 through -O3 level still applies independently to LLVM in every pipeline.
A summary line follows: N passed, N failed, N skipped in <elapsed>. Its elapsed value retains the complete compile-and-run test time. Failure details (expected vs. actual stdout) are printed after the table.
Examples
# Run all tests in the default location
ashes test
# Run tests in a specific directory
ashes test tests/
# Run a single test file
ashes test tests/hello.ash
# Run tests for a named project
ashes test --project path/to/ashes.json
# Compare normal and raw-lowered semantic pipelines
ashes test --pipeline both tests/ashes fmt
Format .ash source files. ashes fmt resolves formatting from the nearest .editorconfig (walking upward from each file), supporting:
indent_style = space|tabindent_size = <int>|tabtab_width = <int>end_of_line = lf|crlfashes_prefer_pipelines = true|false(write a chain of two or more nested calls as a|>pipeline; see the formatter reference)
Defaults when not provided are 4 spaces, platform newline, and no pipeline rewriting. Without -w, formatted output is printed to stdout. With -w, files are updated in place.
Synopsis
ashes fmt <file|dir> [-w]Arguments
| Name | Type | Required | Description |
|---|---|---|---|
<file|dir> | file/directory path | Yes | A single .ash file or a directory. When a directory is given, all .ash files are found recursively. |
Options
| Option | Long form | Value type | Default | Repeatable | Description |
|---|---|---|---|---|---|
-w | --write | bool (flag) | false | No | Write formatted output back to the source file(s) instead of printing to stdout. |
Exit Codes
| Code | Meaning |
|---|---|
0 | Formatting succeeded (or no .ash files found). |
1 | Parse error in one of the source files, or user/input error such as missing path, wrong extension, or path not found. |
2 | Usage error (bad flag or ambiguous arguments). |
Output (stdout / stderr)
- Without
-w: formatted source is written to stdout. When multiple files are formatted, a separator rule is printed before each file. - With
-w: a summary line (OK Formatted N file(s) in <elapsed>.) is written to stdout. Files that are already correctly formatted are not rewritten.
Examples
# Preview formatting of a single file
ashes fmt examples/hello.ash
# Format all .ash files in a directory, writing in place
ashes fmt examples -w
# Preview formatting for all .ash files in a directory
ashes fmt examplesashes init
Create a new Ashes project in the current directory by scaffolding an ashes.json manifest and a starter src/Main.ash entry file.
Synopsis
ashes initArguments
None.
Options
None.
Behaviour
- If
ashes.jsonalready exists in the current directory, the command fails with exit code 1. - Creates
ashes.jsonwithname(derived from the directory name),entryset to"src/Main.ash", andsourceRootsset to["src"]. - Creates the
src/directory if it does not exist. - Creates
src/Main.ashwith a hello-world program. If the file already exists it is not overwritten.
Exit Codes
| Code | Meaning |
|---|---|
0 | Project created successfully. |
1 | ashes.json already exists or an I/O error occurred. |
Examples
mkdir myapp && cd myapp
ashes init
# Created ashes.json
# Created src/Main.ashashes add
Add a dependency to the selected project manifest, or to the nearest ashes.json when --project is omitted.
Synopsis
ashes add <package> [--project <manifest>] [--path <dir>] [--dev]Arguments
| Name | Type | Required | Description |
|---|---|---|---|
<package> | string | Yes | The dependency name to add. |
Options
| Name | Description |
|---|---|
--project <manifest> | Update a specific project manifest instead of discovering the nearest ashes.json. |
--path <dir> | Record a path dependency { "path": "<dir>" } instead of a registry version. |
--dev | Write to devDependencies instead of dependencies. |
Behaviour
- Uses
--projectwhen supplied; otherwise discoversashes.jsonby walking upward from the current directory. - If no
ashes.jsonis found, the command fails with exit code 1. - Adds the dependency to
dependencies(ordevDependencieswith--dev):{ "path": "<dir>" }when--pathis given, otherwise the SemVer string"*". Existing entries are overwritten; other dependencies are preserved.
Exit Codes
| Code | Meaning |
|---|---|
0 | Dependency added successfully. |
1 | No ashes.json found, missing package argument, or I/O error. |
Examples
ashes add json-parser
# Added json-parser to dependencies.
ashes add test-helper --dev --project ashes-test.json
# Added test-helper to devDependencies.ashes remove
Remove a dependency from the selected project manifest, or from the nearest ashes.json when --project is omitted.
Synopsis
ashes remove <package> [--project <manifest>]Arguments
| Name | Type | Required | Description |
|---|---|---|---|
<package> | string | Yes | The package name to remove. |
Options
| Name | Description |
|---|---|
--project <manifest> | Update a specific project manifest instead of discovering the nearest ashes.json. |
Behaviour
- Uses
--projectwhen supplied; otherwise discoversashes.jsonby walking upward from the current directory. - If no
ashes.jsonis found, the command fails with exit code 1. - If the package is not present in
dependencies, the command fails with exit code 1. - Removes the package from the
dependenciesobject and writes the file back. - If the
dependenciesobject becomes empty after removal, the field is omitted from the written JSON.
Exit Codes
| Code | Meaning |
|---|---|
0 | Dependency removed successfully. |
1 | No ashes.json found, missing package argument, package not in dependencies, or I/O error. |
Examples
ashes remove json-parser
# Removed json-parser from dependencies.ashes restore
Resolve and materialize dependencies for the selected project manifest, or for the nearest ashes.json when --project is omitted.
Synopsis
ashes restore [--project <manifest>] [--registry <name-or-url>] [--frozen] [--offline]Options
| Name | Description |
|---|---|
--project <manifest> | Restore a specific project manifest instead of discovering the nearest ashes.json. |
--registry <r> | Registry to resolve registry dependencies against (name or URL; default from config). |
--frozen | Fail (ASH032/ASH033) if a fresh resolution would differ from the selected project's committed lock file; never rewrite it. For CI. |
--offline | Never touch the network; trust the selected project's lock file and only verify its packages are in the cache. |
Behaviour
- Uses
--projectwhen supplied; otherwise discoversashes.jsonby walking upward from the current directory. - Fetches and caches registry dependencies (resolving SemVer constraints across the transitive graph) and writes the selected manifest's lock file; then validates path dependencies (a missing path or non-project fails with
ASH030/ASH031) and lists every resolved dependency with its namespace. - Applies root-level path
overrideswithout changing the registry-only lock graph. Overrides in a dependency's manifest are ignored. The local package must declare the selected package's namespace and exact locked version. - Each cached package's content is verified against the lock's
ash1:hash (ASH034on mismatch). build/run/testauto-restore when a project's lock is missing or a locked package is not cached (against the default registry). Useashes restoreexplicitly to target a specific registry, or with--frozen/--offline.
Examples
ashes restore --registry https://pkg.ashes-lang.org
# Resolved 2 dependencies into ashes.lock.
ashes restore --project ashes-test.json
# Resolved 1 registry dependency into ashes-test.lock.ashes tree
Render the resolved dependency tree (project root → direct dependencies → their transitive dependencies) from the selected project's lock file. Path dependencies are shown as leaves.
ashes tree [--project <manifest>]
# app
# └── Json 1.2.0
# └── Utf8 0.4.3ashes why
Show a path from a project root dependency to the named package, explaining why it is in the graph.
ashes why Utf8 [--project <manifest>]
# Json -> Utf8Project File (ashes.json)
The project file enables multi-module compilation and controls build settings.
Named manifests may be colocated and selected with --project. Each manifest owns the lock file with the same base name: ashes.json maps to ashes.lock, while ashes-test.json maps to ashes-test.lock.
Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
entry | string (file path) | Yes | — | Relative path to the entry-point .ash file. |
name | string | No | Filename stem of entry | Output binary name (without extension). |
sourceRoots | string array | No | ["."] | Directories searched when resolving import statements. |
include | string array | No | [] | Additional directories searched for imported modules. |
outDir | string | No | "out" | Directory where the compiled binary is written. |
target | string (enum) | No | OS default | Default back end target; overridden by --target on the command line. |
dependencies | object | No | {} | Runtime dependency map. Values are registry version constraints or local path objects; publishing rejects local path objects. |
devDependencies | object | No | {} | Build/test dependency map with the same value forms as dependencies; never published. |
overrides | object | No | {} | Root-only map from a direct or transitive registry package to { "path": "..." }. The local namespace and version must exactly match the selected lock entry. Never published. |
Example
{
"name": "myapp",
"entry": "src/Main.ash",
"sourceRoots": ["src"],
"outDir": "build",
"target": "linux-x64"
}Module Import Syntax
Within any .ash file in a project, modules can be imported with:
import ModuleName
import Namespace.ModuleName- Module names must start with an uppercase letter.
- Nested namespaces use dot notation; each component must start with an uppercase letter.
- Import statements are collected from anywhere in the file; idiomatic style is to place them before any expression code.
- Circular imports are detected and reported as an error.
Exit Code Summary
| Code | Meaning |
|---|---|
0 | Success. |
1 | Compilation error, runtime error, test failure, or concrete user/input error. |
2 | Usage / argument error (bad flag, conflicting options, or unknown command syntax). |
The exit code from ashes run is the compiled program's own exit code when compilation succeeds.
Validation Rules and Error Behaviour
| Condition | Error message / behaviour | Exit code |
|---|---|---|
| Unknown command | Help text printed | 2 |
| Unknown flag | Unknown argument. | 2 |
| Missing required compile/run input | Missing input file or --expr. | 1 |
| Missing required fmt path | Missing file or directory. | 1 |
| Input file not found | File not found: <path> | 1 |
| Input file has wrong extension | Input file must have .ash extension (or use --expr). | 1 |
--project combined with file/--expr | Cannot combine --project with input file or --expr. | 2 |
--target receives unknown value | Message indicating unknown target, e.g. Unknown target '<value>'. | 1 |
ashes.json missing entry field | Project file is missing required string field 'entry'. | 1 |
ashes.json entry file not found | Project entry file not found: <path> | 1 |
| Import cycle detected | Import cycle detected: A -> B -> A | 1 |
| Parse / type error in source | Diagnostic message | 1 |
ashes fmt given more or fewer than one path | Provide exactly one file or directory. | 2 |
ashes fmt path does not exist | Path not found: <path> | 1 |
ashes init when ashes.json exists | ashes.json already exists in this directory. | 1 |
ashes add without package argument | Missing package name. | 1 |
ashes add when no ashes.json found | No ashes.json found. Run 'ashes init' first. | 1 |
ashes remove without package argument | Missing package name. | 1 |
ashes remove when no ashes.json found | No ashes.json found. Run 'ashes init' first. | 1 |
ashes remove when package not a dependency | Package '<name>' is not a dependency. | 1 |
ashes restore when no ashes.json found | No ashes.json found. Run 'ashes init' first. | 1 |
Compiler Reports
--explain prints what the compiler decided about a program. It is accepted by compile, run, and test, may be given more than once, and takes an optional :selector restricting the report to matching functions.
ashes compile app.ash --explain ownership
ashes run app.ash --explain rc --explain reuse
ashes compile app.ash --explain traits
ashes compile app.ash --explain authority
ashes compile app.ash --explain concurrency
ashes compile app.ash --explain memory:Map.set| Kind | Reports |
|---|---|
ownership | The inferred contracts: per-parameter borrowed or consumed, move safety, uniqueness, which parameters the result may alias, whether the result is fresh, and captured values. |
rc | The Perceus operations in the final semantic IR — dups, drops, uniqueness checks, allocations, reused allocations, reuse tokens, and copies. Functions with no such operations are omitted. |
reuse | In-place-reuse decisions: whether a specialization was generated, the candidate, the outcome, and a stable reason code with the source location of the site. |
traits | Hidden immutable dictionary parameters and the concrete implementation selected for each resolved trait requirement. |
authority | The inferred ambient-authority row of each public binding and the declared row of each external function. |
concurrency | Structured scopes, forks, joins, and explicitly detached spawns in the final task IR. |
memory | Ownership, RC, reuse, trait evidence, and physical representation correlated per source function. |
The selector matches a function's source name, qualified name, generated label, or the source function a generated function came from, case-insensitively and by substring — so --explain rc:loop also covers the reuse specializations, droppers, and coroutines generated for loop, and you never need a generated symbol name. A selector matching nothing prints (no functions matched) and succeeds.
Output goes to stderr, so a program's own stdout is unaffected:
ashes run app.ash --explain rc > output.txt # output.txt holds only the program's outputAn unknown kind is a usage error listing the valid values, with exit code 2. Nothing is printed when compilation fails before the report data exists; ordinary diagnostics remain primary.
Which one to reach for
The three facilities overlap deliberately, and it is worth knowing how:
| You want to know | Use |
|---|---|
| How much reference counting a function does | --explain rc |
| Which trait implementation was selected | --explain traits |
| Which ambient authority public functions and externals require | --explain authority |
| Which tasks use structured versus detached concurrency | --explain concurrency |
| Which operations, on which values, and where | --emit-ir final |
| What the optimizer changed | --emit-ir lowered and --emit-ir final, diffed |
--explain rc counts exactly the instructions --emit-ir final prints — every category is one instruction type, so the numbers agree by construction. It is the summary view, not separate information, and it earns its place by naming concepts rather than opcodes (you should not need to know that reuse tokens are DropReuse) and by staying readable across a program with dozens of functions.
--emit-ir lowered is the only one showing something the others cannot. The optimizer removes a large share of the operations lowering emits — often most of them — so the lowered stage answers a different question from either of the other two.
IR Dumps
--emit-ir prints the semantic IR itself, rather than a report about it. It takes lowered (as lowering emitted it) or final (what code generation receives), may be given more than once, and accepts the same :selector as --explain.
ashes compile app.ash --emit-ir final
ashes compile app.ash --emit-ir lowered:loop --emit-ir final:loop # diff the two stagesIR (final)
==========
trait evidence
dictionary-parameter function=show source=app.ash:42 index=0 trait=Ashes.Trait.Show methods=[show] supertraits=[]
resolved requirement=Ashes.Trait.Show(Int) implementation=Ashes.Trait (<std:Ashes.Trait>:5237)
function lambda_55 [SourceFunction from build]
locals=2 temps=4
LoadLocal Target=0 Slot=1 (app.ash:3:34)
TextFromInt Target=1 ValueTemp=0 (app.ash:3:15)
ConcatStr Target=3 Left=1 Right=2 RuntimeManaged=true (app.ash:3:15)
Return Source=3 (app.ash:3:1)Operands left at their unset value — false, null, or -1 for an optional slot or temp — are omitted, so what is printed is what carries meaning. Instructions carry no ordinal: requesting both stages and diffing them is the main reason to read a dump, and an absolute index would make every line after an inserted or removed instruction register as changed. Labels anchor position where position matters.
When traits are present, the leading trait evidence block identifies every hidden dictionary parameter in ABI order and every concrete implementation selected during resolution. Paths and source offsets are stable source identities; no runtime pointer or process-specific address is printed.
Unlike the explain reports, the dump's shape is not a contract. Its content is the instruction set, so it changes whenever an instruction does; treat it as a debugging artifact rather than something to snapshot. Like the reports, it goes to stderr and cannot change generated code.
What the reports are not
These are static reports of compile-time decisions. They do not show how often anything executed, allocate counters, or profile a run — runtime profiling is a separate concern, and so are heap visualization and report viewers. Nor do they explain LLVM's own optimizations: everything reported here is an Ashes-level decision made before code generation.
rc describes the Ashes IR handed to the backend, not optimized LLVM IR. It observes after the Ashes-level optimizer in the default ashes test --pipeline optimized mode, matching the code that ships. With --pipeline lowered, it describes the raw lowered IR instead. --pipeline both emits a report for each pipeline.
Requesting a report cannot change generated code: the same source compiles to identical bytes with and without --explain.
Debug Mode
The --debug (or -g) flag instructs the compiler to embed debug information in the output binary for source-level debugging. The exact debug format is target-dependent.
Behaviour
| Aspect | Effect |
|---|---|
| Debug metadata | Debug information is emitted into the output binary; the exact sections and format depend on the target platform. |
| Optimization | Without an explicit -O flag, optimization defaults to -O0 for the most faithful single-stepping. An explicit -O1/-O2/-O3 is honored (no cap), so a profiled debug build matches the optimized binary's inlining. The DWARF stays valid at every level — the backend re-verifies the module after optimization when debug info is requested. |
| Compile summary | An extra Debug: yes line is printed in the ashes compile success output. |
| Target triple | --debug does not change the Windows LLVM target triple; the current implementation continues to target x86_64-pc-windows-msvc. |
Interaction with Optimization Levels
| User flags | Effective optimization |
|---|---|
--debug (no -O) | -O0 |
--debug -O0 | -O0 |
--debug -O1 | -O1 |
--debug -O2 | -O2 |
--debug -O3 | -O3 |
-O3 (no --debug) | -O3 (unchanged) |
Example
# Compile with debug info (defaults to -O0)
ashes compile --debug examples/hello.ash -o hello
# Compile with debug info and explicit -O1
ashes compile -g -O1 examples/hello.ash
# Run under debugger
ashes run --debug examples/hello.ashCompatibility Rules
Breaking Changes
The following changes would break existing users or tooling and must be treated as breaking:
- Removing a command or flag.
- Changing the meaning of an existing flag.
- Changing exit codes for existing conditions.
- Changing the format of the success output line for
ashes compile.
Non-Breaking Changes
The following changes are considered non-breaking:
- Adding new commands or flags.
- Adding new
--targetvalues. - Adding new fields to
ashes.json(with defaults). - Changing diagnostic or error message wording (not the exit code).
- Changing the visual styling of terminal output (colours, table borders).
