Luce Base
LuceSourceLuciaOS

3. Source text

3.1 Encoding#

Source is UTF-8. A byte-order mark is accepted and ignored only at byte zero. NUL bytes, invalid UTF-8, the Unicode bidirectional format characters (U+202A to U+202E and U+2066 to U+2069), and characters confusable with ASCII punctuation are rejected. CRLF is normalised for parsing while source positions keep correct byte and line mappings.

Identifiers are ASCII: a letter or _, followed by letters, digits, or _, at most 128 bytes long. The standalone _ is the pattern wildcard; _unused is an ordinary name. Unicode is fully supported inside text and comments.

3.2 Layout#

A statement suite after : is either one simple statement on the same physical line or a newline followed by a block indented four spaces. Type, enum, union, and interface bodies always use the indented form.

if cached: return result

if image.width > maximum_width:
    let scale = maximum_width / f64(image.width)
    image.resize(scale)
  • A same-line suite cannot contain a compound statement (if, while, for, match, with, asm), a nested suite, or a second statement.
  • Tabs are errors with a machine-applicable replacement. There are no semicolons and no line continuations.
  • Newlines terminate statements except inside (), [], or {}, where they are spacing. Inside delimiters, a : that ends its line opens an indented suite, so a match expression or a catch handler can be written as an argument.
  • A trailing comma is accepted in multi-line parameter, argument, tuple, array, and payload lists.
  • Blank lines do not affect meaning. The formatter, luce fmt, is the single authority on line breaking and indentation.
  • An asm suite (§8.9) is captured raw: its lines are not tokenised.

Why. Base keeps Luce's layout rather than adopting braces because a brace dialect would mean two parsers, two formatters, and a permanent question of which style a file is in. Programmers who know Python already read this layout. The one-line suite form covers the short if x: return cases that are most awkward to write in indented form.

3.3 Comments and documentation#

# An ordinary comment.

## A public summary used by `luce doc`.
## Further paragraphs are Markdown.
pub func area(width: f64, height: f64) -> f64:
    return width * height

# begins a comment outside a string. Consecutive ## comments immediately before a declaration are its documentation. A ## block at the top of a file, separated from what follows by a blank line, documents the module.

A line holding only #: file:line:column is a position directive: the lines after it, until the next directive, were compiled from that position of another source, and a trap on them reports it (§11.5); a bare #: restores the file's own positions. To everything else it is a comment. A compiler that writes Base from another language writes one before each statement it emits, so the program's traps name the author's source.

3.4 Naming#

Types, interfaces, and unions are PascalCase; functions, methods, bindings, fields, cases, modules, and packages are snake_case; acronyms are words, HttpClient. Violations are formatter and linter diagnostics, never a change of meaning.

3.5 Scope#

Names resolve lexically. A module's declarations share one namespace and are order-independent. Members of a type have their own namespace. Locals are sequential; use before declaration is rejected. A local or a parameter may not shadow another visible local, a parameter, an imported name, or a declaration of its module; renaming is the repair. A loop, catch, if let, or match binding owns its nested scope. A loop label (§8.5) lives in its own namespace.

The compiler-known core namespace cannot be redeclared: no declaration that binds a name in a scope, a binding, a parameter, a function, a type, a label, or an alias, may take a core name. A member binds nothing, a field, a method, an interface requirement, or an enum case, since it is reached through its value or its type; nor does an extern function's parameter, which has no body. So they may take any name but a reserved word: a type's print method shadows nothing. A compiler carries exactly this dictionary:

assert error trap print
bool i8 i16 i32 i64 isize u8 u16 u32 u64 usize f16 f32 f64 char str
unit never void fmt Error ErrorCode

The reserved words of §3.6 are excluded the same way, by the lexer. A standard module's name, io or c, binds only where it is imported, so a local named c in a module that does not import c is ordinary. The prelude alone may declare core names, since it is what those names mean. Everything else the compiler answers belongs to a standard module (§16.6) and is reached as its other names are: memory.size_of(T) after import memory, or size_of(T) after from memory import size_of; such a call parses as an ordinary call whose argument may be a type, and only its checked type and meaning are special. luce.location and its pieces are not calls: they are compile-time replacements.

Why. No shadowing removes a refactoring hazard and keeps every diagnostic that names a binding unambiguous. Keeping the core to the language's own four functions and its type names keeps the namespace a program cannot use small: what a C programmer reaches for by habit, sizeof or a formatter, sits in the module it belongs to, as a Python programmer finds math.floor and not a builtin, and a library may call a method format or hash without asking.

3.6 Reserved words#

and as asm break catch const continue defer elif else enum errdefer
extern false for free from func if import in interface let
match new none not or pub recover return self static struct
test true try type union var volatile while with

Contextual words, meaningful only in the positions stated: void before *; local before a module-level var (§6.3); export before a declaration (§17.6); packed, align, naked, weak, used, noinline, cold, section, linked, and inline before a declaration (§9.8); extend before a type's name (§9.5); handle and destroy in a handle declaration (§17.7); inout before an asm operand (§8.9); noalias before a parameter type; blocking and out in extern declarations; reg and options in an asm operand list. Anywhere else each is an ordinary name. class, weak as a field marker, and spawn belong to full Luce and are rejected in Base with a diagnostic that names the tier they belong to. none is a literal (§4.1) and is never a case name.

Why so few. A reserved word is a name no program may use, so a word earns the place only where the grammar cannot do without it. What a method does to its receiver is in its body (§9.5); an allocation is new whatever it leaves in the memory (§12.2); and a word that matters in one position, local or export, is read in that position and nowhere else. static stays: whether a function in a type has a receiver at all is the function's contract with its callers, which a reader should see in the declaration rather than work out from the body.