Skip to content

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.json starting from the current working directory and walking up parent directories.
  • The first ashes.json found is used.
  • If none is found, the CLI behaves as "single-file mode" (existing behavior) and requires an explicit .ash file 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. A directory may contain multiple named project manifests, for example ashes.json and ashes-test.json; commands act only on the selected manifest.

Each project manifest has its own lock file beside it. The final .json extension is replaced with .lock, so ashes.json uses ashes.lock and ashes-test.json uses ashes-test.lock. This keeps colocated projects' resolved dependency graphs independent.


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 .ash file.
  • 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 / overrides (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 by ashes restore into 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:

json
"dependencies": {
  "json":      "^1.2.0",
  "local-lib": { "path": "../local-lib" }
},
"devDependencies": {
  "test-helper": { "path": "../test-helper" }
}

Runtime path dependencies are useful for applications that are never published, but they are not a portable package contract. ashes publish rejects an object-valued entry in dependencies; published libraries must name every runtime dependency with a registry version constraint. Path dependencies in devDependencies remain local-only and are allowed.

Use a root-level overrides map to develop against a local checkout while preserving the registry contract and lock graph:

json
"dependencies": {
  "B": "^1.2.0"
},
"overrides": {
  "B": { "path": "../B" }
}

An override may target a direct or transitive registry dependency. It replaces that package's cached sources for the current root project only; overrides declared by dependencies are ignored. The local project must declare the same namespace and the exact version selected for that package in ashes.lock. This makes the lock portable and prevents a local checkout from silently changing the resolved package identity. ashes publish removes overrides and devDependencies from the archived manifest.

Namespace discipline. A dependency is imported under its namespace — its namespace field, else the PascalCase of its name (json-parser → JsonParser). Dots in an explicitly written dependency key are preserved as module separators (AshesCompiler.Frontend stays AshesCompiler.Frontend), allowing a registry dependency to name a package with an explicit dotted namespace. 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:

json
"defaults": {
  "optimize": true
}

4. Imports and Module Resolution ​

4.1 Import syntax ​

Imports are written in source as:

  • import Foo
  • import Foo.Bar

4.2 Module name to relative path mapping ​

A module name maps to a relative .ash file path. A dotted module name may live either in a nested directory or in a dotted file name whose trailing segments stay in the file name — the same convention the shipped standard library uses (lib/Ashes/IO.Path.ash is Ashes.IO.Path):

  • Foo → Foo.ash
  • Foo.Bar → Foo/Bar.ash or Foo.Bar.ash
  • Foo.Bar.Baz → Foo/Bar/Baz.ash, Foo/Bar.Baz.ash, or Foo.Bar.Baz.ash

Directory names never contain dots; only the file name may carry the trailing dotted segments. Path separators are normalized for the host platform.

4.3 Search order ​

To resolve an import, the compiler searches in two stages:

  1. Project-local roots: every directory in sourceRoots, then every directory in include
  2. Compiler-shipped libraries in the compiler installation lib/ folder

For each root directory, the compiler checks every candidate from the mapping above (the fully nested form and each dotted-file-name form).

Project-local resolution must produce exactly one match. If a module exists in multiple project roots — or in more than one candidate form — 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 export their top-level values and types. By default this is the compatibility export-all interface; a leading export (...) declaration restricts the public surface and may expose a type abstractly, with selected constructors, or with every constructor. It also controls which direct inline modules are public. See Language Reference for the grammar and complete rules.

  • import Module brings the module's exported names into local scope.
  • Qualified access Module.name resolves only exported names.
  • Hidden values and constructors remain available inside their declaring module.
  • For multi-segment imports such as import Foo.Bar, short qualification Bar.name also resolves when Bar is 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 ​

  • Ashes is 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.name syntax.
  • Multi-segment module paths such as Foo.Bar.value are supported.
  • If Foo.Bar is imported and Bar is unique among imported leaf qualifiers, Bar.value is 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.json with name, entry, and sourceRoots fields.
  • Creates src/Main.ash with a hello-world program.
  • The project name defaults to the current directory name.
  • Fails if ashes.json already exists.

6.2 ashes add <package> [--project <manifest>] [--path <dir>] [--dev] ​

Adds a dependency to the project manifest.

  • Uses the manifest selected by --project, or locates ashes.json by 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 to devDependencies instead of dependencies.
  • Fails if no ashes.json is found.

6.3 ashes remove <package> [--project <manifest>] ​

Removes a dependency from the project manifest.

  • Uses the manifest selected by --project, or locates ashes.json by walking upward from the current directory.
  • Removes the package from dependencies or devDependencies; an emptied map is omitted.
  • Fails if no ashes.json is found or the package is not a dependency.

6.4 ashes restore [--project <manifest>] ​

Materializes and validates dependencies.

  • Uses the manifest selected by --project, or locates ashes.json by 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 the selected manifest's lock file. --frozen fails if resolution would change that lock (ASH033); --offline trusts it and only verifies the cache.
  • Applies root-level path overrides after registry resolution. Each local package must match the selected lock entry's namespace and version; the registry-only lock graph is unchanged.
  • ashes build / run / test auto-restore first when the lock is missing or stale, so running restore explicitly is rarely necessary.

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:

sh
ashes run -- hello world

7.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 .ash test 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.