Skip to content

Projects and hoopoe.toml ​

A Hoopoe project is identified by a hoopoe.toml manifest. Tools discover the nearest manifest by searching the starting directory and then each ancestor. The directory containing the manifest is the project root.

Creating a project ​

Create a binary package at a new destination with:

sh
hoo new hello-world

The destination basename becomes the package name. Names must start with a lowercase ASCII letter and may contain only lowercase ASCII letters, digits, and hyphens. Missing parent directories are created. An existing destination is accepted only when it is an empty directory; files and nonempty directories are refused without modification.

The generated binary tree and exact initial source are:

text
hello-world/
├── hoopoe.toml
└── src/
    └── main.hoo
toml
[package]
name = "hello-world"
version = "0.1.0"
hoo
func main(): void = {}

Pass --lib to generate src/lib.hoo instead:

sh
hoo new hello-lib --lib
text
hello-lib/
├── hoopoe.toml
└── src/
    └── lib.hoo
hoo
public func hello(): string = "Hello, world!"

Git is initialized by default, but no initial commit is created. Use --no-git to skip repository initialization. Project creation is noninteractive and staged before publication, so a missing or failing Git executable and other initialization errors do not leave a partial destination.

The generated binary can be checked from its root with hoo check. Until library-target metadata is part of the manifest schema, check a generated library explicitly with hoo check src/lib.hoo.

Discovery has three outcomes: a valid manifest selects project mode; finding no manifest in the search chain permits loose-file mode; and finding a manifest that cannot be read or validated is a fatal project error. A broken nearby manifest is authoritative and must be fixed (or the source moved out of its search chain); tools never ignore it and retry the source as a loose file.

Pass the global --manifest <PATH> option to select a manifest explicitly. The selected path is authoritative: tools read exactly that file, do not discover hoopoe.toml or fall back to loose-file mode, and report any read or parse error for that path. There is no --config alias or environment-based project configuration.

Every manifest requires a [package] table:

toml
[package]
name = "hello"
version = "0.1.0"
# src = "src" # optional; defaults to src

[dependencies] # optional; defaults to empty
utilities = "^1.0"

[build] # optional
entry = "main.hoo" # optional; defaults to main.hoo

package.src is relative to the project root and defines the source root. build.entry is a contained .hoo path relative to that source root; it may not be absolute or escape with ... Source files below the source root map to canonical compiler module paths by removing .hoo and joining components with / (for example, src/network/http.hoo maps to network/http).

Selecting a command target ​

run, build, and check use the same target-selection rules:

Project found?File argument?Selected target
YesOmittedThe manifest's build.entry, relative to package.src
YesExplicitThat file, which must be below the project's source root
NoExplicitThat loose .hoo file
NoOmittedError: pass a .hoo file or run inside a project

The manifest entry is an executable entry module for build and check; other explicit project files and loose files are libraries. run always requires the selected target to declare a valid main. Entry selection never depends on whether a file happens to be named main.hoo.

check resolves the same complete import graph and uses the same embedded ambient core and std/… sources as build and run. It stops after parsing, binding, and semantic checking: it emits no JavaScript, creates no .mjs artifact, and executes neither the selected module nor Node.

Inspecting fully expanded source ​

hoo expand <module-path> prints one project's fully expanded runtime module as formatted Hoopoe. The module path is relative to package.src, uses / between components, and omits both the .hoo suffix and import prefixes. For example:

sh
hoo expand main
hoo expand network/http
hoo --manifest ../app/hoopoe.toml expand generated/routes

Forms such as @/network/http, ./network/http, network/http.hoo, absolute paths, and paths containing .. are rejected. Unlike check, build, and run, expand is project-only and always requires an explicit module path; it does not accept a loose source file or default to build.entry.

The output is the final project expansion fixed point. It includes generated imports and declarations, applies direct expansions in every grammar destination and attached macros, and omits const-only declarations and macro markers. The output goes to stdout only after the complete project passes expansion, parsing, and analysis. Failures produce the normal source diagnostics on stderr, return a nonzero status, and print no partial source.

Executable roots and the Node launcher ​

After aliases are normalized, main must have no parameters and exactly one of these result shapes:

text
void
Option<void>
Result<void, E>
Task<void>
Task<Option<void>>
Task<Result<void, E>>

