hd-lang

#Modules

Status: language specification draft.

This chapter defines modules, packages, the prelude, program initialization, entry points, and the Wasm boundary.

  1. module.kind.moduleModules organize names by source path.
  2. module.kind.packagePackages organize compilation, dependencies, and Wasm artifacts.
fn demo(text: string) -> List[string]:
    text.trim().split(" ")   # names resolve through modules

#Package Manifest

An hd-lang package has an hd.toml manifest:

[package]
name = "my_app"

[source]
root = "src"

[dependencies]
billing = "github.com/acme/billing@1.2.0"
  1. module.manifest.fileAn hd-lang package has an hd.toml manifest.
  2. module.manifest.source-rootThe default source root is src.
  3. module.manifest.dependencyThe language-level use model assumes that the manifest maps each dependency name to one resolved package.
  4. module.manifest.no-registryThere is no package registry. A dependency is fetched from the version control repository that its host path names.
  5. module.manifest.targetsA package may have a library, executables, or both.
  6. module.manifest.no-featuresA manifest declares no optional features, and source has no conditional compilation.

Why. Source never names where a dependency comes from, so moving to version control hosts changes no use line. One build per package version keeps one interface per version.

See also: Tooling, ABI, And Unsupported Extensions, Command Line for executables, tasks, and package mode.

#Dependency Requirements

A dependency requirement maps a dependency key to a host path and a minimum version:

[dependencies]
json = "github.com/acme/json@2.1.0"
json_v1 = "github.com/acme/json@1.9.0"
lint = "github.com/acme/tools/lint@2.3.0"
billing = "git.example.com/shop/billing.git@0.4.2"
pdf = "github.com/acme/pdf@0.4.1-0.20260912081500-3f2c9e1a7b6d"
  1. module.dep.key-nameEach key of [dependencies] and [dev-dependencies] names one dependency, which source writes as dep.NAME. NAME is the key with each - replaced by _, so the key my-app is dep.my_app.
  2. module.dep.key-name.collisionTwo keys of one manifest whose NAMEs are equal, such as my-app and my_app, are an error. Error: invalid-requirement.
  3. module.dep.requirement-formEach value is a dependency requirement PATH@VERSION, or a path requirement on a local package. A dependency requirement is a host path, @, and a version or pseudo-version without a leading v.
  4. module.dep.path-manifest-onlyA host path appears only in the manifest. Source names a dependency only through its key.
  5. module.dep.identityA resolved package's identity is its host path and its compatibility line.
  6. module.dep.no-self-pathA manifest does not state its own host path. A fetched package's host path is the one that the dependency requirement which fetched it names.
  7. module.dep.two-linesTwo compatibility lines of one host path are two dependencies with two keys, as json and json_v1 above.
  8. module.dep.one-key-per-lineTwo keys of one manifest that name the same host path and compatibility line are an error, since one package would then have two names. Error: invalid-requirement.
  9. module.dep.no-major-suffixA host path carries no major-version suffix such as /v2.

Why. Go puts /v2 in the path because its imports repeat the path. In hd, source says dep.json, so a second key tells two lines apart, and a major upgrade edits one manifest line.

#Host Paths

A host path names a repository, and optionally a package directory inside it:

Host Repository part Example
github.com the host, an owner, and a repository segment github.com/acme/json
any other host the path up to a segment that ends in .git git.example.com/shop/billing.git
  1. module.repo.githubgithub.com is the one known host. On it, the first three segments, the host, an owner, and a repository, name the repository.
  2. module.repo.git-suffixOn any other host, the repository part must end with a segment that ends in .git.
  3. module.repo.no-discoveryThe toolchain never fetches a web page to discover a repository, as Go's go-import meta tag does.
  4. module.repo.subdirectorySegments after the repository part name the directory that holds the package. One repository may hold several packages this way.
  5. module.repo.credentialsA private repository is fetched with git's own credentials, such as its credential helpers and SSH keys.
  6. module.repo.no-stored-credentialsThe toolchain stores no credentials.
[dependencies]
json = "github.com/acme/json@2.3.0"
billing = { path = "../billing" }

#Versions

A package's versions are the git tags of its repository:

Package Tag Version in a dependency requirement
at the repository root v2.3.0 2.3.0
in the subdirectory lint lint/v2.3.0 2.3.0
an untagged commit after v0.4.0 none 0.4.1-0.20260912081500-3f2c9e1a7b6d
  1. module.version.tagA version is a git tag vMAJOR.MINOR.PATCH, with an optional SemVer 2.0.0 pre-release suffix.
  2. module.version.tag-prefixA package in a subdirectory uses tags prefixed with that subdirectory's path and /, such as lint/v2.3.0.
  3. module.version.tag-onlyThe tag is the only version. A manifest does not state its own package's version.
  4. module.version.pseudoA pseudo-version names one untagged commit. Its form depends on the closest earlier tag of the package, as the table below shows. The closest earlier tag is the greatest version tag, with the package's tag prefix, among the commit's ancestors, as Go chooses it.
  5. module.version.pseudo.commitIn a pseudo-version, TIME is the commit's UTC time as yyyymmddhhmmss, and HASH is the first 12 hexadecimal digits of the commit hash.
  6. module.version.orderVersions, pseudo-versions included, are ordered by SemVer 2.0.0 precedence.
  7. module.version.lineThe compatibility line of a version is its major number when the major is at least 1, and 0.MINOR when the major is 0.
  8. module.version.pseudo.releaseA tagged version's manifest may require a pseudo-version.
  9. module.version.no-bare-path-releaseA tagged version's manifest must not hold a path requirement that carries no version. Fetching such a version is an error, and the toolchain does not tag one. Error: invalid-requirement.
  10. module.version.tag-missingA dependency requirement whose version is not a pseudo-version, and whose package has no tag for that version, is an error. So lint = "github.com/acme/tools/lint@2.4.1" is an error when the repository has no tag lint/v2.4.1. Error: unknown-version.
  11. module.version.no-fallbackThe toolchain never falls back to an untagged commit or to a nearby version in place of a missing tag.
  12. module.version.pseudo-missingA pseudo-version whose HASH names no commit of the package's repository, or whose TIME is not that commit's time, is an error. A missing tag is an error too. Error: unknown-version.
  13. module.version.pseudo-missing.no-fallbackThe toolchain never falls back to another commit or version in place of such a pseudo-version.

Pseudo-versions take Go's three forms:

Closest earlier tag Pseudo-version
none 0.0.0-TIME-HASH
a release vX.Y.Z X.Y.(Z+1)-0.TIME-HASH
a pre-release vX.Y.Z-PRE X.Y.Z-PRE.0.TIME-HASH

Note. A pseudo-version is a pre-release, so it orders below the release it precedes, and pseudo-versions of one base order by time.

Why. A requirement names one exact release, so a typo or a deleted tag fails the build rather than silently choosing other code. Go reports the same case as unknown revision, and rejects a pseudo-version that does not match its commit. The error is unknown-version, by cli.dep.unknown-version.

git tag v2.3.0        # version 2.3.0 at the repository root
git tag lint/v2.3.0   # version 2.3.0 of the package in lint/

#Version Selection

Selection is minimal version selection: each dependency requirement is a minimum, and the build uses the largest minimum stated for each package:

Manifest Requires
the root package json@2.1.0, billing@1.4.2
billing 1.4.2 json@2.3.0
selected json 2.3.0, billing 1.4.2
  1. module.select.minimumA dependency requirement states a minimum. Its version, or any later version in the same compatibility line, satisfies it.
  2. module.select.reachSelection starts at the root package, or at every member of a workspace, and reads the manifest of each version that a dependency requirement reaches.
  3. module.select.dev-dependenciesSelection reads the dev dependencies of the root package or workspace members only. A dependency's dev dependencies are never read.
  4. module.select.largestFor each host path and compatibility line, the selected version is the largest minimum that any reached manifest states.
  5. module.select.one-per-lineA package graph therefore holds at most one version per compatibility line. Two lines of one host path may coexist as two packages.
  6. module.select.no-lockThe manifests alone determine the selection. There is no lockfile of versions.
  7. module.select.pathSelection also reads the manifest of each package that a path requirement reaches, from the root package, a member, or another such package. Its dependency requirements join the selection.

Note. A package that a path requirement reaches is a dependency. So its dev dependencies are never read, by module.select.dev-dependencies, unless it is also a member of the workspace.

Why. Selection reads only the manifests of versions someone names, which needs no registry index. A new tag reaches no build until some manifest names it.

[dependencies]
json = "github.com/acme/json@2.1.0"   # a minimum: 2.3.0 may be selected

#Integrity

A committed hd.sum file is what makes a fetched dependency trusted.

  1. module.sum.fileA root package or workspace has an hd.sum file beside its manifest. It records a hash of the source tree of each selected version.
  2. module.sum.committedhd.sum is kept under version control with the manifest.
  3. module.sum.mismatchA fetched tree whose hash differs from its hd.sum entry is an error. It is never only a warning. Error: sum-mismatch.
  4. module.sum.onlyhd.sum is the only integrity source. A build must not require a checksum log, a proxy, or any service besides the repository hosts.
  5. module.sum.pathhd.sum holds no line for a package that a path requirement reaches, neither for its tree nor for its manifest. Its files are local and are never fetched.

Why. hd runs no paid servers. A checksum log or a caching proxy may be added later only if it needs no infrastructure, or reuses free public infrastructure.

hd fetch     # fetch every selected version the cache lacks
cat hd.sum   # one hash per selected version; a mismatch is rejected

#Workspaces

A workspace builds several packages of one repository as one graph.

  1. module.workspace.definitionA workspace is a set of packages that one workspace manifest, an hd.toml at the workspace root, lists as members.
  2. module.workspace.committedThe workspace manifest may be kept under version control, so every checkout builds the same graph.
  3. module.workspace.selectionSelection runs once for the whole workspace, so all members use the same selected versions.
  4. module.workspace.sumA workspace has one hd.sum, beside its workspace manifest.
  5. module.workspace.no-host-pathA member does not require another member by host path.
  6. module.workspace.fetched-memberA fetched package may require a member's host path. Selection then treats that host path as any other and fetches the selected version, which is a package separate from the local member.
  7. module.workspace.fetched-member.twoThe build then holds two packages, the local member and the fetched version. Neither stands in for the other.

Note. A workspace is optional. It shares one selection and one hd.sum among its members; a path requirement works with or without one.

Why. A member states no host path (module.dep.no-self-path), so nothing ties it to a fetched version of the same repository. Cargo likewise treats a path source and a git source of one crate as two packages.

[workspace]
members = ["libs/ui", "libs/shared"]

#Path Requirements

A path requirement names a local package by its directory, in a workspace or outside one:

[dependencies]
billing = { path = "../billing" }
ui = { path = "../ui", version = "0.4.2" }
json = "github.com/acme/json@2.1.0"
  1. module.path-dep.formA path requirement, { path = "DIR" }, names the package in the directory DIR, relative to the requiring manifest's directory.
  2. module.path-dep.any-packageDIR may hold any local package. It need not be a member of the requiring package's workspace, and neither package needs a workspace.
  3. module.path-dep.no-packageA path requirement whose DIR holds no manifest that declares a package is an error. Error: invalid-requirement.
  4. module.path-dep.root-selectionThe selection and hd.sum of the root package, or of its workspace, hold for every package that a path requirement reaches. That package's own hd.sum and workspace are not read.
  5. module.workspace.path-versionA path requirement may also carry a version, as in { path = "../ui", version = "0.4.2" }.
  6. module.workspace.path-version.locallyWhen the requiring package is built from local files, such a requirement names the package at its path. This covers a build in its workspace or in a checkout of its repository. The requirement's version is not used.
  7. module.workspace.path-version.fetched-versionIn a fetched version of the requiring package, path is ignored, and the requirement is a dependency requirement on version.
  8. module.workspace.path-version.fetched-hostIts host path comes from the host path by which the requiring package was fetched, as module.dep.no-self-path gives every fetched package its host path.
  9. module.path-dep.fetched-outsideIn a fetched version, a path requirement whose DIR lies outside the repository is an error, since no host path names that directory. Error: invalid-requirement.

Note. A package whose path requirements all carry a version is released without a manifest edit, by module.version.no-bare-path-release, as Cargo's multiple locations allow. A package with a bare path requirement must name the other package by dependency requirement in its release manifest.

Why. A path requirement is for local development, as Cargo's path dependencies are. Cargo records no checksum for a path dependency, and builds it with the root's lockfile, never its own. A workspace only adds a shared selection and hd.sum.

#Toolchain Version

A manifest states which toolchain versions can build its package.

  1. module.toolchain.minimumA manifest may state a minimum toolchain version, which is also its minimum std version.
  2. module.toolchain.graph-minimumA build whose toolchain is older than the minimum of any package in the selected graph is an error. Error: toolchain-too-old.
  3. module.toolchain.pinOnly a root manifest may pin one exact toolchain version.
  4. module.toolchain.no-editionsThere are no language editions.
[package]
name = "shop"
hd = "0.2.0"   # the minimum toolchain that builds this package

#Path-Inferred Modules

A source file's path relative to the source root determines its module name:

src/user/types.hd    # user.types
src/user/service.hd  # user.service
  1. module.path.nameA source file's path relative to the source root determines its module name.
  2. module.path.no-declarationThere is no module or package declaration in source.

#Directory Modules

A directory is itself a module only when it contains mod.hd:

src/user/mod.hd      # user
src/user/types.hd    # user.types
  1. module.path.directoryA directory is itself a module only when it contains mod.hd.
  2. module.path.mod-filemod.hd is the directory module's source file and public index.
  3. module.path.no-child-importChild modules are not brought automatically into the parent.
  4. module.path.no-parent-scopeParent declarations are not implicitly visible in children.
  5. module.path.no-std-child-importstd follows the same rule. A std module that wants to expose a child module's items re-exports them with pub use. After use std.testing, the path testing.arbitrary.x is an error. Error: unknown-import.
Error example
use std.testing

fn make(x: testing.arbitrary.With) -> i32:  # error: unknown-import
    0

#Root Files

Two files directly under the source root have fixed roles:

File Role
src/lib.hd the package root module, which pkg names
src/main.hd the default executable's entry, its own program
  1. module.path.lib-filelib.hd directly under the source root, src/lib.hd, is the package root module. Its public declarations are what pkg.{X} names, and what a dependent names through its dependency key.
  2. module.path.lib-indexsrc/lib.hd is the root's public index, as a directory's mod.hd is the directory's.
  3. module.path.main-filemain.hd directly under the source root, src/main.hd, is an entry module and its own program, never part of the package's library.
  4. module.path.main-no-useA use of src/main.hd from another module is an error. Error: unknown-module.
  5. module.path.lib-only-rootsrc/lib.hd is the only root file of a package's library; the source root itself is never a directory module.
  6. module.path.no-root-modA mod.hd directly under the source root, src/mod.hd, is an error, and its message suggests renaming it src/lib.hd. Error: reserved-module-name.
  7. module.path.lib-targetA package has a library exactly when it has src/lib.hd.
  8. module.path.executable-onlyA package without src/lib.hd is executable-only: no dependent can use its modules.
  9. module.path.no-lib-dependencyDepending on a package that has no library is an error. Error: invalid-requirement.
  10. module.path.reserved-pkgA module named pkg directly under the source root, as src/pkg.hd or src/pkg/mod.hd, is an error. pkg names the root module. Error: reserved-module-name.
