# Define the CLI and build behavior Type: grilling Status: resolved Blocked by: 10, 11, 12 Parent: [Find the ASPL MVP specification](../map.md) ## Question What exact inputs, outputs, flags, exit codes, artifact naming rules, dependency lookup behavior, and diagnostic-language controls should `aspl run`, `aspl build`, and `aspl check` expose as the complete MVP command-line contract? ## Answer The MVP exposes one strict, one-source-file CLI. `check` runs the complete compiler and bytecode validator without writing an artifact. `run` executes the validated program in-process with source-mapped diagnostics. `build` publishes one self-contained native program by atomically replacing its destination. No command exposes standalone bytecode, accepts several source files, starts another build tool, or supplies arguments to ASPL `main`. Arabic is the default language for all CLI-owned diagnostics and help. English is available only through an explicit option. The process locale, source vocabulary, identifier script, environment, and terminal direction never choose a language. ### Command grammar The accepted command forms are: ~~~text aspl [COMMON_OPTIONS] check [--] SOURCE aspl [COMMON_OPTIONS] run [--] SOURCE aspl [COMMON_OPTIONS] build [-o OUTPUT | --output=OUTPUT] [--] SOURCE ~~~ `COMMON_OPTIONS` contains exactly: ~~~text --diagnostic-language=ar|en --color=auto|always|never ~~~ Options and values are case-sensitive. Each option may appear at most once. The parser does not accept abbreviations, combined short options, a separate value for either common long option, `--output OUTPUT`, or options after the source operand. Common options appear before the command. The build output option appears after `build` and before the optional `--`. `-o` consumes its next argument even when that argument starts with `-`. `--` permits a source path that starts with `-`. The successful informational forms are: ~~~text aspl [COMMON_OPTIONS] -h aspl [COMMON_OPTIONS] --help aspl [COMMON_OPTIONS] -V aspl [COMMON_OPTIONS] --version aspl [COMMON_OPTIONS] check -h|--help aspl [COMMON_OPTIONS] run -h|--help aspl [COMMON_OPTIONS] build -h|--help ~~~ Top-level `-V` and `--version` are not valid after a command. Help or version must be the only action after any preceding common options. Extra operands and incompatible options remain usage failures rather than being ignored. There is no `help` command or command alias. Invoking `aspl` without an argument is a usage failure and prints the top-level synopsis to stderr. Help follows the selected diagnostic language and never contains color. Each help screen states its exact accepted synopsis, command purpose, defaults, exit-status table, and every option valid at that level. It also states that `run` executes in-process, `build` replaces its destination atomically, `check` and `build` do not load program libraries, and diagnostics default to Arabic. Prose and layout may improve without creating a compatibility change. Version output is the language-independent line `aspl `. Successful help and version output goes to stdout. A failure to write CLI-owned output is a host failure. ### Source input Every workflow requires exactly one nonempty filesystem operand whose final bytes end in the case-sensitive suffix `.aspl`. Standard input is not a source input, so `-` is rejected. Source path bytes are not translated or resolved for display. Diagnostics use the exact operand as their label and safely escape bytes that cannot be printed. The CLI follows source symlinks, opens the source once, and requires the resulting object to be a regular file. Directories, devices, FIFOs, and sockets are host failures. It rejects a reported size above the source limit before reading and also stops if the bytes actually read cross that limit. A file that changes concurrently may yield the complete bytes read through the opened descriptor or a host failure. The compiler never receives a partial buffer. Argument parsing and the `.aspl` suffix check finish before any filesystem access. The CLI does not call `realpath`. Relative paths and symlink spellings remain visible as supplied. ### Workflow contracts All three commands use the native Linux HostPolicy. Foreign declarations are valid inputs under that policy. `aspl check` performs these steps: 1. Parse the command line. 2. Open and read the source. 3. Run every compiler stage, including lowering, canonical serialization, and the canonical bytecode validator. 4. Render source errors and warnings. 5. Discard successful in-memory `CompiledBytecode` and return. It does not open a foreign library, execute the program, package an ELF, or write an intermediate file. Fresh bytecode rejected by the validator remains the settled `fresh-bytecode-rejected` internal failure rather than a user load failure. `aspl run` performs these steps: 1. Parse the command line. 2. Open and read the source. 3. Run the same complete compilation and validation path as `check`, retaining its in-memory source map. 4. Render every source diagnostic. 5. If there are no source errors and warning output succeeded, prepare every foreign import in canonical table order. 6. Execute `main` in a fresh VM execution and use the source map for mapped runtime reporting. 7. Render a load failure, trap, limit stop, host failure, or internal failure if execution does not halt normally. The program runs inside the `aspl` process. It receives no arguments and inherits the current directory, environment, stdin, stdout, and stderr. The CLI does not sandbox, capture, reorder, or prefix program output. Stdout belongs to the running program. The safe `اطبع` or `print` builtin writes its exact string bytes there through the host callback fixed by [Choose the MVP builtin environment](19-choose-builtin-environment.md). Unsafe foreign code may terminate, crash, corrupt, or stall the process outside ASPL's guarantees. `aspl build` performs these steps: 1. Parse the command line. 2. Open and read the source. 3. Run the same complete compilation and validation path as `check`. 4. Render every source diagnostic. 5. If there are no source errors and warning output succeeded, package and validate the complete ELF in memory. 6. Inspect the destination for prohibited collisions and directories. 7. Create an exclusive temporary in the destination directory, write the complete bytes, set its final mode, and close it. 8. Atomically rename the temporary over the destination. It never opens a program library or checks whether one is installed. It publishes no partial artifact after a source, packaging, validation, diagnostic-output, or filesystem failure. Warnings never block a workflow or change its process status. The CLI renders warnings as soon as source compilation completes, before foreign preparation, execution, or ELF publication. A later phase failure follows those warnings on stderr. Records keep the settled ordering within their own structured result; records from different phases are not merged and resorted. ### Native artifact name and publication Without `-o`, `build` removes one final lowercase `.aspl` suffix from the source path's last component. `path/to/demo.aspl` therefore produces `path/to/demo`. The remaining final component must be nonempty. A source named `.aspl` can be checked, run, or built with an explicit output, but a default build reports `output-name-required`. An explicit output is used byte for byte. It must be a nonempty pathname other than `-`. The CLI never adds or removes an extension and never creates missing parent directories. Before creating a temporary, the builder rejects an output operand byte-identical to the source operand. If a non-symlink destination already exists, it also rejects a destination whose device and inode match the opened source, including a hard link. This is the usage failure `output-is-source`. An existing destination symlink is not followed and may be replaced as a directory entry. The builder creates a uniquely named temporary in the destination directory with exclusive creation. Its published mode is `0777` filtered through the process umask. It does not preserve an old destination's mode, ownership, or timestamps. After writing the validated in-memory ELF, it sets the final mode, closes the temporary, then renames it over any existing non-directory entry. A destination directory causes host-failure status `8`. The rename gives atomic visibility to readers on the same filesystem. The temporary resides in the destination directory, so publication does not cross filesystems. The CLI does not promise to detect every concurrent destination replacement. Inspection is advisory; at the rename point it replaces the non-directory entry then present or fails if a directory is present. A build that fails before rename leaves the old destination untouched. The CLI makes a best effort to remove a failed temporary. A cleanup failure replaces the original outcome only when no stronger failure is already being reported and a temporary may remain. Any reported temporary path is safely escaped. The MVP does not call `fsync` and promises no destination or directory durability across power loss. For a fixed ASPL release, exact source bytes, and native HostPolicy, artifact contents are byte-identical. The source or destination path, current directory, environment, locale, file metadata, umask, existing destination, and temporary name do not enter the ELF bytes. Umask affects only the published mode. ### Foreign-library lookup and process environment The CLI adds no library-search option, project library directory, name rewriting, or dynamic-loader environment override. `check` and `build` validate declarations and adapters without calling `dlopen` or `dlsym`. `run` and a generated native program pass each serialized library byte string unchanged to `dlopen` using the eager procedure fixed by [Define dynamic C interoperability](09-define-dynamic-c-interoperability.md). A bare name uses the normal glibc loader search. A name containing `/` uses glibc's path rules. Loader configuration and environment such as `LD_LIBRARY_PATH` may affect execution, but never checking, building, or artifact contents. ### Generated native program interface A generated native program ignores `argv[0]` and accepts at most these two arguments: ~~~text --aspl-diagnostic-language=ar|en --aspl-color=auto|always|never ~~~ Each may appear once and only in its full `--name=value` form. Parsing is left to right. Generated programs accept no abbreviations, separate values, `--`, operands, short options, `--help`, or `--version`. Any other argument is a usage failure before native-envelope validation, bytecode loading, foreign-library preparation, or `main`. These names reserve VM controls without claiming an ASPL program-argument interface. With no option, native diagnostics use Arabic and automatic color. A successful `main` returns its exact `i64` status from 0 through 255 as the process status. ### Diagnostic language, color, and streams Source diagnostics, usage failures, load failures, traps, limit stops, host failures, and internal failures go to stderr. Successful `check` and `build` are silent. `run` adds no success text around program output. Language selection begins in Arabic. A successfully parsed Arabic or English option changes the language for everything that follows, including a later argument error. An invalid value does not change the current language. A duplicate valid language option is reported in the language selected by the first. Generated native options follow the same rule. Color selection also changes only after a valid option. `never` emits no ANSI styling. `always` emits the renderer's ANSI styling even when redirected. `auto` emits styling exactly when stderr is a terminal. It does not inspect `TERM`, `NO_COLOR`, locale variables, or a configuration file. Help and version output are never colored. The environment, a project file, source contents, and source metadata provide no hidden CLI options. Paths, identifiers, C symbols, library strings, and host detail follow the escaping and directional-isolation rules from [Define diagnostics and the safety boundary](12-define-diagnostics-and-safety.md). If the CLI cannot render compilation warnings, it stops before opening libraries, executing `main`, or publishing an artifact. If rendering a later failure stops after program side effects, output status `8` replaces the earlier failure status. It does not try to print another diagnostic through the failed stream. An error returned while the safe output builtin writes stdout is `host-failure/host-operation-failed` with operation `write-program-output`; unsafe foreign output remains the program's responsibility. ### Process statuses `check`, `build`, direct execution, and generated programs use this stable failure table: | Status | Outcome | | ---: | --- | | `0` | Successful `check` or `build`, including warnings | | `1` | Source errors | | `2` | Invalid command-line usage | | `3` | Host-policy rejection | | `4` | Bytecode, native-envelope, or foreign-import load failure | | `5` | ASPL trap | | `6` | Configured limit stop | | `7` | Cooperative cancellation | | `8` | Host failure, including filesystem, allocation, CLI-owned output, or builtin program-output failure | | `9` | Internal compiler, packager, or VM failure | A normally completed `run` or generated program returns the exact `main` result from 0 through 255. These normal statuses deliberately overlap failure statuses. When failure rendering succeeds, its stderr diagnostic distinguishes the outcome. Shell status alone is not an ASPL result protocol. The Linux CLI and generated programs install no signal handlers. Normal Unix signal behavior applies. In particular, a builtin write to a closed stdout pipe may terminate the process with `SIGPIPE`; if the environment ignores or blocks that signal and the write returns `EPIPE`, the VM reports the builtin host failure and status 8. A shell commonly reports `128 + signal` after signal termination, but that value is not an ASPL status assignment. Status `7` remains available to a host that invokes the core's cooperative cancellation control; ordinary Ctrl-C does not use it. ### Usage and host-failure identities Argument parsing reports one usage failure, selected left to right, then stops without accessing source or output paths. Usage failures use status `2`, the active language, stderr, the relevant one-line synopsis, and one of these stable reasons: - `missing-command`; - `unknown-command`; - `unknown-option`; - `duplicate-option`; - `missing-option-value`; - `invalid-option-value`; - `option-not-allowed`; - `missing-source`; - `too-many-operands`; - `invalid-source-extension`; - `output-is-source`; or - `output-name-required`. The record carries the offending command, option, value, operand, or path when one exists. These fields are exact and safely rendered rather than translated. Generated native argument parsing uses the applicable option and operand reasons from the same model. CLI, generated-program, filesystem, and output failures use `host-operation-failed` plus one of these stable operation values: - `open-source`; - `inspect-source`; - `read-source`; - `inspect-output`; - `create-output-temporary`; - `write-output-temporary`; - `set-output-mode`; - `close-output-temporary`; - `publish-output`; - `remove-output-temporary`; or - `write-diagnostic`; or - `write-program-output`. The failure carries the safely escaped user-facing path when relevant. Sanitized OS text may appear as unstable host detail but never changes the class, reason, operation, or conformance expectation. Allocation failures continue to use the settled `allocation-failed` host reason. Invalid templates and completed artifacts continue to use their settled internal reasons. ### Fixed native host limits One MiB is 1,048,576 bytes. The CLI and generated native programs publish these fixed MVP ceilings: | Resource | Ceiling | | --- | ---: | | Source bytes | 16 MiB | | Lexical items | 1,048,576 | | Syntax nodes | 1,048,576 | | Syntactic nesting | 256 | | Compiler working memory | 512 MiB | | Bytecode bytes | 64 MiB | | Functions | 65,536 | | Foreign imports | 4,096 | | `i64` constants | 1,048,576 | | `f64` constants | 1,048,576 | | String references | 1,048,576 | | String bytes | 16 MiB | | Bytecode instructions | 4,194,304 | | Registers per function | 65,536 | | Runtime frames | 4,096 | | Runtime value slots | 1,048,576 | | VM-owned execution memory | 256 MiB | The source count includes every byte read, including a leading BOM. Lexical-item and syntax-node limits apply when the compiler attempts to create the next item. The nesting limit applies before entering the next nested construct. Compiler working memory counts all live allocations owned by one compilation. This includes source storage, diagnostics, intermediate representations, emitted bytecode, validator state, and the in-memory source map. VM-owned execution memory includes frames, slots, and temporary adapter storage such as C-string loans. It excludes immutable program storage, loaded shared libraries, allocations owned by foreign code, and C-library bookkeeping the VM cannot measure. If a structural and memory ceiling fail at the same checked operation, the structural reason wins. A system allocator failure below a configured memory ceiling is `allocation-failed`; crossing the configured ceiling is a limit stop. Numeric constant and string-reference ceilings use `constant-count-limit` with the table kind as a typed argument. Other ceilings use the reason already assigned to that resource by the diagnostics decision. Native CLI execution configures no product time or output ceiling. It also sets no executed-instruction ceiling below the VM counter's representational maximum, so ordinary long-running loops do not become CLI failures. Frame, slot, and VM-memory ceilings still bound recursive and allocation-backed VM state. The core retains configurable instruction, time, and output limits for the browser host. ### Conformance contract CLI conformance tests must cover: - every accepted command, help, and version form plus rejection of reordered, repeated, abbreviated, missing, and extra arguments; - Arabic defaults, explicit English, stateful handling of malformed or duplicate language and color options, terminal and redirected color behavior, and absence of locale or environment selection; - exact source suffix handling, dash-prefixed paths through `--`, regular files through symlinks, rejection of other file types, concurrent-size defense, exact display labels, and source limits on both reported and observed size; - proof that all commands run lowering, serialization, and validation while only `run` prepares libraries or executes; - silence on successful `check` and `build`, unmodified program streams during `run`, exact builtin bytes on inherited stdout, warning ordering, diagnostic-write failure before side effects, and diagnostic-write failure after side effects; - default and explicit artifact names, `.aspl` with an explicit output, hard-link and lexical source collisions, replacement rather than traversal of an output symlink, directory rejection, missing parents, and existing non-directory replacement; - a fault at every temporary publication operation, old-destination preservation before rename, best-effort cleanup, final mode under several umasks, atomic visibility, and no durability claim; - byte-identical ELF contents across destination paths, current directories, umasks, file metadata, locales, environments, and repeated builds; - proof that `check` and `build` need no declared library while `run` and generated programs use unchanged library strings and normal glibc lookup; - every generated-program option form and proof that invalid arguments stop before loading or `main`; - every status class, normal `main` values across 0 through 255 including overlaps, interrupted and short builtin writes, partial output before a returned write failure, ordinary signal termination, and closed-pipe behavior with default and ignored `SIGPIPE`; and - exact boundary and one-past-boundary cases for every fixed native resource ceiling, memory accounting, reason selection, and cleanup.