# Evaluation order

> main is an ordinary identifier, not a reserved keyword. At the top level it has this entrypoint role. The type checker requires one Harness parameter named harness or _harness...

Website: https://harnlang.com/spec/language/06-evaluation-order.html

This page documents Harn, which is pre-1.0. Language, standard library, and CLI APIs may change. If the intended version is unclear, clarify before using this page.

---

<!-- Generated from spec/chapters/*.md by scripts/sync_language_spec.harn -->

## Evaluation order

### Program entry

1. All top-level nodes are scanned. Pipeline declarations are registered by
   name. Import declarations are processed (loaded and evaluated).
2. If the file declares a pipeline, the pipeline named `"default"` is selected.
   If there is no `"default"` pipeline, the first pipeline is selected.
3. The selected pipeline's body is executed. A file with any pipeline does not
   auto-call a top-level function named `main`.
4. If the file declares no pipeline, its top-level statements execute in source
   order as an implicit entry point (script mode).
5. After those statements, a top-level `fn main(harness: Harness)` is called
   automatically with the runtime's root `Harness` value when present.

`main` is an ordinary identifier, not a reserved keyword. At the top level it
has this entrypoint role. The type checker requires one `Harness` parameter
named `harness` or `_harness`, with no default or rest marker.

### Pipeline parameters

If the pipeline parameter list includes `task`, it is bound to `context.task`.
If it includes `project`, it is bound to `context.projectRoot`.
A `context` dict is always injected with keys `task`, `project_root`, and `task_type`.

Pipeline parameters use the same required `name: TypeExpr` annotations as
function parameters. The type checker uses those types in the pipeline body and
at local or imported call sites:

```harn
pub pipeline deploy(config: DeployConfig, dry_run: bool) -> bool {
  return !dry_run
}
```

An unannotated pipeline parameter is `HARN-TYP-028` under plain `harn check`.
Pipeline default values and rest parameters are rejected because pipeline
invocation does not define those runtime semantics.

### Pipeline return type

Pipelines may declare a return type with the same `-> TypeExpr` syntax
as functions:

```harn
pipeline ghost_text(
  harness: Harness,
  task: dict,
) -> {text: string, code: int} {
  return {text: "hello", code: 0}
}
```

The type checker verifies every `return <expr>` statement against the
declared type. Mismatches are reported as `return type doesn't match`
errors.

A declared return type is the typed contract that a host or bridge
(ACP, A2A) can rely on when consuming the pipeline's output.

Public pipelines (`pub pipeline`) without an explicit return type emit the
`pipeline-return-type` lint warning by default. Return types remain inferable,
so this is a public-contract lint rather than a checker error.

### Pipeline inheritance

`pipeline child(x) extends parent { ... }`:

- If the child body contains `override` declarations, the resolved body is the parent's
  body plus any non-override statements from the child.
  Override declarations are available for lookup by name.
- If the child body contains no `override` declarations, the child body entirely replaces the parent body.

### Statement execution

Statements execute sequentially. The last expression value in a block is the block's result,
though this is mostly relevant for closures and parallel bodies.

### Import resolution

`import "path"` resolves in this order:

1. If path starts with `std/`, loads embedded stdlib module (e.g. `std/text`)
2. Relative to current file's directory; auto-adds `.harn` extension
3. `<current-generation>/packages/<path>` in the leased package snapshot
   published by the nearest ancestor package root (the search walks upward
   and stops at a `.git` boundary).
4. Package manifest `[exports]` mappings under that snapshot's
   `packages/<package>/harn.toml`
5. Package directories with `lib.harn` entry point

Package manifests can publish stable module entry points without forcing
consumers to import the on-disk file layout directly:

```toml
[exports]
capabilities = "runtime/capabilities.harn"
providers = "runtime/providers.harn"
```

With the example above, `import "acme/capabilities"` resolves to the
declared file inside the installed `acme` package.

#### Export visibility

A module's **export surface** — the set of names other modules can import,
whether by wildcard (`import "m"`) or selectively (`import { x } from "m"`) —
is exactly the declarations it marks `pub`, plus any `pub import` re-exports.
`pub` may prefix any top-level declaration: `fn`, `tool`, `skill`, `eval_pack`,
`struct`, `enum`, `type`, `pipeline`, and — for shared configuration and prompt
constants — top-level `const` and `let` value bindings. Non-`pub` declarations
are private to the module: usable by the module's own functions, but not
importable by name or by wildcard. A module that marks nothing `pub` exports
nothing.

A `pub const` / `pub let` is exported **by value**: the binding's value is
computed once when the module is instantiated, then bound into each importer.
Later mutation of a `pub let` in its defining module is not observed by
importers (cross-module values are by-value, like every other imported value).

This is the explicit-visibility model of Rust, Go, and TypeScript. Harn does
**not** have a "public-by-default until the first `pub`" rule: such a rule
makes adding the first `pub` a silent breaking change, because it would flip
every *other* function from importable to private. Requiring `pub` up front
keeps a module's export surface stable as it grows.

The same rule applies to both import forms — a selective import cannot reach a
private function that a wildcard import would not see. Importing a non-`pub`
name is an error (`HARN-IMP-002`) at `harn check` time and at load time; the
message points at the import and suggests marking the symbol `pub`.

Public struct and enum declarations use this same export contract at runtime.
Importing a public struct binds its constructor; importing a public enum binds
its variant namespace, whose members construct zero-field or field-bearing
variants. A private type used as an enum payload does not need to be exported.
Type-only declarations (interfaces and type aliases) remain valid static
imports, with schema-capable public aliases also available in schema expression
positions. The module graph and VM must derive these projections from the same
declaration-kind table so a checker-approved import cannot fail only when the
module executes.

**Testing private functions.** A non-`pub` function is visible to any
`pipeline` or `fn` declared in the **same file**, so co-locate unit tests with
the code under test (the Rust/Go white-box pattern) rather than importing the
private name into a separate test module.

Selective imports: `import { name1, name2 } from "module"` imports only
the specified functions, each of which must be part of the module's export
surface (see above).

Scoped selective imports are shorthand for slash-delimited module paths:
`import std::personas::prelude::{verify_then_act}` is equivalent to
`import { verify_then_act } from "std/personas/prelude"`.

Public re-exports: prefixing any `import` with `pub` re-exports the
imported symbols as part of the importing module's public surface, so
downstream importers see them as if they were declared there directly:

- `pub import "module"` — re-export every name the target module
  exports. Equivalent to wildcard re-export.
- `pub import { name } from "module"` — re-export only the listed
  names. Other names from the source module remain private to the
  importing module.

Re-exports compose: a facade module that `pub import`s from another
facade transitively forwards every reachable name. Two re-exports of
the same name from different sources — or a re-export that shadows a
local `pub` declaration — are reported by `harn check` as a re-export
conflict naming every contributing module.

A re-exported callable keeps its complete parameter and return-type
contract. Callers do not need separate imports for aliases used only by
that signature. Those aliases remain type-checking dependencies; they do
not become public names unless the module also exports them.

Imported pipelines are registered for later invocation.
Non-pipeline top-level statements (fn declarations, let bindings) are executed immediately.

Import cycles: modules may import each other (directly or transitively).
A plain `import "m"` or selective `import { name } from "m"` that resolves
to a module still mid-load is **bound late** — once every module in the
cycle finishes loading, the name resolves for both bare references and
calls, regardless of the order the modules happened to load in. A
`pub import` re-export across a cycle is **not** supported: re-exporting
must publish the name into the importing module's public surface
immediately, but that surface does not exist yet while the cycle is
loading, so it is a load error that names the cycle. Use a plain `import`
inside the cycle and re-export from a module outside it.

### Static cross-module resolution

`harn check`, `harn run`, `harn bench`, and the LSP build a **module graph**
from the entry file that transitively loads every `import`-reachable
`.harn` module. The graph drives:

- **Typechecker**: when every import in a file resolves, call targets
  that are not builtins, not local declarations, not struct constructors,
  not callable variables, and not introduced by an import produce a
  `call target ... is not defined or imported` **error** (not a lint
  warning). This catches typos and stale imports before the VM loads.
- **Linter**: wildcard imports are resolved via the same graph; the
  `undefined-function` rule can now check against the actual exported
  name set of imported modules rather than silently disabling itself.
- **LSP go-to-definition**: cross-file navigation walks the graph's
  `definition_of` lookup, so any reachable symbol (through any number of
  transitive imports) can be jumped to.

Resolution conservatively **degrades to the pre-v0.7.12 behavior** when
any import in the file is unresolved (missing file, parse error,
non-existent package directory), so a single broken import does not
avalanche into a sea of false-positive undefined-name errors. The
unresolved import itself still surfaces via the runtime loader.

---

## Read next

- [Destructuring patterns](https://harnlang.com/spec/language/05-destructuring-patterns.md)
- [Runtime values](https://harnlang.com/spec/language/07-runtime-values.md)