src/lib.hd     # the package root module
src/mod.hd     # invalid: rename it src/lib.hd
src/pkg.hd     # invalid: pkg names the package root module

Note. src/main.hd reaches the library's declarations through pkg or self, as in use self.{Config}.

Why. One root file per role, as in Cargo: src/lib.rs makes the library, and src/main.rs the default binary. pkg is reserved as Rust reserves crate, so a module never shares the root's name.

#Module Identity

  1. module.path.uniqueTwo files must not map to the same module identity. Error: duplicate-module-name.
  2. module.path.case-sourceCase sensitivity of module paths is defined by the source language rather than the host filesystem, by the rules below.
  3. module.path.identifierEvery non-mod.hd path component used in a module identity must be an NFC Unicode identifier under the source identifier rules. Error: invalid-module-path.
  4. module.path.case-sensitiveModule identities are case-sensitive.
  5. module.path.case-collisionA package is rejected if two source paths collide after full Unicode case folding followed by NFC normalization of each module path component. This holds even when the host filesystem could otherwise distinguish them. Error: duplicate-module-name.
  6. module.path.suffixThe final .hd suffix and the special filename mod.hd are lowercase and case-sensitive.
  7. module.path.unicode-versionThe compiler applies the declared Unicode data version from the lexical rules to path validation and collision checks.

Why. Checking paths this way rejects ambiguous module names before host filesystem case behavior can change the module graph.

src/user/types.hd    # user.types
src/User/types.hd    # a different module: identities are case-sensitive

#Test Modules

A file whose name ends in _test.hd is a test module, and the test root holds integration tests:

src/billing.hd         # billing
src/billing_test.hd    # billing_test, a test module
tests/checkout.hd      # an integration test program, with no module path
tests/common/mod.hd    # a shared test module, reached as self.common
  1. module.test.moduleA source file whose name ends in _test.hd is a test module, such as src/billing_test.hd, whose module is billing_test.
  2. module.test.integrationAn integration test module is a module under the package's test root, which is tests by default.
  3. module.test.codeTest code is a package's tests: blocks, test modules, and integration test modules. Only a test build, such as hd test makes, compiles it.
  4. module.test.code.docThe package's doc tests are test code too.
  5. module.test.module.viewA test module is otherwise an ordinary module of its package: it sees public declarations package-wide and may use other test modules.
  6. module.test.module.importSeeing a public declaration means a test module may import it: the declaration is in its scope only through a use, as for any module.
  7. module.test.integration.viewAn integration test module sees the package as a dependent package does: its public declarations, built without its test code.
  8. module.test.integration.programEach file directly under the test root, such as tests/checkout.hd, is an integration test program: its own program, compiled separately from the others.
  9. module.test.integration.sharedA module in a subdirectory of the test root, such as tests/common/mod.hd, is a shared test module. Every integration test program of the package may use it.
  10. module.test.integration.beside-directoryA file directly under the test root beside a directory of the same name, such as tests/common.hd beside tests/common/, is an error. It has no fix-it, and its message names the file's target, tests/common/mod.hd. Error: duplicate-module-name.
  11. module.test.integration.program-useA use of an integration test program from another module is an error. Error: unknown-module.
  12. module.test.integration.shared-copyEach integration test program gets its own copy of the shared test modules it uses, so their top-level statements run once per program.
  13. module.test.integration.shared-unused.lintLint reports an unused shared-test declaration only when no integration test program uses that declaration.
  14. module.test.integration.pkg-rootIn an integration test module, the pkg root names the package's library modules, each with only its public declarations.
  15. module.test.integration.self-sharedAn integration test program uses a shared test module through self, as in use self.common for tests/common/mod.hd.
  16. module.test.no-tests-rootThere is no tests use root: test code reaches the test root only through self and super. A use path that starts with tests is an error. Error: unknown-module.
  17. module.test.integration.no-pathIntegration test programs and shared test modules are not modules of the package: they have no module path, so a library module such as src/tests/checkout.hd never clashes with tests/checkout.hd.
  18. module.test.no-tests-blockA test module or an integration test module must not contain a tests: block. Error: misplaced-tests-block.
  19. module.test.dev-dependencyA dev dependency is a dependency that the manifest declares in [dev-dependencies]. Test code may use it, and a dependent package never sees it.
  20. module.test.non-test-use.test-moduleNon-test code that uses a test module is an error. Error: test-only-use.
  21. module.test.non-test-use.dev-dependencyCode under the source root, other than test code, that uses a dev dependency is an error. Error: test-only-use.
  22. module.test.cyclic-dev-unitA dev dependency that itself depends on the package must not be used from a tests: block or a test module. Error: cyclic-test-dependency.
  23. module.test.cyclic-dev-allowedIntegration test modules and tasks may use such a dev dependency.
  24. module.test.dev-dependency.in-testsA use inside a tests: block may name a dev dependency that does not depend on the package.
# src/lib.hd of package shop, whose dev-dependency fx supplies sample data
fn order_count(lines: List[string]) -> usize:
    lines.len()

tests:
    use dep.fx.{sample_orders}
    use std.testing.assert_equal

    it("counts the sample orders"):
        assert_equal(order_count(sample_orders()), 2, reason="two sample orders")

Why. Each integration test program builds on its own, as each Cargo integration test is its own crate. So helpers go in a subdirectory such as tests/common/. A dev dependency that depends back would give a unit test a second copy of the package, whose types differ from the ones under test. An integration test sees only the one normal build. A test module is already test code throughout, so it holds its test cases at top level rather than in a tests: block.

Note. A task may also use dev dependencies, and the tasks directory shares modules as the test root does, through self.

See also: Test Blocks, Standard Testing.

#Doc Tests

A fenced hd block in a documentation comment is a test of its own, which hd test compiles and runs:

# src/text.hd
## Turns a title into a URL slug: each space becomes a hyphen.
##
## ```hd
## use pkg.text.{slugify}
## use std.testing.assert_equal
##
## assert_equal(slugify("Ship It Now"), "Ship-It-Now", reason="each space becomes a hyphen")
## ```
pub fn slugify(title: string) -> string:
    let slug = ""
    for letter in title.chars():
        if letter == ' ':
            slug = "${slug}-"
        else:
            slug = "${slug}${letter}"
    slug
  1. module.test.doc.blockEach fenced code block whose info string is hd, inside a documentation comment of a module under the source root, is a doc test.
  2. module.test.doc.fenceA fence with any other info string, such as text, is not a doc test, and no build compiles its contents.
  3. module.test.doc.programEach doc test compiles as its own program, separately from the package's other test code, and holds exactly one test case.
  4. module.test.doc.usesA doc test's use declarations must come first in its block, and they are the uses of its program.
  5. module.test.doc.bodyThe rest of the block is the body of that test case, as a trailing block given to it is.
  6. module.test.doc.viewA doc test sees the package as an integration test module does, by module.test.integration.view and module.test.integration.pkg-root.
  7. module.test.doc.relativeA use path in a doc test that starts with self or super is an error. Error: unknown-module.
  8. module.test.doc.not-integrationA doc test is not an integration test module, and a rule for those applies to it only where this section cites the rule.
  9. module.test.doc.requirementsThe runner binds a doc test's requirements by module.testing.integration-row and module.testing.integration-row.unbound, as for an integration test case.
  10. module.test.doc.private-itemsThe documentation comment of a private declaration or member holds doc tests too, and they still see only the package's public declarations.
  11. module.test.doc.compile-failA doc test that holds a line comment # error: CODE is a compile-fail doc test. It never runs, and it passes only when compiling it reports CODE.
  12. module.test.doc.compile-fail.failsA compile-fail doc test whose compilation reports no diagnostic with that code fails, including when it compiles.
  13. module.test.doc.no-attributesA doc test has no other attributes: no hidden lines, no ignore option, and no comparison of its output.

A compile-fail doc test shows a misuse that the compiler rejects:

Error example
# src/slug.hd
## A URL slug. Its text is private, so every slug comes from `make_slug`.
##
## ```hd
## use pkg.slug.{make_slug}
##
## slug := make_slug("Ship It")
## _ := slug.text  # error: private-member
## ```
pub struct Slug:
    text: string

pub fn make_slug(title: string) -> Slug:
    let text = ""
    for letter in title.chars():
        if letter == ' ':
            text = "${text}-"
        else:
            text = "${text}${letter}"
    Slug { text: text }

Note. A doc test is test code, so it may use the package's dev dependencies, by module.test.dev-dependency. It checks output with snapshot, as in snapshot(slugify("Ship It"), expect="Ship-It").

Note. hd_run!(command) takes a std.process.Command and runs only in an integration test module (std-testing.hd-run.integration-only), so a doc test that calls it is a test-only-use error.

Why. A doc test checks an example as a reader would copy it, so it sees only what a dependent sees. Blocks on private items run too, as rustdoc runs them, so internal docs stay true. snapshot already compares output, so a block needs no output attribute.

See also: Test Runs, for how hd test names, filters, and reports doc tests.

#Use Roots

Every absolute use path begins with one of these roots:

Root Names
pkg the current package
std the standard library
dep.<name> a manifest dependency
  1. module.root.absoluteEvery absolute use path begins with one of the roots in the table.
  2. module.root.manifestThe package manifest distinguishes standard library, current package, and external dependency namespaces.
  3. module.root.dep-prefixSource syntax does not require a different prefix for each dependency beyond dep.<name>.
  4. module.root.use-onlyOnly a use declaration may begin a path with a root. Only a use brings a module name into scope.
  5. module.root.use-only.errorSo an absolute path in an expression or a type, such as std.time.s(seconds) with no use std.time, is an error. Error: unknown-name.
Error example
fn timeout(seconds: i64) -> void:
    std.time.s(seconds)  # error: unknown-name

#Relative Uses

Relative use paths use self and super:

# src/user/service.hd
use self.types.{Query}          # src/user/service/types.hd
use super.types.{User, UserId}  # src/user/types.hd

Relative lookup starts at the source file's own module, which self names. A root file starts at its root instead:

Source file self super
src/a.hd a the package root namespace
src/user/service.hd user.service user
src/user/mod.hd the user module itself the package root namespace
src/lib.hd, src/main.hd the package root namespace an error
tests/checkout.hd the test root an error
  1. module.relative.keywordsRelative use paths use self and super.
  2. module.relative.base.currentExcept in a root file, relative lookup starts at the source file's own module, as the table shows. In mod.hd, that module is the directory module.
  3. module.relative.root-fileA root file is src/lib.hd, src/main.hd, or an integration test program. Relative lookup in a root file starts at its root. That is the package root for a file under src, and the test root for one under tests.
  4. module.relative.self.currentself names the module where relative lookup starts, so self.x names its child module x.
  5. module.relative.superEach leading super moves to its parent.
  6. module.relative.root-file.superA super in a root file is an error. Error: unknown-module.
  7. module.relative.example.nestedConsequently, from src/user/service.hd, self.types resolves to pkg.user.service.types, in src/user/service/types.hd, and super.types resolves to pkg.user.types, in src/user/types.hd.
  8. module.relative.example.top-levelFrom src/a.hd, self.x resolves to pkg.a.x, in src/a/x.hd. A sibling src/b.hd is super.b, since super from a file directly under src, other than a root file, is the package root.
  9. module.relative.example.mod-fileFrom src/a/mod.hd, self.x resolves to pkg.a.x, in src/a/x.hd, since self there is the directory module.
  10. module.relative.example.mainFrom src/main.hd or src/lib.hd, self.x resolves to pkg.x, in src/x.hd.
  11. module.relative.above-rootMoving above the package root is a compile-time error. Error: unknown-module.
  12. module.relative.no-crossRelative use paths cannot cross into std or a dependency. A relative path that names one is an error. Error: unknown-module.
  13. module.relative.test-root.currentIn an integration test module, relative lookup works as it does under src, with the test root in place of the package root. From tests/checkout.hd, a root file, self.common.x resolves to tests.common.x, in tests/common/x.hd.
  14. module.relative.above-test-rootIn an integration test module, a super that moves above the test root is an error. Error: unknown-module.
Error example
# tests/checkout.hd
use self.common.{expected}   # valid: tests/common/mod.hd
use super.common.{expected}  # error: unknown-module

Why. self names the current module, as Rust's self does. A file's own child modules then live in the directory of the same name, and a sibling is always super.NAME. A root file, like a crate root, is where its tree starts, so it has no parent.

#Single-File Programs

A single-file program is one source file compiled with no package, such as a script run on its own:

Error example
use std.process.ExitCode  # valid: std is the one root it may use
use self.util.{helper}    # error: unknown-module
  1. module.single-file.definitionA single-file program is one source file compiled with no package. It is its own entry module, and it may use only std.
  2. module.single-file.rootsIn a single-file program, a use path that starts with pkg, dep, self, or super is an error, reported at the use. Error: unknown-module.

Note. The diagnostic says that the file is in no package, rather than that a name is unknown.

See also: Single Files for when hd compiles a file as a single-file program.

#Use Forms

A use names a single public declaration or a module namespace:

use std.format.DebugWriter
use pkg.user.types
use pkg.user.types as user_types

A grouped use names selected public declarations:

use pkg.user.types.{User, UserId}
use dep.billing.types.{UserId as BillingUserId}
  1. module.use.singleA use may name a single public declaration or a module namespace.
  2. module.use.groupedA grouped use names selected public declarations.
  3. module.use.trailing-commaGrouped uses may have a trailing comma.
  4. module.use.no-wildcardWildcard uses are not supported. A wildcard use is an error. Error: syntax-error.
  5. module.use.no-variantEnum variants are members, not module declarations, and cannot be used directly. Error: direct-variant-use.
  6. module.use.private-or-missingUsing a private or missing declaration is a compile-time error.
  7. module.use.missing-nameA use that names a declaration which an existing module of std, pkg, or a dependency does not declare is an error. Error: unknown-import.
  8. module.use.private-nameA use that names a declaration which its module declares without pub is an error. Error: private-import.
  9. module.use.pub-groupedOnly the grouped form accepts a pub prefix. A pub single use is an error. Error: syntax-error.
  10. module.use.facadePublic facades expose selected declarations rather than module namespace aliases.
  11. module.use.whole-moduleUse declarations introduce names for the whole module and are resolved before type checking.
  12. module.use.ambiguousA single use that names both a module and a declaration of its parent module, as use pkg.words, is an error. Error: ambiguous-import.
