Luce / engineering
Learn Luce LuciaOS

Source loading and modules

Before func can be a keyword, it is four bytes from a file. The source stage proves those bytes are safe UTF-8, gives the file an identity, and remembers enough position information to point back to a bad character later.

In the running example: this stage reads journey.luc and any imports. It does not know what clamp_double means. Its output is trusted text plus a path and coordinate system.

Preparing one source file

  1. Bound the inputReject oversized or binary-looking input before later stages allocate structures proportional to it.
  2. Validate encodingAccept valid UTF-8, ignore a leading UTF-8 BOM, and reject NUL or unsupported encodings.
  3. Normalize line endingsSupported CRLF becomes LF; a stray carriage return is diagnosed rather than guessed around.
  4. Register the fileOwn its bytes, assign a source identity, and build line indexes used by every later diagnostic and trace.

The result is not merely a string. It is text with an identity and a coordinate system. Later stages keep byte spans; the source registry turns those spans into path, line, and column only when a human-facing report needs them.

Loading the module graph

Breadth-first module loadingStandard modules and project modules share one registry
root: main.luc

import std.math
import model

std.math

Embedded source, reached only through the reserved std. namespace.

model.luc

Loaded by the host-provided loader under the project root identity.

A module is loaded once. Cycles between modules are allowed because Luce has no module initialization phase; a semantic cycle that implies an impossible value layout or recursive constant is rejected later by the stage that understands that meaning.

The host supplies files; the compiler supplies meaning

The compiler asks a compile.Loader for source by logical module request. It never opens a path itself. The command-line app can implement that request with a filesystem and manifest; a test can answer from memory; an editor can compile a buffer that has never been saved.

Why this matters: the same language compiler runs in every host context without smuggling filesystem policy into src/luce/. Loading failures still point at the import that requested them because request and source identities live in this stage.

Positions survive every transformation

Tokens carry source spans. AST nodes combine them. HIR records the span of the checked operation. MIR origins freeze source, line, and column onto emitted instructions. Debug artifacts retain those origins for traps; release artifacts may strip line detail while keeping function names.

byte 148source registry
token spanlexer
node spanparser / HIR
originMIR / trace

Decisions kept here

QuestionAnswerReason
Who opens files?The host loaderThe language module stays host-independent.
Who normalizes CRLF?SourceEvery later stage receives one representation.
Can imports initialize in order?No initialization phase existsModule graph cycles are therefore structurally safe.
Can two names load one module twice?NoOpaque root identity and exact resolution prevent accidental duplicate worlds.