#Modules
Status: language specification draft.
This chapter defines modules, packages, the prelude, program initialization, entry points, and the Wasm boundary.
- module.kind.moduleModules organize names by source path.
- 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"
- module.manifest.fileAn hd-lang package has an
hd.tomlmanifest. - module.manifest.source-rootThe default source root is
src. - module.manifest.dependencyThe language-level use model assumes that the manifest maps each dependency name to one resolved package.
- module.manifest.no-registryThere is no package registry. A dependency is fetched from the version control repository that its host path names.
- module.manifest.targetsA package may have a library, executables, or both.
- 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
useline. 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"
- module.dep.key-nameEach key of
[dependencies]and[dev-dependencies]names one dependency, which source writes asdep.NAME.NAMEis the key with each-replaced by_, so the keymy-appisdep.my_app. - module.dep.key-name.collisionTwo keys of one manifest whose
NAMEs are equal, such asmy-appandmy_app, are an error. Error:invalid-requirement. - 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 leadingv. - module.dep.path-manifest-onlyA host path appears only in the manifest. Source names a dependency only through its key.
- module.dep.identityA resolved package's identity is its host path and its compatibility line.
- 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.
- module.dep.two-linesTwo compatibility lines of one host path are two dependencies with two keys, as
jsonandjson_v1above. - 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. - module.dep.no-major-suffixA host path carries no major-version suffix such as
/v2.
Why. Go puts
/v2in the path because its imports repeat the path. In hd, source saysdep.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 |
- module.repo.github
github.comis the one known host. On it, the first three segments, the host, an owner, and a repository, name the repository. - module.repo.git-suffixOn any other host, the repository part must end with a segment that ends in
.git. - module.repo.no-discoveryThe toolchain never fetches a web page to discover a repository, as Go's
go-importmeta tag does. - module.repo.subdirectorySegments after the repository part name the directory that holds the package. One repository may hold several packages this way.
- module.repo.credentialsA private repository is fetched with git's own credentials, such as its credential helpers and SSH keys.
- 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 |
- module.version.tagA version is a git tag
vMAJOR.MINOR.PATCH, with an optional SemVer 2.0.0 pre-release suffix. - module.version.tag-prefixA package in a subdirectory uses tags prefixed with that subdirectory's path and
/, such aslint/v2.3.0. - module.version.tag-onlyThe tag is the only version. A manifest does not state its own package's version.
- 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.
- module.version.pseudo.commitIn a pseudo-version,
TIMEis the commit's UTC time asyyyymmddhhmmss, andHASHis the first 12 hexadecimal digits of the commit hash. - module.version.orderVersions, pseudo-versions included, are ordered by SemVer 2.0.0 precedence.
- module.version.lineThe compatibility line of a version is its major number when the major is at least 1, and
0.MINORwhen the major is 0. - module.version.pseudo.releaseA tagged version's manifest may require a pseudo-version.
- 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. - 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 taglint/v2.4.1. Error:unknown-version. - module.version.no-fallbackThe toolchain never falls back to an untagged commit or to a nearby version in place of a missing tag.
- module.version.pseudo-missingA pseudo-version whose
HASHnames no commit of the package's repository, or whoseTIMEis not that commit's time, is an error. A missing tag is an error too. Error:unknown-version. - 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 isunknown-version, bycli.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 |
- module.select.minimumA dependency requirement states a minimum. Its version, or any later version in the same compatibility line, satisfies it.
- 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.
- module.select.dev-dependenciesSelection reads the dev dependencies of the root package or workspace members only. A dependency's dev dependencies are never read.
- module.select.largestFor each host path and compatibility line, the selected version is the largest minimum that any reached manifest states.
- 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.
- module.select.no-lockThe manifests alone determine the selection. There is no lockfile of versions.
- 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.
- module.sum.fileA root package or workspace has an
hd.sumfile beside its manifest. It records a hash of the source tree of each selected version. - module.sum.committed
hd.sumis kept under version control with the manifest. - module.sum.mismatchA fetched tree whose hash differs from its
hd.sumentry is an error. It is never only a warning. Error:sum-mismatch. - module.sum.only
hd.sumis the only integrity source. A build must not require a checksum log, a proxy, or any service besides the repository hosts. - module.sum.path
hd.sumholds 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.
- module.workspace.definitionA workspace is a set of packages that one workspace manifest, an
hd.tomlat the workspace root, lists as members. - module.workspace.committedThe workspace manifest may be kept under version control, so every checkout builds the same graph.
- module.workspace.selectionSelection runs once for the whole workspace, so all members use the same selected versions.
- module.workspace.sumA workspace has one
hd.sum, beside its workspace manifest. - module.workspace.no-host-pathA member does not require another member by host path.
- 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.
- 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.sumamong 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"
- module.path-dep.formA path requirement,
{ path = "DIR" }, names the package in the directoryDIR, relative to the requiring manifest's directory. - module.path-dep.any-package
DIRmay hold any local package. It need not be a member of the requiring package's workspace, and neither package needs a workspace. - module.path-dep.no-packageA path requirement whose
DIRholds no manifest that declares a package is an error. Error:invalid-requirement. - module.path-dep.root-selectionThe selection and
hd.sumof the root package, or of its workspace, hold for every package that a path requirement reaches. That package's ownhd.sumand workspace are not read. - module.workspace.path-versionA path requirement may also carry a version, as in
{ path = "../ui", version = "0.4.2" }. - 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.
- module.workspace.path-version.fetched-versionIn a fetched version of the requiring package,
pathis ignored, and the requirement is a dependency requirement onversion. - 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-pathgives every fetched package its host path. - module.path-dep.fetched-outsideIn a fetched version, a path requirement whose
DIRlies 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.
- module.toolchain.minimumA manifest may state a minimum toolchain version, which is also its minimum
stdversion. - 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. - module.toolchain.pinOnly a root manifest may pin one exact toolchain version.
- 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- module.path.nameA source file's path relative to the source root determines its module name.
- module.path.no-declarationThere is no
moduleorpackagedeclaration 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- module.path.directoryA directory is itself a module only when it contains
mod.hd. - module.path.mod-file
mod.hdis the directory module's source file and public index. - module.path.no-child-importChild modules are not brought automatically into the parent.
- module.path.no-parent-scopeParent declarations are not implicitly visible in children.
- module.path.no-std-child-import
stdfollows the same rule. A std module that wants to expose a child module's items re-exports them withpub use. Afteruse std.testing, the pathtesting.arbitrary.xis an error. Error:unknown-import.
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 |
- module.path.lib-file
lib.hddirectly under the source root,src/lib.hd, is the package root module. Its public declarations are whatpkg.{X}names, and what a dependent names through its dependency key. - module.path.lib-index
src/lib.hdis the root's public index, as a directory'smod.hdis the directory's. - module.path.main-file
main.hddirectly under the source root,src/main.hd, is an entry module and its own program, never part of the package's library. - module.path.main-no-useA use of
src/main.hdfrom another module is an error. Error:unknown-module. - module.path.lib-only-root
src/lib.hdis the only root file of a package's library; the source root itself is never a directory module. - module.path.no-root-modA
mod.hddirectly under the source root,src/mod.hd, is an error, and its message suggests renaming itsrc/lib.hd. Error:reserved-module-name. - module.path.lib-targetA package has a library exactly when it has
src/lib.hd. - module.path.executable-onlyA package without
src/lib.hdis executable-only: no dependent can use its modules. - module.path.no-lib-dependencyDepending on a package that has no library is an error. Error:
invalid-requirement. - module.path.reserved-pkgA module named
pkgdirectly under the source root, assrc/pkg.hdorsrc/pkg/mod.hd, is an error.pkgnames 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 moduleNote.
src/main.hdreaches the library's declarations throughpkgorself, as inuse self.{Config}.
Why. One root file per role, as in Cargo:
src/lib.rsmakes the library, andsrc/main.rsthe default binary.pkgis reserved as Rust reservescrate, so a module never shares the root's name.
#Module Identity
- module.path.uniqueTwo files must not map to the same module identity. Error:
duplicate-module-name. - module.path.case-sourceCase sensitivity of module paths is defined by the source language rather than the host filesystem, by the rules below.
- module.path.identifierEvery non-
mod.hdpath component used in a module identity must be an NFC Unicode identifier under the source identifier rules. Error:invalid-module-path. - module.path.case-sensitiveModule identities are case-sensitive.
- 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. - module.path.suffixThe final
.hdsuffix and the special filenamemod.hdare lowercase and case-sensitive. - 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
- module.test.moduleA source file whose name ends in
_test.hdis a test module, such assrc/billing_test.hd, whose module isbilling_test. - module.test.integrationAn integration test module is a module under the package's test root, which is
testsby default. - module.test.codeTest code is a package's
tests:blocks, test modules, and integration test modules. Only a test build, such ashd testmakes, compiles it. - module.test.code.docThe package's doc tests are test code too.
- 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.
- 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. - module.test.integration.viewAn integration test module sees the package as a dependent package does: its public declarations, built without its test code.
- 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. - 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. - module.test.integration.beside-directoryA file directly under the test root beside a directory of the same name, such as
tests/common.hdbesidetests/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. - module.test.integration.program-useA use of an integration test program from another module is an error. Error:
unknown-module. - 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.
- module.test.integration.shared-unused.lintLint reports an unused shared-test declaration only when no integration test program uses that declaration.
- module.test.integration.pkg-rootIn an integration test module, the
pkgroot names the package's library modules, each with only its public declarations. - module.test.integration.self-sharedAn integration test program uses a shared test module through
self, as inuse self.commonfortests/common/mod.hd. - module.test.no-tests-rootThere is no
testsuse root: test code reaches the test root only throughselfandsuper. A use path that starts withtestsis an error. Error:unknown-module. - 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.hdnever clashes withtests/checkout.hd. - module.test.no-tests-blockA test module or an integration test module must not contain a
tests:block. Error:misplaced-tests-block. - 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. - module.test.non-test-use.test-moduleNon-test code that uses a test module is an error. Error:
test-only-use. - 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. - 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. - module.test.cyclic-dev-allowedIntegration test modules and tasks may use such a dev dependency.
- 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 atests:block.
Note. A task may also use dev dependencies, and the
tasksdirectory shares modules as the test root does, throughself.
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- 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. - module.test.doc.fenceA fence with any other info string, such as
text, is not a doc test, and no build compiles its contents. - 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.
- module.test.doc.usesA doc test's
usedeclarations must come first in its block, and they are the uses of its program. - module.test.doc.bodyThe rest of the block is the body of that test case, as a trailing block given to
itis. - module.test.doc.viewA doc test sees the package as an integration test module does, by
module.test.integration.viewandmodule.test.integration.pkg-root. - module.test.doc.relativeA use path in a doc test that starts with
selforsuperis an error. Error:unknown-module. - 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.
- module.test.doc.requirementsThe runner binds a doc test's requirements by
module.testing.integration-rowandmodule.testing.integration-row.unbound, as for an integration test case. - 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.
- module.test.doc.compile-failA doc test that holds a line comment
# error: CODEis a compile-fail doc test. It never runs, and it passes only when compiling it reportsCODE. - module.test.doc.compile-fail.failsA compile-fail doc test whose compilation reports no diagnostic with that code fails, including when it compiles.
- 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:
# 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 withsnapshot, as insnapshot(slugify("Ship It"), expect="Ship-It").
Note.
hd_run!(command)takes astd.process.Commandand runs only in an integration test module (std-testing.hd-run.integration-only), so a doc test that calls it is atest-only-useerror.
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.
snapshotalready 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 |
- module.root.absoluteEvery absolute use path begins with one of the roots in the table.
- module.root.manifestThe package manifest distinguishes standard library, current package, and external dependency namespaces.
- module.root.dep-prefixSource syntax does not require a different prefix for each dependency beyond
dep.<name>. - module.root.use-onlyOnly a use declaration may begin a path with a root. Only a use brings a module name into scope.
- module.root.use-only.errorSo an absolute path in an expression or a type, such as
std.time.s(seconds)with nouse std.time, is an error. Error:unknown-name.
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.hdRelative 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 |
- module.relative.keywordsRelative use paths use
selfandsuper. - 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. - 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 undersrc, and the test root for one undertests. - module.relative.self.current
selfnames the module where relative lookup starts, soself.xnames its child modulex. - module.relative.superEach leading
supermoves to its parent. - module.relative.root-file.superA
superin a root file is an error. Error:unknown-module. - module.relative.example.nestedConsequently, from
src/user/service.hd,self.typesresolves topkg.user.service.types, insrc/user/service/types.hd, andsuper.typesresolves topkg.user.types, insrc/user/types.hd. - module.relative.example.top-levelFrom
src/a.hd,self.xresolves topkg.a.x, insrc/a/x.hd. A siblingsrc/b.hdissuper.b, sincesuperfrom a file directly undersrc, other than a root file, is the package root. - module.relative.example.mod-fileFrom
src/a/mod.hd,self.xresolves topkg.a.x, insrc/a/x.hd, sinceselfthere is the directory module. - module.relative.example.mainFrom
src/main.hdorsrc/lib.hd,self.xresolves topkg.x, insrc/x.hd. - module.relative.above-rootMoving above the package root is a compile-time error. Error:
unknown-module. - module.relative.no-crossRelative use paths cannot cross into
stdor a dependency. A relative path that names one is an error. Error:unknown-module. - 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. Fromtests/checkout.hd, a root file,self.common.xresolves totests.common.x, intests/common/x.hd. - module.relative.above-test-rootIn an integration test module, a
superthat moves above the test root is an error. Error:unknown-module.
# tests/checkout.hd
use self.common.{expected} # valid: tests/common/mod.hd
use super.common.{expected} # error: unknown-moduleWhy.
selfnames the current module, as Rust'sselfdoes. A file's own child modules then live in the directory of the same name, and a sibling is alwayssuper.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:
use std.process.ExitCode # valid: std is the one root it may use
use self.util.{helper} # error: unknown-module- 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. - module.single-file.rootsIn a single-file program, a use path that starts with
pkg,dep,self, orsuperis 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_typesA grouped use names selected public declarations:
use pkg.user.types.{User, UserId}
use dep.billing.types.{UserId as BillingUserId}- module.use.singleA use may name a single public declaration or a module namespace.
- module.use.groupedA grouped use names selected public declarations.
- module.use.trailing-commaGrouped uses may have a trailing comma.
- module.use.no-wildcardWildcard uses are not supported. A wildcard use is an error. Error:
syntax-error. - module.use.no-variantEnum variants are members, not module declarations, and cannot be used directly. Error:
direct-variant-use. - module.use.private-or-missingUsing a private or missing declaration is a compile-time error.
- 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. - module.use.private-nameA use that names a declaration which its module declares without
pubis an error. Error:private-import. - module.use.pub-groupedOnly the grouped form accepts a
pubprefix. Apubsingle use is an error. Error:syntax-error. - module.use.facadePublic facades expose selected declarations rather than module namespace aliases.
- module.use.whole-moduleUse declarations introduce names for the whole module and are resolved before type checking.
- 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 declarationuse 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-errorNote. A qualified name that reaches a declaration through a module name reports these codes too, by
expr.name.qualified.privateandexpr.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 |
- module.folder.holderA source file's folder is the directory that holds it, except for a file with child modules.
- module.folder.parent-fileA file
x.hdwhose directoryx/beside it holds source files, its child modules, is in folderx/, asx/mod.hdwould be. - module.folder.interchangeableSo
x.hdandx/mod.hdare interchangeable: each is in the folder of its children. - 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.
- module.folder.mod-fileA
mod.hdfile is in the folder of its directory, like the other files there. - module.folder.nestedNested directories are separate folders: a file in
src/shop/ordersis not in foldersrc/shop.
src/shop/mod.hd # module shop, folder src/shop
src/shop/cart.hd # module shop.cart, folder src/shop#Folder Graph
- module.cycle.folder-edgeThe folder graph of a package has an edge from folder
Ato a different folderB. The edge exists when a file inAuses a module inB. Auseorpub useuses the module its path reaches. Souse pkg.shop.{Item}anduse pkg.shop.Itemboth useshop. - module.cycle.same-packageOnly uses of the package's own modules make edges. Uses of
stdand of dependencies make none. - module.cycle.test-codeA use in test code makes no edge.
- module.cycle.nestedA folder and its parent or child folder are separate nodes, and an edge between them counts like any other.
- module.cycle.acyclicThe folder graph must be acyclic. A folder that depends on itself through other folders is an error. Error:
folder-cycle. - module.cycle.within-folderUses between files of one folder make no edge, so those files may use each other in any pattern, loops included.
- 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:
# 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-cycleWhy. 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.hdtosrc/error/mod.hdkeeps the module nameerror, so nouseline changes, and breaks the loop above.
#Cycle Diagnostic
- module.cycle.diagnostic.loopThe
folder-cyclediagnostic must show one shortest loop of folders, with theusedeclaration that makes each edge. - module.cycle.diagnostic.sizeIt must show the size of the tangle: the number of folders that lie on some loop with the shown ones.
- module.cycle.diagnostic.fixIt must offer a fix-it that moves a file
x.hdon the loop tox/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.hdSee 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 |
-
module.prelude.iterator-traitThe prelude name
Iteratornames the cursor traitstd.iter.Iterator[T]. -
module.prelude.standard-surfaceEvery module implicitly has the public standard-library names in the table in scope, but for the
std.testingrow. -
module.prelude.test-assertionsThe names
it,assert, andassert_equalare implicitly in scope only in test code:tests:blocks, test modules, and integration test modules. -
module.prelude.test-only.outsideOutside test code, such a name is unknown, as an undeclared name is. Error:
unknown-name. -
module.prelude.fixed-usesThe prelude is equivalent to fixed
usedeclarations. -
module.prelude.no-authorityThe prelude does not create ambient host authority.
-
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. -
module.prelude.same-useA
usethat names the same declaration the prelude already supplies, under the same name, is allowed and changes nothing.
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
HashOpen in playgroundfn helper() -> void:
it("outside test code"): # error: unknown-name
passWhy. Test code registers cases and checks assertions without repeated imports. A program built without its test code never reaches
std.testingthrough the prelude. Implementer names stay explicit imports.
#Standard Names Outside The Prelude
- module.prelude.importedStandard traits outside this table are imported.
- module.prelude.implementer-imports
Hasherfromstd.hashandPoll,PollContext, andWakerfromstd.taskrequire explicit imports. - module.prelude.from-errorThe conversion trait
std.convert.Fromand the error traitstd.error.Errorare not prelude names. - module.prelude.own-from-errorA module may therefore declare its own
FromorError. - module.prelude.question-fromPostfix
?still finds the standardFromwithout an import. - module.prelude.inspect-reference-apiLikewise
std.inspectdeclaresInspectableandTypeId, which code imports, as inuse std.inspect.{Inspectable, TypeId}. - module.prelude.error-inspectable
std.error.ErrorextendsInspectablewithout its users importing it. - module.prelude.function-items
std.functiondeclares the function type constructorsFnandSuspendFnand the marker traitTuple, which code imports where it writes them, as inuse std.function.{Fn, Tuple}. - module.prelude.function-sugarThe function type sugar
fn(...) -> Tneeds no import. - module.prelude.annotation-targets
std.annotationalso declaresTarget,Annotate, andannotate, which code imports to limit a fact type's target kinds, as inuse std.annotation.annotate. - module.prelude.ops-literal-markers
std.opsdeclaresNumSuffix,num_suffix,StrPrefix,str_prefix, andTemplate, which code imports to declare a literal function, as inuse std.ops.num_suffixoruse std.ops.{Template, str_prefix}. - module.prelude.no-literal-fnThe prelude supplies no literal suffix and no string prefix.
- module.prelude.ops-operator-traits
std.opsalso declares the operator traits, andIndexandIndexSet. Code imports one to name it, as inuse std.ops.Add; operator syntax needs no import. - module.prelude.ops-call-traits
std.opsalso declaresApplyandUpdate, the traits of callable values. Code imports one to name it;v()andv() = xneed no import. - module.prelude.num
std.numdeclares the numeric traitsNum,Integer, andFloat, which code imports, as inuse std.num.Num. - module.prelude.ops-disposable
std.opsalso declaresDisposable, the trait of using bindings. Code imports it to name it, as inuse std.ops.Disposable; ausingbinding 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 useNote. More standard names outside the prelude are stdlib tier. Examples are the string prefix
rofstd.text,FromIteratorofstd.iter, andDurationand its suffixes ofstd.time.
See also: Conversion Trait, Function Type Constructors, Error Trait, Literal Suffixes, Prefixed Strings, Runtime Type Identity.
#Collection Type Names
- module.prelude.collectionsThe built-in collection types are
ListandMap. - module.prelude.capitalizedLike every nominal type outside the primitives, they are capitalized.
- module.prelude.lowercaseThe lowercase names
listandmapare not prelude names; they resolve like any other identifier. - module.prelude.lowercase-unknownA type written
list[i32]is therefore an error unless a declaration in scope supplieslist. Error:unknown-type.
fn count() -> i32:
let values: list[i32] = [1, 2] # error: unknown-type
return 2#Prelude Functions
- module.prelude.panicThe prelude function
panichas the signaturepanic(message: string) -> never. - module.prelude.printlnThe prelude function
printlnhas the signatureprintln[T < Display](value: T) -> void $ Console. - module.prelude.it-functionThe prelude function
itis the test-case function that Test Cases specifies. - module.prelude.debugThe prelude function
debughas the signaturedebug[T < Debug](value: T) -> string, as Debug Trait specifies. - module.prelude.dbgThe prelude name
dbgis the function with the signature that Debug Printing specifies, whose body the compiler supplies.
pub fn main() -> void $ Console:
println("hi") # prelude: no import neededOpen 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- module.dbg.signature
std.formatdeclaresdbgas@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. - module.dbg.checkA
dbgcall is checked by the plain call rules and the vararg rules. Sodbg(),dbg(x), anddbg(a, b, c)collect their arguments intoArgsasfn.vararg.collect.tuple-exprsays. No rule givesdbga type that the declaration does not. - module.dbg.tuple-argument
dbg((a, b, c))is a call with one argument, a tuple, asfn.vararg.no-auto-spreadsays. It prints one line for the tuple, wheredbg(a, b, c)prints one line for each argument. - module.dbg.body.printThe body evaluates the arguments from left to right. It prints the value of each element of
valueson a line of its own when it is evaluated, as Debug Values says. It needs noDebugbound on any element type. A call with a spread orvalues=evaluates every argument first, then prints each element. A call with positional arguments prints each one as it is evaluated. - 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.
- module.dbg.body.valueA
dbgused as a function value, as inlet 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. - module.dbg.no-requirementA
dbgcall needs no requirement and adds none to a row. It is valid in a pure function, in a unit test, and in generic code. - module.dbg.not-behaviorWhat
dbgprints 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. - module.dbg.printer.declaration
std.formatprivately declares@intrinsic fn dbg_printer[T]() -> fn(T, *mut DebugWriter) -> void, with only its body compiler-supplied. - module.dbg.printer.selection
dbg_printer[T]()selects an ordinary hd printer forTaccording to Debug Values. - module.dbg.printer.structureA selected structural printer uses the
Structurederivation machinery without adding public trait implementations toT. - 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 locationWhy. Rust's
dbg!writes to standard error. A print added while chasing a bug must not change a function's signature, sodbgis the one way to print withoutConsole. Its output is for the person debugging, so the program's behavior never depends on it. Returning nothing keepsdbga plain statement: deleting it never changes a type.
#Debug Lines
- module.dbg.lineEach argument of a direct
dbgcall with positional arguments prints one line: the call's location,:, the argument's source text,=, and its value. - module.dbg.line.bareA call that has no per-argument source text, as
module.dbg.body.sitesays, prints one line for each element ofvalues. The line holds the call's location,:, and the element's value. - module.dbg.locationThe location is
FILE:LINE:COLUMN: the file as diagnostics name it, and the line and column where the call begins. - module.dbg.streamA
dbgline goes to the program's debug output, which a program thathdruns 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
- module.dbg.value.debugA value whose static type implements
Debugprints as itsDebugtext, asdebugreturns it. - 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
Debugprints through it. - module.dbg.value.no-boundNeither rule needs a trait bound, so every value prints.
- module.dbg.value.sourceA printed value is hd source wherever its parts have a source form, as
std-format.debug.sourcespecifies. 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. - module.dbg.value.genericInside generic code, a value whose static type mentions a type parameter prints through
Debugwhen that type implementsDebugunder the bounds in scope. One such bound isT < 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 }
accountWhy. A type should not need
Debugbefore it can be inspected, and the person debugging needs every field. A type's ownDebugstill wins, so a type that hides a secret can print it masked.
#Large Values
- 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.
- 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>. - module.dbg.cycle.untrackedOther values, enums included, are not tracked, and print again each time they are reached.
- module.dbg.limit.entriesA list or map prints at most its first 100 entries, then
โฆ N morefor the N entries it leaves out. - module.dbg.limit.depthA part nested more than 10 levels deep prints
โฆ. - 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 moreNote. The limits keep one
dbgcall 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
- module.dbg.releaseA reference to
dbgin 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. - 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. - module.dbg.release.debug-buildA debug or test build accepts a reference to
dbgwith no warning. - 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.
- module.dbg.dependencyIn a package fetched for a version requirement, a
dbgcall prints nothing in every build. It still evaluates its arguments, asmodule.dbg.body.printsays. - module.dbg.dependency.lintA fetched package containing such a call produces one lint finding naming the package. Warning:
dbg-in-dependency. - module.dbg.dependency.function-valueA function value referring to
dbgin a fetched package follows the same suppression and warning rules as a direct call.
Why. A
dbgcall 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
- module.dbg.debug-lintAn expression statement discarding prelude
debugtext produces a lint finding whose message suggestsdbg. Warning:unused-debug-text.
fn trace(prices: List[i32]) -> void:
debug(prices) # warning: unused-debug-textSee 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)-
module.console.host-trait
Consoleis a host capability trait. -
module.console.write-ref.mutable
write!takes&mut self, soConsoleis a mutable requirement trait,$.use(Console)is a&mut dyn Consolebyreq.ref.use.reference-values, and a provider may record what it writes. -
module.console.write-error.raw
write_error!writes its text unchanged to error output, which a host keeps apart from the output ofwrite!. -
module.console.write-error.forwardingThe default
write_error!body callswrite!once with the same text. -
module.console.error
ConsoleErroris its standard boundary-safe error type, andConsoleErrorimplementsDisplay. -
module.console.error.variants
ConsoleErroris a public enum withClosed,NotGranted(subject: string), andOther(message: string). -
module.console.printlnThus
printlnis convenient to name but not a global host API. Each call must be covered by aConsolerequirement row or a lexical provider scope. Error:missing-requirement. -
module.console.println-two-writes
println(value)calls the covering provider'swrite!once withvalue.to_string(), then once with"\n"after the first drive returns.Completed(.Ok(())). -
module.console.println-drive.each-write
printlndrives each write withblock_on, completing the text write before starting the newline write. -
module.console.println-drive.pending-each-writeWhen a poll of either call returns
Pendingon a host write,printlnkeeps driving that call until it finishes. -
module.console.println-non-suspending
printlnstays non-suspending: a call of it is not a bang call and needs no driver context. -
module.console.println-write-error.eitherIf either write returns
.Err(ConsoleError),printlnpanics. -
module.console.println-write-cancelled.remainingIf either drive returns
.Cancelledor.Abandoned,printlnreturns without starting another write, preserving text already delivered. -
module.console.println-write.cancel-source
printlnholds the only reference to each write's suspension, so only the provider can cancel it. -
module.console.println-std
printlnis an ordinary function of the standard library's prelude. -
module.console.println-panicsIts panics are ordinary panics that
stdraises, each with a messagestddefines. No panic category is specific toprintln. -
module.console.println-error.categoryThe
.Errpanic is an ordinarypaniccall instd, so its category isexplicit-panic. Panic:explicit-panic. -
module.console.println-block-on
printlnfollows every rule ofblock_on: its forbidden contexts, the error and the panic there, and its behavior under an active driver. -
module.console.println-block-on.under-driverSo a
printlncall while a driver is active, as inmain!or a test body, writes its line and returns. -
module.console.println-block-on.forbidden-directA
printlncall written directly in adefersuite, 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 alsomissing-requirementbymodule.init.requirement-free.non-script, and the compiler reports both. -
module.console.println-block-on.indirectA
printlncall reached through another call while one of those contexts runs panics. Panic:suspension-forbidden-context. -
module.console.println-scriptA
printlncall at the top level of a script is valid, since only non-entry module initialization bansblock_on. -
module.console.write.raw
write!writes its text unchanged, without adding a line ending. -
module.console.write.signature
Consoledeclaresfn write!(&mut self, text: string) -> Result[void, ConsoleError]. -
module.console.write-error.signature
Consoledeclaresfn write_error!(&mut self, text: string) -> Result[void, ConsoleError]. -
module.console.error.closed-meaning
Closedreports a closed console stream. -
module.console.error.not-granted
NotGranted(subject)reports console access that was not granted. -
module.console.error.other
Other(message)reports another console failure with its message.
pub fn main() -> void:
println("missing") # error: missing-requirement
fn report() -> void $ Console:
defer:
println("done") # error: suspension-forbidden-context
passOpen in playgroundA 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, AdaOpen in playgroundSuspending 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 playgroundWhy. Rust's
println!and Go'sfmt.Printlnare synchronous, so every caller keeps its signature. Rust'sprintln!also panics when the write to standard output fails.printlnis ordinarystdcode that drives each write asblock_ondoes, so it inheritsblock_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.
eprintln, which writes throughwrite_error!, and the input traitConsoleInputare stdlib tier: Console instd.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]- module.process.decl-struct
Processis a host capability trait thatstd.processdeclares, with the struct typesCommandandProcessOutputand the enumProcessError.run!takes itsCommandas a value and returns aProcessOutputvalue. - module.process.run-command
run!(command)starts the program namedcommand.programas the rules below say, and completes when that program exits. - module.process.args-no-shellEach item of
command.argsis one argument of the program. No shell reads the arguments, so no item is split, expanded, or unquoted. - module.process.envThe program starts with the environment of the program that calls
run!, then each pair ofcommand.env, in order: a later pair for a name replaces an earlier one. - module.process.current-dirThe program starts in
command.current_dirwhen it is.Some, a relative path being from the working directory of the program that callsrun!, 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. - module.process.stdinThe program reads
command.stdinfrom its standard input, and finds the end of its input after it. - module.process.named-outputWhen the program starts,
run!returns.Okof aProcessOutputholding its stdout, stderr, exit status, and program name. - module.process.named-output-defaultsThe fields
stdout,stderr, andprogramof aProcessOutputdefault to"", andstatusto 0. - module.process.nonzero-okA non-zero exit status is still an ordinary
.Okresult; the caller reads it fromstatus. - module.process.not-found-program
run!returns.Err(ProcessError.NotFound(program))when its provider has no program namedprogram. - module.process.deniedIt returns
.Err(ProcessError.PermissionDenied(program))when the program exists but the operating system does not allow starting it. - module.process.not-grantedIt returns
.Err(ProcessError.NotGranted)when its provider refusesprogrambecause the program's run-time grant does not cover it. - module.process.otherIt returns
.Err(ProcessError.Other(message))when the program cannot start for any other reason, with a message that says why. - module.process.error-traits
ProcessErrorimplementsEq,Debug,Display, andError. - module.process.mut-ref
run!takes&mut self, soProcessis a mutable requirement trait, asConsoleis,$.use(Process)is a&mut dyn Process, and a provider may record what it runs. - module.process.names
Process,Command,ProcessOutput, andProcessErrorare not prelude names; code imports them fromstd.process. - module.process.output-programA provider fills
ProcessOutput.programfrom the command'sprogramfield. - module.process.node.windows-launcherA Windows Node host requires an explicit
processLauncherpath to anhdexecutable for itsProcessprovider. - module.process.node.windows-no-launcherWithout that path,
run!returns.Err(ProcessError.Other(message))with a message naming the missingprocessLauncher. - 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 thatNotGrantedreports 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, theDisplaytext of eachProcessErrorvariant, and the stdlib providerScriptedProcessare stdlib tier: Process instd.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 aResult, 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
- module.prelude.any-subtraits
AnyValandAnyRefare the two sealed marker subtraits ofAny. - module.prelude.anyref-members.reference-values
AnyRefis implemented by reference types, trait value references included, suspensions and runtime handles that have identity. - module.prelude.anyref-excludes
AnyRefis not implemented by primitives, tuples, enums (optionals included), or functions. - module.prelude.anyval-members
AnyValis implemented by exactly the primitives,string, tuples (voidincluded), enums (optionals andResultincluded), structs,List,Map, function types, and newtypes whose base type implementsAnyVal. - module.prelude.newtype-categoryA newtype implements
AnyRefexactly when its base type does. - module.prelude.any-sealedUser code cannot implement either.
fn demo[T < AnyRef](value: T) -> bool:
value is valueSee 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 |
- module.method.normativeThe built-in methods in the table are normative.
- module.method.changing-refThe methods of the changing rows take
&mut selfand 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. - module.method.iter-cursorA list's or a map's
iteris itsIterablemethod. Each call returns a new cursor value: astd.iter.ListIter[T]for a list and astd.iter.MapEntries[K, V]for a map. - module.method.usize-sizesLengths, indices, and byte offsets use
usize. - module.method.no-setNo
settype is part of the core prelude. - module.method.list-pop
popremoves the last element of the list and returns it as.Some. - module.method.list-pop.empty
popon an empty list returns.Noneand leaves the list unchanged. - module.method.list-insert
insert(index, value)putsvalueatindexand moves each element fromindexon one place up. - module.method.list-insert.end
insertatlen()appendsvalue, aspushdoes. - module.method.list-insert.rangeAn
insertindex greater thanlen()is a checked runtime panic. Panic:index-out-of-bounds. - module.method.list-remove-at
remove_at(index)removes the element atindex, moves each later element one place down, and returns the removed element. - module.method.list-remove-at.rangeA
remove_atindex that is not less thanlen()is a checked runtime panic. Panic:index-out-of-bounds. - module.method.list-clear
clearremoves every element, solen()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,popandclearchange the list, not an iterator taken from it before the change, which iterates the list asiter()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
- module.map.complexityMap lookup, insertion, and removal take expected amortized O(1) time.
- module.map.complexity.operationsThis covers
get,remove, readingentries[key], and inserting or replacing throughentries[key] = value. - module.map.complexity.stepEach call of the key type's
HashorEqimplementation 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- module.string.utf8String methods operate on valid UTF-8 strings without locale.
- module.string.byte-offsetsEvery position that a string method takes or returns is a byte offset.
- module.string.chars
charsyields the string's scalar values in order, each as achar. - module.string.char-indices
char_indicesyields each scalar value in order, as(offset, char), whereoffsetis the byte offset at which its encoding starts. - module.string.bytes
bytesyields the string's bytes in order, each as au8. - module.string.cursors
chars,char_indices, andbytesreturn new cursor values of thestd.texttypesChars,CharIndices, andBytes, which implementIterator. Advancing one needs a writable binding, as for any cursor. - module.string.slice
slice(start, end)returns the bytes from offsetstartup to but not including offsetend, in constant time. - module.string.slice.sharedThe result shares the original string's bytes rather than copying them.
- 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. - module.string.slice.reversedA
startgreater thanendis a checked runtime panic, even when both are scalar boundaries. Panic:index-out-of-bounds.
Note.
char_indicesgives the offsets that Go'srangeover a string gives.text[start..end]gives the same substring astext.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- module.testing.exports
std.testingexports the normative assertion functionsassertandassert_equalwith the signatures above. - module.testing.reason
reasonis required and must explain the checked condition. - module.testing.assert-panicA failed assertion causes a runtime panic, inside a test case or not. Panic:
assertion-failed. - module.testing.shows-reasonA failed
assertorassert_equalshows itsreason. - module.testing.uses-eq
assert_equalusesEq.eq. - module.testing.no-implicit-eq
assert_equaldoes not grant implicit equality to its argument type. An argument type withoutEqis an error. Error:unsatisfied-trait-bound. - module.testing.assert-equal-debug
assert_equalalso requiresT < Debug, and a failure shows both values asdebugrenders them. A type withoutDebugis an error. Error:unsatisfied-trait-bound.
@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- module.testing.it-function
itis an ordinary function with the signature above, whichstd.testingdeclares and the prelude supplies. Each call in test position registers one test case. - 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.
- module.testing.it.bodyThe body has type
fn!() -> T $ RwithT < std.process.Termination, so a trailing block body is a suspending closure. - module.testing.it.nameThe name must be a string literal without interpolation. Any other name is an error. Error:
non-literal-test-argument. - module.testing.it.options-stringsThe named options are those of the signature above:
ignore,expect_panic, andtimeout. Anignoreorexpect_panicvalue must be a string literal without interpolation. Any other value for them is an error. Error:non-literal-test-argument. - module.testing.it.unknown-optionAny other named argument is an error. Error:
unknown-named-argument. - module.testing.test-positionTest position is the top level of a
tests:block, of a test module, or of an integration test module. - module.testing.position-statementsEvery statement in test position must be a test registration call: a direct call of
it,it_each,it_prop, orit_prop_with(Registration Calls). Any other statement is an error. Error:invalid-test-statement. - module.testing.direct-call
it,it_each,it_prop, andit_prop_withmay 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. - 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. |
- module.testing.option.expect-panic.knownAn
expect_panicvalue that names no panic category is an error. Error:unknown-panic-category.
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-caseNote.
itis a prelude name, so a module cannot declare, use, or bind anotherit(Prelude). A call spelledit(...)always registers a test case, and a tool can list test cases without running them.
Why.
itis 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
timeoutparameter ofithas typestd.time.Duration?.Durationis 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 $ Rit_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- module.testing.reg.calls-ref
it_each,it_prop, andit_prop_withtake test registration calls with the signatures above;std.testingdeclares them. The test-position rules ofitapply to them. - module.testing.reg.importNone of the three is a prelude name. Code imports them from
std.testing, as inuse std.testing.it_each. - 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.
- module.testing.reg.identity.spellingsSo a bare
it_each(...), a call of a renamed import such asuse std.testing.{it_each as each}, andtesting.it_each(...)afteruse std.testingall callit_each. - module.testing.reg.identity.allThe same holds for
it,it_prop, andit_prop_with.itis a prelude name, and code may also import it under another name or call it astesting.it. - module.testing.reg.row-body-closureThe body of
it_eachhas a parameter, so it is an explicitfn!closure, not a trailing block. After omitted options, a call passes it by name, as inbody=fn!(value: i32): .... - module.testing.reg.row-name-clashAnother test case of the module must not be named
name[i]for any indexiof anit_eachcall's rows. Error:duplicate-test-name. - module.testing.reg.prop-registersA call of
it_proporit_prop_within test position registers one property test case. - module.testing.reg.nameThe name of an
it_each,it_prop, orit_prop_withcall must be a string literal without interpolation. Any other name is an error. Error:non-literal-test-argument. - module.testing.reg.optionsEach takes the options of
it,ignore,expect_panic, andtimeout, under the same rules. - module.testing.reg.body-result.reference-valuesThe body's result follows Propagation In Test Blocks:
void, orResult[void, *dyn Error]when it uses?. - module.testing.reg.body-closure-result.reference-valuesThe body closure of an
it_each,it_prop, orit_prop_withcall that writes no result type gets its result type byexpr.try.test.with-try.reference-valuesandexpr.try.test.without-try. A trailing block given toitgets its result type the same way. - module.testing.reg.prop-debug
it_propandit_prop_withrequireT < Debug. A property whose input type does not implementDebugis an error. Error:unsatisfied-trait-bound.
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-argumentEvery spelling of a registration function registers test cases:
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-caseNote.
Arbitrary,Choices, andDurationare 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([])- module.testing.instanceEach test case runs in its own fresh program instance, after module initialization.
- module.testing.no-reuseInstances are not reused between test cases.
- module.testing.drivenThe runner drives the body's suspension to completion, as the host drives
main!. - 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. - 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 holdTestRunnerand no other key, so every other requirement comes from a$.withprovider scope. Error:missing-requirement. - module.testing.unit-row.anywhereA unit test is a test case in a
tests:block or a test module. Rulemodule.testing.unit-row.test-runnerholds for it wherever its file lies. - 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 getsTestRunneralone. - 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.
- module.testing.profileA test run compiles against one runtime profile, the default profile unless the run selects another.
- 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
mainthe same way. - 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. - module.testing.integration-row.unbound.errorOtherwise checking reports
missing-requirementwith the hintbind it with $.with(Trait=...). - module.testing.passesA test case passes when its body completes, its result holds no
.Err, andreport()on its result returnsExitCode(0). - module.testing.fail-casesIt fails when its result holds an
.Err, whenreport()returns another code, or when its body panics, including by a failed assertion. A case ofit_eachorit_propfollows the same rules. - 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. - 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, andmodule.entry.err-sources-to-stderrdoes not apply. - module.testing.expect-panic-failWith
expect_panic, the test case instead fails when its body completes or panics with another category.
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-requirementWhy. 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
TestRunnerstill 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.
TestRunneris a stdlib-tier trait (Runner Capabilities). The language tier names it in these rules only and specifies none of its methods.
Note. The
missing-requirementerror in a unit test suggests a std fake for the missing trait, such asManualClockforClock(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- module.testing.snapshot.literalAn
expectargument must be a string literal without interpolation. Any other value is an error. Error:non-literal-test-argument.
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-argumentWhy. A literal
expectlets a tool rewrite it in place. So an update run records a new or changed expectation, and an emptyexpectis 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:
use std.rt.memory.{grow, load_u32} # error: std-only-use
pub fn main() -> void:
_ := grow(1)Open in playground- module.rt.modulesThe runtime modules are
std.rtand every module whose path begins withstd.rt.. - module.rt.std-onlyOnly a module of the standard library may use a runtime module, or a name declared in one.
- 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- module.rt.memory.declEach function of the block is an ordinary hd declaration marked
@intrinsic, and the compiler supplies only its body, as fordbg. - module.rt.memory.heapThe heap is an array of bytes in one program instance, at the
usizeaddresses from 0. Its size is a whole number of pages of 65,536 bytes, and it never shrinks. - module.rt.memory.pages
pages()returns the heap's size in pages. - module.rt.memory.grow
grow(count)addscountpages at the heap's end and returns.Some(before), wherebeforeis the heap's size in pages before the call. Every added byte is 0. - module.rt.memory.grow.refusedWhen the heap cannot grow by
countpages,grow(count)returns.Noneand the heap keeps its size. A heap never holds more than 65,536 pages. - module.rt.memory.grow.ownerThe pages that a
growcall adds belong to its caller. No other part of the program reads or writes them. - module.rt.memory.load
load_u8,load_u16,load_u32, andload_u64return the 1, 2, 4, or 8 bytes fromaddressas an unsigned integer, least significant byte first. - module.rt.memory.store
store_u8,store_u16,store_u32, andstore_u64write the bytes ofvaluefromaddress, least significant byte first. - module.rt.memory.unalignedA load or a store may use any address, aligned or not.
- module.rt.memory.copy
copy(to, from, count)writes thecountbytes fromfromto thecountbytes fromto. The two ranges may overlap: the bytes written are the ones read before the call. - module.rt.memory.fill
fill(to, byte, count)sets each of thecountbytes fromtotobyte. - 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- module.rt.ptr.type
Ptr[T]is an ordinary struct. Itsaddressis the heap address of the first ofsize_of[T]()bytes that may hold a value of typeT. - module.rt.ptr.copyA copy of a
Ptr[T]copies the address. It never copies, counts, or drops the value at the address. - module.rt.ptr.decl
read,write,offset, andsize_ofare ordinary hd declarations marked@intrinsic, as for the memory intrinsics. - module.rt.ptr.size-of
size_of[T]()returns the number of heap bytes one value ofTtakes. It is the same for every value ofTin a program instance. - module.rt.ptr.size-of.numberFor
bool,u8, andi8it is 1, foru16andi162, foru32,i32,f32, andchar4, and foru64,i64, andf648. Forusizeit is the target's pointer width in bytes. - module.rt.ptr.offset
p.offset(count)returns thePtr[T]whose address isp.address + count * size_of[T](). - module.rt.ptr.write
p.write(value)stores a copy ofvalueatp, so the heap there holds a value ofT. The copy shares and counts references as any copy of aTdoes. - module.rt.ptr.write.overA write replaces the bytes at
pwithout dropping the value they held. - module.rt.ptr.read
p.read()moves the value atpout and returns it, so the heap there no longer holds a value. Its bytes do not change. - 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.
- module.rt.ptr.holdsOnly the standard library reads or writes through a
Ptr[T], and it reads only where the heap holds a value ofT. - 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. - module.rt.buffer.typeA
Buffer[T]is a counted heap object with room forcapacity()values ofT, of which it holds the firstlen(). Its fields are private. - 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. - module.rt.buffer.new
Buffer::new(capacity)returns a new buffer whose capacity iscapacityand whose length is 0. A capacity whose bytes do not fit the heap panics. Panic:heap-exhausted. - module.rt.buffer.at
b.at(index)returns thePtr[T]of valueindexof the buffer's room:b.at(0).offset(index). It checks no bound; reads and writes through it followmodule.rt.ptr.holds. - module.rt.buffer.len
b.set_len(len)sets the length thatb.len()returns and changes no value. Before it raises the length, std writes each new value throughat; before it lowers the length, std reads each value past the new length out throughat. - 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. - module.rt.string.bytes
string_bytes(text)returns thePtr[u8]of the first of thetext.len()bytes oftext's UTF-8 encoding. The bytes stay at that address whiletextor a copy of it lives. - module.rt.string.new
new_string(len)returns a new string oflenbytes, whose values are unspecified until written.new_string(0)returns the empty string. - module.rt.string.writeOnly the bytes of a string that
new_stringreturned are written throughstring_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::readandptr::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'srawstringandunsafe.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.
- module.init.scriptA script is an entry module whose top-level executable statements are the entry behavior and which has no
maindeclaration. - 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.
- module.init.entry.statements-before-mainAn entry module with
mainruns requirement-free top-level initialization beforemain; it is not a script. - 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 scriptOpen in playground#Initialization Order
- 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.
- 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.
- module.init.cycle.onceEvery reachable cycle is initialized exactly once per program instance, after every other cycle it uses has been initialized.
- 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- 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.
- 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.
- module.init.cycle.earliestStatements are ordered by fully qualified module identity, then by source position.
- 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
- module.init.declarationsWithin one module, named declarations are available before initialization.
- 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.
- module.init.usesUse declarations do not execute as statements.
- module.init.bindingTop-level bindings are initialized at their statement, before later function bodies may access them.
- module.init.storageTheir storage remains available to functions in that module for the lifetime of the program instance.
- 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
- 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. - module.init.definite.implicitReferences passed as values and functions reached by trait dispatch, interpolation, iteration, or another implicit call are included.
- 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.
- module.init.definite.whole-moduleThis definite-initialization check covers the whole module value-flow and call graph.
- module.init.definite.localThe check is local to one module.
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
- module.init.script-bodyAfter dependency initialization, a script executes its top-level statements as that module's initialization.
- 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, andhdreplays no statement. The top level of any other module is not a driver context, also when it is an entry module that has amain. - 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. - module.init.script-try.reference-valuesA
?at a script's top level leaves the script asmain!leaves with aResult[void, *dyn Error]. The operand is aResultwhose error type implementsError. An.Erris then handled by Entry Results: the host prints it, and the script exits with the status of itsreport(). A?on an optional there is an error. Error:invalid-result-propagation. - module.init.mainAn executable package then invokes
mainafter its entry module has initialized. - module.init.testsTest runners initialize the module under test and the modules it uses before running its test cases.
- module.init.test-casesThe
itcalls of atests:block or a test module, and their bodies, are not part of module initialization. - module.init.script-emptyAn entry module with no
mainand no top-level executable statements is a script with no entry behavior. Running it does nothing and exits with status 0. - 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.
- 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.
# 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 ofmain. The runner binds no host for initialization, so a tested script keeps its work inmain, 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
- module.init.requirement-free.non-scriptTop-level code in non-entry modules and entry modules with
mainmust be requirement-free initialization. Error:missing-requirement. - 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. - 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 callWhy. Only script entry behavior has the default profile. Requirements declared by
maindo not authorize its earlier initialization. Requirement-freeblock_onremains permitted in entry initialization.
#Program Instances
- module.init.per-instanceThis initialization rule governs one program instance.
- 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- module.vis.private-defaultDeclarations are module-private by default.
- module.vis.pubPrefix
pubmakes 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}- module.pub-use.facade
pub usein amod.hdfile exposes a package-facing API, whose names another module can use through the directory module. - module.pub-use.public-sourceA publicly used declaration must already be public in its defining module.
- module.pub-use.binding
pub useintroduces the same local binding asuseand additionally exposes that binding to other modules. - module.pub-use.identity
pub usedoes not create a new declaration identity. - module.pub-use.chainA
pub usechain must end at a declaration. Following eachpub useof a name to the module it names must reach the module that declares the name. - module.pub-use.chain.loopA chain that returns to a
pub useit has already passed is an error. Error:re-export-loop. - module.pub-use.chain.loop-useA plain
usewhose name leads into such a loop is an error with the same code. Error:re-export-loop.
# 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-loopWhy. Loops of
uselines are allowed inside a folder, so a facade and its children may use each other. A name still needs one declaration.
#Member Visibility
- module.vis.no-package-privateThere is no package-private visibility modifier: another module in the same package can use only
pubdeclarations. - module.vis.membersNamed fields and inherent methods are module-private unless individually marked
pub. - module.vis.variantsEnum variants inherit their enum's visibility.
- module.vis.trait-methodsTrait methods follow their trait's visibility.
- module.vis.impl-targetA usable implementation additionally requires its target type to be visible.
struct User:
pub name: string
id: i32#Public Signatures
- module.vis.signatureA public declaration's complete source-level signature must not expose a module-private declaration. Error:
private-type-leak. - 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.
- module.vis.bodyA private implementation detail may occur in a public function body but not in its public typed interface.
struct Secret:
value: string
pub fn reveal() -> Secret: # error: private-type-leak
Secret { value: "hidden" }See also: Field Visibility.
#Name Resolution Across Packages
- module.package.identityAn absolute qualified name identifies one declaration after dependency resolution.
- module.package.no-sharingTwo dependencies do not share declarations merely because their package names and source paths match.
- module.package.identity-partResolved package identity is part of a declaration's identity.
- module.package.public-surfaceA package must not access another package except through declarations reachable from that package's public module surface.
- module.package.separateImplementations may compile packages separately.
use std.json.parse#Fully Annotated Declarations
- module.package.annotatedEvery public declaration is fully annotated: its complete signature is written in source.
- 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.
- module.package.no-inferenceNothing in a public signature is inferred from a function body.
- module.package.no-pub-bindingTop-level bindings cannot be public. A
pubbinding is an error. Error:syntax-error. - module.package.template-privateA private item that a template body names follows the signature rules of a public declaration.
- 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. - module.package.template-private.rowSuch a function without a requirement clause has the empty row, as
req.row.omitted.empty-pubstates for a public function. - module.package.template-private.no-bindingA template body must not name a top-level binding. Naming one is an error. Error:
template-names-binding.
pub answer := 42 # error: syntax-errortrait 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-bindingHere 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 |
- module.interface.contentsA package interface must contain every item in the table.
- A hidden item is in the interface, but only the code of an instantiated template may name it. Downstream source cannot name it.
- module.interface.generic-bodiesAn interface may also carry ordinary generic bodies to enable inlining, but downstream compilation must not require them.
- module.interface.generic-compilationHow generic code is compiled across packages is not observable, as
types.generic.unobservablesays. An interface fixes no strategy for it. - module.interface.fact-expressionsA package interface records each fact of its declarations by the fact's expression and type, not by its value.
- 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.
- 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.
- 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.
- module.interface.coherenceCoherence is checked at link time over the complete set of resolved interface files.
- 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:
...- module.entry.definitionAn entry point is a public top-level function named
mainormain!with no parameters. - module.entry.result-terminationIts result type must implement
std.process.Termination, as for an ordinary trait bound, so it may bevoid,ExitCode, orResult[T, E]withT < TerminationandE < Error. Any other result type is an error. Error:unsatisfied-trait-bound. - module.entry.rowIt may declare a requirement row.
- 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. - module.entry.private-mainA top-level
mainthat is not public is an ordinary function and is not an entry point. - module.entry.suspendingA suspending entry point is spelled
main!. - module.entry.private-main.lintA private top-level
mainormain!in an entry module produces the lint message "main is not pub, so it is not the entry point". Warning:private-main. - module.entry.parametersA public top-level
mainormain!in an entry module that declares parameters or generic parameters is an error. Error:entry-point-parameters. - module.entry.private-main.fixThe
private-mainwarning must offer a fix-it insertingpubbefore the function keyword. - module.entry.private-main.fix.conflictWhen adding
pubcould clash with another publicmain, the fix-it must remain a suggestion.
fn main() -> void: # warning: private-main
passOpen in playgroundpub fn main(args: List[string]) -> void: # error: entry-point-parameters
passOpen in playgroundWhy. Lint suggests adding
pubbefore an execution request needs an entry point. Checking still accepts private helper functions namedmain.
See also: Mutable Providers.
#Entry Results
- module.entry.exit-reportWhen an entry point returns, the process exits with the
u8held by theExitCodethatreport()returns for its result, as Exit Status describes. - module.entry.err-host-printsWhen the result holds an
.Err, the host prints that error by the rules below before it exits. - module.entry.err-dynamic.termination
Result[T, *dyn Error]is a valid entry result whenT < Termination, since erasedErrorsatisfiesE < Error. - module.entry.err-render-chainThe host prints the error's
Displaytext. Then it prints each cause that the standard-librarychainyields after it. - module.entry.err-render-chain.source-lineEach source is printed on its own line as
source:followed by the source'sDisplaytext. - module.entry.err-sources-to-stderrWhen an entry point returns
.Err(e), the host writes the text andsource:lines ofeto standard error. Then it exits with the status thatreport()returns, as Exit Status describes. - module.entry.panicA panic exits with a distinct nonzero status selected by the runtime profile and poisons the program instance.
pub struct HiddenError: pass
pub fn main() -> Result[void, HiddenError]: # error: unsatisfied-trait-bound
.Err(HiddenError {})Open in playgroundSee 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- module.entry.exit-code
std.processdeclares the newtypeExitCode, whoseu8is a process exit code. Everyu8is valid, and 0 means success. - module.entry.termination
std.processdeclares the traitTermination, whose one method isfn report(self) -> ExitCode. - module.entry.termination.void
voidimplementsTermination; itsreportreturnsExitCode(0). - module.entry.termination.exit-code
ExitCodeimplementsTermination; itsreportreturns the code itself. - module.entry.termination.result
Result[T, E]implementsTerminationwhenT < TerminationandE < Error. - module.entry.termination.okFor
.Ok(value), itsreportreturnsvalue.report(). - module.entry.termination.err-oneFor
.Err(_), itsreportreturnsExitCode(1). - module.entry.termination.no-print
reportonly computes the code. The host or test runner prints the error, as Entry Results describes. - module.entry.process-import
ExitCodeandTerminationare not prelude names; code imports them fromstd.process, as inuse 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 playgroundA 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 playgroundThis main prints usage: tool FILE and exits with status 2.
Why. One trait serves entry points and tests, as Rust's
Terminationdoes. An exit code concerns the command-line entry, not the error, soErrorhas no code.mainpicks codes in one place, as a Rustmaindoes.
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
- module.entry.no-arguments
mainhas no source-level arguments. - 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()
_ := argsOpen 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
- 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.
- 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&selfor&mut selfmethod, the provider is a trait value reference, byreq.ref.host.reference-values. - module.profile.buildThe compiler receives the selected profile as build configuration.
- module.profile.defaultThe default profile contains at least the prelude
Consoletrait. - module.profile.otherAnother profile may add or omit host traits explicitly.
- module.profile.toolchain-namesProfile names are defined by the toolchain. A manifest cannot define a profile.
- module.profile.panic-statusA profile's panic exit status is never 101.
Why.
hdexits 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 ConsoleOpen in playground#Host Results
A profile's boundary adapter checks every value that comes in from the host before hd code sees it:
- 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 |
- module.profile.host-result.no-coercionThe adapter never rounds, truncates, wraps, or otherwise converts a host value to make it fit.
- 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. - 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
300for au8has 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:
- module.profile.host-float.rawA call of a host capability trait's method passes each
f32orf64argument as a raw IEEE 754 value of that width. It returns each such result the same way. - module.profile.host-float.specialSo a NaN, both infinities, and
-0.0cross the call unchanged, in either direction. - 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 valuesWhy. 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, whichmodule.boundary.float-bitscovers.
#Registration
- module.register.pub
pubis module visibility, not Wasm export registration. - module.register.explicitA tool, workflow, or other host-callable function becomes visible only through explicit registration provided by its library.
- module.register.entryEvery registered boundary function is an entry point for provider checking.
- module.register.contract-traitsIts registration contract selects a runtime profile and declares which requirement traits that profile can bind.
- module.register.applicationThat bindable set may include application traits such as
Databasewhen the adapter explicitly supports them. - 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.
- 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 registrationOpen in playground#Boundary-Safe Values
Registered boundaries initially allow recursively structural values:
- primitive scalar types and
string; - tuples;
List[T]andMap[K, V]whose contents are boundary-safe;- structs and enums whose complete fields and payloads are boundary-safe;
T?andResult[T, E]whose contained types are boundary-safe.
- module.boundary.allowedRegistered boundaries initially allow the recursively structural values in the list above.
- module.boundary.not-safeDynamic trait values (including
Inspectablevalues),std.inspect.TypeId, closures, and live runtime handles are not boundary-safe. - module.boundary.rowsRequirement-row entries are host bindings and are not serialized parameters.
- module.boundary.erased-errorIn particular, the erased error
std.error.Errornever crosses a registered boundary. - 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-safeNote. An error type with a member of type
Erroris therefore not boundary-safe either. Before such an error crosses a boundary, code converts it with the standard-libraryreport_ofto anErrorReport, a boundary-safe snapshot of its message and causes.
See also: Error Trait.
#Boundary Encoding
- module.boundary.treeBoundary values have tree semantics.
- module.boundary.cycleEncoding a cycle is a boundary error. Boundary failure:
boundary-cycle. - 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.
- module.boundary.sharingWhen an acyclic graph shares a node, each incoming path encodes a duplicate tree value, and decoding does not restore sharing.
- module.boundary.map-decodeDecoding a map invokes the key type's ordinary
EqandHashimplementations.
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- module.boundary.decoder-panicIf either panics, the adapter reports a boundary failure and does not enter the registered function. Boundary failure:
boundary-decoder-panic. - module.boundary.decoder-poisonThe adapter treats that event as an ordinary poisoning panic: the program instance must be discarded.
- module.boundary.float-bitsA boundary value or host-call result serialized as text, as in a replay record, writes an
f64as its IEEE 754 bit pattern. That is 16 lowercase hex digits, most significant first. - module.boundary.float-bits.f32An
f32is written the same way, as 8 lowercase hex digits. - 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.0has 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):
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- module.boundary.outA value of a struct type with a field that is not
pubcrosses a boundary out of hd only when the type implementsstd.serde.Serialize. - module.boundary.inSuch a value crosses a boundary into hd only when the type implements
std.serde.Deserialize. - module.boundary.serialize.exactA value of a type that implements
std.serde.Serializecrosses out of hd as exactly what itsserializewrites: private fields the implementation does not write do not cross. - module.boundary.deserialize.exactA value crosses into hd only as built by its type's
std.serde.Deserializeimplementation; adeserializeerror is ahost-contractpanic the caller sees. - module.boundary.deserialize.no-dynDeserialization at a boundary never selects a type from the data: the target type comes from the signature.
- module.boundary.publicA struct type whose fields are all
pubcrosses in either direction without either trait. An enum's payloads have no visibility, so they need neither trait. - 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.
- 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
serializedecides what leaves hd, and only itsdeserializecan 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, andInstantimplement both traits, so aClockprovider's results cross into hd (Time).
#Instances And Threads
- module.instance.threadOne program instance executes on one thread and has no shared-memory parallelism or source-level atomics.
- module.instance.parallelHosts may run multiple Wasm instances in parallel only by exchanging boundary-safe values.
- 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
- module.host.wasmThe official compiler targets Wasm only.
- module.host.rc-runtimeThe official runtime manages values with reference counting and cycle collection and uses a WASI-compatible host boundary.
- module.host.providersAuthority-bearing providers originate at that boundary.
- module.host.boundary-abiHow values, strings included, cross the host boundary is an implementation detail, not part of the language.
- module.host.tooling-hooksHooks that a toolchain adds for development, testing, or tracing are implementation tooling, not a language boundary.
- module.host.requirementsEvery host facility is injected through an ordinary requirement trait.
- module.host.capability-setThe eventual standard capability-trait set is runtime and library work.
- 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
externdeclaration 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, bymodule.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- module.host.package.markerBare
@hostmarks a package-owned public trait as a host capability trait. - module.host.package.target
@hoston any other declaration or member is an error. Error:decorator-target-kind. - module.host.package.bareArguments and type arguments on
@hostare errors. Error:decorator-target-kind. - module.host.package.fact
@hostis a compiler marker and attaches no fact value. - module.host.package.identityA fetched host trait's nominal identity includes its canonical package host path, exact selected version, and package-relative trait path.
- module.host.package.local-identityA local host trait's nominal package identity hashes its canonical directory, so moving that directory requires rebinding.
- module.host.package.fakeA package host trait uses ordinary requirement rows,
$.use, and$.withreplacement. - module.host.package.js-valuesJavaScript package providers receive and return JavaScript values rather than serialized text.
- module.host.package.js-integersA structural 64-bit integer crosses as
BigInt, and a narrower structural integer crosses asNumber. - module.host.package.js-private-serdeA private-field value crosses through its directional
SerializeorDeserializeimplementation, with Serde integers represented asBigInt. - module.host.package.js-private-floatsA Serde float crosses as
Numberwhile retaining its numeric kind and finite IEEE 754 bits, including negative zero.
@host() # error: decorator-target-kind
pub trait Surface:
fn render!(&mut self, commands: List[string]) -> voidSee 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]- module.resource.closedA closable runtime handle's operations return its module's error type, which has the payload-free variant
Closed. - 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. - module.resource.close.repeatedClosing is itself an operation, so a repeated close returns
.Err(Closed). - module.resource.aliasesThis checked behavior applies through every alias.
- module.resource.disposableA closable runtime handle's trait has
std.ops.Disposableas a supertrait, so ausingbinding disposes the handle. - module.resource.disposeDisposing a handle closes it and reports nothing. Disposing a closed handle does nothing.
Why.
Closedinside the module's own error lets?carry a use after close as any other failure of the module, as Go'snet.ErrCloseddoes. A close that can fail stays an explicit call that returns aResult, sincedispose!reports nothing.
#Tooling, ABI, And Unsupported Extensions
- module.tooling.abiThe exact Wasm component boundary and registration mechanism belong to the runtime ABI.
- 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 visibilitySee also: Command Line, which defines package tooling: package
mode, executables, tasks, and the hd commands.