# in package pkg: src/words.hd beside a root declaration words
use pkg.words  # ambiguous-import: rename the module or the declaration
Error example
use std.testing.*                 # error: syntax-error
use std.text.{nope}               # error: unknown-import
use pkg.models.{Secret}           # error: private-import
use pkg.status.{Status.Queued}    # error: direct-variant-use
pub use std.testing.assert_equal  # error: syntax-error

Note. A qualified name that reaches a declaration through a module name reports these codes too, by expr.name.qualified.private and expr.name.qualified.missing.

#Dependency Cycles

The files of one folder may use each other in a loop, but folders must not depend on each other in a loop:

# src/shop/mod.hd
pub use pkg.shop.cart.{Cart}
pub use pkg.shop.item.{Item}

# src/shop/cart.hd: a loop with mod.hd, inside folder src/shop
use pkg.shop.{Item}

#Folders

A source file's folder is the directory that holds it, or the directory of its child modules:

Source file Module Folder
src/shop/mod.hd shop src/shop
src/shop.hd, when src/shop/ holds its child modules shop src/shop
src/shop/cart.hd shop.cart src/shop
src/shop/orders/order.hd shop.orders.order src/shop/orders
src/shop.hd, with no child modules shop src
src/lib.hd the package root src
  1. module.folder.holderA source file's folder is the directory that holds it, except for a file with child modules.
  2. module.folder.parent-fileA file x.hd whose directory x/ beside it holds source files, its child modules, is in folder x/, as x/mod.hd would be.
  3. module.folder.interchangeableSo x.hd and x/mod.hd are interchangeable: each is in the folder of its children.
  4. module.folder.parent-childA parent module and its child modules share one folder, so a parent and a child that use each other make no folder edge.
  5. module.folder.mod-fileA mod.hd file is in the folder of its directory, like the other files there.
  6. module.folder.nestedNested directories are separate folders: a file in src/shop/orders is not in folder src/shop.
src/shop/mod.hd      # module shop, folder src/shop
src/shop/cart.hd     # module shop.cart, folder src/shop

#Folder Graph

  1. module.cycle.folder-edgeThe folder graph of a package has an edge from folder A to a different folder B. The edge exists when a file in A uses a module in B. A use or pub use uses the module its path reaches. So use pkg.shop.{Item} and use pkg.shop.Item both use shop.
  2. module.cycle.same-packageOnly uses of the package's own modules make edges. Uses of std and of dependencies make none.
  3. module.cycle.test-codeA use in test code makes no edge.
  4. module.cycle.nestedA folder and its parent or child folder are separate nodes, and an edge between them counts like any other.
  5. module.cycle.acyclicThe folder graph must be acyclic. A folder that depends on itself through other folders is an error. Error: folder-cycle.
  6. module.cycle.within-folderUses between files of one folder make no edge, so those files may use each other in any pattern, loops included.
  7. module.cycle.packageThe dependency graph of packages must be acyclic: a package that depends on itself, directly or through other packages, is an error. Error: package-cycle.

A root facade that uses a child folder, beside a shared root file that the child uses, makes a loop of folders:

Error example
# src/lib.hd, in folder src
pub use pkg.shop.{Cart}

# src/error.hd, in folder src
pub enum Error:
    Empty

# src/shop/mod.hd, in folder src/shop
use pkg.error.{Error}  # error: folder-cycle

Why. Every signature that another file sees is written out, so files are checked in parallel whatever their loops. The folder rule bounds the largest set of files that must be compiled together at one folder. A folder then compiles from the signatures of the folders it uses, as a Go package compiles from its imports' export data.

Note. Shared declarations go in a leaf folder. Moving src/error.hd to src/error/mod.hd keeps the module name error, so no use line changes, and breaks the loop above.

#Cycle Diagnostic

  1. module.cycle.diagnostic.loopThe folder-cycle diagnostic must show one shortest loop of folders, with the use declaration that makes each edge.
  2. module.cycle.diagnostic.sizeIt must show the size of the tangle: the number of folders that lie on some loop with the shown ones.
  3. module.cycle.diagnostic.fixIt must offer a fix-it that moves a file x.hd on the loop to x/mod.hd, which keeps its module name.
folder-cycle: src/shop -> src/cart -> src/shop
tangle of 3 folders; move src/cart.hd to src/cart/mod.hd

See also: Use Declarations, Initialization Order.

#Prelude

The prelude is the implicit scope of public standard-library names that every module has:

Origin module Implicit names
std.core never, bool, i8, i16, i32, i64, u8, u16, u32, u64, usize, f32, f64, char, string, void, List, Map, Any, AnyVal, AnyRef, Option, Result, panic
std.format Display, Debug, debug, dbg
std.cmp Eq, PartialOrd, Ord, Ordering
std.hash Hash
std.iter Iterator, Iterable
std.console Console, ConsoleError, println
std.testing it, assert, assert_equal
std.task Suspend
  1. module.prelude.iterator-traitThe prelude name Iterator names the cursor trait std.iter.Iterator[T].

  2. module.prelude.standard-surfaceEvery module implicitly has the public standard-library names in the table in scope, but for the std.testing row.

  3. module.prelude.test-assertionsThe names it, assert, and assert_equal are implicitly in scope only in test code: tests: blocks, test modules, and integration test modules.

  4. module.prelude.test-only.outsideOutside test code, such a name is unknown, as an undeclared name is. Error: unknown-name.

  5. module.prelude.fixed-usesThe prelude is equivalent to fixed use declarations.

  6. module.prelude.no-authorityThe prelude does not create ambient host authority.

  7. module.prelude.no-shadowA module declaration, use, type parameter, parameter, or local binding must not bind a prelude name to another declaration. Every conflict is an error. Error: prelude-name-shadow.

  8. module.prelude.same-useA use that names the same declaration the prelude already supplies, under the same name, is allowed and changes nothing.

Error example
use std.hash.Hash                                    # ok: the prelude's Hash
use std.cmp.{Ordering as Display}                    # error: prelude-name-shadow

fn println() -> void: pass                           # error: prelude-name-shadow
fn consume(Console: i32) -> i32: Console             # error: prelude-name-shadow
fn identity[Result](value: Result) -> Result: value  # error: prelude-name-shadow

fn main() -> i32:
    let Hash: i32 = 42  # error: prelude-name-shadow
    Hash
Open in playground
Error example
fn helper() -> void:
    it("outside test code"):  # error: unknown-name
        pass

Why. Test code registers cases and checks assertions without repeated imports. A program built without its test code never reaches std.testing through the prelude. Implementer names stay explicit imports.

#Standard Names Outside The Prelude

  1. module.prelude.importedStandard traits outside this table are imported.
  2. module.prelude.implementer-importsHasher from std.hash and Poll, PollContext, and Waker from std.task require explicit imports.
  3. module.prelude.from-errorThe conversion trait std.convert.From and the error trait std.error.Error are not prelude names.
  4. module.prelude.own-from-errorA module may therefore declare its own From or Error.
  5. module.prelude.question-fromPostfix ? still finds the standard From without an import.
  6. module.prelude.inspect-reference-apiLikewise std.inspect declares Inspectable and TypeId, which code imports, as in use std.inspect.{Inspectable, TypeId}.
  7. module.prelude.error-inspectablestd.error.Error extends Inspectable without its users importing it.
  8. module.prelude.function-itemsstd.function declares the function type constructors Fn and SuspendFn and the marker trait Tuple, which code imports where it writes them, as in use std.function.{Fn, Tuple}.
  9. module.prelude.function-sugarThe function type sugar fn(...) -> T needs no import.
  10. module.prelude.annotation-targetsstd.annotation also declares Target, Annotate, and annotate, which code imports to limit a fact type's target kinds, as in use std.annotation.annotate.
  11. module.prelude.ops-literal-markersstd.ops declares NumSuffix, num_suffix, StrPrefix, str_prefix, and Template, which code imports to declare a literal function, as in use std.ops.num_suffix or use std.ops.{Template, str_prefix}.
  12. module.prelude.no-literal-fnThe prelude supplies no literal suffix and no string prefix.
  13. module.prelude.ops-operator-traitsstd.ops also declares the operator traits, and Index and IndexSet. Code imports one to name it, as in use std.ops.Add; operator syntax needs no import.
  14. module.prelude.ops-call-traitsstd.ops also declares Apply and Update, the traits of callable values. Code imports one to name it; v() and v() = x need no import.
  15. module.prelude.numstd.num declares the numeric traits Num, Integer, and Float, which code imports, as in use std.num.Num.
  16. module.prelude.ops-disposablestd.ops also declares Disposable, the trait of using bindings. Code imports it to name it, as in use std.ops.Disposable; a using binding needs no import.
use std.cmp.Eq

@derive(Eq)
struct Point:
    x: i32

fn same(a: Point, b: Point) -> bool:
    a == b   # Eq is available through the use

Note. More standard names outside the prelude are stdlib tier. Examples are the string prefix r of std.text, FromIterator of std.iter, and Duration and its suffixes of std.time.

See also: Conversion Trait, Function Type Constructors, Error Trait, Literal Suffixes, Prefixed Strings, Runtime Type Identity.

#Collection Type Names

  1. module.prelude.collectionsThe built-in collection types are List and Map.
  2. module.prelude.capitalizedLike every nominal type outside the primitives, they are capitalized.
  3. module.prelude.lowercaseThe lowercase names list and map are not prelude names; they resolve like any other identifier.
  4. module.prelude.lowercase-unknownA type written list[i32] is therefore an error unless a declaration in scope supplies list. Error: unknown-type.
Error example
fn count() -> i32:
    let values: list[i32] = [1, 2]  # error: unknown-type
    return 2

#Prelude Functions

  1. module.prelude.panicThe prelude function panic has the signature panic(message: string) -> never.
  2. module.prelude.printlnThe prelude function println has the signature println[T < Display](value: T) -> void $ Console.
  3. module.prelude.it-functionThe prelude function it is the test-case function that Test Cases specifies.
  4. module.prelude.debugThe prelude function debug has the signature debug[T < Debug](value: T) -> string, as Debug Trait specifies.
  5. module.prelude.dbgThe prelude name dbg is the function with the signature that Debug Printing specifies, whose body the compiler supplies.
pub fn main() -> void $ Console:
    println("hi")   # prelude: no import needed
Open in playground

#Debug Printing

dbg prints values while a bug is being chased. It is a statement that returns nothing:

# src/cart.hd
fn line_total(price: i32, qty: i32) -> i32:
    dbg(price * qty)  # prints src/cart.hd:2:5: price * qty = 1250
    price * qty + 50

fn check_order(id: i32, count: i32) -> bool:
    dbg(id, count)  # two lines, one per argument
    dbg()           # prints src/cart.hd:7:5
    id > 0 && count > 0
  1. module.dbg.signaturestd.format declares dbg as @intrinsic pub fn dbg[Args < Tuple](values...: Args) -> void, and the prelude supplies it. The declaration is ordinary hd, and the compiler supplies only its body.
  2. module.dbg.checkA dbg call is checked by the plain call rules and the vararg rules. So dbg(), dbg(x), and dbg(a, b, c) collect their arguments into Args as fn.vararg.collect.tuple-expr says. No rule gives dbg a type that the declaration does not.
  3. module.dbg.tuple-argumentdbg((a, b, c)) is a call with one argument, a tuple, as fn.vararg.no-auto-spread says. It prints one line for the tuple, where dbg(a, b, c) prints one line for each argument.
  4. module.dbg.body.printThe body evaluates the arguments from left to right. It prints the value of each element of values on a line of its own when it is evaluated, as Debug Values says. It needs no Debug bound on any element type. A call with a spread or values= evaluates every argument first, then prints each element. A call with positional arguments prints each one as it is evaluated.
  5. module.dbg.body.siteThe body prints what it knows of the call site, which no ordinary hd parameter can supply, as Debug Lines says. A direct call with only positional arguments and no explicit type arguments knows each argument's source text. Any other call knows only the location.
  6. module.dbg.body.valueA dbg used as a function value, as in let show: fn((i32, string)) -> void = dbg, has no call site. So its body prints each element of its one tuple argument on a line of its own, with no location.
  7. module.dbg.no-requirementA dbg call needs no requirement and adds none to a row. It is valid in a pure function, in a unit test, and in generic code.
  8. module.dbg.not-behaviorWhat dbg prints is diagnostic output, not program behavior. It is not an effect, a replay does not record it, and tools that judge a program's output ignore it.
  9. module.dbg.printer.declarationstd.format privately declares @intrinsic fn dbg_printer[T]() -> fn(T, *mut DebugWriter) -> void, with only its body compiler-supplied.
  10. module.dbg.printer.selectiondbg_printer[T]() selects an ordinary hd printer for T according to Debug Values.
  11. module.dbg.printer.structureA selected structural printer uses the Structure derivation machinery without adding public trait implementations to T.
  12. module.dbg.printer.algorithmsThe selected printers perform traversal, labeling, delimiter rendering, and composite iteration in ordinary hd.
fn total(prices: List[i32]) -> i32:
    let sum = +0
    for price in prices:
        sum = sum + price
        dbg(sum)  # valid: no Console in the row
    sum

fn show_all(pair: (i32, string)) -> void:
    dbg(pair...)                           # a line per element, after the location
    dbg(values=pair)                       # the same
    let show: fn((i32, string)) -> void = dbg
    show(pair)                             # a line per element, no location
Error example
pub fn main() -> void:
    let x: i32 = dbg(1)  # error: type-mismatch
Open in playground

Why. Rust's dbg! writes to standard error. A print added while chasing a bug must not change a function's signature, so dbg is the one way to print without Console. Its output is for the person debugging, so the program's behavior never depends on it. Returning nothing keeps dbg a plain statement: deleting it never changes a type.

#Debug Lines

  1. module.dbg.lineEach argument of a direct dbg call with positional arguments prints one line: the call's location, : , the argument's source text, =, and its value.
  2. module.dbg.line.bareA call that has no per-argument source text, as module.dbg.body.site says, prints one line for each element of values. The line holds the call's location, : , and the element's value.
  3. module.dbg.locationThe location is FILE:LINE:COLUMN: the file as diagnostics name it, and the line and column where the call begins.
  4. module.dbg.streamA dbg line goes to the program's debug output, which a program that hd runs writes to standard error. Debug Output says where it goes in tests and the REPL.
fn demo(x: i32) -> i32:
    dbg(x)   # prints "FILE:LINE:COLUMN: x = 1" to standard error
    x * 2

