Ashes Project Specification
This document defines the ashes.json project file format and the rules the Ashes CLI and compiler use to discover sources, resolve imports, and choose defaults.
The goal is to keep the project model minimal, deterministic, and easy to implement.
1. File name and discovery
1.1 Default project file name
The default project file name is:
ashes.json
1.2 Discovery rules
When the user runs ashes commands without specifying a project file:
- The CLI searches for
ashes.jsonstarting from the current working directory and walking up parent directories. - The first
ashes.jsonfound is used. - If none is found, the CLI behaves as "single-file mode" (existing behavior) and requires an explicit
.ashfile or--expr.
1.3 Explicit project selection
The CLI supports selecting a project file explicitly:
--project <path/to/ashes.json>
The path may be relative or absolute.
2. Project file format (ashes.json)
2.1 JSON shape
ashes.json is a UTF-8 JSON object with the following fields:
Required:
entry(string)
Optional:
name(string)sourceRoots(string array)include(string array)outDir(string)target(string)defaults(object)
Unknown fields:
- Unknown fields must be ignored for forward compatibility.
2.2 Path resolution
All paths in ashes.json are resolved relative to the directory containing ashes.json.
Example:
If ashes.json is at C:\proj\ashes.json and contains "entry": "src/Main.ash", then the entry file path is C:\proj\src\Main.ash.
3. Fields
3.1 entry (required)
The entry file to compile and/or run.
- Must point to an existing
.ashfile. - Example:
"entry": "src/Main.ash"
3.2 name (optional)
A human-readable project name.
- Example:
"name": "hello-ashes"
3.3 sourceRoots (optional)
A list of directories that form the primary module search roots.
- Default:
["."] - Example:
"sourceRoots": ["src"]
3.4 include (optional)
Additional module search roots. Intended for:
lib- vendor folders
These are still project-local roots. Compiler-shipped libraries under the compiler installation lib/ folder are resolved separately and do not need to be listed here.
- Default:
[] - Example:
"include": ["stdlib", "lib"]
3.5 outDir (optional)
Default directory for compiler outputs.
- Default:
"out" - Example:
"outDir": "out"
3.6 target (optional)
Default backend target.
- Example:
"target": "linux-x64"
If omitted, the CLI may choose a reasonable default based on the host OS.
3.7 dependencies / devDependencies (optional)
A map of dependency names to a version constraint (string) or an object. devDependencies has the same shape but is only needed to build and test the project — never part of a published library's surface.
- String value — a registry SemVer constraint (e.g.
"^1.2.0","*"). Registry dependencies are resolved byashes restoreinto a lock file and cache once a registry is configured. - Object value — carries a source plus optional fields:
{ "path": "../local-lib" }— a path dependency, resolved live from disk: the dependency's source roots are added to the build and its modules are imported under its namespace."namespace": "..."— overrides the dependency's default namespace.
Example:
"dependencies": {
"json": "^1.2.0",
"local-lib": { "path": "../local-lib" }
},
"devDependencies": {
"test-helper": { "path": "../test-helper" }
}Namespace discipline. A dependency is imported under its namespace — its namespace field, else the PascalCase of its name (json-parser → JsonParser). Every module a dependency exports must live under that namespace directory (its own entry file excepted), and no two dependencies may claim the same namespace; violations are ASH028 / ASH029 (see the diagnostics reference).
3.8 defaults (optional)
A future-facing object for CLI defaults. In v0.x this is allowed but not required to be used.
Example:
"defaults": {
"optimize": true
}4. Imports and Module Resolution
4.1 Import syntax
Imports are written in source as:
import Fooimport Foo.Bar
4.2 Module name to relative path mapping
A module name maps to a relative .ash file path:
Foo→Foo.ashFoo.Bar→Foo/Bar.ash
Path separators are normalized for the host platform.
4.3 Search order
To resolve an import, the compiler searches in two stages:
- Project-local roots: every directory in
sourceRoots, then every directory ininclude - Compiler-shipped libraries in the compiler installation
lib/folder
For each root directory, the compiler checks:
<root>/<modulePath>.ash
Project-local resolution must produce exactly one match. If a module exists in multiple project roots, compilation fails with an ambiguity error. If no project-local module exists, the compiler checks the shipped lib/ folder.
4.4 Ambiguity
If a module exists in multiple project roots, compilation fails.
For non-reserved modules, shipped libraries are only considered after project-local resolution fails to find any match. Reserved Ashes.* standard-library modules are compiler-provided and are not overridable by project-local modules.
A module path may also be satisfied by an inline module — a module Name = ... declaration inside a file, which is lifted to the submodule <File>.<Name> and is addressable across files exactly like a separate <File>/<Name>.ash (see Language Reference §13.1). A path that is satisfied by both an inline module and a file (e.g. module Vec inside Geom.ash and a file Geom/Vec.ash) is a compile-time ambiguity error (ASH022). This keeps inline ↔ file promotion transparent: moving an inline module out to its own file leaves every import and call site unchanged, provided no file already occupies the path.
4.5 Cycles
Import cycles are not allowed.
- The compiler must detect cycles and report a clear diagnostic, showing the cycle chain.
4.6 Exported values and imported names
Project modules are single-value modules.
- A module exports the final value produced by its body.
- When a module body has the shape
let name = expr in nameorlet recursive name = expr in name,import Modulealso bringsnameinto local scope for the importing module. - Qualified access
Module.nameresolves to that exported name. - For multi-segment imports such as
import Foo.Bar, short qualificationBar.namealso resolves whenBaris the unique imported leaf module qualifier. - If two imported modules would introduce the same unqualified exported name, compilation fails with an import-name collision diagnostic.
- If two imported modules would use the same short leaf qualifier, short qualification is ambiguous and compilation must require full qualification.
4.7 Reserved module names
Ashesis reserved for compiler-provided standard-library modules.- User projects cannot define or import a user module named
Ashes. - Compiler-shipped libraries also cannot define a top-level module named
Ashes;Ashes.*remains reserved for compiler-provided standard-library modules.
4.8 Qualified names
- Qualified identifiers use
Module.namesyntax. - Multi-segment module paths such as
Foo.Bar.valueare supported. - If
Foo.Baris imported andBaris unique among imported leaf qualifiers,Bar.valueis also valid. - Referring to a module that is not imported is a compile-time error.
- Referring to a name that the imported module does not export is a compile-time error.
5. Compilation unit and ordering
5.1 Unit of compilation
In project mode, the compilation unit is:
- the transitive closure of the entry file + all imported modules
5.2 Ordering
Source files must be processed in a deterministic order that respects imports:
- a module is processed before any module that depends on it
If a cycle exists, compilation fails.
6. CLI project management commands
6.1 ashes init
Creates a new Ashes project in the current directory.
- Creates
ashes.jsonwithname,entry, andsourceRootsfields. - Creates
src/Main.ashwith a hello-world program. - The project name defaults to the current directory name.
- Fails if
ashes.jsonalready exists.
6.2 ashes add <package> [--path <dir>] [--dev]
Adds a dependency to the project manifest.
- Locates
ashes.jsonby walking upward from the current directory. - With
--path <dir>, records{ "path": "<dir>" }(a path dependency); otherwise records the SemVer string"*"(a registry dependency). - With
--dev, writes todevDependenciesinstead ofdependencies. - Fails if no
ashes.jsonis found.
6.3 ashes remove <package>
Removes a dependency from the project manifest.
- Locates
ashes.jsonby walking upward from the current directory. - Removes the package from
dependenciesordevDependencies; an emptied map is omitted. - Fails if no
ashes.jsonis found or the package is not a dependency.
6.4 ashes restore
Materializes and validates dependencies.
- Locates
ashes.jsonby walking upward from the current directory. - Resolves and validates path dependencies — a missing path or non-project fails with
ASH030/ASH031— and lists each with its namespace. - Resolves registry dependencies against the configured registry: it selects the highest version satisfying all SemVer constraints across the transitive graph, downloads and content-verifies each package into the shared cache, and writes the resolved graph to
ashes.lock.--frozenfails if resolution would change the lock (ASH033);--offlinetrusts the lock and only verifies the cache. ashes build/run/testauto-restore first when the lock is missing or stale, so runningrestoreexplicitly is rarely necessary.ashes installis retired; userestore(oraddto add a dependency).
7. CLI behavior in project mode
7.1 ashes run
- Compiles the project entry and all imports
- Runs the produced executable
- Passes command-line args after
--to the executable
Example:
ashes run -- hello world7.2 ashes compile
- Compiles the project entry and all imports
- Writes output to:
<outDir>/<projectName or entryName>by default, or-o <path>if specified
7.3 ashes test
- Uses existing
.ashtest runner behavior - Tests may still run outside project mode; v0.x does not require tests to use
ashes.json
8. Versioning
v0.x:
- The project schema is intentionally minimal.
- Unknown fields are ignored.
- Future versions may add:
- dependencies/packages
- multiple entry points
- per-target settings
- optimization flags
No explicit version field is required in v0.x.
