Skip to content

Source files and packages

A Geas source file is a list of top level declarations: contracts, records, sums, provisional clauses, and imports. A file is a module, and a directory of files is a package.

Source files are UTF-8 and end in .geas. Statements and declarations are terminated by a newline; there are no semicolons. A string literal that is not valid UTF-8 is a compile error, not a silent replacement.

An import names a module by a dotted path:

import std.math
import std.collections

A dotted path resolves beside the root file first and then under GEAS_HOME. A directory is a package and a file is a module within it.

Every imported file parses into one shared tree, with each file’s spans mapped back to its own source, so a diagnostic names the right place even when the fault is in an imported module.

Two conditions on the import graph are named errors rather than silent behavior:

  • A cycle in the import graph is an error. The compiler reports it instead of looping or picking an arbitrary order.
  • A duplicate import is an error. Importing the same module twice from one file is refused, not deduplicated quietly.

A module lends another its record and sum shapes, its error sums, its provisional clauses, and its contracts themselves. A pledge in one module can sign a contract declared in another, fulfill its pledges, and break it. That is how the standard library works: import std.math brings the MathOps contract into scope, and any pledge body can sign it. See Standard library.

Visibility is by package, and it is always stated by the shape of the code, never inferred.

Exported by default. A declaration is exported from its package by default and visible to any importer. There is no pub keyword to remember; the default is open.

internal confines to the directory. A declaration marked internal is invisible outside its own directory:

internal contract Helper {
pledge assist(x: Int) -> Result<Int, CommonError>
}

An internal contract or provisional clause cannot be incorporated, signed, or named in a type annotation from another package. An internal contract never reaches the descriptor tables or the iname table a foreign host discovers, so internal is a real boundary at the ABI, not a style hint.

The boundary holds at every position, including nested ones. An internal type cannot leak through a public signature. If an exported pledge returns Result<Secret, E> and Secret is internal to another package, that is an error, and the same rule applies at any nesting depth: a list of an internal record, an internal sum in the error slot, an internal type inside a tuple. The compiler checks every position where a type can appear.

internal applies to contracts and provisional clauses:

ContractDecl ::= [ "internal" ] "contract" IDENT [ Attrs ] "{" NL { Member NL } "}"
ProvClause ::= [ "internal" ] "provisional" "clause" IDENT "{" NL { ClauseSig NL } "}"

Two builds of one source emit byte identical module C. This is a checked property, not an aspiration: the repository carries a determinism gate that builds twice and compares the output.

Determinism matters here because the module C is the interop artifact. The descriptor tables, the mangled names, and the shape hashes a foreign host resolves against all come out of that emission, so a build that varied from run to run would make the discovery story unreliable. Instead, the same source always produces the same module, the same hashes, and the same names.

ConceptMeaning
FileA module, one .geas file
DirectoryA package, the unit of visibility
import a.bBring module b from package a into scope
ResolutionBeside the root file first, then under GEAS_HOME
Default visibilityExported to any importer
internalConfined to the declaring directory, held at every type position
Import cycleNamed error
Duplicate importNamed error
Build outputByte identical C for identical source
  • The geas CLI covers GEAS_HOME and GEAS_ROOT.
  • Type system covers the types a module declares and shares.
  • Grammar has the full production rules for imports and declarations.