#Debug Values

  1. module.dbg.value.debugA value whose static type implements Debug prints as its Debug text, as debug returns it.
  2. module.dbg.value.structuralA value of any other static type prints its structure, as the table below says. Each part prints by these same rules, so a part whose type implements Debug prints through it.
  3. module.dbg.value.no-boundNeither rule needs a trait bound, so every value prints.
  4. module.dbg.value.sourceA printed value is hd source wherever its parts have a source form, as std-format.debug.source specifies. So it pastes back as an expression that builds an equal value. A string prints as a string literal, and a char as a char literal.
  5. module.dbg.value.genericInside generic code, a value whose static type mentions a type parameter prints through Debug when that type implements Debug under the bounds in scope. One such bound is T < Debug. Otherwise what it prints is implementation-defined.
Rule Static type Prints
module.dbg.value.dataStruct a struct type its name and each field with its name, private fields included, as Point { x: 1, y: 2 }
module.dbg.value.variantEnum an enum its variant as hd source constructs it, qualified by the enum, with positional payloads first and named ones as name=value: Shape.Dot, Shape.Pair(1, 2), or Shape.Circle(radius=2.0)
module.dbg.value.newtypeNewtype a newtype its name and its base value, as Meters(1.5)
module.dbg.value.compositeBuilt-in composite a list, map, tuple, T?, or Result as its standard Debug implementation writes it, as [1, 2], {"a": 1}, (1, "a"), Option.Some(1), or Result.Ok(1)
module.dbg.value.functionFunction a function or closure <fn name(i32) -> i32> when the argument names a function, else <fn(i32) -> i32>
module.dbg.value.handleHandle a live runtime handle, such as a suspension <handle>
module.dbg.value.referenceReference a &T or &mut T whose T does not implement Debug the value it refers to, by these same rules, with no trace of the reference
struct Account:
    owner: string
    balance: i32  # private, and dbg shows it

fn audit(account: Account) -> Account:
    dbg(account)  # prints ...: account = Account { owner: "Ada", balance: 120 }
    account

Why. A type should not need Debug before it can be inspected, and the person debugging needs every field. A type's own Debug still wins, so a type that hides a secret can print it masked.

#Large Values

  1. module.dbg.layoutA value prints on its line when the whole line fits in about 80 columns. Otherwise it prints over several lines, each field or entry on its own line and indented by its depth.
  2. module.dbg.cycle.referenceA value that a reference refers to, and that is reached again through a cycle of references while it is being printed, prints <cycle>.
  3. module.dbg.cycle.untrackedOther values, enums included, are not tracked, and print again each time they are reached.
  4. module.dbg.limit.entriesA list or map prints at most its first 100 entries, then โ€ฆ N more for the N entries it leaves out.
  5. module.dbg.limit.depthA part nested more than 10 levels deep prints โ€ฆ.
  6. module.dbg.limit.stringA string longer than 1000 characters prints its first 1000 characters, then โ€ฆ.
fn demo(items: List[i32]) -> void:
    dbg(items)   # over 100 entries: the rest print as โ€ฆ N more

Note. The limits keep one dbg call of a large or cyclic value readable, and keep it from running without end. Every cycle passes through a reference, because a value holds no other value that refers back to it, so <cycle> still ends each one.

#Release Builds And Dependencies

hd build --release   # error: dbg-in-release; the fix-it deletes the dbg statement
  1. module.dbg.releaseA reference to dbg in the user's own code, a call or a function value, is an error in a release build. Release is one of the build profiles. Error: dbg-in-release.
  2. module.dbg.release.deleteThe fix-it of a call deletes the whole dbg(...) statement. A reference that is not a call has no fix-it, because the code that uses the value must change.
  3. module.dbg.release.debug-buildA debug or test build accepts a reference to dbg with no warning.
  4. module.dbg.own-codeThe user's own code is the root package, every member of its workspace, and every package that a path requirement reaches.
  5. module.dbg.dependencyIn a package fetched for a version requirement, a dbg call prints nothing in every build. It still evaluates its arguments, as module.dbg.body.print says.
  6. module.dbg.dependency.lintA fetched package containing such a call produces one lint finding naming the package. Warning: dbg-in-dependency.
  7. module.dbg.dependency.function-valueA function value referring to dbg in a fetched package follows the same suppression and warning rules as a direct call.

Why. A dbg call is never meant to ship, so a release build stops it and its fix-it removes it. Removing a statement changes no types. A published package that forgot one must not print into its users' programs or break their release builds.

#Debug Text Hint

  1. module.dbg.debug-lintAn expression statement discarding prelude debug text produces a lint finding whose message suggests dbg. Warning: unused-debug-text.
fn trace(prices: List[i32]) -> void:
    debug(prices)  # warning: unused-debug-text

See also: Debug Trait, Debug Output, Build Profiles.

#Console

The standard console surface includes:

trait Console:
    fn write!(&mut self, text: string) -> Result[void, ConsoleError]

    fn write_error!(&mut self, text: string) -> Result[void, ConsoleError]:
        self.write!(text)

pub enum ConsoleError:
    Closed
    NotGranted(subject: string)
    Other(message: string)
  1. module.console.host-traitConsole is a host capability trait.

  2. module.console.write-ref.mutablewrite! takes &mut self, so Console is a mutable requirement trait, $.use(Console) is a &mut dyn Console by req.ref.use.reference-values, and a provider may record what it writes.

  3. module.console.write-error.rawwrite_error! writes its text unchanged to error output, which a host keeps apart from the output of write!.

  4. module.console.write-error.forwardingThe default write_error! body calls write! once with the same text.

  5. module.console.errorConsoleError is its standard boundary-safe error type, and ConsoleError implements Display.

  6. module.console.error.variantsConsoleError is a public enum with Closed, NotGranted(subject: string), and Other(message: string).

  7. module.console.printlnThus println is convenient to name but not a global host API. Each call must be covered by a Console requirement row or a lexical provider scope. Error: missing-requirement.

  8. module.console.println-two-writesprintln(value) calls the covering provider's write! once with value.to_string(), then once with "\n" after the first drive returns .Completed(.Ok(())).

  9. module.console.println-drive.each-writeprintln drives each write with block_on, completing the text write before starting the newline write.

  10. module.console.println-drive.pending-each-writeWhen a poll of either call returns Pending on a host write, println keeps driving that call until it finishes.

  11. module.console.println-non-suspendingprintln stays non-suspending: a call of it is not a bang call and needs no driver context.

  12. module.console.println-write-error.eitherIf either write returns .Err(ConsoleError), println panics.

  13. module.console.println-write-cancelled.remainingIf either drive returns .Cancelled or .Abandoned, println returns without starting another write, preserving text already delivered.

  14. module.console.println-write.cancel-sourceprintln holds the only reference to each write's suspension, so only the provider can cancel it.

  15. module.console.println-stdprintln is an ordinary function of the standard library's prelude.

  16. module.console.println-panicsIts panics are ordinary panics that std raises, each with a message std defines. No panic category is specific to println.

  17. module.console.println-error.categoryThe .Err panic is an ordinary panic call in std, so its category is explicit-panic. Panic: explicit-panic.

  18. module.console.println-block-onprintln follows every rule of block_on: its forbidden contexts, the error and the panic there, and its behavior under an active driver.

  19. module.console.println-block-on.under-driverSo a println call while a driver is active, as in main! or a test body, writes its line and returns.

  20. module.console.println-block-on.forbidden-directA println call written directly in a defer suite, a default expression, a fact expression, or non-entry module initialization is an error. Error: suspension-forbidden-context. In non-entry module initialization the call is also missing-requirement by module.init.requirement-free.non-script, and the compiler reports both.

  21. module.console.println-block-on.indirectA println call reached through another call while one of those contexts runs panics. Panic: suspension-forbidden-context.

  22. module.console.println-scriptA println call at the top level of a script is valid, since only non-entry module initialization bans block_on.

  23. module.console.write.rawwrite! writes its text unchanged, without adding a line ending.

  24. module.console.write.signatureConsole declares fn write!(&mut self, text: string) -> Result[void, ConsoleError].

  25. module.console.write-error.signatureConsole declares fn write_error!(&mut self, text: string) -> Result[void, ConsoleError].

  26. module.console.error.closed-meaningClosed reports a closed console stream.

  27. module.console.error.not-grantedNotGranted(subject) reports console access that was not granted.

  28. module.console.error.otherOther(message) reports another console failure with its message.

Error example
pub fn main() -> void:
    println("missing")  # error: missing-requirement

fn report() -> void $ Console:
    defer:
        println("done")  # error: suspension-forbidden-context
    pass
Open in playground

A recording provider receives the text and newline as separate writes:

struct MemoryConsole:
    lines: List[string]

impl Console for MemoryConsole:
    fn write!(&mut self, text: string) -> Result[void, ConsoleError]:
        self.lines.push(text)
        .Ok(())

    fn write_error!(&mut self, text: string) -> Result[void, ConsoleError]:
        self.write!(text)

fn greet(name: string) -> void $ Console:
    println("hello, ${name}")

pub fn main() -> void $ Console:
    let mut console = MemoryConsole { lines: [] }
    $.with(Console=&mut console):
        greet("Ada")
    println("recorded: ${console.lines[0]}${console.lines[1]}")  # recorded: hello, Ada
Open in playground

Suspending code, such as main! or a test body, may also call write! directly:

pub fn main!() -> Result[void, ConsoleError] $ Console:
    let mut console = $.use(Console)
    console.write!("done")
Open in playground

Why. Rust's println! and Go's fmt.Println are synchronous, so every caller keeps its signature. Rust's println! also panics when the write to standard output fails. println is ordinary std code that drives each write as block_on does, so it inherits block_on's rules. Separate writes avoid a concatenated string; a cancelled newline cannot retract text already delivered.

Note. Code that must handle a console error calls write! directly.

Note. print, eprintln, which writes through write_error!, and the input trait ConsoleInput are stdlib tier: Console in std.console.

See also: Driving A Stored Suspension, Mutable Providers.

#Processes

std.process declares the host capability trait that starts a program, and the Command it takes:

pub struct Command:
    pub program: string
    pub args: List[string] = []
    pub env: List[(string, string)] = []
    pub current_dir: Path? = .None
    pub stdin: string = ""

pub struct ProcessOutput:
    pub stdout: string = ""
    pub stderr: string = ""
    pub status: i32 = 0
    pub program: string = ""

pub enum ProcessError:
    NotFound(program: string)
    PermissionDenied(program: string)
    NotGranted
    Other(message: string)
    Exited(output: ProcessOutput)

pub trait Process:
    fn run!(&mut self, command: Command) -> Result[ProcessOutput, ProcessError]
  1. module.process.decl-structProcess is a host capability trait that std.process declares, with the struct types Command and ProcessOutput and the enum ProcessError. run! takes its Command as a value and returns a ProcessOutput value.
  2. module.process.run-commandrun!(command) starts the program named command.program as the rules below say, and completes when that program exits.
  3. module.process.args-no-shellEach item of command.args is one argument of the program. No shell reads the arguments, so no item is split, expanded, or unquoted.
  4. module.process.envThe program starts with the environment of the program that calls run!, then each pair of command.env, in order: a later pair for a name replaces an earlier one.
  5. module.process.current-dirThe program starts in command.current_dir when it is .Some, a relative path being from the working directory of the program that calls run!, and in that working directory when it is .None. A directory that does not exist gives .Err(ProcessError.Other(message)), with a message that names the directory.
  6. module.process.stdinThe program reads command.stdin from its standard input, and finds the end of its input after it.
  7. module.process.named-outputWhen the program starts, run! returns .Ok of a ProcessOutput holding its stdout, stderr, exit status, and program name.
  8. module.process.named-output-defaultsThe fields stdout, stderr, and program of a ProcessOutput default to "", and status to 0.
  9. module.process.nonzero-okA non-zero exit status is still an ordinary .Ok result; the caller reads it from status.
  10. module.process.not-found-programrun! returns .Err(ProcessError.NotFound(program)) when its provider has no program named program.
  11. module.process.deniedIt returns .Err(ProcessError.PermissionDenied(program)) when the program exists but the operating system does not allow starting it.
  12. module.process.not-grantedIt returns .Err(ProcessError.NotGranted) when its provider refuses program because the program's run-time grant does not cover it.
  13. module.process.otherIt returns .Err(ProcessError.Other(message)) when the program cannot start for any other reason, with a message that says why.
  14. module.process.error-traitsProcessError implements Eq, Debug, Display, and Error.
  15. module.process.mut-refrun! takes &mut self, so Process is a mutable requirement trait, as Console is, $.use(Process) is a &mut dyn Process, and a provider may record what it runs.
  16. module.process.namesProcess, Command, ProcessOutput, and ProcessError are not prelude names; code imports them from std.process.
  17. module.process.output-programA provider fills ProcessOutput.program from the command's program field.
  18. module.process.node.windows-launcherA Windows Node host requires an explicit processLauncher path to an hd executable for its Process provider.
  19. module.process.node.windows-no-launcherWithout that path, run! returns .Err(ProcessError.Other(message)) with a message naming the missing processLauncher.
  20. module.process.node.windows-jobThe Windows Node host assigns a child to its Job before the child's first instruction.

Why. The Job retains descendants after their parent exits, so cancellation can kill them. Standalone embeddings supply the launcher explicitly.

A scripted provider answers each program from a table:

use std.process.{Command, Process, ProcessError, ProcessOutput}

struct ScriptedProcess:
    outputs: Map[string, ProcessOutput]

impl Process for ScriptedProcess:
    fn run!(&mut self, command: Command) -> Result[ProcessOutput, ProcessError]:
        match self.outputs.get(command.program):
            .Some(output) =>
                let mut named = output
                named.program = command.program
                .Ok(named)
            .None => .Err(.NotFound(command.program))

fn version!() -> string $ Process:
    match $.use(Process).run!(Command { program: "git", args: ["--version"] }):
        .Ok(output) => output.stdout
        .Err(.NotFound(_)) => "no git"
        .Err(error) => "git did not start: ${error}"

Note. Which programs a provider starts is the provider's choice. The test runner binds one whose programs are the package's executables (cli.test.process). The run-time grant that NotGranted reports is CLI tier (Capability Grants).

Note. Hosts keep a nonzero status as output data. The explicit check and checked free helper are in Process. The builder methods of Command, the Display text of each ProcessError variant, and the stdlib provider ScriptedProcess are stdlib tier: Process in std.process.

Why. Starting a program is a host capability like any other. So a function that starts one states it in its row, and a test can install a scripted provider. A missing program and a refused start are failures, not absence, so run! returns a Result, as the file system traits do. A program that ran and exited with an error still ran, so its status is ordinary output.

