Source files, names, and modules
A tulam source file normally has four layers: a module header, imports, an optional export policy, and declarations. Small single-file experiments may omit the header; project modules may not.
module Geometry.Point;
import Core;
import Math.Algebra (Field, zero);
export (Point, origin, translate);
type Point = x:Float64 * y:Float64;
value origin = Point(0.0, 0.0);
function translate(point:Point, dx:Float64, dy:Float64) : Point =
Point(point.x + dx, point.y + dy);Module identity
Module components begin with capital letters and are separated by dots. Within a project source root, the filename must agree with the module:
src/Geometry/Point.tl → module Geometry.Point;
The project planner rejects mismatches, duplicates, missing imports, and cycles. It builds environments for the complete reachable graph before type checking module bodies, so declarations do not depend on accidental file-loading order.
Imports and exports
Imports are explicit. Importing A does not automatically import everything that A imported.
import Math.Algebra;
import Math.Algebra (Field, zero);
import Math.Algebra hiding (unsafeApproximation);
open Math.Algebra;The parenthesized form selects names; hiding excludes names. open brings all exported names into unqualified scope and is best used sparingly in public libraries. Qualified names remain available when unqualified names would be ambiguous.
An export list defines the public surface:
export (Point, origin, translate);private prevents one declaration from being exported. opaque type exports a type identity while hiding its constructors, allowing clients to use the type only through its public operations.
Naming conventions
- Types, constructors, classes, and module components begin with uppercase:
List,Cons,Text.Buffer. - Functions, values, fields, local variables, and instance tags begin with lowercase or underscore:
map,answer,nextValue. - Symbolic functions are declared parenthesized:
function (+)(x,y) = ....
Identifiers may contain letters, digits, underscores, and apostrophes. A trailing # exists for legacy/compiler bindings; new APIs should use ordinary names and declared intrinsics.
Comments and documentation
// A line comment.
/* A block comment.
Block comments do not nest. */
/// Documentation for the following declaration.
function square(x:Int) : Int = x * x;Documentation comments belong immediately before the declaration they explain. Prefer describing contracts and invariants rather than repeating a name.
Reserved and contextual words
Keywords such as type, function, value, match, and class cannot be used as identifiers. Some words are contextual: on has special meaning in a declaration placement clause, and with after handle; elsewhere they may be ordinary names. resume is introduced only inside resumable handler clauses.
When uncertain, consult Appendix A or let tulam check identify the lexical conflict. Syntax highlighting is helpful, but the compiler is authoritative.
Separators
The rule is simple:
- semicolons separate declarations and sequential statements;
- commas separate data such as arguments, tuple fields, and constraints.
let { x = 1; y = 2; total = x + y } in totalTrailing semicolons are allowed in declaration blocks. Consistent separators make generated code and formatting predictable.
Current implementation
Module headers, filters, visibility, project path validation, and reachable graph planning are implemented. A future separate-compilation phase will add serialized typed interfaces and library artifacts; current project libraries are checked and consumed from source.
Common mistakes
- Do not rely on an import becoming visible through another imported module.
- Keep a project’s module name and source-root-relative path identical.
- Use semicolons between declarations and commas only inside data lists.
Recap
A module has a stable dotted identity, explicit dependencies, and a deliberate public surface. These rules let projects reject ambiguous graphs before type checking begins.
Normative details: Language Reference §2–3.