E must implement Display. The compiler chooses the adapter from the resolved static type; the runtime never guesses by inspecting a value. hoo build produces an inert importable ES module. Only hoo run (and future explicitly runnable Node artifacts) adds the Node launcher.

The launcher writes no successful root value. void, Some(void), and Ok(void) exit 0; None writes error: main returned None and exits 1; Error(error) writes error: followed by Display(error) and one final newline, then exits 1. Embedded newlines are preserved. Synchronous and task-shaped roots have identical terminal behavior after the root task context joins.

Cancellation writes error: execution cancelled: unsourced or SIGINT cancellation exits 130 and SIGTERM cancellation exits 143. A defect uses the runtime-owned renderer and exits 101; it never invokes user Display or Debug. If displaying an application error defects, that is a defect. The first termination signal requests cooperative cancellation and cleanup; a second may force exit.

echo and intentional output ​

echo expression is a compiler observation expression. It evaluates its operand exactly once, returns that identical value, preserves the operand's type and effects, and adds no !Io.

hoo
struct Credential(public user: string, private token: string)
func inspect(value: Credential): Credential = echo value

Development observations are atomic stderr lines containing source location and a recursive debug render. Rendering is visibility-independent: private and internal fields are included. echo never dispatches Debug, calls host getters, or traverses opaque externals, functions, or managed resources. Visibility controls source access, not secrecy from development output.

Release builds retain operand evaluation and its effects but erase the observer and all source-site metadata. The root package's echo-in-release lint is configurable as allow, warn, or deny; dependencies own their policy when built as roots. Use effectful print/println or telemetry for intentional program output.

From a project directory, all three commands select build.entry:

sh
hoo check
hoo build
hoo run

An explicit project file or a standalone loose file remains supported:

sh
hoo check src/network/http.hoo
hoo build scratch.hoo
hoo run script.hoo

Because manifest fields are based on the selected manifest's directory, an explicit manifest works from anywhere. As a global option, it may appear before or after the subcommand:

sh
hoo --manifest ../hello/hoopoe.toml check
hoo build --manifest ../hello/hoopoe.toml
hoo run --manifest ../hello/hoopoe.toml

An explicit source argument is still resolved within the selected manifest's package.src; a source outside that root is rejected rather than causing discovery of another project.

Interactive evaluation ​

hoo repl starts a persistent read-eval-print loop. It discovers the nearest hoopoe.toml from the current directory and resolves @/… imports from that project's package.src. --manifest <PATH> selects exactly that manifest and never falls back. Only the absence of a discovered manifest starts a loose session; loose sessions still include ambient core and embedded std/… modules.

Declarations, imports, and their evaluated values remain available to later submissions. A newer declaration may shadow a name without changing the meaning of older declarations that captured the previous binding. Each submission is staged through the normal project compiler and committed only after its generated program executes successfully, so syntax, type, compiler, and runtime failures preserve the last good session state.

In a terminal the primary prompt is > and incomplete syntax uses ... . Continuation is determined by lexer/parser end-of-input state, so multiline blocks, string interpolation, and block comments do not require an extra blank line. Ctrl-D (EOF) exits cleanly. Values are rendered with Hoopoe's Debug semantics, including user implementations, rather than JavaScript object inspection.

Redirected input prints neither the banner nor prompts. This makes transcripts deterministic and scriptable:

sh
printf 'let x = 40\nx + 2\n' | hoo repl
# 42

Formatting sources ​

hoo format [FILES...] rewrites explicitly named .hoo files in normalized, deterministic order. Duplicate paths are processed once. With no files it discovers the authoritative project and formats every .hoo file below package.src, without following symlinks or entering generated/dependency directories. Use --manifest <PATH> to select a project authoritatively; explicit files outside that project's source root are rejected.

Formatting writes atomically and leaves already formatted files untouched. hoo format --check [FILES...] performs the same selection without writing. Exit status 0 means success (and, in check mode, no changes), 1 means check mode found files that would change, and 2 means selection, I/O, or syntax errors. Errors do not prevent other selected files from being checked or formatted.

Dependency declarations may use a version string or a table with version, path, or git. Dependency fetching and resolution are not yet implemented.