#Value-Category Traits

  1. module.prelude.any-subtraitsAnyVal and AnyRef are the two sealed marker subtraits of Any.
  2. module.prelude.anyref-members.reference-valuesAnyRef is implemented by reference types, trait value references included, suspensions and runtime handles that have identity.
  3. module.prelude.anyref-excludesAnyRef is not implemented by primitives, tuples, enums (optionals included), or functions.
  4. module.prelude.anyval-membersAnyVal is implemented by exactly the primitives, string, tuples (void included), enums (optionals and Result included), structs, List, Map, function types, and newtypes whose base type implements AnyVal.
  5. module.prelude.newtype-categoryA newtype implements AnyRef exactly when its base type does.
  6. module.prelude.any-sealedUser code cannot implement either.
fn demo[T < AnyRef](value: T) -> bool:
    value is value

See also: Trait Values And Any, types.sealed.never for never, Sealed Traits.

#Built-In Methods

The following built-in methods are normative:

Receiver Methods
string len(self) -> usize; chars(self) -> Chars; char_indices(self) -> CharIndices; bytes(self) -> Bytes; slice(self, start: usize, end: usize) -> string
List[T] len(self) -> usize; iter(self) -> ListIter[T]
List[T], changing push(&mut self, value: T) -> void; pop(&mut self) -> T?; insert(&mut self, index: usize, value: T) -> void; remove_at(&mut self, index: usize) -> T; clear(&mut self) -> void
Map[K, V] len(self) -> usize; get(self, key: K) -> V?; iter(self) -> MapEntries[K, V]
Map[K, V], changing remove(&mut self, key: K) -> V?
Display to_string(self) -> string
  1. module.method.normativeThe built-in methods in the table are normative.
  2. module.method.changing-refThe methods of the changing rows take &mut self and change the collection. Calling one needs the list or map in a writable place, or a list element or a map value of one, as Collection Values states. Error: not-writable.
  3. module.method.iter-cursorA list's or a map's iter is its Iterable method. Each call returns a new cursor value: a std.iter.ListIter[T] for a list and a std.iter.MapEntries[K, V] for a map.
  4. module.method.usize-sizesLengths, indices, and byte offsets use usize.
  5. module.method.no-setNo set type is part of the core prelude.
  6. module.method.list-poppop removes the last element of the list and returns it as .Some.
  7. module.method.list-pop.emptypop on an empty list returns .None and leaves the list unchanged.
  8. module.method.list-insertinsert(index, value) puts value at index and moves each element from index on one place up.
  9. module.method.list-insert.endinsert at len() appends value, as push does.
  10. module.method.list-insert.rangeAn insert index greater than len() is a checked runtime panic. Panic: index-out-of-bounds.
  11. module.method.list-remove-atremove_at(index) removes the element at index, moves each later element one place down, and returns the removed element.
  12. module.method.list-remove-at.rangeA remove_at index that is not less than len() is a checked runtime panic. Panic: index-out-of-bounds.
  13. module.method.list-clearclear removes every element, so len() is 0 after it.
Call on [1, 2, 3] List after
insert(1, 9) [1, 9, 2, 3]
insert(3, 9) [1, 2, 3, 9]
remove_at(0), which returns 1 [2, 3]
clear() []

Note. push, insert, remove_at, pop and clear change the list, not an iterator taken from it before the change, which iterates the list as iter() found it (flow.for.cursor.snapshot).

See also: Text for the string methods above these, such as trim and split. Iterators covers map on a list or an optional, and Collections covers the list method view.

fn demo(items: List[string]) -> usize:
    items.len()

#Map Complexity

  1. module.map.complexityMap lookup, insertion, and removal take expected amortized O(1) time.
  2. module.map.complexity.operationsThis covers get, remove, reading entries[key], and inserting or replacing through entries[key] = value.
  3. module.map.complexity.stepEach call of the key type's Hash or Eq implementation counts as one step.
fn demo(table: Map[string, i32], key: string) -> i32?:
    table.get(key)   # expected amortized O(1)

See also: Indexing.

#String Methods

A string method takes and returns byte offsets, and iteration over a string is explicit:

fn first_word(text: string) -> string:
    for (offset, letter) in text.char_indices():
        if letter == ' ':
            return text.slice(0, offset)
    text
  1. module.string.utf8String methods operate on valid UTF-8 strings without locale.
  2. module.string.byte-offsetsEvery position that a string method takes or returns is a byte offset.
  3. module.string.charschars yields the string's scalar values in order, each as a char.
  4. module.string.char-indiceschar_indices yields each scalar value in order, as (offset, char), where offset is the byte offset at which its encoding starts.
  5. module.string.bytesbytes yields the string's bytes in order, each as a u8.
  6. module.string.cursorschars, char_indices, and bytes return new cursor values of the std.text types Chars, CharIndices, and Bytes, which implement Iterator. Advancing one needs a writable binding, as for any cursor.
  7. module.string.sliceslice(start, end) returns the bytes from offset start up to but not including offset end, in constant time.
  8. module.string.slice.sharedThe result shares the original string's bytes rather than copying them.
  9. module.string.slice.bad-offsetAn offset that is not a scalar boundary, inside a scalar's encoding or past the end, is a checked runtime panic. Panic: index-out-of-bounds.
  10. module.string.slice.reversedA start greater than end is a checked runtime panic, even when both are scalar boundaries. Panic: index-out-of-bounds.

Note. char_indices gives the offsets that Go's range over a string gives. text[start..end] gives the same substring as text.slice(start, end), through Slicing.

#Standard Testing

std.testing exports these normative assertion functions:

fn assert(condition: bool, reason: string) -> void
fn assert_equal[T < Eq & Debug](actual: T, expected: T, reason: string) -> void
  1. module.testing.exportsstd.testing exports the normative assertion functions assert and assert_equal with the signatures above.
  2. module.testing.reasonreason is required and must explain the checked condition.
  3. module.testing.assert-panicA failed assertion causes a runtime panic, inside a test case or not. Panic: assertion-failed.
  4. module.testing.shows-reasonA failed assert or assert_equal shows its reason.
  5. module.testing.uses-eqassert_equal uses Eq.eq.
  6. module.testing.no-implicit-eqassert_equal does not grant implicit equality to its argument type. An argument type without Eq is an error. Error: unsatisfied-trait-bound.
  7. module.testing.assert-equal-debugassert_equal also requires T < Debug, and a failure shows both values as debug renders them. A type without Debug is an error. Error: unsatisfied-trait-bound.
Error example
@derive(Debug)
struct Error:
    message: string

tests:
    use std.testing.assert_equal

    it("result equality needs Eq"):
        let actual: Result[i32, Error] = .Ok(1)
        assert_equal(actual, .Ok(1), reason="values match")  # error: unsatisfied-trait-bound

#Test Cases

A call of the prelude function it registers one test case:

fn add(a: i32, b: i32) -> i32: a + b

tests:
    use std.testing.assert_equal

    it("adds two values"):
        assert_equal(add(2, 3), 5, reason="small sums")

    it("adds large values", ignore="slow on shared runners"):
        assert_equal(add(1000, 2000), 3000, reason="large sums")

std.testing declares it as an ordinary function:

pub fn it[T < Termination, $R](name: string, ignore: string? = .None, expect_panic: string? = .None,
                             timeout: Duration? = .None, body: fn!() -> T $ R) -> void $ R
  1. module.testing.it-functionit is an ordinary function with the signature above, which std.testing declares and the prelude supplies. Each call in test position registers one test case.
  2. module.testing.it.formA call passes the test name as its one positional argument, then optional named options, then the body as its final argument. The body is usually a trailing block.
  3. module.testing.it.bodyThe body has type fn!() -> T $ R with T < std.process.Termination, so a trailing block body is a suspending closure.
  4. module.testing.it.nameThe name must be a string literal without interpolation. Any other name is an error. Error: non-literal-test-argument.
  5. module.testing.it.options-stringsThe named options are those of the signature above: ignore, expect_panic, and timeout. An ignore or expect_panic value must be a string literal without interpolation. Any other value for them is an error. Error: non-literal-test-argument.
  6. module.testing.it.unknown-optionAny other named argument is an error. Error: unknown-named-argument.
  7. module.testing.test-positionTest position is the top level of a tests: block, of a test module, or of an integration test module.
  8. module.testing.position-statementsEvery statement in test position must be a test registration call: a direct call of it, it_each, it_prop, or it_prop_with (Registration Calls). Any other statement is an error. Error: invalid-test-statement.
  9. module.testing.direct-callit, it_each, it_prop, and it_prop_with may be used only in a test registration call in test position. Any other use, including a call elsewhere or a use as a value, is an error. Error: misplaced-test-case.
  10. module.testing.it.uniqueTwo test cases of one module must not have the same name. Error: duplicate-test-name.
Rule Option Value Effect
module.testing.option.ignoreIgnore ignore a reason The runner does not run the test case and reports it as ignored, with the reason.
module.testing.option.expect-panicExpected panic expect_panic a panic category The test case passes only when its body panics with that category.
  1. module.testing.option.expect-panic.knownAn expect_panic value that names no panic category is an error. Error: unknown-panic-category.
Error example
fn name_of() -> string: "computed"

tests:
    let shared: i32 = 0  # error: invalid-test-statement

    it("counts"):
        pass

    it("counts"):  # error: duplicate-test-name
        pass

    it(name_of()):  # error: non-literal-test-argument
        pass

    it("rejects a category", expect_panic="index-out-of-range"):  # error: unknown-panic-category
        pass

    it("waits", timeout="5s"):  # error: type-mismatch
        pass

    fn helper() -> void:
        it("nested"):  # error: misplaced-test-case
            pass

    fn register() -> void:
        make := it  # error: misplaced-test-case

Note. it is a prelude name, so a module cannot declare, use, or bind another it (Prelude). A call spelled it(...) always registers a test case, and a tool can list test cases without running them.

Why. it is an ordinary function because its options may precede its final body parameter (Default Values). Allowing only direct calls in test position keeps every test case statically listable.

Note. The timeout parameter of it has type std.time.Duration?. Duration is a stdlib-tier type (Time): the language tier names it in this signature only and specifies none of its values.

See also: Test Timeout in the stdlib tier, for what the timeout option does.

#Registration Calls

std.testing declares three more functions a test registration call may call. it_each registers one test case per row:

pub fn it_each[A, T < Termination, $R](name: string, rows: List[A], ignore: string? = .None,
                                      expect_panic: string? = .None, timeout: Duration? = .None,
                                      body: fn!(A) -> T $ R) -> void $ R

it_prop and it_prop_with register one property test case each:

pub fn it_prop[T < Arbitrary & Debug, R < Termination, $Q](name: string, ignore: string? = .None,
                                                           expect_panic: string? = .None, timeout: Duration? = .None,
                                                           cases: usize = 100, shrink: usize = 500, examples: List[T] = [],
                                                           prop: fn!(T) -> R $ Q) -> void $ Q
pub fn it_prop_with[T < Debug, R < Termination, $Q](name: string, gen: fn(&mut Choices) -> T, ignore: string? = .None,
                                                    expect_panic: string? = .None, timeout: Duration? = .None,
                                                    cases: usize = 100, shrink: usize = 500, examples: List[T] = [],
                                                    prop: fn!(T) -> R $ Q) -> void $ Q
  1. module.testing.reg.calls-refit_each, it_prop, and it_prop_with take test registration calls with the signatures above; std.testing declares them. The test-position rules of it apply to them.
  2. module.testing.reg.importNone of the three is a prelude name. Code imports them from std.testing, as in use std.testing.it_each.
  3. module.testing.reg.identityA call is a test registration call by declaration identity, not by spelling. The test-position rules and the rules of this section apply to it under any spelling.
  4. module.testing.reg.identity.spellingsSo a bare it_each(...), a call of a renamed import such as use std.testing.{it_each as each}, and testing.it_each(...) after use std.testing all call it_each.
  5. module.testing.reg.identity.allThe same holds for it, it_prop, and it_prop_with. it is a prelude name, and code may also import it under another name or call it as testing.it.
  6. module.testing.reg.row-body-closureThe body of it_each has a parameter, so it is an explicit fn! closure, not a trailing block. After omitted options, a call passes it by name, as in body=fn!(value: i32): ....
  7. module.testing.reg.row-name-clashAnother test case of the module must not be named name[i] for any index i of an it_each call's rows. Error: duplicate-test-name.
  8. module.testing.reg.prop-registersA call of it_prop or it_prop_with in test position registers one property test case.
  9. module.testing.reg.nameThe name of an it_each, it_prop, or it_prop_with call must be a string literal without interpolation. Any other name is an error. Error: non-literal-test-argument.
  10. module.testing.reg.optionsEach takes the options of it, ignore, expect_panic, and timeout, under the same rules.
  11. module.testing.reg.body-result.reference-valuesThe body's result follows Propagation In Test Blocks: void, or Result[void, *dyn Error] when it uses ?.
  12. module.testing.reg.body-closure-result.reference-valuesThe body closure of an it_each, it_prop, or it_prop_with call that writes no result type gets its result type by expr.try.test.with-try.reference-values and expr.try.test.without-try. A trailing block given to it gets its result type the same way.
  13. module.testing.reg.prop-debugit_prop and it_prop_with require T < Debug. A property whose input type does not implement Debug is an error. Error: unsatisfied-trait-bound.
Error example
fn label() -> string: "halves"

tests:
    use std.testing.it_each

    it_each("doubles", [1, 2], body=fn!(value: i32): pass)

    it("doubles[0]"):  # error: duplicate-test-name
        pass

    it_each(label(), [1, 2], body=fn!(value: i32): pass)  # error: non-literal-test-argument

Every spelling of a registration function registers test cases:

Error example
use std.testing
use std.testing.{it as check, it_each as each}

tests:
    check("adds"):
        pass

    each("doubles", [1, 2], body=fn!(value: i32): pass)

    testing.it_each("halves", [2, 4], body=fn!(value: i32): pass)

fn helper() -> void:
    each("rows", [1, 2], body=fn!(value: i32): pass)  # error: misplaced-test-case

Note. Arbitrary, Choices, and Duration are stdlib-tier types (Property Tests, Time). The language tier names them in these signatures only and specifies none of their members.

Why. The compiler lists every test case without running it. So it knows each registration function by its declaration, where its calls stand, and which of their arguments must be literals.

See also: Table-Test Rows, for how an it_each call expands and names its rows. Property Tests tells how the runner generates, shrinks, and reports a property's inputs.

#Test Outcomes

Each test case runs alone and passes or fails by its result:

fn first(items: List[i32]) -> i32: items[0]

