Local semantic and type analysis: bindings, scopes, and analysis diagnostics.
| Status | Active — file-local v0; type/literal direction locked below |
| Owners | echo_semantics |
| Related | docs/syntax.md, docs/parser.md, docs/modules.md, ADR 0001 |
| CLI | xo check [--diag-codes] <file> |
- Types of bindings, params, returns, fields are inferred.
- Programmers do not annotate those sites with colon-style types.
- No English keywords and no user-written type names (
int,result,option, …). Kinds enter the language only through surface syntax (literals, leaders, shapes) and checker/runtime labels used in diagnostics.
Neither is a surface keyword or generics. Both appear only in checker diagnostics / inference.
| Label | Role | Unify |
|---|---|---|
unknown |
Soft hole — not enough information yet | unknown ⊔ T → T (adopts, then freezes) |
value |
Universal ABI slot — intentionally dynamic | value ⊔ T → value (stays open) |
unknown examples: empty list element ([] → list[unknown] until first
element); other soft placeholders. First concrete use freezes the kind (lists
stay homogeneous once known).
value examples: unconstrained fn/method params that only deep-equal,
store/load, or pass through to flexible callees (runtime.*, …). After the
body, free param vars are pinned to value. Fields that shared those vars
(e.g. entry.key) become value too. Bare fields like map.table are not
auto-pinned (they often become a named struct).
List / option element free vars (structural “I only walk the container”) pin
to unknown, not value. Pinning them to value rewrote the caller’s
note on the same variable after count(xs) / similar helpers, so later
xs[i] > xs[j] failed. A call must not change how we treat the argument’s
values outside the callee — only check the argument and type the result.
Allowed on value |
Rejected on value |
|---|---|
Deep == / != |
Arithmetic / bit ops / ordering (>, <, …) |
Mixed call sites (put(1) and put("a")) |
Field / method access (need a concrete / named kind) |
std/reflect inputs; map/set keys |
— |
Runtime always knows the concrete kind of each slot (heap header or bare
int) even when the checker says value. Use / std/reflect (kind,
kind_name, key_bytes) to branch in userland. See runtime-abi.md.
Method receiver . is the enclosing % Shape, so ^ . is that named shape —
not collapsed to value.
Echo has no surface type-annotation language and no generics. The only explicit kind-related surface (v1) is a width tag on a numeric literal that fixes storage/precision of that literal only:
$ a = <i32> 123_456
$ b = <f64> 3.14
$ c = <i32> -32
| Form | Role |
|---|---|
<width> number |
Prefix only (locked) — tag before the numeric literal |
Formatting preference (not required): one space after >:
$ ok = <i32> 1
$ also_ok = <i32>1
$ neg = <i32> -32
$ neg2 = <i32>-32
Sign: the minus is part of the literal after the tag. A width tag
cannot follow a unary (- / !): write <i32> -32, not -<i32> 32.
Integer / float widths (locked): signed i*, unsigned ui*, floats f*.
| Family | Widths |
|---|---|
| Signed | i8 i16 i32 i64 |
| Unsigned | ui8 ui16 ui32 ui64 |
| Float | f32 f64 |
| Alias | byte ≡ ui8 (same kind; not a third family) |
| Untagged lit | Default width |
|---|---|
integer (decimal / 0x / 0b) |
i64 |
| float | f64 |
Integer bases: decimal, 0x/0X hex, 0b/0B binary; _ separators
allowed in the digit body. Untagged ints run as signed i64.
Width mixing (locked):
- Untagged numbers use the defaults above.
- Two different explicit width tags never mix (e.g.
<i32> 1 + <i64> 2→ error). - Tagged + untagged: untagged keeps its default; unifies only if that default
matches the tag (
<i64> 1 + 2ok;<i32> 1 + 2error because2isi64). - No silent widen/narrow between widths (including
ui8→ui64). Use an explicit width cast on a value:<ui64> x(see syntax). Hash / crypto code should useui64lanes and zext fromui8explicitly. %data field width from default — when a shape member has= exprandexprhas a definite width (e.g.~ v0 = <ui64> 0), that is the field’s width for loads, stores, and ops. No separate: ui64ascription required. Writes must match (or cast into the field). Fields without a default stay unconstrained until a monomorphic write.- Default
i64yields to a more specific int width — untagged integer literals (and other default-i64values) adopt the other operand’s lane in binary ops and unifies (s.v0 + 1,rotl(s.v1, 13)whens.v1isui64). Two different explicit non-default widths still do not mix (<i32> 1 + <ui64> 2→ error). Free-fn parameters still cross a universal ABI (re-tag at entry if you need logical>>inside the callee). - Unsigned
>>is logical; signed>>is arithmetic. - No suffix form
42<i32>. Not bind ascription ($ x : …is out). - Tags apply to numeric literals and explicit casts of integer/float values — not strings, bytes blobs, durations, etc.
- Spellings
u8/u16… are not in the language; useui*.
Checker/runtime labels only. Users never write these as types. Each kind is
introduced by syntax (or by % name for named shapes):
| Kind (internal label) | Surface that produces it |
|---|---|
| integers / floats | number lits; optional <i32> / <i64> / <f32> / <f64> tags |
| bool | | / _ |
| string | '…' / "…" (pure vs rich is lit syntax only) |
| bytes | b'…' / b"…" |
| duration | 5s, 10ms, 2m, 1h |
| list | [a, b, c]; [] element unknown until use (then homogeneous) |
| anon product | { k: v } — not a map |
| named shape | % name + name { … } / mod.name { … } |
| result | any ! path in a function (^ ok / ! err) — not a struct |
| option | bare ^ + valued ^, no ! — not a struct; no ?expr |
| function | (…){ … } values |
| map / set keys | stdlib (/ std/collections/…); checker key kind often value |
result / option are not user types: they are return shapes of
functions, produced and consumed only via ^ / ! and | match arms. There is
no % result, no constructor, no keyword. Identifiers like result remain free
names (no keywords).
$ xs = [1, 2, 3] ; List (homogeneous once elements known)
$ row = { name: "Ada", n: 1 } ; anon struct (product), NOT a map
$ u = user { name: "Ada", visits: 0 } ; nominal struct
$ m = map.make() ; std map — keys may be int / string / … (value)
m.put(1, 10)
m.put("a", 20)
[]is the list literal — not set/map.- Lists infer a single element kind (
unknown→ first concrete); mixedxs[] = 1thenxs[] = "a"is a check error unless the element kind is alreadyvalue. { k: v }= anonymous struct (structural fields). Not a map.- Maps / sets / hash_table live in stdlib (
docs/stdlib.md). Membership uses deep==and SipHash overreflect.key_bytes(kind-tagged so1≠"1"). Map:keys/values/entries/to_list(= entries). Set:values/to_list(members). For-in (* x : …) is list or range only at the language level — iterate a snapshot:* x : s.to_list() { … },* e : m.to_list() { … }(e.key/e.value).
Produce (return shape only; no ?expr, no none assignment lit):
$ find_user = (id) {
? missing {
^ ; none
}
^ u ; some(u)
}
| Rule | Meaning |
|---|---|
Valued ^ and bare ^, no ! |
Function result is Option[T] |
Bare ^ |
Return none |
^ v |
Return some(v) |
Expr ?expr / none assignment lit |
Out |
Any ! in body |
Result (see below); + bare/value ^ → Result(Option[T], E) |
std/option |
No |
Statement ? |
if only |
Consume (locked) — | match; arm dialect for Option:
| find_user(id) {
$ user {
render(user)
}
: {
render_missing()
}
}
| Arm | Meaning |
|---|---|
$ name { … } |
some — payload as name |
: { … } |
none — no payload |
Unhandled Option → compile error.
$ a = b'raw'
$ b = b"with\nescapes"
$ abs = p'/home/user' ; absolute path
$ rel = p'home/user' ; relative path
$ url = p'http://xo.run' ; full URI/URL
-
Bytes are language literals, parallel to strings (pure
b'…'/ richb"…"). Richb"…"interpolates{name}/{.field}like a rich string;#consts bake at lower time (including# B = b'raw'/# P = p'/tmp'). Lit\xHHbytes stay raw (not UTF-8 lossy). A{name}that is already bytes copies its payload; strings and locators copy UTF-8 text. -
Through run: heap bytes handle (not a string). Print only after
str.from_bytes(UTF-8 lossy). Content equality via==. No string/bytesconcatenation with+(forbidden; use rich string or bytes interp for text). -
pliterals are a single locator kind (URI/URL family), not plainString. Classification is of the stored UTF-8 text as written (no resolve, no..clean, no percent-decode):Class Code Rule URI 2[A-Za-z][A-Za-z0-9+.-]*://…(examplehttp://xo.run)Absolute path 1not a URI, and starts with /(Linux baseline)Relative path 0everything else ( home/user,mailto:x,C:, empty)mailto:and Windows drives are relative text in v1 (no://).std/path.class/is_abs/is_uriaccept a string or locator. -
Pure
p'…'and richp"…"parallel string/bytes (escapes/interp on rich). Locked escapes:\n\t\r\\\"\{\}\xHH. Unknown →lex-escape. -
Through run: heap locator handle (distinct from string). Print via
str.from_locator(path/URI text). Content==. No+concat. No path normalization in v1 (stored text is the payload as written).
| Form | Meaning |
|---|---|
{name} |
Local / param / # const by name |
{.field} |
Method body only — field of the receiver . |
# consts mixed with live names |
Consts bake into the string at lower time |
No {module.export} or {a.b} paths in v1. No string +.
$ t = 5s
$ d = 10ms
$ u = 100us
V1 suffixes (locked): us, ms, s, m, h
(microseconds, milliseconds, seconds, minutes, hours).
Through run: stored as i64 nanoseconds. + / - on two durations;
content ==. Print via str.from_duration (largest exact unit among
h/m/s/ms/us, else ns). Not mixed with plain integers.
# const: duration lits fold (# D = 5s); + / - / == on other #
durations fold. List lits (# XS = [1, 2]), inclusive ranges (# R = 1..3),
named struct lits (# P = point { x: 1, y: 2 }), and anon products
(# Q = { a: 1 }) fold; nested fields/elements must themselves be
#-foldable. Omitted % field defaults that #-fold (lits / other #) are
applied when folding a named struct const, so # N = I.n works when n
was omitted and defaulted. Field name.f and index xs[i] on other #
values fold (missing field / OOB index → sem-const). Calls are not
#-folded.
- Fields with a default on the
%shape ($/~/#member with= expr) may be omitted in the lit; the default is applied at lower time. - Fields without a default must appear in the lit (
sem-struct-missing-field). - Unknown field names →
sem-struct-unknown-field. - Method members cannot be set in a lit →
sem-struct-method-field. - Duplicate field keys in one lit →
sem-struct-dup-field. - Defaults should be lowerable expressions (v1: lits / simple values).
% user {
$ name
~ visits = 0
}
$ u = user { name: "Ada" } ; visits defaults to 0
A function body (free or method) may only use:
| Allowed | Notes |
|---|---|
| Parameters | Introduced on the function |
| Locals defined in this function | Including loop * item, match binds, etc. |
. / .field / .method |
Only when this activation is a method call |
# constants |
Compile-time; not runtime capture |
| Import modules | e.g. io.print, str.from_int |
| Forbidden | Diagnostic / status |
|---|---|
Outer data $ / ~ (no function return shape) as values |
sem-capture — pass as a parameter or put state on a struct |
| Outer function values (binds with a return shape) | Allowed as value or callee — code refs, not env capture |
Outer params / data $/~ holding a function |
Forbidden in nested closed bodies — pass the handle as a param to the nested function, or call in the outer body |
Function values are nameless. $ name = (params) { … } names a binding,
not the function. Nested binds are ordinary closed values (no env):
$ apply = (x) {
$ double = (n) {
^ n + n
}
^ double(x)
}
Not in the language: closure environments / capture of outer $/~.
Need outer state → parameter or method on a % value.
| Capability | Status |
|---|---|
Bind $ f = (params) { … } (incl. nested) |
Yes — value is a closed body (FnRef / FnValue) |
Call f(args) when f names a function bind |
Yes — direct body call |
Pass / rebind / store f as a value |
Yes — handle { code, ret_shape } |
| Return a function value | Yes — ^ d then call the result |
Store on struct / anon field; call b.f(args) |
Yes — field load + indirect call (methods still win if same name) |
Call through a param/local (f(x) when f holds a function) |
Yes — indirect call; plain or result/option |
| Match on call-through result/option | Yes — ` |
| Nested body uses outer free fn bind | Yes — allowed code-ref (not env) |
| Nested body uses outer param as call/value | No — sem-capture (value and callee) |
| Methods as first-class values | No — methods stay recv.method() |
Design: functions are values like numbers (closed, no capture). Runtime value =
KIND_FN handle with code pointer + return shape (plain / result / option).
Direct calls still use the body’s LLVM type; indirect calls pick i64 vs i128
from the stored shape. See docs/hir.md.
Call-through of outer params as callees used to skip capture and SEGV.
Callees now share check_name_use with value uses (echo26/check/capture/003–004).
A name is in scope only after its bind in that region. Same rule for every kind of value — numbers, function values, anything:
; illegal
$ a = b + 4
$ b = 5
; illegal (same rule — not a special “function forward ref” ban)
$ a = b()
$ b = () {
^ 1
}
; ok
$ b = 5
$ a = b + 4
Unbound use → sem-unbound. Function-value binds introduce the name before
the body is checked so self-calls (fact(n-1) inside $ fact = …) work; that
is still “name already bound,” not a second namespace.
; Good: explicit param
$ add_n = (n, x) {
^ n + x
}
; Good: state on struct + method
% counter {
~ n
$ inc = () {
~ .n = .n + 1
}
}
; Error: outer ~ not visible inside f
~ n = 0
$ f = () {
^ n ; sem-capture
}
A method body and a free function body use the same rules for returns and shape:
| Produce | Meaning |
|---|---|
^ expr |
ok / plain value return |
bare ^ |
option none (when that shape applies) |
! expr |
result err |
Any ! path → result-shaped (method or free). Consume with | the same way
($ name ok/some, ! name err, : none).
Difference is entry, not the body language:
- Method call
recv.method(args)injects the receiver as.for that activation. - Free call
f(args)has no.. - Methods are not freestanding values; free function values are.
In a method body only, if the function is plain (no !, no option bare-^
pattern) and a control path falls off the end without ^ / !, that path
returns the receiver .. If the method uses ! (or option bare-^), fall-off
→ . does not apply — same as free functions: shape is owned by ^ / !.
$ inc = () {
~ .n = .n + 1
; no ^ — same as ^ .
}
c.inc().value()
| Context | Fall-off means |
|---|---|
| Method, plain shape | return . |
| Free function | return plain 0 / no value (unchanged) |
| Option / Result shape (method or free) | not this rule (bare ^ / ! / ^ v own the shape) |
Explicit ^ expr |
always wins (including ^ . and ^ .field) |
$ n = c.inc().value()
$ n2 = c.inc().inc().value()
- Receiver may be a name,
.(in a method), or another call that returns the same struct (self-returning methods:^ .or plain fall-off). - Struct type flows through self-returning methods so the next
.methodresolves. - Field access on a call result (
c.inc().n) works: a plain method that falls off returns., so the call has that named shape and.nis a field load.
Always: params / rebind / assignment copy the binding.
Implementation (MIR): monomorphic free-fn call sites flow named-struct types
onto callee parameters so f(c) with c: % conn makes methods on the param
resolve (c.read). See collect_free_fn_param_structs in echo_mir.
What that copies depends on the value class — there is no freestanding userland “pointer type” between them.
| Class | Pass / rebind | Share object? | === |
|---|---|---|---|
Ref (RefValue) |
copy the reference | yes | same object |
Value (StaticValue) |
copy the value | no | same as content / bits |
| Kind | Class | Example | Notes |
|---|---|---|---|
| Named struct | Ref | % conn { … }, user { … } |
Methods live here |
| Anon struct | Ref | { k: v } |
Product, not a map |
| List | Ref | [1, 2, 3] |
Shared aggregate |
Int (i64 / <i32> / …) |
Value | 42, 0xff |
Bits |
Float (f64 / <f32> / …) |
Value | 3.14 |
Bits (like int; not a heap-ref type) |
| Bool | Value | |, _ |
0/1 |
| String | Value | 'hi', "a{x}" |
Content value |
| Bytes | Value | b'…' |
Not a string |
| Locator | Value | p'/tmp' |
Path/URI text |
| Duration | Value | 5s |
Nanos bits |
| Range | Value | 1..10 |
Inclusive range value |
| Function value | Value | $ f = (x) { ^ x } |
Closed callable; methods are not values |
One-liner:
struct or list → copy the reference (share)
anything else → copy the value
Every managed allocation is assigned an owning lexical or dynamic scope. Semantic lifetime analysis lowers scope transitions into MIR promotion, demotion, and release. Every control-flow edge leaving a scope deterministically disposes of values still owned by that scope.
This is the reclamation model (not tracing GC). Full map: memory.md.
Value-vs-ref pass rules above are orthogonal: ref types may still share storage,
but ownership for dispose is scope-based. Impl: MIR emits scope ops;
promotion is graph evacuation (root + reachable allocs still owned by the
source frame); runtime frees immediately on scope exit (optional
enqueue/drain for batching). Public: /docs/memory.
Every data operand is a Value:
Value
├── StaticValue # ints, floats, bool, string, bytes, locator, duration, range, fn, …
└── RefValue
├── Struct # named + anon
└── List
Option / Result are return shapes (not leaves under Value); after |
match the payload is one of the kinds above. Module objects and methods are not
first-class data values.
Not language kinds and not RefValue::Socket.
| Layer | What it is |
|---|---|
Runtime (runtime.tcp_listen, tcp_accept, …) |
Opaque heap handles (KIND_TCP_*) or a small anon product (e.g. accept → { conn, remote }) — free functions only |
| Std / userland | Named structs only: % listener, % conn, … with a handle field holding those opaque bits |
Passing a “socket” in Echo means passing a struct by reference:
% conn {
$ handle ; opaque runtime stream id — field only
$ read = (n) { ^ runtime.tcp_read(.handle, n) }
}
$ c = tcp.connect(addr) ; RefValue::Struct (% conn)
$ f = (peer) { … } ; peer is the same: struct ref
f(c) ; copy the ref → share one connection
Accept bridge (std must reify):
$ a = runtime.tcp_accept(.handle)
; a is an anon struct product { conn: OpaqueHandle, remote: String }
; not a user socket type — reify immediately:
^ conn {
remote: a.remote,
handle: a.conn,
open: |
}
a= anon struct ref (temporary bridge).- What callers pass after
accept/listen/connect=% conn/% listenerstruct refs. - Sharing I/O = sharing that struct; close/mutations via one name affect all aliases.
See stdlib.md § Runtime vs std surface.
Implementation note: ABI packing (string heap cells, float box at universal
i64 slots, raw tcp handles) must not reappear as a third userland category.
==/!=— deep content equality:- ints/bools/floats: numeric bits
- strings / bytes / locators: payload content
- lists: same length + recursive deep eq on elements
- structs: same field names + recursive deep eq on values
===/!==— identity:- structs and lists: same object (same ref)
- StaticValue kinds: same as deep equality (no separate user-visible “same heap cell” for strings)
- Different kinds → check error when types known; at runtime mixed kinds → not equal (no silent cross-kind true).
| Op | Rule |
|---|---|
+ - * on ints |
same int width; result that width |
+ - * on floats |
same float width; result that width |
/ int ÷ int |
integer division, truncate toward zero |
/ float ÷ float |
float division |
| int with float | error (no implicit mix) |
| different explicit widths | error (see width mixing above) |
- Multiple return paths with different value kinds → result is a union of those kinds (track in inference).
- Fall-off / bare
^participates as nothing in that union when mixed with valued returns. - Result when any
!path exists; plain value when no!in the body.
- Methods exist only on named
%shapes (and values known to be that type). / runtimeexports are free functions only — never method receivers.- Std must wrap runtime resources in
% conn/% listener/ …` so userland only sees struct refs (plus ordinary values) — never bare socket handles as a language type. Typing uses normal struct return / match refine. - See
docs/stdlib.md§ Runtime vs std surface.
When every valued plain ^ is a named struct lit (type { … }), the function
returns those types (one or several):
$ shape = (k) {
? k == 0 {
^ circle { r: 1 }
}
^ rect { w: 2, h: 3 }
}
| Rule | Meaning |
|---|---|
| Single type | Call result has that struct type (methods/fields flow as today) |
| Multiple types (union) | Call result is not monomorphic — no method call on the raw result until refined |
| Refine | | x { % circle { … } % rect { … } } — inside a % T arm, x is typed as T for field/method flow |
| Runtime | Named lits still set type tags; % T match uses struct_type_is |
| Exhaustiveness | Default : { } allowed; static exhaustiveness of all union members is not required in v0 |
Not a user-written type syntax — inferred from return paths only (same spirit as result/option shapes).
| Mechanism | Role |
|---|---|
^ expr / bare ^ |
Return ok value, none (option-shaped), or plain value |
! expr |
Function return err payload (result shape) — not process abort |
| option | Only as fn shape from bare ^ + valued ^ (no ?expr) |
| result | Fn outcome ok | err when any ! path exists (syntax-driven) |
| Hard process abort | Not designed (no panics) |
Inside a function:
$ f = (x) {
? x < 0 {
! "negative"
}
^ x
}
^ x→ ok branch when the function is result-shaped! "negative"→ err branch- If any
!path exists, the function is result-shaped (ok | err).- Valued
^+!→ result of those payloads. - Bare
^+ valued^+!→ result whose ok side is option-shaped.
- Valued
- If no
!, but bare^+ valued^: option-shaped (not result). - If no
!and no bare-^option pattern: plain value (union of returns).
$ r = 1..3 ; value: integers 1, 2, 3
* x : r { … } ; iterate
* y : 4..6 { … } ; same with a range expr
| n {
1..10 { … } ; match if n is in the range (inclusive)
}
| Rule | Meaning |
|---|---|
| Form | lo..hi (token ..) |
| Bounds | Inclusive when lo ≤ hi; empty when lo > hi |
| Ends | Integer-like values (inferred i64) |
| Value | First-class handle (heap); deep == compares lo/hi |
| For-in | * item : range yields each integer in order |
| Match arm | Syntactic lo..hi means membership, not “equals a range object” |
Ordinary scrutinees (not Option/Result dialect) use value arms and/or type arms:
| x {
1, 2, y {
; runs if x == 1 or x == 2 or x == y (deep ==)
}
4..6 {
; runs if 4 ≤ x ≤ 6
}
% circle {
; runs if x is a named struct with type tag `circle`
}
: {
; default
}
}
- Each value arm head is one or more expressions (no trailing comma).
- Match if scrutinee deep-equals any listed value, or lies in a syntactic range.
- Expressions need not be literals (names, calls, arith, …) — same as any value.
% TypeName { … }— type arm: match when the scrutinee’s runtime type tag equalsTypeName(set when constructing a tagged litTypeName { … }). The type name must resolve to a%struct in scope (sem-match-type).- Default still
: { … }. No ordinary$ namearm. - Cannot mix value/
% typearms with$/!Option/Result arms on the same match. - Empty
| expr { }(zero arms) is incomplete (sem-match-incomplete). A default-only arm is still an arm.
Shared rule: $ name { … } = success-with-payload arm.
| Scrutinee | Success arm | Empty / fail arm |
|---|---|---|
| Option | $ name { … } some |
: { … } none |
| Result | $ name { … } ok |
! name { … } err |
; Option
| find_user(id) {
$ user { render(user) }
: { render_missing() }
}
; Result
| load_user(id) {
$ user { render(user) }
! error { io.log(error) }
}
- Statement
! exprand match arm! nameare the same error story (produce vs bind). Dual-use by position. - Statement
:(else) / match default / Option none arm: same glyph, contextual meaning. - Unhandled Option or Result → compile error.
- No propagate glyph.
Imports stay module-scoped (http.serve, not bare flood). See modules.md.
echo_source → echo_lexer → echo_parser → echo_semantics
↓
echo_diagnostics
| Rule | Code |
|---|---|
| No shadowing / reintroduce of a visible name | sem-shadow |
~ cannot assign through an immutable / const binding |
sem-immutable |
# name must be SCREAMING_SNAKE |
sem-hash-name |
# init is const (lits + # only; no calls) |
sem-const — folded: int/float/bool/string/bytes/locator/duration/list/lo..hi/named+anon struct; duration +/-/==; list/struct ==/!=; range ==; field name.f; index xs[i]; omitted % defaults that #-fold; no calls |
Receiver . / .field only in method bodies |
sem-receiver |
< / > only inside loops |
sem-break / sem-continue |
Top-level is the program body; ^ returns process status |
(no sem-return at top-level) |
| Name used before its bind (any value, incl. calls) | sem-unbound |
| Assign target name must already exist | sem-unbound |
List push ~ xs[] = e / ~ a.b[] = e |
append via runtime list_push (index omitted) |
module.foo not an export |
sem-module-export |
| Unhandled Result / Option value | sem-unhandled-result / sem-unhandled-option (suppressed inside & effect blocks) |
Effect block (&) |
Body auto-unwraps free/module call results that are result/option; bind is Value (ok payload or err/none payload) |
| Incomplete/wrong ` | ` arms |
! outside a function (incl. top-level / file-scope ?) |
sem-error-return (task { } bodies count as functions) |
| Kind mismatch | sem-type-mismatch |
| Call non-function | sem-not-callable |
| Wrong arity | sem-arity |
| Missing field | sem-no-field |
Unknown width tag (<u8>, <int>) |
sem-width-unknown |
Width tag after unary (-<i32> 1) |
sem-width-unary |
<width> expr on a non-numeric |
sem-width-cast |
Method used as a value ($ f = c.inc) |
sem-method-value |
$/#introduce immutable / const once;~intro or update mutable.- Method bodies = function values that are struct members.
- Imports: last path segment → module name; use
module.export.
Runs after name/effect checks (infer.rs + unify.rs):
- Scalars, lists, index, anon/named structs, fields
- Ops (no int/float mix; int
/truncates) - Calls (
sem-not-callable,sem-arity; imported fns use export param count). Known-arity import params arevalue(count only, no param kinds), so mixed call sites stay open. Runtime primitives still have unknown arity. When the defining module has a known leaf return kind (string,bool,float,bytes,duration), the importer uses that kind (str.from_int→ string). Integers, structs, and result/option wrappers stay open so width tags and handle methods do not freeze. Params stayvalue. - Codes:
sem-type-mismatch,sem-not-callable,sem-arity,sem-no-field
Locator path vs URI classification is locked in Bytes and locators above.