slovo/docs/language/SPEC-v0.md
2026-05-22 08:38:43 +02:00

1744 lines
59 KiB
Markdown

# Slovo v0 Specification
> This is not a complete language specification.
>
> This is the supported v0 contract for the first Slovo parser, formatter,
> checker, diagnostics, test runner, and LLVM-oriented compiler prototype.
---
## 1. Status
Version: `v0`
File extension: `.slo`
Primary backend target: **LLVM IR**
Primary design goals:
- parse Slovo forms
- format Slovo forms
- type-check a small core
- run tests
- lower simple programs toward LLVM IR
- produce structured diagnostics
Compiler-supported fixtures for the frozen v0 contract:
```text
examples/supported/add.slo
examples/supported/top-level-test.slo
examples/supported/local-variables.slo
examples/supported/if.slo
examples/supported/while.slo
examples/supported/struct.slo
examples/supported/array.slo
examples/supported/option-result.slo
examples/supported/unsafe.slo
```
For strict-manifest iteration1, "compiler-supported" means the form is parsed,
lowered, type-checked, emitted as LLVM IR or handled by its required tool mode,
and covered by an automated Glagol test.
This file uses "current" only inside the v0 boundary. Post-v0 promotions live
in `SPEC-v1.md` and may expand the repository's supported surface without
changing the frozen v0 contract.
Supported v0 subset, as exercised by the v0 fixtures:
- one `(module name)` form
- top-level `(fn ...)` forms
- top-level `(struct Name (field i32)...)` forms
- top-level `(test "name" body... final-expression)` forms, where body forms
are optional local declarations, assignments, or first-pass loops
- explicitly typed `i32` function parameters and `i32` return values
- direct constructor-return functions with `(option i32)` and
`(result i32 i32)` return types under section 14
- integer literals used as `i32` values
- function parameter references
- binary integer addition with `+`
- equality comparison `=` as a bool-producing test expression
- ordering comparison `<` as a bool-producing `i32` condition
- user-defined function calls
- the temporary compiler intrinsic `(print_i32 value)` call as a statement-like
expression returning builtin `unit`
- local `i32` bindings with `(let name i32 value)` and `(var name i32 value)`
- assignment to mutable local `i32` bindings with `(set name value)`
- local references after declaration
- value-producing `(if condition then-expression else-expression)`
- first-pass `(while condition body...)` as a non-final sequential body form
- first-pass struct constructor expressions and immediate field access
- first-pass fixed `i32` array constructors, immutable array locals, and
literal checked indexing
- first-pass `i32` option/result constructors used only as direct function
returns
- lexical `(unsafe body... final-expression)` expression blocks whose body
forms and final expression are otherwise supported safe v0 forms
- final-expression function returns
Implementation-recognized forms are not automatically supported language
features. A form remains a design target until it has parser/lowerer behavior,
checker behavior, backend behavior or explicit unsupported diagnostics,
formatter behavior where applicable, and tests.
All examples under `examples/speculative/` are v0 design targets, not current
compiler-supported fixtures.
Formatter fixtures under `examples/formatter/` are canonical-layout fixtures for
the same strict supported syntax. They are not new language features and must
not include design-target forms.
Top-level `(test "name" body... final-expression)` is supported only in the
strict form defined in section 16.
Non-goals for v0:
- macros
- generics
- concurrency
- advanced ownership
- package manager
- full standard library
- direct x86/ARM backend
---
## 2. Source Files
A Slovo source file uses the `.slo` extension.
A source file contains one top-level module form.
```slo
(module main)
```
A practical file usually contains:
```slo
(module main)
(fn main () -> i32
0)
```
---
## 3. Lexical Syntax
Whitespace separates tokens but has no semantic meaning.
Tabs and spaces are equivalent.
Line comments begin with `;`.
```slo
; this is a comment
(+ 1 2)
```
A form is a parenthesized tree:
```slo
(name arg arg arg)
```
Identifiers may contain letters, digits, `_`, `-`, `?`, `!`, and selected symbolic operator names.
String literals use double quotes.
```slo
"Hello, Slovo"
```
Integer literals:
```slo
0
42
-7
```
Current compiler-supported integer literals are signed `i32` values in the
inclusive range `-2147483648` to `2147483647`. Glagol must reject literals
outside that range with `IntegerOutOfRange` before narrowing them into the AST.
Floating-point literals:
```slo
3.14
-0.5
```
Floating-point literals are a design target, not current compiler-supported
syntax.
---
## 4. Top-Level Forms
Current compiler-supported top-level forms:
```text
module
fn
test
struct
```
Design-target top-level forms:
```text
import
```
`test` is supported only as the top-level form described in section 16.
`struct` is supported only as the first-pass top-level form described in
section 12. `import` remains a broader design target.
---
## 5. Types
Current compiler-supported scalar type:
```text
i32
```
First-pass struct declarations introduce nominal struct types as described in
section 12. Those types are supported only as constructor temporaries consumed
by immediate field access; they are not valid current supported function
parameter types, function return types, or local declaration types.
First-pass fixed arrays are specified in section 13. `(array i32 N)` is a
current compiler-supported immutable local declaration type only when the local
is initialized directly from an `(array i32 value...)` constructor with matching
length. It is not a current supported function parameter type or function
return type.
First-pass option/result constructors are specified in section 14. `(option
i32)` and `(result i32 i32)` are current compiler-supported function return
types only for functions whose single body expression is the matching direct
constructor. They are not current supported function parameter types or local
declaration types.
`print_i32` status: `print_i32` is a temporary compiler intrinsic in v0. It is
not yet specified as a user-defined function, standard-library binding, runtime
binding, import, or foreign function. Programs may use `(print_i32 value)` only
as part of the current strict compiler subset described in section 1.
`unit` status: `unit` is a valid internal/builtin result type for unit-like
expressions such as `print_i32` and the supported `(set name value)` form.
It is not a user-declarable supported function return type until Glagol supports
parsing, checking, lowering, formatting, diagnostics, backend behavior, and
tests for user-declared `unit` returns.
Design-target primitive types:
```text
bool
i8 i16 i64
u8 u16 u32 u64
f32 f64
char
string
unit
never
```
Broader design-target compound types:
```slo
(ptr T)
(array T N)
(slice T)
(option T)
(result T E)
```
The first-pass `(array i32 N)`, `(option i32)`, and `(result i32 i32)` cases
above are the only promoted compound-type exceptions. Generic, nested, and
non-`i32` compound-type payloads remain design targets.
---
## 6. Functions
Function form:
```slo
(fn name ((arg Type) ...) -> ReturnType
body...)
```
Example:
```slo
(fn add ((a i32) (b i32)) -> i32
(+ a b))
```
v0 rules:
- parameters are explicitly typed
- return type is explicit
- body contains one or more expressions
- the last expression is the return value
- all control paths must produce the declared return type
- current supported fixtures use `i32` returns, except for the direct
option/result constructor-return functions described in section 14
---
## 7. Variables
Status: supported for explicit `i32` local bindings in function bodies and
top-level test bodies.
Immutable binding:
```slo
(let name type value)
```
Mutable binding:
```slo
(var name type value)
(set name value)
```
Supported implementation scope:
- Explicit local types are required in v0.
- `i32` locals are supported with `let` and `var`; immutable `(array i32 N)`
locals are supported only under the first-pass array contract in section 13.
- Local declarations are allowed only inside function bodies, top-level test
bodies, and promoted lexical unsafe blocks contained by those bodies.
- Local declarations and `set` forms are sequential body forms before the final
result expression.
- A function or test body must still have a final result expression.
Binding rules:
- `(let name type value)` creates a new immutable local binding initialized from
`value`.
- `(var name type value)` creates a new mutable local binding initialized from
`value`.
- The initializer is checked before the new local is introduced, so a binding
may not refer to itself.
- The initializer expression must exactly match the explicit local type.
- The explicit local type must be `i32`.
- Local bindings are visible only after their declaration and only within the
containing function body, test body, or lexical unsafe block. Bindings from
an outer function or test body remain visible inside a nested unsafe block.
- Lexical unsafe blocks introduce the only current nested local scope. `if`
branches and first-pass `while` bodies do not introduce local declaration
scopes.
Shadowing and redeclaration:
- v0 forbids shadowing in local bodies.
- A local binding name must not match any parameter name in the same function.
- A local binding name must not match any earlier local binding in the same
function or test body.
- A local binding name must not match a top-level function name or compiler
intrinsic visible in the same module.
- Parameters are immutable for v0 and are not valid `set` targets.
- Top-level function names are not local bindings. A local reference resolves to
a parameter or local binding; a call resolves to a callable top-level
function or compiler intrinsic unless a later namespace rule says otherwise.
Assignment:
- `(set name value)` assigns an existing mutable local binding.
- `set` produces builtin `unit`.
- `set` may appear only as a non-final sequential body form, because supported
function/test results are not `unit`.
- The assigned expression must exactly match the mutable local's type.
- Assigning an unknown name is an `UnknownVariable` diagnostic.
- Assigning a parameter is a `CannotAssignParameter` diagnostic.
- Assigning an immutable `let` binding is a `CannotAssignImmutableLocal`
diagnostic.
- Assigning a value with the wrong type is a `TypeMismatch` diagnostic.
- Duplicating a local name is a `DuplicateLocal` diagnostic.
- Redeclaring a parameter as a local is a `LocalRedeclaresParameter`
diagnostic.
- Colliding with a visible function or compiler intrinsic is a
`LocalShadowsCallable` diagnostic.
- Declaring any non-`i32` local is an `UnsupportedLocalType` diagnostic.
- Using a local declaration in an inline expression is a
`LocalDeclarationNotAllowed` diagnostic.
Typed-core meaning:
```text
LocalLet {
name,
type: i32,
init: TExpr<i32>,
mutable: false,
span,
name_span,
type_span,
init_span
}
LocalVar {
name,
type: i32,
init: TExpr<i32>,
mutable: true,
span,
name_span,
type_span,
init_span
}
SetLocal {
target: LocalId,
value: TExpr<i32>,
type: unit,
span,
name_span,
value_span
}
```
Lowering:
- `let` and `var` lower to typed local storage or SSA values at the
implementation's discretion, provided source order, mutability checks, and
diagnostics are preserved.
- `set` lowers to an assignment/store to the resolved mutable local.
- A local that is never read may be accepted in v0; unused-local warnings are
outside the v0 required diagnostics.
Formatter behavior:
```slo
(fn add_then_double ((a i32) (b i32)) -> i32
(let sum i32 (+ a b))
(var doubled i32 (+ sum sum))
(set doubled (+ doubled 1))
doubled)
```
The formatter must print each local declaration and `set` as one body form
indented by two spaces. Simple `let`, `var`, and `set` forms stay inline when
their initializer or assigned expression is an inline expression under the
current expression formatter. The final body expression follows the same
two-space indentation and may refer to earlier locals.
---
## 8. Expressions
Slovo v0 is expression-oriented.
Most forms produce a value.
Current compiler-supported expressions are the expression forms required by the
supported fixtures: integer literals, parameter references, local references,
local declarations, local assignment, binary `+`, user function calls,
temporary intrinsic `print_i32` calls, equality comparison in tests, ordering
comparison in loop conditions, value-producing `if`, first-pass `while`, struct
construction with immediate field access, fixed `i32` array construction,
literal array indexing, first-pass option/result constructors, lexical
`unsafe` blocks containing otherwise supported safe body forms, and
final-expression returns.
Struct construction and immediate field access are supported under the strict
first-pass contract in section 12. Array construction, immutable array locals,
and literal checked indexing are supported under the strict first-pass contract
in section 13. Option/result constructors are supported under the strict
first-pass direct-return contract in section 14. Lexical `unsafe` blocks are
supported under the strict first-pass contract in section 15.
Arithmetic:
```slo
(+ a b)
(- a b)
(* a b)
(/ a b)
```
Comparison:
```slo
(= a b)
(< a b)
(> a b)
(<= a b)
(>= a b)
```
v0 rules:
- operands must have compatible exact types
- numeric casts are explicit
- comparisons return `bool`
Status note: arithmetic and comparison beyond the supported fixture are design
targets until covered by the strict support rule in section 1.
The top-level `test` contract in section 16 introduces an expected-`bool`
context, but it does not promote any expression form by itself. A promoted
test fixture must use only bool-producing expressions that also satisfy the
strict support rule, such as equality comparison once it is accepted as part of
a promoted fixture.
---
## 9. Canonical Formatter
Status: current strict supported syntax only.
The formatter is part of the Slovo language contract. For the current supported
subset, canonical output follows these rules:
- use LF line endings and end every file with one trailing newline
- remove trailing whitespace
- use spaces, not tabs
- put top-level forms at column 1
- keep `(module name)` on one line
- separate top-level forms with exactly one blank line
- print each supported top-level `fn` as a multi-line form
- print the function header on one line as
`(fn name ((arg Type) ...) -> ReturnType`
- print an empty parameter list as `()`
- keep supported parameter lists inline as `((name i32) (name i32))`
- indent each function body expression by two spaces
- print one supported body expression per line
- keep supported expression forms inline: integer literals, parameter
references, local references, local declarations, `set`, binary `+`,
user-defined function calls, `(print_i32 value)`, and first-pass
option/result constructors
- keep nested supported calls inline, for example `(print_i32 (add 20 22))`
- place the closing `)` for a function on the final body-expression line when
that expression is inline
- preserve full-line comments attached to the nearest following form, without
reflowing comment text
Canonical current fixture:
```slo
(module main)
(fn add ((a i32) (b i32)) -> i32
(+ a b))
(fn main () -> i32
(print_i32 (add 20 22))
0)
```
Formatter rules for `let`, `var`, and `set` are part of the current supported
formatter fixture set. The `if` formatter contract and first-pass `while`
formatter contract are also part of the current formatter fixture set.
The first-pass formatter rules for `struct` are also part of the current
formatter fixture set. Section 13 specifies the first-pass array/index
formatter contract, and `examples/formatter/array.slo` is the canonical
formatter fixture for that promoted subset. Section 14 specifies the
first-pass option/result constructor formatter contract, and
`examples/formatter/option-result.slo` is the canonical formatter fixture for
that promoted subset. Section 15 specifies the lexical `unsafe` formatter
contract, and `examples/formatter/unsafe.slo` is the canonical formatter
fixture for that promoted subset. Formatter rules for strings, slices, broader
option/result value flow, pointer types, raw memory operations, and general
user-declared `unit` returns remain design targets.
Top-level `test` formatter contract:
```slo
(test "add works"
(= (add 2 3) 5))
```
The formatter must print each top-level test as a multi-line top-level form,
keep the string name on the opening line, indent the single test expression by
two spaces, and place the closing `)` on the expression line when that
expression is inline. Test forms follow the same top-level blank-line and
comment attachment rules as `fn`.
---
## 10. Conditionals
Status: compiler-supported value expression through `examples/supported/if.slo`
under the strict support rule in section 1.
Conditional form:
```slo
(if condition then-expression else-expression)
```
v0 rules:
- condition must be `bool`
- both branches must have the same type
- `if` returns that type
- `if` is an expression form and may be used where its result type is expected.
- Branches are expressions, not declaration scopes. A local declaration inside
an `if` branch remains unsupported in this pass.
- The formatter prints `if` as a multi-line expression:
```slo
(if (< value 3)
10
20)
```
Diagnostics:
- `MalformedIfForm`: missing condition, then expression, else expression, or an
invalid operand shape. Span: whole `if` form. Expected:
`(if condition then-expression else-expression)`.
- `IfConditionNotBool`: checked condition type is not `bool`. Span: condition
expression. Expected: `bool`. Found: checked condition type.
- `IfBranchTypeMismatch`: then and else branches do not have the same checked
type. Span: whole `if` form. Expected: then-branch type. Found:
else-branch type.
---
## 11. Loops
Status: compiler-supported first-pass contract through
`examples/supported/while.slo` under the strict support rule in section 1.
First-pass v0 loop form:
```slo
(while condition
body-form...)
```
Surface placement:
- `(while condition body-form...)` is a sequential body form.
- It is allowed only inside function bodies and top-level test bodies.
- In the first supported implementation, it may appear only before the final
result expression of supported `i32` functions and `bool` tests.
- It is not valid as an inline expression argument.
- It is not valid as the final result expression of a supported `i32` function
or `bool` test, because it returns builtin `unit`.
Checking rules:
- The condition is checked in the current local environment and must have type
`bool`.
- The condition is re-evaluated before each iteration.
- A false initial condition executes zero body iterations.
- The body must contain one or more body forms.
- `while` returns builtin `unit`.
- The first implementation does not introduce nested local-scope semantics.
Local declarations inside a `while` body are not supported.
- Supported first-pass loop body forms are limited to assignments to existing
mutable locals with `(set name value)` and supported unit-producing calls such
as `(print_i32 value)`.
- Loop body forms must each check as `unit`.
- Existing locals visible before the `while` are visible in the condition and
body. Assignments in the body update the existing mutable local.
- `break` and `continue` are not part of this pass.
- Nested `while` is not part of the first-pass support target.
Typed-core meaning:
```text
While {
condition: TExpr<bool>,
body: [TBodyForm<unit>],
type: unit,
span,
condition_span,
body_spans
}
```
Lowering:
- A `while` lowers to a condition block, a body block, and an exit block.
- Control enters the condition block first.
- If the condition evaluates to true, control enters the body block; after the
body completes, control branches back to the condition block.
- If the condition evaluates to false, control branches to the exit block.
- The exit continuation has builtin `unit` as the loop result and then
continues with the next sequential body form.
- Locals assigned in the body must preserve source-order mutation semantics.
The implementation may use storage slots or equivalent SSA construction, but
it must not treat the loop body as a new declaration scope in this first pass.
Formatter behavior:
```slo
(fn sum_to ((n i32)) -> i32
(var i i32 0)
(var sum i32 0)
(while (< i n)
(set sum (+ sum i))
(set i (+ i 1)))
sum)
```
The formatter must print `while` as a multi-line body form. The opening line
contains `(while` and the formatted condition. Each loop body form is printed on
its own line, indented two spaces deeper than the `while` form. The closing `)`
is placed on the final body-form line when that body form is inline. The
following function or test body form resumes at the original body indentation.
Diagnostics:
- `MalformedWhileForm`: missing condition or an invalid operand shape. Span:
whole `while` form or offending operand. Expected:
`(while condition body...)`.
- `WhileConditionNotBool`: checked condition type is not `bool`. Span:
condition expression. Expected: `bool`. Found: checked condition type.
- `EmptyWhileBody`: a `while` has no body forms after the condition. Span:
whole `while` form or the location where the first body form was expected.
- `LocalDeclarationInWhileBodyUnsupported`: a `let` or `var` appears directly
in a first-pass `while` body. Span: offending local declaration. Hint:
declare the local before the loop and update an existing mutable local with
`set`.
- `NestedWhileUnsupported`: a nested `while` appears directly in a first-pass
`while` body. Span: nested `while`. Hint: keep first-pass loop bodies flat.
- `WhileBodyFormNotUnit`: a loop body form does not produce `unit`. Span:
offending body form. Expected: `unit`. Found: checked body-form type.
- Existing diagnostics such as `UnknownVariable`, `CannotAssignParameter`,
`CannotAssignImmutableLocal`, `TypeMismatch`, `ArityMismatch`, and
`UnsupportedBackendFeature` apply inside `while` conditions and bodies.
- Using a `while` as the final expression of an `i32` function must report
`ReturnTypeMismatch` with expected `i32` and found `unit`.
- Using a `while` as the final expression of a top-level test must report
`TestExpressionNotBool` with expected `bool` and found `unit`.
Minimal comparison dependency:
The promoted first fixture for `while` uses `<` to produce the loop condition.
Promoting `while` does not by itself promote the broader comparison family or
unrelated arithmetic. The supported comparison surface for this pass is the
exact `i32, i32 -> bool` `<` form exercised by the fixture, with parser/lowerer
support, checker behavior, LLVM behavior, formatter behavior, and tests.
---
## 12. Structs
Status: compiler-supported first-pass contract through
`examples/supported/struct.slo` under the strict support rule in section 1.
Promotion boundary:
- The supported fixture may use only top-level `i32` struct declarations,
constructor expressions, and field access returning `i32`.
- Local struct storage is a follow-up. The first pass does not support
`(let p Point ...)`, `(var p Point ...)`, assigning a struct value, passing a
struct as a parameter, or returning a struct from a function.
- Methods, generics, field mutation, nested struct fields, recursive structs,
struct params/returns, layout reflection, cross-module structs, and ABI
promises are explicitly outside the first pass.
Struct definition:
```slo
(struct Point
(x i32)
(y i32))
```
Struct declarations are top-level forms, sibling to `fn` and `test`.
Surface rules:
- The form is `(struct Name (field i32)...)`.
- `Name` is a normal identifier.
- Each `field` is a normal identifier.
- A struct must declare one or more fields.
- First-pass fields must be exactly `i32`.
- Field order is source order and is part of the v0 typed-core contract.
- Struct names are nominal. Two structs with the same fields are not the same
type.
- A struct name must be unique in the module and must not collide with a
top-level function name or compiler intrinsic visible in constructor
expression position.
- Field names must be unique within the struct.
- A struct declaration is not an expression and produces no runtime value.
Constructor expression:
```slo
(Point (x 3) (y 4))
```
Constructor rules:
- The callee position must be the declared struct name.
- Each constructor field is written as `(field value)`, not as a keyword token.
- Constructor fields must list every declared field exactly once.
- Constructor fields must appear in declaration order for the first pass.
- Each field value is checked in source order and must exactly match the
declared field type. Since first-pass fields are `i32`, each value must check
as `i32`.
- Constructor evaluation preserves field value evaluation order.
- A constructor produces a value of the nominal struct type.
- In the first promotion target, a struct constructor may be used only as the
immediate value being inspected by field access, for example
`(. (Point (x 3) (y 4)) x)`. Wider struct value flow requires the follow-up
local/signature/storage contract.
Field access:
```slo
(. p x)
```
First-pass examples should use immediate constructor access:
```slo
(. (Point (x 3) (y 4)) x)
```
Field access rules:
- The form is `(. value field)`.
- `field` is a normal identifier, not a keyword token or string.
- `value` must check as a known struct type.
- The named field must exist on that struct type.
- The expression result type is the field type. In the first pass this is
always `i32`.
- The value expression is evaluated once.
- Field access is read-only. There is no field mutation form in the first pass.
Checking rules:
- The checker collects and validates top-level struct declarations before
checking function and test bodies in the same module.
- Struct declaration checking validates top-level placement, identifier shape,
module-level name uniqueness, field-name uniqueness, and `i32` field types.
- Constructor checking resolves the callee name in the struct namespace, then
checks constructor field names, field order, field completeness, duplicate
fields, extra fields, and each field value type.
- Field access checking first checks the value expression, then resolves the
requested field against the value's nominal struct type.
- A struct constructor or other struct-typed expression outside direct field
access is not part of the first pass. It must report
`StructValueFlowUnsupported`, except for the more specific local and
signature diagnostics below.
- Existing expected-type diagnostics apply after field access. For example,
`(. (Point (x 3) (y 4)) x)` checks as `i32` and may be used anywhere an
`i32` expression is currently supported.
Typed-core meaning:
```text
StructDecl {
name: StructId,
fields: [StructField],
span,
name_span
}
StructField {
name: FieldId,
type: i32,
index: FieldIndex,
span,
name_span,
type_span
}
StructConstruct {
struct_id: StructId,
fields: [(FieldId, TExpr<i32>)],
type: Struct(StructId),
span,
name_span,
field_spans
}
StructFieldAccess {
value: TExpr<Struct(StructId)>,
field_id: FieldId,
type: i32,
span,
value_span,
field_span
}
```
Lowering:
- A struct declaration lowers to type metadata in the checked module, not to a
runtime declaration by itself.
- A constructor lowers to an aggregate value or equivalent typed-core value with
fields in declaration order.
- Field access lowers to extracting the selected field from that aggregate.
- For the first promotion target, an implementation may lower direct
constructor field access without materializing addressable struct storage.
- No stable LLVM, C, FFI, or cross-module ABI layout is promised in v0.
- If a checked struct form reaches a backend gap, Glagol must report
`UnsupportedBackendFeature` instead of panicking.
Formatter behavior:
```slo
(struct Point
(x i32)
(y i32))
(fn point_x () -> i32
(. (Point (x 3) (y 4)) x))
```
The formatter must print each top-level `struct` as a multi-line top-level
form. The opening line is `(struct Name`; each field declaration is printed on
its own line indented two spaces; the closing `)` is placed on the final field
line. Top-level blank-line and comment attachment rules match `fn` and `test`.
Constructor expressions with inline `i32` field values stay inline as
`(Name (field value)...)`. Field access stays inline when its value expression
is inline. If a future formatter must break a constructor over multiple lines,
the constructor opening line is `(Name`, each `(field value)` pair is indented
two spaces, and field order remains declaration order.
Diagnostics:
- `MalformedStructForm`: missing name, non-identifier name, missing fields, or
invalid field shape. Span: whole `struct` form or offending operand.
Expected: `(struct Name (field i32)...)`.
- `StructDeclarationNotTopLevel`: a `struct` form appears anywhere other than
the top level. Span: offending `struct` form.
- `EmptyStructUnsupported`: a struct declares no fields. Span: whole `struct`
form. Hint: declare at least one `i32` field.
- `DuplicateStruct`: duplicate struct name in the same module. Span:
duplicate name, with a related span on the original struct name.
- `StructNameConflictsCallable`: struct name collides with a top-level function
name or compiler intrinsic visible in constructor position. Span: struct
name, with a related span when the colliding source name exists.
- `DuplicateStructField`: duplicate field name within one struct. Span:
duplicate field name, with a related span on the original field name.
- `UnsupportedStructFieldType`: field type is not `i32`. Span: field type.
Expected: `i32`. Found: written field type.
- `MalformedStructConstructor`: constructor operand shape is invalid. Span:
whole constructor or offending field pair. Expected:
`(Name (field value)...)`.
- `UnknownStruct`: constructor name does not resolve to a struct. Span:
constructor name.
- `DuplicateStructConstructorField`: constructor lists a field more than once.
Span: duplicate constructor field, with a related span on the first use.
- `MissingStructField`: constructor omits a declared field. Span:
whole constructor. Expected: missing field name.
- `UnknownStructField`: constructor or field access names a field not declared
by the struct. Span: extra constructor field name or accessed field name.
- `StructConstructorFieldOrderMismatch`: constructor field order differs from
declaration order. Span: first out-of-order constructor field. Expected:
declared field name at that position. Found: written field name.
- `TypeMismatch`: constructor field value does not match its declared field
type. Span: field value. Expected: declared field type. Found: checked value
type.
- `MalformedFieldAccess`: field access is not `(. value field)`. Span: whole
access form or offending operand.
- `FieldAccessOnNonStruct`: field access value does not check as a struct type.
Span: value expression. Expected: struct value. Found: checked value type.
- `UnsupportedStructLocal`: a struct type is used as a local `let` or `var`
type in the first pass. Span: local type.
- `UnsupportedStructSignatureType`: a struct type is used as a function
parameter or return type in the first pass. Span: signature type.
- `UnsupportedStructFieldAccess`: field access uses a non-immediate struct
constructor value in the first pass. Span: field access expression.
- `FieldMutationUnsupported`: an implementation that accepts a generalized
assignment target must reject field mutation in the first pass. Span:
attempted field assignment target.
---
## 13. Arrays and Checked Indexing
Status: strict first-pass compiler-supported syntax, promoted through
`examples/supported/array.slo` and `examples/formatter/array.slo`.
Promotion boundary:
- The first pass supports only fixed-length arrays with `i32` elements.
- Array lengths must be positive integer literals. Zero-length arrays are not
supported in this pass.
- Array constructors produce immutable temporary array values.
- Immutable `let` locals may store `(array i32 N)` values only when initialized
directly from a matching array constructor.
- The first promoted fixture may index only an immediate array constructor or
an immutable array local with a non-negative integer literal index known at
compile time.
- Dynamic indices are not supported in this pass, so v0 does not yet define a
runtime bounds trap or test-runner trap result for arrays.
- Mutable array locals, array parameters, array returns, array mutation, nested
arrays, arrays of structs, slices, and unchecked indexing are follow-up work.
Fixed array type:
```slo
(array i32 N)
```
Type rules:
- `N` is a source integer literal in type position, not an expression.
- `N` must be greater than `0`.
- The only first-pass element type is `i32`.
- `(array i32 N)` is supported only as an immutable local declaration type in
the first pass. Function parameter and return types remain unsupported.
- `(array T N)` with any `T` other than `i32` is rejected by the first pass.
Array constructor expression:
```slo
(array i32 value...)
```
Constructor rules:
- The first operand after `array` is the element type and must be `i32`.
- At least one value is required.
- Each value is checked in source order and must check exactly as `i32`.
- The constructor length is inferred from the value count.
- The constructor result type is `(array i32 count)`.
- In an expected-type context, a constructor of type `(array i32 count)` is
valid for `(array i32 N)` only when `count == N`.
- Constructor value evaluation order is left to right.
Checked index expression:
```slo
(index array-expr index-expr)
```
First-pass supported example:
```slo
(index (array i32 10 20 30) 1)
```
```slo
(let values (array i32 3) (array i32 4 5 6))
(index values 2)
```
Index rules:
- The form is exactly `(index array-expr index-expr)`.
- `array-expr` must check as `(array i32 N)`.
- For the first promotion target, `array-expr` must be an immediate array
constructor or an immutable array local.
- `index-expr` must be a non-negative `i32` integer literal in the first pass.
- The literal index must satisfy `0 <= index < N`.
- The result type is `i32`.
- The array expression is evaluated once.
- Because accepted first-pass indices are compile-time literals, bounds
checking happens during checking. A dynamic checked-index form must be
rejected until runtime trap behavior is specified and tested.
Typed-core meaning:
```text
ArrayType {
element: i32,
length: NonZeroUsize,
span,
element_span,
length_span
}
ArrayConstruct {
element: i32,
length: NonZeroUsize,
values: [TExpr<i32>],
type: Array(i32, length),
span,
element_span,
value_spans
}
ArrayIndex {
array: TExpr<Array(i32, length)>,
index: usize,
type: i32,
span,
array_span,
index_span
}
```
Lowering:
- An array constructor lowers to an aggregate value or equivalent typed-core
value with elements in source order.
- A first-pass index lowers to extracting the compile-time constant in-bounds
element from that aggregate.
- An immutable array local lowers to implementation-owned storage sufficient
for literal indexing inside the current function or test body.
- The backend must not generate unchecked dynamic array access for this
contract. Dynamic indices remain unsupported until runtime checked-index trap
behavior is specified.
- For the first promotion target, an implementation may lower immediate
constructor indexing without materializing addressable array storage, and may
lower immutable local indexing through local aggregate storage.
- No stable LLVM, C, FFI, or cross-module ABI layout is promised in v0.
- If a checked array form reaches a backend gap, Glagol must report
`UnsupportedBackendFeature` instead of panicking.
Formatter behavior:
```slo
(fn second () -> i32
(index (array i32 10 20 30) 1))
(fn local_sum () -> i32
(let values (array i32 3) (array i32 4 5 6))
(+ (index values 0) (index values 2)))
```
The formatter prints `(array i32 N)` types inline. It prints constructor
expressions with inline `i32` values as `(array i32 value...)`. It prints
first-pass index expressions inline as `(index array-expr index-expr)` when the
array expression and index expression are inline. Function and test body
indentation follows the existing one-body-form-per-line rule.
Diagnostics:
- `MalformedArrayType`: array type syntax is not `(array i32 N)`. Span: whole
type form or offending operand. Expected: `(array i32 N)`.
- `UnsupportedArrayElementType`: array element type is not `i32`. Span:
element type. Expected: `i32`. Found: written element type.
- `ZeroLengthArrayUnsupported`: array length is zero. Span: array type. Hint:
use one or more `i32` elements.
- `MalformedArrayConstructor`: constructor syntax is not
`(array i32 value...)`. Span: whole constructor or offending operand.
- `InvalidArrayElementType`: constructor element type syntax is invalid. Span:
element type. Hint: first-pass arrays use `i32` elements.
- `EmptyArrayUnsupported`: constructor has no element values. Span: whole
constructor. Hint: provide one or more `i32` values.
- `ArrayLengthMismatch`: constructor value count does not match the expected
`(array i32 N)` length. Span: whole constructor. Expected: expected length.
Found: value count.
- `TypeMismatch`: constructor element value does not check as `i32`. Span:
element value. Expected: `i32`. Found: checked value type.
- `MalformedArrayIndex`: index syntax is not `(index array-expr index-expr)`.
Span: whole index form or offending operand.
- `IndexOnNonArray`: `array-expr` does not check as an array type. Span:
array expression. Expected: `(array i32 N)`. Found: checked value type.
- `ArrayIndexNotI32`: `index-expr` does not check as `i32`. Span: index
expression. Expected: `i32`. Found: checked value type.
- `UnsupportedArrayIndexBase`: index uses an array value that is not an
immediate constructor or immutable array local. Span: array expression. Hint:
use `(index (array i32 ...) N)` or index a `let` array local.
- `DynamicArrayIndexUnsupported`: `index-expr` is not an integer literal. Span:
index expression. Hint: first-pass checked indexing requires a literal index.
- `ArrayIndexOutOfBounds`: literal index is outside `0 <= index < N`. Span:
index literal. Expected: valid index range. Found: literal index.
- `UnsupportedArrayLocalInitializer`: an array local is initialized from
anything other than a direct array constructor. Span: initializer expression.
Hint: use `(let values (array i32 N) (array i32 ...))`.
- `MutableArrayLocalUnsupported`: `(array i32 N)` is used as a mutable `var`
local. Span: local type. Hint: declare array locals with `let`.
- `UnsupportedArraySignatureType`: `(array i32 N)` is used as a function
parameter or return type in the first pass. Span: signature type.
- `UnsupportedArrayEquality`: an array value is compared with `=`. Span:
equality expression.
- `UnsupportedArrayPrint`: an array value is passed to `print_i32`. Span:
print argument.
- `ArrayMutationUnsupported`: an implementation that accepts a generalized
assignment target must reject element mutation in the first pass. Span:
attempted array element assignment target.
Slices remain a broader design target. No slice type, slice constructor,
borrowed view, pointer decay, or slice indexing behavior is part of this
first-pass array contract.
---
## 14. Option and Result
Status: strict first-pass compiler-supported syntax, promoted through
`examples/supported/option-result.slo` and
`examples/formatter/option-result.slo`.
Nullable absence uses `option`.
Recoverable failure uses `result`.
Promotion boundary:
- The first pass supports only `(option i32)` and `(result i32 i32)`.
- Constructors are the only promoted option/result value operations.
- Constructors are supported only as the single final body expression of a
function whose declared return type exactly matches the constructor result.
- Function parameters, local declarations, mutable storage, assignment,
matching, unwrap, equality, printing, and calls that pass or receive
option/result values are not part of this pass.
- Nested option/result types, non-`i32` payloads, strings, arrays, structs,
slices, and pointers as payloads are not part of this pass.
- No stable LLVM, C, FFI, cross-module, or serialized ABI/layout promise is
made for option/result values in v0.
First-pass option type:
```slo
(option i32)
```
First-pass result type:
```slo
(result i32 i32)
```
Constructor expressions:
```slo
(some i32 value)
(none i32)
(ok i32 i32 value)
(err i32 i32 value)
```
Type rules:
- `(option i32)` is supported only as a function return type under the direct
constructor-return rule above.
- `(result i32 i32)` is supported only as a function return type under the
direct constructor-return rule above.
- `(some i32 value)` checks `value` as `i32` and produces `(option i32)`.
- `(none i32)` produces `(option i32)` and has no payload expression.
- `(ok i32 i32 value)` checks `value` as `i32` and produces
`(result i32 i32)`.
- `(err i32 i32 value)` checks `value` as `i32` and produces
`(result i32 i32)`.
- Constructor type operands are type names in source, not expressions.
- Constructor payload expressions use the already-supported `i32` expression
rules. The constructor itself may not be nested in another expression for
this first pass.
- Pattern matching is not v0. There is no promoted operation that observes,
destructures, compares, prints, or unwraps an option/result value.
Typed-core meaning:
```text
OptionType {
payload: i32,
span,
payload_span
}
ResultType {
ok: i32,
err: i32,
span,
ok_span,
err_span
}
OptionConstruct {
variant: Some | None,
payload: i32,
value: Option<TExpr<i32>>,
type: Option(i32),
span,
payload_span,
value_span
}
ResultConstruct {
variant: Ok | Err,
ok: i32,
err: i32,
value: TExpr<i32>,
type: Result(i32, i32),
span,
ok_span,
err_span,
value_span
}
```
Lowering and backend boundary:
- A first-pass option/result constructor lowers to a compiler-owned tagged
value representation sufficient to return the value from the direct-return
function.
- The backend may choose any internal representation that preserves the
checked variant and `i32` payload for future typed-core use.
- The backend must not expose or rely on a stable option/result ABI, layout,
discriminant value, padding, or cross-module representation in v0.
- No storage, mutation, comparison, print, unwrap, pattern-match, or parameter
passing lowering is required or supported by this contract.
- If a checked option/result form reaches a backend gap, Glagol must report
`UnsupportedBackendFeature` instead of panicking.
Formatter behavior:
```slo
(fn some_value ((value i32)) -> (option i32)
(some i32 value))
(fn no_value () -> (option i32)
(none i32))
(fn ok_value ((value i32)) -> (result i32 i32)
(ok i32 i32 value))
(fn err_value ((code i32)) -> (result i32 i32)
(err i32 i32 code))
```
The formatter prints `(option i32)` and `(result i32 i32)` return types inline.
It prints first-pass constructors inline as `(some i32 value)`, `(none i32)`,
`(ok i32 i32 value)`, and `(err i32 i32 value)` when the payload expression is
inline. Function body indentation follows the existing one-body-form-per-line
rule.
Diagnostics:
- `MalformedOptionType`: option type syntax is not `(option i32)`. Span: whole
type form or offending operand. Expected: `(option i32)`.
- `UnsupportedOptionPayloadType`: option payload type is not `i32`. Span:
payload type. Expected: `i32`. Found: written payload type.
- `MalformedResultType`: result type syntax is not `(result i32 i32)`. Span:
whole type form or offending operand. Expected: `(result i32 i32)`.
- `UnsupportedResultPayloadType`: result ok or err type is not `i32`. Span:
offending payload type. Expected: `i32`. Found: written payload type.
- `MalformedOptionConstructor`: constructor syntax is not `(some i32 value)`
or `(none i32)`. Span: whole constructor or offending operand.
- `MalformedResultConstructor`: constructor syntax is not
`(ok i32 i32 value)` or `(err i32 i32 value)`. Span: whole constructor or
offending operand.
- `TypeMismatch`: constructor payload value does not check as `i32`. Span:
payload value. Expected: `i32`. Found: checked value type.
- `UnsupportedOptionResultReturn`: an option/result return type is used by a
function whose body is not exactly one matching direct constructor
expression. Span: return type or final expression.
- `UnsupportedOptionResultSignatureType`: an option/result type is used as a
function parameter type. Span: signature type.
- `UnsupportedOptionResultLocal`: an option/result type is used as a local
declaration type. Span: local type.
- `UnsupportedOptionResultFlow`: an option/result constructor or value is
used anywhere other than the supported direct-return position. Span: value
expression.
- `UnsupportedOptionResultEquality`: an option/result value is compared with
`=`. Span: equality expression.
- `UnsupportedOptionResultPrint`: an option/result value is passed to
`print_i32`. Span: print argument.
- `UnsupportedOptionResultPatternMatch`: a match/destructure/unwrap form tries
to observe an option/result value. Span: observing expression. Hint: pattern
matching is not part of the v0 option/result constructor pass.
---
## 15. Unsafe
Status: strict lexical marker support, promoted through
`examples/supported/unsafe.slo` and `examples/formatter/unsafe.slo`.
First-pass unsafe form:
```slo
(unsafe
body-form...
final-expression)
```
Promotion boundary:
- The first pass supports only the lexical `unsafe` expression block.
- The marker is lexically visible in source; there is no implicit unsafe
context in v0.
- An unsafe block may contain the same sequential body forms already supported
in normal function and top-level test bodies, followed by one final
expression.
- The block returns the final expression's checked type.
- The block marks a lexical unsafe context for diagnostics only. It does not
make raw memory operations supported.
- Raw allocation, deallocation, pointer loads or stores, pointer arithmetic,
unchecked indexing, raw reinterpretation, and FFI calls remain unsupported.
Example using only supported safe forms:
```slo
(fn add_one_in_unsafe ((value i32)) -> i32
(unsafe
(let one i32 1)
(+ value one)))
```
Surface rules:
- The form is exactly `(unsafe body-form... final-expression)`.
- The form must contain at least one expression after `unsafe`.
- `unsafe` is an expression form and may be used wherever its final
expression's type is expected.
- Body forms before the final expression use the same first-pass sequential
body-form rules as the surrounding function or top-level test body. Today
that means local declarations, local assignment, supported unit-producing
calls, and first-pass loops only where those forms are otherwise supported.
- Local declarations inside an unsafe block are scoped to that block. They are
visible only after their declaration and only until the end of the block.
- Names declared inside an unsafe block must not shadow parameters, locals from
an outer body, earlier locals in the same unsafe block, top-level functions,
or compiler intrinsics under the existing v0 no-shadowing rules.
- A nested unsafe block is allowed only if its body forms and final expression
satisfy this same lexical contract.
Typed-core meaning:
```text
UnsafeBlock {
body: [TBodyForm],
result: TExpr<T>,
type: T,
span,
body_spans,
result_span
}
```
The checker carries an `in_unsafe` lexical flag while checking the block body
and final expression. That flag is only used to choose diagnostics for unsafe
operation heads; it does not widen the set of supported expression forms.
Unsafe operation heads requiring the lexical marker:
```text
alloc
dealloc
load
store
ptr_add
unchecked_index
reinterpret
ffi_call
```
These names are reserved as unsafe operation heads in expression position for
v0. They do not resolve as ordinary user-defined calls in this pass.
Diagnostic boundary for those heads:
- Outside an unsafe block, any expression whose head is one of the names above
must report `UnsafeRequired`. Span: whole operation form, with the head span
as the primary operation name span. Hint: wrap the operation in an `unsafe`
block.
- Inside an unsafe block, the same operation heads must report
`UnsupportedUnsafeOperation`. Span: whole operation form, with the head span
as the primary operation name span. Hint: raw memory operations are outside
the v0 unsafe contract.
Lowering and backend boundary:
- A lexical unsafe block lowers like an ordinary expression block: lower each
checked body form in source order, then lower the final expression and use
its value as the block value.
- The lowered representation must preserve source order and local block scope.
- There is no backend lowering for raw memory operations in this pass.
- If a checked lexical unsafe block containing only supported safe forms reaches
a backend gap, Glagol must report `UnsupportedBackendFeature` instead of
panicking.
Formatter behavior:
```slo
(fn add_one_in_unsafe ((value i32)) -> i32
(unsafe
(let one i32 1)
(+ value one)))
```
The formatter must print `unsafe` as a multi-line expression block. The opening
line contains only `(unsafe`. Each body form and the final expression are
printed on their own lines, indented two spaces deeper than the `unsafe` form.
The closing `)` is placed on the final expression line when that expression is
inline. If an unsafe block appears inside another expression, the block may be
broken over multiple lines using the same indentation rule.
Diagnostics:
- `MalformedUnsafeForm`: an `unsafe` form has no final expression or has an
invalid operand shape. Span: whole `unsafe` form or the location where the
first expression was expected. Expected:
`(unsafe body-form... final-expression)`.
- `UnsafeRequired`: a raw unsafe operation head appears outside a lexical
unsafe block. Span: whole operation form, with the head span as the operation
name.
- `UnsupportedUnsafeOperation`: a raw unsafe operation head appears inside a
lexical unsafe block. Span: whole operation form, with the head span as the
operation name.
- Existing diagnostics for local declarations, assignments, loops, calls,
comparisons, and type mismatches apply inside unsafe blocks.
Out of scope for v0:
- pointer allocation
- pointer load/store
- pointer arithmetic
- unchecked indexing behavior
- raw reinterpretation
- FFI calls
- pointer locals
- stable ABI/layout promises
- unsafe abstractions
---
## 16. Tests
Status: supported in the strict top-level form below.
Current supported top-level test form:
```slo
(test "name"
body-form...
final-expression)
```
Surface rules:
- `test` is a top-level declaration, sibling to `fn`; it is not an expression
and is not valid inside a function body or another expression.
- The form has a string-literal name followed by one or more body expressions.
- The test name is metadata for the test runner, not a runtime `string` value.
- v0 test names must be non-empty printable ASCII without embedded quotes,
backslashes, or newlines until the general string escaping contract is
specified.
- Test names must be unique within a module after decoding.
- A single final expression is supported as the degenerate body.
- Non-final body forms may be local declarations or local assignments:
`(let name i32 value)`, `(var name i32 value)`, or `(set name value)`.
- When `while` is promoted, it is also a valid non-final test body form under
the first-pass loop contract in section 11.
- The final test expression has expected type `bool`; Slovo performs no implicit
conversion to `bool`.
- Tests may refer to functions in the same module using the same name
resolution rules as function bodies. Tests are not callable and do not
introduce names into the value namespace.
- Normal compilation must parse, lower, and check tests, but must not run them.
Test mode runs top-level tests in source order.
Typed-core meaning:
```text
TypedTest {
name: TestName,
body: [TBodyForm],
final_expr: TExpr<bool>,
span,
name_span,
body_spans
}
```
Top-level tests use the same sequential local-body rule as function bodies:
```slo
(test "local update"
(let base i32 20)
(var total i32 (+ base 1))
(set total (+ total 1))
(= total 22))
```
Local declarations and `set` forms may appear before the final test expression.
The final expression remains the test result and must check as `bool`. This
extension does not promote any broader block syntax.
The checker resolves and checks each test's final expression in a test context
with expected type `bool`. A false result is a failed test result, not a
compile-time diagnostic. Compile-time diagnostics are reserved for malformed
test forms, invalid names, duplicate names, name-resolution failures, type
errors, and unsupported implementation gaps.
Lowering and execution:
- `TypedTest` entries are kept in a module test list separate from normal
functions.
- Tests do not affect `main`, exported functions, or the normal program ABI.
- In test-runner lowering, each `TypedTest` may lower to an internal
zero-argument `bool`/LLVM `i1` thunk plus a registry entry containing the test
name and source span.
- A test passes when its expression evaluates to true, fails when it evaluates
to false, and errors if evaluation traps or the runner cannot execute it.
- In normal LLVM emission, an implementation may omit test thunks after parsing,
lowering, and checking them. It must not silently ignore malformed or invalid
tests.
Formatter behavior:
```slo
(test "add works"
(= (add 2 3) 5))
```
The canonical formatter keeps the test name on the opening line. In the current
supported subset, it prints each body form and the final expression on separate
two-space-indented lines. Each expression itself uses the normal canonical
expression formatter.
Diagnostics:
- `MalformedTestForm`: missing name, non-string name, missing final expression,
wrong operand count, or invalid body form. Span: whole test form or offending
operand. Expected: `(test "name" body... final-expression)`.
- `InvalidTestName`: empty name or a name outside the v0 name subset. Span:
string literal name.
- `DuplicateTestName`: duplicate decoded name in the same module. Span:
duplicate name, with a related span on the original test name.
- `TestExpressionNotBool`: checked expression type is not `bool`. Span: test
expression. Expected: `bool`. Found: checked expression type.
- Existing expression diagnostics such as `UnknownVariable`, `ArityMismatch`,
`TypeMismatch`, and `UnsupportedBackendFeature` apply inside test
expressions.
Supported example:
```slo
(test "add works"
(= (add 2 3) 5))
```
Supported local-body example:
```slo
(test "locals work"
(let base i32 2)
(var value i32 (add_local base))
(set value (+ value 1))
(= value 5))
```
---
## 17. Diagnostics
Diagnostics should have both human-readable and machine-readable forms.
Status: Glagol currently has diagnostics with byte spans and human/machine
rendering. Slovo v0 requires both byte spans and line/column ranges for
user-facing and machine-readable diagnostics. Exact diagnostic snapshots remain
an implementation/test contract, but implementations must not report only
unstructured text or only byte offsets.
Machine-readable diagnostic example:
```slo
(error
(code TypeMismatch)
(expected i32)
(found string)
(message "expected i32, found string")
(span "main.slo"
(bytes 42 49)
(range 12 8 12 15))
(hint "Use an integer value or convert explicitly."))
```
Required diagnostic fields:
- code
- primary source span as a zero-based, half-open byte range
- primary source range as one-based start line, start column, end line, and end
column
- expected, when applicable
- found, when applicable
- message
- hint, when useful and safe
Byte spans are the canonical location for tools. Line/column ranges are
required for human-readable output and machine diagnostics so editors and
external tools can display errors without reimplementing Slovo's source mapper.
Line and column values are derived from the original source file, not formatter
output. Columns are one-based byte columns within a UTF-8 source line; a tab in
source counts as one input byte for the machine range, even though canonical
formatting uses spaces.
Checked forms that reach a backend feature gap must return a structured
`UnsupportedBackendFeature` diagnostic instead of panicking. Unsupported
signature types, string literals without runtime lowering, and other
speculative forms must remain out of `examples/supported/` until they have
backend behavior or explicit diagnostics plus tests.
Test-specific diagnostics introduced by the top-level `test` contract are
`MalformedTestForm`, `InvalidTestName`, `DuplicateTestName`, and
`TestExpressionNotBool`. They follow the same required fields above and must use
original source spans, not formatter output. `DuplicateTestName` includes a
related span pointing to the original test name.
Loop-specific diagnostics introduced by the first-pass `while` contract are
`MalformedWhileForm`, `WhileConditionNotBool`, `EmptyWhileBody`,
`LocalDeclarationInWhileBodyUnsupported`, `NestedWhileUnsupported`, and
`WhileBodyFormNotUnit`. They follow the same required fields above and must use
original source spans, not formatter output.
Struct-specific diagnostics introduced by the first-pass `struct` contract are
listed in section 12. They follow the same required fields above, must use
original source spans, and must keep related spans for duplicate names or
fields when applicable.
Array-specific diagnostics introduced by the first-pass array/indexing contract
are listed in section 13. They follow the same required fields above and must
use original source spans, including byte ranges for malformed array types,
constructors, literal indices, and unsupported array value-flow forms.
Option/result-specific diagnostics introduced by the first-pass constructor
contract are listed in section 14. They follow the same required fields above
and must use original source spans, including byte ranges for malformed
option/result types, constructors, and unsupported option/result value-flow
forms.
---
## 18. LLVM Lowering Sketch
Slovo v0 should lower through an internal typed core before LLVM IR.
Example Slovo:
```slo
(fn add ((a i32) (b i32)) -> i32
(+ a b))
```
Approximate LLVM IR shape:
```llvm
define i32 @add(i32 %a, i32 %b) {
entry:
%0 = add i32 %a, %b
ret i32 %0
}
```
---
## 19. v0 Implementation Milestones
1. Parse forms
2. Print parsed tree
3. Canonical formatter
4. Basic name resolution
5. Primitive type checker
6. Function checker
7. `if` checker
8. `let` / `var` / `set`
9. Struct definitions
10. Top-level `test` checker and test runner
11. Structured diagnostics
12. Lower simple functions to LLVM IR
13. Compile and run `examples/supported/add.slo`
14. Preserve the `glagol` binary CLI contract for supported v0 modes, stderr
diagnostics, and exit codes
15. Keep native executable output as an explicit LLVM-plus-runtime linking step
until a later backend contract