tests:
    it("an empty list has no first item", expect_panic="index-out-of-bounds"):
        _ := first([])
  1. module.testing.instanceEach test case runs in its own fresh program instance, after module initialization.
  2. module.testing.no-reuseInstances are not reused between test cases.
  3. module.testing.drivenThe runner drives the body's suspension to completion, as the host drives main!.
  4. module.testing.runner-providerA test run's profile also binds the host capability trait std.testing.TestRunner. The runner binds a provider of it for the body of every test case.
  5. module.testing.unit-row.test-runnerA test case in a tests: block or a test module gets no other host provider. Its body's requirement row may hold TestRunner and no other key, so every other requirement comes from a $.with provider scope. Error: missing-requirement.
  6. module.testing.unit-row.anywhereA unit test is a test case in a tests: block or a test module. Rule module.testing.unit-row.test-runner holds for it wherever its file lies.
  7. module.testing.unit-row.placesSo a tests: block under the source root, in a task or a shared task module, or in a single-file program gets TestRunner alone.
  8. module.testing.kind-decidesThe kind of a test case decides what the runner binds for it, never the directory of its file. Only an integration test case and a doc test get the profile's providers.
  9. module.testing.profileA test run compiles against one runtime profile, the default profile unless the run selects another.
  10. module.testing.integration-rowFor a test case in an integration test module, the runner binds the body's requirement row from that profile. The host binds the row of main the same way.
  11. module.testing.integration-row.unboundAn integration test case whose body needs a trait that the profile does not bind must bind it explicitly with $.with.
  12. module.testing.integration-row.unbound.errorOtherwise checking reports missing-requirement with the hint bind it with $.with(Trait=...).
  13. module.testing.passesA test case passes when its body completes, its result holds no .Err, and report() on its result returns ExitCode(0).
  14. module.testing.fail-casesIt fails when its result holds an .Err, when report() returns another code, or when its body panics, including by a failed assertion. A case of it_each or it_prop follows the same rules.
  15. module.testing.cancelledIt also fails when its body turns out cancelled, once the body's cleanup has ended, as a cancelled entry ends by req.entry.cancelled.
  16. module.testing.err-runner-source-reportWhen the result holds an .Err, the runner reports the error and its cause chain, rendered as Entry Results describes. The report is the runner's own text or JSON report on standard output, and module.entry.err-sources-to-stderr does not apply.
  17. module.testing.expect-panic-failWith expect_panic, the test case instead fails when its body completes or panics with another category.
Error example
trait Clock:
    fn now(self) -> i32

struct FixedClock:
    time: i32

impl Clock for FixedClock:
    fn now(self) -> i32: self.time

fn stamp() -> i32 $ Clock:
    $.use(Clock).now()

tests:
    it("stamps with a fake clock"):
        $.with(Clock=FixedClock { time: 7 }):
            _ := stamp()

    it("has no host clock"):
        _ := stamp()  # error: missing-requirement

Why. Source cannot catch a panic, so only the runner can judge an expected one, from the panic's stable category. Unit tests run on fakes alone, so they pass on every machine; only integration tests reach real providers.

Why. The runner is not a resource that a fake would replace. So a unit test that reaches it through TestRunner still passes on every machine.

Why. What a test may touch follows from what it tests, not from where its code lives. A task's helper is checked on fakes as a library function is, and a test that needs the real system is an integration test.

Note. TestRunner is a stdlib-tier trait (Runner Capabilities). The language tier names it in these rules only and specifies none of its methods.

Note. The missing-requirement error in a unit test suggests a std fake for the missing trait, such as ManualClock for Clock (Unit Test Providers). It also suggests moving the test case to the test root.

See also: Propagation In Test Blocks, Exit Status.

#Snapshots

std.testing declares snapshot, which compares text with an expectation written in the source:

pub fn snapshot(text: string, expect: string = "") -> void
  1. module.testing.snapshot.literalAn expect argument must be a string literal without interpolation. Any other value is an error. Error: non-literal-test-argument.
Error example
fn greeting(name: string) -> string: "hello, " + name

tests:
    use std.testing.snapshot

    it("greets by name"):
        snapshot(greeting("Ada"), expect="hello, Ada")

    it("computes the expectation"):
        snapshot(greeting("Ada"), expect=greeting("Ada"))  # error: non-literal-test-argument

Why. A literal expect lets a tool rewrite it in place. So an update run records a new or changed expectation, and an empty expect is filled on the first update.

See also: Snapshot Files in the stdlib tier, for how snapshot and snapshot_file compare text, update runs, and where snapshot files live.

#Runtime Modules

The runtime modules hold the standard library's own runtime: the code that reports an entry's result, and the memory intrinsics that a linear-memory runtime is written over. Only std code uses them:

Error example
use std.rt.memory.{grow, load_u32}  # error: std-only-use

pub fn main() -> void:
    _ := grow(1)
Open in playground
  1. module.rt.modulesThe runtime modules are std.rt and every module whose path begins with std.rt..
  2. module.rt.std-onlyOnly a module of the standard library may use a runtime module, or a name declared in one.
  3. module.rt.std-only.errorSo a use declaration in any other package whose path names a runtime module, or a name inside one, is an error. Error: std-only-use.

Why. The runtime can then change how it represents values, or what its functions take, without breaking any program.

#Memory Intrinsics

std.rt.memory declares the intrinsics that read and write the heap:

pub fn pages() -> usize
pub fn grow(count: usize) -> Option[usize]
pub fn load_u8(address: usize) -> u8
pub fn load_u16(address: usize) -> u16
pub fn load_u32(address: usize) -> u32
pub fn load_u64(address: usize) -> u64
pub fn store_u8(address: usize, value: u8) -> void
pub fn store_u16(address: usize, value: u16) -> void
pub fn store_u32(address: usize, value: u32) -> void
pub fn store_u64(address: usize, value: u64) -> void
pub fn copy(to: usize, from: usize, count: usize) -> void
pub fn fill(to: usize, byte: u8, count: usize) -> void
  1. module.rt.memory.declEach function of the block is an ordinary hd declaration marked @intrinsic, and the compiler supplies only its body, as for dbg.
  2. module.rt.memory.heapThe heap is an array of bytes in one program instance, at the usize addresses from 0. Its size is a whole number of pages of 65,536 bytes, and it never shrinks.
  3. module.rt.memory.pagespages() returns the heap's size in pages.
  4. module.rt.memory.growgrow(count) adds count pages at the heap's end and returns .Some(before), where before is the heap's size in pages before the call. Every added byte is 0.
  5. module.rt.memory.grow.refusedWhen the heap cannot grow by count pages, grow(count) returns .None and the heap keeps its size. A heap never holds more than 65,536 pages.
  6. module.rt.memory.grow.ownerThe pages that a grow call adds belong to its caller. No other part of the program reads or writes them.
  7. module.rt.memory.loadload_u8, load_u16, load_u32, and load_u64 return the 1, 2, 4, or 8 bytes from address as an unsigned integer, least significant byte first.
  8. module.rt.memory.storestore_u8, store_u16, store_u32, and store_u64 write the bytes of value from address, least significant byte first.
  9. module.rt.memory.unalignedA load or a store may use any address, aligned or not.
  10. module.rt.memory.copycopy(to, from, count) writes the count bytes from from to the count bytes from to. The two ranges may overlap: the bytes written are the ones read before the call.
  11. module.rt.memory.fillfill(to, byte, count) sets each of the count bytes from to to byte.
  12. module.rt.memory.boundsA call that reaches a byte outside the heap stops the program instance with no panic category. Only the standard library makes these calls, and it never makes one that reaches outside.

Note. The heap is the memory of a linear-memory implementation. The bytes below the size the heap starts with hold what the implementation places there, such as static data, so std code reads and writes only the pages it grew.

#Typed Memory

std.rt.memory also declares a pointer type, whose intrinsics read and write a value of any type T in the heap, a counted buffer of such values, and two views of a string's bytes:

pub struct Ptr[T]:
    pub address: usize

impl[T] Ptr[T]:
    pub fn read(self) -> T
    pub fn write(self, value: T) -> void
    pub fn offset(self, count: usize) -> Ptr[T]

pub fn size_of[T]() -> usize

pub struct Buffer[T]

impl[T] Buffer[T]:
    pub fn new(capacity: usize) -> Buffer[T]
    pub fn capacity(self) -> usize
    pub fn len(self) -> usize
    pub fn set_len(self, len: usize) -> void
    pub fn at(self, index: usize) -> Ptr[T]

pub fn string_bytes(text: string) -> Ptr[u8]
pub fn new_string(len: usize) -> string
  1. module.rt.ptr.typePtr[T] is an ordinary struct. Its address is the heap address of the first of size_of[T]() bytes that may hold a value of type T.
  2. module.rt.ptr.copyA copy of a Ptr[T] copies the address. It never copies, counts, or drops the value at the address.
  3. module.rt.ptr.declread, write, offset, and size_of are ordinary hd declarations marked @intrinsic, as for the memory intrinsics.
  4. module.rt.ptr.size-ofsize_of[T]() returns the number of heap bytes one value of T takes. It is the same for every value of T in a program instance.
  5. module.rt.ptr.size-of.numberFor bool, u8, and i8 it is 1, for u16 and i16 2, for u32, i32, f32, and char 4, and for u64, i64, and f64 8. For usize it is the target's pointer width in bytes.
  6. module.rt.ptr.offsetp.offset(count) returns the Ptr[T] whose address is p.address + count * size_of[T]().
  7. module.rt.ptr.writep.write(value) stores a copy of value at p, so the heap there holds a value of T. The copy shares and counts references as any copy of a T does.
  8. module.rt.ptr.write.overA write replaces the bytes at p without dropping the value they held.
  9. module.rt.ptr.readp.read() moves the value at p out and returns it, so the heap there no longer holds a value. Its bytes do not change.
  10. module.rt.ptr.integerA value of an integer type is stored as its bytes, least significant first, so the loads and stores of its width read and write it.
  11. module.rt.ptr.holdsOnly the standard library reads or writes through a Ptr[T], and it reads only where the heap holds a value of T.
  12. module.rt.ptr.boundsA read or a write that reaches a byte outside the heap stops the program instance with no panic category, as for module.rt.memory.bounds.
  13. module.rt.buffer.typeA Buffer[T] is a counted heap object with room for capacity() values of T, of which it holds the first len(). Its fields are private.
  14. module.rt.buffer.copyA copy of a Buffer[T] is the same object with one count more. It never copies the values the buffer holds.
  15. module.rt.buffer.newBuffer::new(capacity) returns a new buffer whose capacity is capacity and whose length is 0. A capacity whose bytes do not fit the heap panics. Panic: heap-exhausted.
  16. module.rt.buffer.atb.at(index) returns the Ptr[T] of value index of the buffer's room: b.at(0).offset(index). It checks no bound; reads and writes through it follow module.rt.ptr.holds.
  17. module.rt.buffer.lenb.set_len(len) sets the length that b.len() returns and changes no value. Before it raises the length, std writes each new value through at; before it lowers the length, std reads each value past the new length out through at.
  18. module.rt.buffer.dropWhen the last count of a buffer is released, the buffer drops each of its first len() values, then its storage is freed.
  19. module.rt.string.bytesstring_bytes(text) returns the Ptr[u8] of the first of the text.len() bytes of text's UTF-8 encoding. The bytes stay at that address while text or a copy of it lives.
  20. module.rt.string.newnew_string(len) returns a new string of len bytes, whose values are unspecified until written. new_string(0) returns the empty string.
  21. module.rt.string.writeOnly the bytes of a string that new_string returned are written through string_bytes, and std writes all of them, as valid UTF-8, before the string is used in any other way.

Note. A read and a write of the same value back copy it out, and a read whose result is discarded drops the value in place.

Why. These are Rust's ptr::read and ptr::write: one count per value held, moved by a read and taken by a write, so std's collections place, move, and drop their elements with no other intrinsic. A buffer is the one counted object they need, as Go's slice backing array is. The two string views are Go's rawstring and unsafe.StringData, so concatenation, comparison, and slicing are std code over bytes.

#Module Initialization

This section defines which modules initialize, in what order, and what their top-level code may do.

Note. Shared enum constructor data are part of their declaring module's initialization.

  1. module.init.scriptA script is an entry module whose top-level executable statements are the entry behavior and which has no main declaration.
  2. module.init.entry-module.selectedAn entry module is the module that a program starts from. That is a module that the toolchain selects in a package, or the file of a single-file program.
  3. module.init.entry.statements-before-mainAn entry module with main runs requirement-free top-level initialization before main; it is not a script.
  4. module.init.program-instanceA program instance is one instantiated Wasm module graph together with its module storage, provider bindings, and execution state.
pub fn main() -> void:
    pass   # an executable entry module, not a script
Open in playground

#Initialization Order

  1. module.init.use-graphBefore execution, the compiler resolves the use graph reachable from the selected script or executable entry module. The graph may have loops inside one folder.
  2. module.init.cycleA module cycle is a strongly connected component of that graph: modules that use each other, or one module alone as a trivial cycle.
  3. module.init.cycle.onceEvery reachable cycle is initialized exactly once per program instance, after every other cycle it uses has been initialized.
  4. module.init.cycle.ready-orderWhen several cycles are otherwise ready, the least fully qualified module identity in each cycle orders them lexicographically.

Why. Ordering by module identity makes initialization independent of filesystem enumeration.

start := +1

fn total() -> i32:
    start + 1

demo := total()

#Order Inside A Cycle

Top-level statements of a cycle run in dependency order, then in file order, as Go orders the variables of one package:

# src/shop/catalog.hd
use super.prices

let featured = prices.price_of("tea")  # runs after prices.markup

# src/shop/prices.hd
use super.catalog

let markup = 5

pub fn price_of(sku: string) -> i32:
    catalog.base_price(sku) + markup
  1. module.init.cycle.dependencyA top-level statement depends on each top-level binding of its cycle in its transitive read set. The read set is computed as for definite initialization, across every module of the cycle.
  2. module.init.cycle.stepA cycle initializes one top-level executable statement at a time. Each step runs the earliest remaining statement whose dependencies are all initialized.
  3. module.init.cycle.earliestStatements are ordered by fully qualified module identity, then by source position.
  4. module.init.cycle.stuckWhen statements remain and none of them is ready, they form an initialization cycle, which is an error. Error: top-level-read-before-initialization.

Note. In a cycle of one module, definite initialization makes every statement ready in turn, so its statements run in source order.

#Top-Level Statements

  1. module.init.declarationsWithin one module, named declarations are available before initialization.
  2. module.init.source-order-singleIn a cycle of one module, top-level executable statements run in source order. A larger cycle follows Order Inside A Cycle.
  3. module.init.usesUse declarations do not execute as statements.
  4. module.init.bindingTop-level bindings are initialized at their statement, before later function bodies may access them.
  5. module.init.storageTheir storage remains available to functions in that module for the lifetime of the program instance.
  6. module.init.no-stepA module with no top-level executable statements has no observable initialization step.
count := +0

fn demo() -> i32:
    count + 1   # storage lives for the program instance

#Definite Initialization

  1. module.init.definiteBefore accepting a top-level executable statement, the compiler checks the transitive read set of each function or closure it references. Every top-level binding in that set must already be initialized. Error: top-level-read-before-initialization.
  2. module.init.definite.implicitReferences passed as values and functions reached by trait dispatch, interpolation, iteration, or another implicit call are included.
  3. module.init.definite.dispatchA trait method call through a generic bound or a dynamic trait value reaches every implementation of that method in the module.
  4. module.init.definite.whole-moduleThis definite-initialization check covers the whole module value-flow and call graph.
  5. module.init.definite.localThe check is local to one module.
Error example
first := apply(first_name)  # error: top-level-read-before-initialization
let names: List[string] = ["Ada"]

fn apply(callback: fn() -> string) -> string:
    callback()

fn first_name() -> string:
    names[0]

Note. An implementation can compute the check from a per-function summary of the top-level bindings each function reads, combined bottom-up over the module's call graph. It never needs another module's function bodies. Ordering a larger group combines the same summaries across the group's modules, after their bodies are checked.

#Entry Behavior

  1. module.init.script-bodyAfter dependency initialization, a script executes its top-level statements as that module's initialization.
  2. module.init.script-driverA script's top level is a suspension driver context, as main!'s body is, so it may make bang calls. The whole script runs as one suspending entry. It stays one module, and hd replays no statement. The top level of any other module is not a driver context, also when it is an entry module that has a main.
  3. module.init.script-profileA script declares no requirement row. Its top level may use every host capability trait of the default profile, as a REPL input may by cli.repl.host.default-profile.
  4. module.init.script-try.reference-valuesA ? at a script's top level leaves the script as main! leaves with a Result[void, *dyn Error]. The operand is a Result whose error type implements Error. An .Err is then handled by Entry Results: the host prints it, and the script exits with the status of its report(). A ? on an optional there is an error. Error: invalid-result-propagation.
  5. module.init.mainAn executable package then invokes main after its entry module has initialized.
  6. module.init.testsTest runners initialize the module under test and the modules it uses before running its test cases.
  7. module.init.test-casesThe it calls of a tests: block or a test module, and their bodies, are not part of module initialization.
  8. module.init.script-emptyAn entry module with no main and no top-level executable statements is a script with no entry behavior. Running it does nothing and exits with status 0.
  9. module.init.tests.no-entryA test run runs no entry behavior. So the module under test is not an entry module in it, even when it is a script or a task.
  10. module.init.tests.initialization-empty-rowA script's top-level statements are then its module initialization, which must be requirement-free by module.init.requirement-free.non-script. Error: missing-requirement.
Error example
# tasks/seed.hd, under `hd test`
fn rows() -> List[string]: ["apple", "pear"]

println("seeded ${rows().len()} rows")  # error: missing-requirement

tests:
    use std.testing.assert_equal

    it("seeds two rows"):
        assert_equal(rows().len(), 2, reason="the seed rows")

Why. A test run never runs main, and a script's top level plays the part of main. The runner binds no host for initialization, so a tested script keeps its work in main, as a Go or Rust program does.

A script reads, waits, and ends on an error, with no main!:

use std.console.read_line
use std.time.{ms, sleep}

sleep!(ms(10))
name := read_line!()?                  # an .Err ends the script with status 1
println("hello ${name.unwrap_or("world")}")

Why. A script is the REPL's top level kept in a file. The REPL already runs an input as a driver context under the default profile, so a script that works there works from a file. Making main! necessary for one bang call would send every quick script through a boilerplate entry point.

#Requirement-Free Initialization

  1. module.init.requirement-free.non-scriptTop-level code in non-entry modules and entry modules with main must be requirement-free initialization. Error: missing-requirement.
  2. module.init.requirement-free.operationsIt must not use $.use, enter a provider scope, make a bang call, or call a callable whose requirement row is not empty.
  3. module.init.entry.row-free-block-onAn entry module's requirement-free initialization may call block_on; its existing forbidden contexts still apply.
limit := +10

fn demo(n: i32) -> bool:
    n < limit   # requirement-free: no $.use, no bang call

Why. Only script entry behavior has the default profile. Requirements declared by main do not authorize its earlier initialization. Requirement-free block_on remains permitted in entry initialization.

#Program Instances

  1. module.init.per-instanceThis initialization rule governs one program instance.
  2. module.init.historiesInteractive cell re-execution and durable replay have separate runtime histories, described in REPL and Determinism.
tests:
    it("each case runs in a fresh instance"):
        _ := +1

#Public Uses And Visibility

Declarations are module-private by default, and prefix pub makes a declaration available to other modules:

pub type UserId(string)

pub struct User:
    pub id: UserId
    pub email: string
  1. module.vis.private-defaultDeclarations are module-private by default.
  2. module.vis.pubPrefix pub makes a declaration available to other modules.

#Public Uses

Use pub use in a mod.hd file to expose a package-facing API:

pub use pkg.user.types.{User, UserId}
pub use pkg.user.service.{load_user, save_user}

Another module can then use the names through the directory module:

use pkg.user.{User, UserId, load_user}
  1. module.pub-use.facadepub use in a mod.hd file exposes a package-facing API, whose names another module can use through the directory module.
  2. module.pub-use.public-sourceA publicly used declaration must already be public in its defining module.
  3. module.pub-use.bindingpub use introduces the same local binding as use and additionally exposes that binding to other modules.
  4. module.pub-use.identitypub use does not create a new declaration identity.
  5. module.pub-use.chainA pub use chain must end at a declaration. Following each pub use of a name to the module it names must reach the module that declares the name.
  6. module.pub-use.chain.loopA chain that returns to a pub use it has already passed is an error. Error: re-export-loop.
  7. module.pub-use.chain.loop-useA plain use whose name leads into such a loop is an error with the same code. Error: re-export-loop.
Error example
# src/shop/a.hd
pub use pkg.shop.b.{Token}  # error: re-export-loop

# src/shop/b.hd
pub use pkg.shop.a.{Token}

# src/main.hd
use pkg.shop.a.{Token}  # error: re-export-loop

Why. Loops of use lines are allowed inside a folder, so a facade and its children may use each other. A name still needs one declaration.

#Member Visibility

  1. module.vis.no-package-privateThere is no package-private visibility modifier: another module in the same package can use only pub declarations.
  2. module.vis.membersNamed fields and inherent methods are module-private unless individually marked pub.
  3. module.vis.variantsEnum variants inherit their enum's visibility.
  4. module.vis.trait-methodsTrait methods follow their trait's visibility.
  5. module.vis.impl-targetA usable implementation additionally requires its target type to be visible.
struct User:
    pub name: string
    id: i32

#Public Signatures

  1. module.vis.signatureA public declaration's complete source-level signature must not expose a module-private declaration. Error: private-type-leak.
  2. module.vis.signature.coverageThis check recursively covers function parameters and results, struct fields (every embedded field included), and enum constructor data and payloads. It also covers alias/newtype underlying types, trait bounds, supertraits, public generic arguments, and every requirement-row key. An implementation whose trait and target are both public is covered too: the type each associated type binds, nested types included, must not be module-private. An implementation with a private trait or a private target is not covered.
  3. module.vis.bodyA private implementation detail may occur in a public function body but not in its public typed interface.
Error example
struct Secret:
    value: string

pub fn reveal() -> Secret:  # error: private-type-leak
    Secret { value: "hidden" }

See also: Field Visibility.

#Name Resolution Across Packages

  1. module.package.identityAn absolute qualified name identifies one declaration after dependency resolution.
  2. module.package.no-sharingTwo dependencies do not share declarations merely because their package names and source paths match.
  3. module.package.identity-partResolved package identity is part of a declaration's identity.
  4. module.package.public-surfaceA package must not access another package except through declarations reachable from that package's public module surface.
  5. module.package.separateImplementations may compile packages separately.
use std.json.parse

#Fully Annotated Declarations

  1. module.package.annotatedEvery public declaration is fully annotated: its complete signature is written in source.
  2. module.package.annotated.partsThat signature includes parameter and result types, requirement rows, suspension, and generic parameters with their bounds and variance. It also includes the types of public fields and enum data.
  3. module.package.no-inferenceNothing in a public signature is inferred from a function body.
  4. module.package.no-pub-bindingTop-level bindings cannot be public. A pub binding is an error. Error: syntax-error.
  5. module.package.template-privateA private item that a template body names follows the signature rules of a public declaration.
  6. module.package.template-private.resultSo a private function a template names must declare its result type. Omitting it is an error. Error: missing-result-type.
  7. module.package.template-private.rowSuch a function without a requirement clause has the empty row, as req.row.omitted.empty-pub states for a public function.
  8. module.package.template-private.no-bindingA template body must not name a top-level binding. Naming one is an error. Error: template-names-binding.
Error example
pub answer := 42  # error: syntax-error
Error example
trait Named:
    fn title() -> string

prefix := "The "

impl[T] Named for T by Structure:
    fn title() -> string:
        let mut d = MemberCount { count: 0 }
        _ := T::describe(&mut d)
        prefix + Structure::name()  # error: template-names-binding

Here MemberCount is a Describer that counts members, as in Templates.

Why. A template is instantiated in the target's module, often in another package. Its helpers therefore reach that package's checking through the interface, which holds signatures, not inferred results.

#Package Interfaces

A package interface must contain:

Interface content
exported declaration identities and complete signatures
visibility
generic kinds, variance, and bounds
requirement rows
associated types
every module-level implementation head, for coherence
each fact's expression and its type
each private item a template names and each private type it names, as a hidden item
  1. module.interface.contentsA package interface must contain every item in the table.
  2. module.interface.hidden-itemA hidden item is in the interface, but only the code of an instantiated template may name it. Downstream source cannot name it.
  3. module.interface.generic-bodiesAn interface may also carry ordinary generic bodies to enable inlining, but downstream compilation must not require them.
  4. module.interface.generic-compilationHow generic code is compiled across packages is not observable, as types.generic.unobservable says. An interface fixes no strategy for it.
  5. module.interface.fact-expressionsA package interface records each fact of its declarations by the fact's expression and type, not by its value.
  6. module.interface.fact-runtimeThe value of a fact is computed by the running program on its first read, never when an interface is made or a program is built.
  7. module.interface.syntax-onlyA package interface's signatures and facts therefore come from the package's declarations alone. They depend on no checked function body.
  8. module.interface.earlyA downstream package can be checked as soon as the interfaces of its dependencies are known. It need not wait for their function bodies to be checked or compiled.
  9. module.interface.coherenceCoherence is checked at link time over the complete set of resolved interface files.
  10. module.interface.link-rejectLinking may therefore reject a graph even when each package compiled independently.
pub fn total(items: List[i32]) -> usize:
    items.len()   # fully annotated: this signature enters the package interface

#Entry Point

The conventional non-suspending entry point is:

pub fn main() -> void:
    ...
  1. module.entry.definitionAn entry point is a public top-level function named main or main! with no parameters.
  2. module.entry.result-terminationIts result type must implement std.process.Termination, as for an ordinary trait bound, so it may be void, ExitCode, or Result[T, E] with T < Termination and E < Error. Any other result type is an error. Error: unsatisfied-trait-bound.
  3. module.entry.rowIt may declare a requirement row.
  4. module.entry.row.hostEvery key in that row must be a host capability trait of the selected runtime profile. Any other key is an error. Error: nonhost-entry-requirement.
  5. module.entry.private-mainA top-level main that is not public is an ordinary function and is not an entry point.
  6. module.entry.suspendingA suspending entry point is spelled main!.
  7. module.entry.private-main.lintA private top-level main or main! in an entry module produces the lint message "main is not pub, so it is not the entry point". Warning: private-main.
  8. module.entry.parametersA public top-level main or main! in an entry module that declares parameters or generic parameters is an error. Error: entry-point-parameters.
  9. module.entry.private-main.fixThe private-main warning must offer a fix-it inserting pub before the function keyword.
  10. module.entry.private-main.fix.conflictWhen adding pub could clash with another public main, the fix-it must remain a suggestion.
fn main() -> void:  # warning: private-main
    pass
Open in playground
Error example
pub fn main(args: List[string]) -> void:  # error: entry-point-parameters
    pass
Open in playground

Why. Lint suggests adding pub before an execution request needs an entry point. Checking still accepts private helper functions named main.

See also: Mutable Providers.

#Entry Results

  1. module.entry.exit-reportWhen an entry point returns, the process exits with the u8 held by the ExitCode that report() returns for its result, as Exit Status describes.
  2. module.entry.err-host-printsWhen the result holds an .Err, the host prints that error by the rules below before it exits.
  3. module.entry.err-dynamic.terminationResult[T, *dyn Error] is a valid entry result when T < Termination, since erased Error satisfies E < Error.
  4. module.entry.err-render-chainThe host prints the error's Display text. Then it prints each cause that the standard-library chain yields after it.
  5. module.entry.err-render-chain.source-lineEach source is printed on its own line as source: followed by the source's Display text.
  6. module.entry.err-sources-to-stderrWhen an entry point returns .Err(e), the host writes the text and source: lines of e to standard error. Then it exits with the status that report() returns, as Exit Status describes.
  7. module.entry.panicA panic exits with a distinct nonzero status selected by the runtime profile and poisons the program instance.
Error example
pub struct HiddenError: pass

pub fn main() -> Result[void, HiddenError]:  # error: unsatisfied-trait-bound
    .Err(HiddenError {})
Open in playground

See also: Dynamic Trait Values, Error Trait, Cause Chain.

#Exit Status

std.process declares the exit code and the trait that produces it:

pub type ExitCode(u8)

pub trait Termination:
    fn report(self) -> ExitCode
  1. module.entry.exit-codestd.process declares the newtype ExitCode, whose u8 is a process exit code. Every u8 is valid, and 0 means success.
  2. module.entry.terminationstd.process declares the trait Termination, whose one method is fn report(self) -> ExitCode.
  3. module.entry.termination.voidvoid implements Termination; its report returns ExitCode(0).
  4. module.entry.termination.exit-codeExitCode implements Termination; its report returns the code itself.
  5. module.entry.termination.resultResult[T, E] implements Termination when T < Termination and E < Error.
  6. module.entry.termination.okFor .Ok(value), its report returns value.report().
  7. module.entry.termination.err-oneFor .Err(_), its report returns ExitCode(1).
  8. module.entry.termination.no-printreport only computes the code. The host or test runner prints the error, as Entry Results describes.
  9. module.entry.process-importExitCode and Termination are not prelude names; code imports them from std.process, as in use std.process.ExitCode.

A program that picks its own code returns an ExitCode:

use std.process.ExitCode
use std.error.Error

fn count_changes() -> Result[i32, *dyn Error]:
    .Ok(3)

pub fn main() -> Result[ExitCode, *dyn Error]:
    changes := count_changes()?
    if changes > 0: .Ok(ExitCode(1)) else: .Ok(ExitCode(0))
Open in playground

A program that needs another code for an error maps its errors in main:

use std.process.ExitCode

enum CliError:
    Usage(string)
    Failed(string)

fn run(args: List[string]) -> Result[void, CliError]:
    if args.len() == 0: .Err(.Usage("usage: tool FILE")) else: .Ok(())

pub fn main() -> ExitCode $ Console:
    match run([]):
        .Ok(_) => ExitCode(0)
        .Err(.Usage(text)) =>
            println(text)
            ExitCode(2)
        .Err(.Failed(text)) =>
            println(text)
            ExitCode(1)
Open in playground

This main prints usage: tool FILE and exits with status 2.

Why. One trait serves entry points and tests, as Rust's Termination does. An exit code concerns the command-line entry, not the error, so Error has no code. main picks codes in one place, as a Rust main does.

Note. A tool that must exit without printing, such as one that stops quietly on a closed pipe, prints what it needs. Then it returns an ExitCode.

#Entry Arguments

  1. module.entry.no-argumentsmain has no source-level arguments.
  2. module.entry.host-facilitiesProcess arguments, console access, environment, and other host facilities are requirements supplied by the runtime through the requirement model.
use std.host.Args

pub fn main() -> void $ Args:
    args := $.use(Args).list()
    _ := args
Open in playground

#Wasm Boundary

This section defines runtime profiles, registered boundary functions, the values that cross a boundary, and the official host boundary.

#Runtime Profiles

  1. module.profile.definitionA runtime profile is a named compile-time set of host capability traits, their boundary adapters, and runtime choices such as panic exit statuses.
  2. module.profile.trait-access-ref.reference-valuesThe profile binds each host provider with the access its trait gives, by req.mut.entry.trait-access. For a trait with a &self or &mut self method, the provider is a trait value reference, by req.ref.host.reference-values.
  3. module.profile.buildThe compiler receives the selected profile as build configuration.
  4. module.profile.defaultThe default profile contains at least the prelude Console trait.
  5. module.profile.otherAnother profile may add or omit host traits explicitly.
  6. module.profile.toolchain-namesProfile names are defined by the toolchain. A manifest cannot define a profile.
  7. module.profile.panic-statusA profile's panic exit status is never 101.

Why. hd exits with 101 when it fails itself (cli.exit.hd-failure), so a panicking program and a failed build never share a status.

See also: Mutable Providers.

pub fn main() -> void $ Console:
    println("hi")   # the default profile binds Console
Open in playground

#Host Results

A profile's boundary adapter checks every value that comes in from the host before hd code sees it:

  1. module.profile.host-result.checkedThe adapter checks each value that a call of a host capability trait's method returns against the result type that the method declares. This holds whether the call suspends or not.
Rule Declared type The value must be
module.profile.host-result.integerInteger an integer type an integer within the type's range
module.profile.host-result.boolbool bool false or true
module.profile.host-result.charchar char a Unicode scalar value
module.profile.host-result.stringstring string valid UTF-8 text
module.profile.host-result.floatFloat f32 or f64 any value of that width, so a NaN, an infinity, or -0.0 is never a contract violation
module.profile.host-result.shapeComposite an optional, Result, tuple, list, map, struct, or enum of that type's shape, with each element, field, and payload checked against its own declared type
  1. module.profile.host-result.no-coercionThe adapter never rounds, truncates, wraps, or otherwise converts a host value to make it fit.
  2. module.profile.host-result.contract-panicA value that fails the check is a host fault. The call panics with a message that names the trait and the method and says that the host broke its contract. Panic: host-contract.
  3. module.profile.host-result.statusLike every panic, it poisons the program instance, and an entry point exits with the profile's panic exit status, by module.entry.panic.

Why. A host that returns 300 for a u8 has a bug outside hd. Stopping at the boundary names the host, where a silently wrapped value would surface later as a wrong answer in hd code.

use std.console.{ConsoleError, ConsoleInput, read_line}

fn demo!() -> Result[string?, ConsoleError] $ ConsoleInput:
    .Ok(read_line!()?)   # the adapter checks the host's answer against the declared result

#Floats At The Host Boundary

A float crosses a live host call as its IEEE 754 value, with no encoding:

  1. module.profile.host-float.rawA call of a host capability trait's method passes each f32 or f64 argument as a raw IEEE 754 value of that width. It returns each such result the same way.
  2. module.profile.host-float.specialSo a NaN, both infinities, and -0.0 cross the call unchanged, in either direction.
  3. module.profile.host-float.nanA NaN's payload may arrive replaced with the canonical NaN of types.display.nan-canonical.
fn demo(x: f64) -> f64:
    x + 0.5   # floats cross calls as raw IEEE 754 values

Why. WIT's f64, wasm-bindgen, and the C ABI all pass floats as raw IEEE 754 values. Special values need care only when a value becomes text, which module.boundary.float-bits covers.

#Registration

  1. module.register.pubpub is module visibility, not Wasm export registration.
  2. module.register.explicitA tool, workflow, or other host-callable function becomes visible only through explicit registration provided by its library.
  3. module.register.entryEvery registered boundary function is an entry point for provider checking.
  4. module.register.contract-traitsIts registration contract selects a runtime profile and declares which requirement traits that profile can bind.
  5. module.register.applicationThat bindable set may include application traits such as Database when the adapter explicitly supports them.
  6. module.register.rowEvery key in the registered function's row must be in that bindable set, and the host must bind all of them before invocation.
  7. module.register.failureOtherwise registration or startup fails before user code executes.
pub fn main() -> void:
    pass   # a tool becomes host-callable only through its library's explicit registration
Open in playground

#Boundary-Safe Values

Registered boundaries initially allow recursively structural values:

  • primitive scalar types and string;
  • tuples;
  • List[T] and Map[K, V] whose contents are boundary-safe;
  • structs and enums whose complete fields and payloads are boundary-safe;
  • T? and Result[T, E] whose contained types are boundary-safe.
  1. module.boundary.allowedRegistered boundaries initially allow the recursively structural values in the list above.
  2. module.boundary.not-safeDynamic trait values (including Inspectable values), std.inspect.TypeId, closures, and live runtime handles are not boundary-safe.
  3. module.boundary.rowsRequirement-row entries are host bindings and are not serialized parameters.
  4. module.boundary.erased-errorIn particular, the erased error std.error.Error never crosses a registered boundary.
  5. module.boundary.domain-errorA registered function returns a boundary-safe error type, such as a domain error enum, and code converts an erased error to such a type explicitly.
struct User:
    name: string

fn demo(user: User) -> string:
    user.name   # a struct crosses: recursively structural values are boundary-safe

Note. An error type with a member of type Error is therefore not boundary-safe either. Before such an error crosses a boundary, code converts it with the standard-library report_of to an ErrorReport, a boundary-safe snapshot of its message and causes.

See also: Error Trait.

#Boundary Encoding

  1. module.boundary.treeBoundary values have tree semantics.
  2. module.boundary.cycleEncoding a cycle is a boundary error. Boundary failure: boundary-cycle.
  3. module.boundary.cycle.trackedThe encoder finds a cycle at a struct value with fields, a list, or a map that it reaches again on its path. It does not track enum values.
  4. module.boundary.sharingWhen an acyclic graph shares a node, each incoming path encodes a duplicate tree value, and decoding does not restore sharing.
  5. module.boundary.map-decodeDecoding a map invokes the key type's ordinary Eq and Hash implementations.
use std.hash.Hash

@derive(Eq, Hash)
struct Tag:
    id: i32

fn demo() -> void:
    let mut table: Map[Tag, string] = {}
    table[Tag { id: +1 }] = "one"   # decoding a map invokes Eq and Hash
  1. module.boundary.decoder-panicIf either panics, the adapter reports a boundary failure and does not enter the registered function. Boundary failure: boundary-decoder-panic.
  2. module.boundary.decoder-poisonThe adapter treats that event as an ordinary poisoning panic: the program instance must be discarded.
  3. module.boundary.float-bitsA boundary value or host-call result serialized as text, as in a replay record, writes an f64 as its IEEE 754 bit pattern. That is 16 lowercase hex digits, most significant first.
  4. module.boundary.float-bits.f32An f32 is written the same way, as 8 lowercase hex digits.
  5. module.boundary.float-bits.nanA NaN is first replaced with the canonical NaN of types.display.nan-canonical.
Value f64 text
1.0 3ff0000000000000
-0.0 8000000000000000
positive infinity 7ff0000000000000

Why. A float's bits make its text exact, while a NaN, an infinity, or -0.0 has no JSON number.

Note. The reference prototype's replay encoder writes host-call results this way.

#Private Fields At A Boundary

A struct type with a private field crosses a boundary only through that type's Serialize and Deserialize implementations (serialization opt-in):

Error example
use std.serde.{Serialize, Deserialize}

@derive(Serialize, Deserialize)
pub struct Token:
    pub owner: string
    secret: i64

pub struct Badge:
    pub owner: string
    code: i64

pub trait Vault:  # a host capability of the selected runtime profile
    fn keep(self, token: Token) -> Token
    fn badge(self) -> Badge  # error: boundary-private-field
  1. module.boundary.outA value of a struct type with a field that is not pub crosses a boundary out of hd only when the type implements std.serde.Serialize.
  2. module.boundary.inSuch a value crosses a boundary into hd only when the type implements std.serde.Deserialize.
  3. module.boundary.serialize.exactA value of a type that implements std.serde.Serialize crosses out of hd as exactly what its serialize writes: private fields the implementation does not write do not cross.
  4. module.boundary.deserialize.exactA value crosses into hd only as built by its type's std.serde.Deserialize implementation; a deserialize error is a host-contract panic the caller sees.
  5. module.boundary.deserialize.no-dynDeserialization at a boundary never selects a type from the data: the target type comes from the signature.
  6. module.boundary.publicA struct type whose fields are all pub crosses in either direction without either trait. An enum's payloads have no visibility, so they need neither trait.
  7. module.boundary.directionA registered function's arguments cross into hd, and its result crosses out. A host capability method's arguments cross out of hd, and its result crosses in.
  8. module.boundary.errorA boundary signature whose type, or a type inside it, would cross without the trait its direction needs is an error, reported at that signature. Error: boundary-private-field.

Why. A host is one more format. A type's serialize decides what leaves hd, and only its deserialize can rebuild private state on the way in, so a redacting implementation redacts for the host exactly as for JSON.

Note. The standard library's Duration, Timestamp, and Instant implement both traits, so a Clock provider's results cross into hd (Time).

#Instances And Threads

  1. module.instance.threadOne program instance executes on one thread and has no shared-memory parallelism or source-level atomics.
  2. module.instance.parallelHosts may run multiple Wasm instances in parallel only by exchanging boundary-safe values.
  3. module.instance.disjointTheir heaps, mutable globals, and suspension drivers are disjoint.
fn demo() -> i32:
    +1   # one thread, no atomics: this is the whole story

#Host Boundary

  1. module.host.wasmThe official compiler targets Wasm only.
  2. module.host.rc-runtimeThe official runtime manages values with reference counting and cycle collection and uses a WASI-compatible host boundary.
  3. module.host.providersAuthority-bearing providers originate at that boundary.
  4. module.host.boundary-abiHow values, strings included, cross the host boundary is an implementation detail, not part of the language.
  5. module.host.tooling-hooksHooks that a toolchain adds for development, testing, or tracing are implementation tooling, not a language boundary.
  6. module.host.requirementsEvery host facility is injected through an ordinary requirement trait.
  7. module.host.capability-setThe eventual standard capability-trait set is runtime and library work.
  8. module.host.abiThe exact host ABI and the registration APIs are runtime and library specification work.
hd build --release src/main.hd   # the official compiler targets Wasm

Note. hd has no extern declaration and no FFI capability. A Wasm module can call only its imports, so a custom host trait that an embedder binds is hd's foreign function interface, by module.register.application.

#Package Host Traits

A package host trait declares an interface whose implementation the invoker supplies:

@host
pub trait Surface:
    fn render!(&mut self, commands: List[string]) -> void
  1. module.host.package.markerBare @host marks a package-owned public trait as a host capability trait.
  2. module.host.package.target@host on any other declaration or member is an error. Error: decorator-target-kind.
  3. module.host.package.bareArguments and type arguments on @host are errors. Error: decorator-target-kind.
  4. module.host.package.fact@host is a compiler marker and attaches no fact value.
  5. module.host.package.identityA fetched host trait's nominal identity includes its canonical package host path, exact selected version, and package-relative trait path.
  6. module.host.package.local-identityA local host trait's nominal package identity hashes its canonical directory, so moving that directory requires rebinding.
  7. module.host.package.fakeA package host trait uses ordinary requirement rows, $.use, and $.with replacement.
  8. module.host.package.js-valuesJavaScript package providers receive and return JavaScript values rather than serialized text.
  9. module.host.package.js-integersA structural 64-bit integer crosses as BigInt, and a narrower structural integer crosses as Number.
  10. module.host.package.js-private-serdeA private-field value crosses through its directional Serialize or Deserialize implementation, with Serde integers represented as BigInt.
  11. module.host.package.js-private-floatsA Serde float crosses as Number while retaining its numeric kind and finite IEEE 754 bits, including negative zero.
Error example
@host()  # error: decorator-target-kind
pub trait Surface:
    fn render!(&mut self, commands: List[string]) -> void

See also: Package Host Declarations, Runtime Profiles, Host Boundary.

#Closable Handles

A closable runtime handle reports a use after close with its module's Closed error, and a using binding disposes it:

use std.ops.Disposable

pub enum StoreError:
    Missing(key: string)
    Closed

pub trait Session < Disposable:
    fn get!(&mut self, key: string) -> Result[string, StoreError]
    fn close(&mut self) -> Result[void, StoreError]
  1. module.resource.closedA closable runtime handle's operations return its module's error type, which has the payload-free variant Closed.
  2. module.resource.closed.afterOnce the handle is closed or disposed, every further operation returns .Err(Closed) and must not trap or access the host resource.
  3. module.resource.close.repeatedClosing is itself an operation, so a repeated close returns .Err(Closed).
  4. module.resource.aliasesThis checked behavior applies through every alias.
  5. module.resource.disposableA closable runtime handle's trait has std.ops.Disposable as a supertrait, so a using binding disposes the handle.
  6. module.resource.disposeDisposing a handle closes it and reports nothing. Disposing a closed handle does nothing.

Why. Closed inside the module's own error lets ? carry a use after close as any other failure of the module, as Go's net.ErrClosed does. A close that can fail stays an explicit call that returns a Result, since dispose! reports nothing.

#Tooling, ABI, And Unsupported Extensions

  1. module.tooling.abiThe exact Wasm component boundary and registration mechanism belong to the runtime ABI.
  2. module.unsupported.visibilityhd-lang has no package-private visibility or independent visibility for enum variants and trait methods.
enum Choice:
    Yes
    No

fn demo() -> Choice:
    Choice.Yes   # variants share the enum's visibility

See also: Command Line, which defines package tooling: package mode, executables, tasks, and the hd commands.