Silk

Static evaluation

Silk has one explicit compile-time execution phase. Static evaluation computes finite values, selects source with static if, and specializes ordinary functions before the residual runtime program is checked for ownership, reachability, target availability, and lowering.

Ordinary function and control-flow rules are defined by functions, callables, and control flow. Runtime ownership is defined by ownership and borrowing. Constants and the foundational types admitted as constant results are defined by values and types.

Terminology

  • A static function is a top-level static fn whose complete body executes during compilation.
  • A mixed function is an ordinary function with static parameters, static local bindings, or static conditionals alongside runtime work.
  • A static parameter is an explicitly marked specialization input omitted from the runtime calling shape.
  • A static binding is a let static local that retains a computed static value for later static use.
  • A static value is finite, deterministic, identity-free compiler data admitted by the static value rules.
  • A static specialization is one application identified by its declaration, target, generic arguments, selected evidence, and canonical static arguments.
  • The residual program is the ordinary runtime work left after static evaluation selects source and embeds representable static results.

Static declarations and function phases

STATIC-001 — A static function is a top-level compile-time declaration

Status: Confirmed

A static function is declared as static fn or pub static fn. It may be private or public, but it must be a top-level module declaration. Every call to it occurs during static evaluation, every argument must be statically available, and its result is one static value or one static diagnostic.

pub static fn positive(value: i32) -> bool {
  return value > 0
}

A static function has no runtime function item, callable representation, parameter lanes, instance, symbol, or backend body.

Boundary: static cannot combine with unsafe, effect, an implementation operation, or a service or interface operation. Nested static function declarations within executable bodies are unavailable. Module declaration groups may select static functions. A static function cannot be captured or passed as a runtime callable.

Diagnostics: An unsupported modifier combination or declaration position reports a syntax diagnostic at the conflicting modifier or declaration. A use requiring a runtime callable reports a static-phase violation at that use.

Evidence: static syntax requirements, static function requirements.

STATIC-002 — Static parameters and bindings make phase crossings explicit

Status: Confirmed

Prefixing an ordinary function parameter with static makes it a specialization input. The parameter retains an explicit declared type but occupies no runtime parameter lane. let static evaluates its required initializer during compilation and retains the result for later static use.

fn render(static enabled: bool, value: i32) -> i32 {
  let static selected = enabled
  static if selected {
    return value
  } else {
    compileError("rendering is disabled")
  }
}

A literal directly supplied to a static parameter, binding initializer, condition, or compileError message is available without a call-site static marker. A non-literal expression is available only when all of its dependencies and operations are static.

Boundary: Static parameters and let static bindings cannot also be mut. An ordinary local remains runtime even when its initializer is a literal; preserving the value for later static use requires let static. Inside a static fn, ordinary locals and control flow are already static and do not require let static.

Diagnostics: Passing a runtime value to a static parameter, reading a runtime local from static evaluation, or using a runtime operation in a static initializer reports SEM0176 at the operation and identifies the static boundary that required the value.

Evidence: explicit static modes, static syntax recovery.

Static selection and compile errors

STATIC-003 — static if selects one semantic arm

Status: Confirmed

static if is a statement form with a statically evaluated bool condition, one block, and an optional else block. Both arms are parsed, but only the selected arm undergoes name resolution, type elaboration, ownership-producing residualization, and call discovery for that specialization.

fn wordSize(static wide: bool) -> u32 {
  static if wide {
    return 64
  } else {
    return 32
  }
}

The selected arm may contain ordinary runtime operations and values. Those operations are retained in the residual program; static evaluation does not execute them. A false static if without an else contributes no residual operation.

At module scope, static if selects a declaration group according to module static selection. Within executable bodies it remains a statement and cannot introduce local declarations.

Boundary: static if is not an expression. An ordinary runtime if, loop, or return cannot decide whether later static source is elaborated. Source that must be excluded during compilation belongs in an unselected static if arm.

Diagnostics: Syntax errors in either arm are reported because both arms are parsed. Name, type, Effect, requirement, ownership, availability, and reachability diagnostics arise only from the selected arm. A non-static or non-bool condition reports its phase or type diagnostic at the condition.

Evidence: static selection requirements, statement-only syntax.

STATIC-004 — compileError terminates only the selected specialization

Status: Confirmed

compileError(message) is dedicated compile-time syntax. Its one argument must produce a static string. Reaching it reports SEM0177, terminates the current specialization, and discards every partial static value and residual operation accumulated for that specialization. It acts as never for the selected static path.

static fn requireEnabled(enabled: bool) -> bool {
  if enabled {
    return enabled
  }
  compileError("expected an enabled feature")
}

A compileError in an unselected static arm or an uncalled static function does not execute and does not report a diagnostic.

Boundary: compileError is not an ordinary function. It cannot be imported, shadowed, captured, or passed as a value, and it accepts exactly one argument with no trailing comma. It does not define a runtime panic or failure mechanism. Runtime control flow cannot suppress a reachable compileError.

Diagnostics: SEM0177 points at the selected compileError and retains the static specialization trace. A runtime message or wrong message type reports its phase or type diagnostic instead.

Evidence: compile-error requirements, dedicated syntax.

Static values and mutation

STATIC-005 — Static values are reusable data, not observable compile-time storage

Status: Confirmed

A static value is finite, deterministic, identity-free, and freely reusable during static evaluation. Admitted values include unit, foundational scalars, scalar enums, static UTF-8 text, and recursively pure aggregates whose members are admitted. Admission excludes values carrying a runtime resource, borrow, opaque identity, callable execution, Effect, service, unsafe pointer, or observable cleanup behavior.

Reusing a static value does not grant its declared runtime type the Copy property and does not change runtime interface selection. When an admitted value is runtime-representable and cleanup-free, residualization may embed it directly without retaining compiler storage.

Boundary: Static evaluation has no addresses, references, loans, mutable aliases, destructors, allocator identities, or observable allocation. It cannot construct or run an Effect, invoke an ordinary runtime function, read a runtime binding, or perform unsafe, external, or host work.

Diagnostics: Attempting a prohibited operation reports SEM0176 at that operation and produces no static result. A value outside the admitted domain is rejected at the static boundary rather than acquiring a second compile-time ownership model.

Evidence: static value requirements, canonical static values.

STATIC-006 — Static mutation replaces one complete local value

Status: Confirmed

Inside a static fn, ordinary let mut creates one mutable evaluator-local slot. Assignment replaces the complete stored static value. Ordinary if, while, break, continue, and return operate in the static function's compile-time phase.

static fn countTo(limit: u32) -> u32 {
  let mut current = 0
  while current < limit {
    current = current + 1
  }
  return current
}

Boundary: Static mutation cannot take a reference to the slot, project a writable place through an alias, mutate one aggregate field in place, partially move a value, observe cleanup, or retain the slot in a residual value. A mixed function cannot declare let static mut; it computes a new static value through a static function instead.

Diagnostics: A static borrow, unsupported place mutation, or runtime-dependent replacement reports SEM0176 at the invalid operation. The failed evaluation exposes no partial value.

Evidence: static mutation limits.

STATIC-007 — silk.static_text inspects UTF-8 bytes without exposing storage

Status: Confirmed

The ordinary standard-library module silk.static_text provides static byte operations for text available during compilation:

MemberResultMeaning
byteLength(value)usizeNumber of bytes in the UTF-8 encoding
byteAt(value, index)u8Byte at one in-bounds byte index
concat(left, right)stringConcatenated static text, retaining left as its diagnostic source anchor
slice(value, start, end)stringText in one byte range whose endpoints are UTF-8 scalar boundaries
import silk.static_text { byteLength, byteAt, concat, slice }

static fn hasAccent(value: string) -> bool {
  return byteLength(value) == 3 && byteAt(value, 1) == 195 &&
    concat(slice(value, 1, 3), "!") == "é!"
}

These are ordinary static fn wrappers. They return admitted values and reveal neither an address nor mutable compiler storage. Their sealed intrinsic primitives are static-only and cannot survive in runtime HIR.

Boundary: byteAt rejects an out-of-bounds index. slice rejects an invalid range and any endpoint that splits a UTF-8 scalar encoding. Both failures are phase violations with source text provenance rather than runtime traps. concat is bounded by the ordinary retained-value budget.

Evidence: static text source actor, static value requirements.

Reflection, static sequences, iteration, and formatting

STATIC-008 — Reflection exposes finite aggregate metadata only during specialization

Status: Confirmed

silk.reflect exposes canonical descriptors for one concrete aggregate type and its fields. Named tuples and anonymous positional aggregates expose positions; named structs and anonymous records expose visible labels. Declaration order, nominal owner identity, generic substitution, aggregate kind, and the reflecting declaration's visibility authority are retained.

MemberResultMeaning
typeOf<Owner>()Intrinsic.Type<Owner>Descriptor for one concrete aggregate owner
fields<Owner>()Intrinsic.Fields<Owner>Ordered heterogeneous field collection
typeKind(descriptor)u8Stable named/positional aggregate-kind code
fieldKind(field)u8Stable labeled/positional member-kind code
fieldLabel(field)stringLabel of a labeled field
fieldOrdinal(field)usizePosition of a positional field
borrowField(owner, static field)&ValueShared runtime projection authorized by the descriptor

Descriptors remain nominal: equal field shapes from separate structs do not become assignment- compatible or share projection authority. Private fields are absent outside their declaring module and their spellings cannot leak through reflection diagnostics.

Boundary: Intrinsic.Type, Intrinsic.Fields, and Intrinsic.Field are phase-only nominals with no runtime layout, address, ownership, or backend representation. borrowField accepts only a shared reference to the descriptor's exact owner and consumes its descriptor during specialization. It cannot project an owned value, use a descriptor at runtime, or extend the result beyond the owner's ordinary borrow lifetime.

Evidence: static reflection requirements, reflection source actor.

STATIC-009 — Static sequences are immutable compile-time values

Status: Confirmed

silk.static_sequence provides empty, append, concat, length, and at over Intrinsic.StaticSequence<Element>. Every operation returns a complete canonical value; append and concatenation do not mutate the input sequence.

import silk.static_sequence { StaticSequence }

static fn values() -> Intrinsic.StaticSequence<i32> {
  let mut result = StaticSequence.empty<i32>()
  result = StaticSequence.append<i32>(move result, 20)
  result = StaticSequence.append<i32>(move result, 22)
  return move result
}

Boundary: A static sequence has no capacity, allocator, reference, mutable alias, destructor, or runtime layout. It cannot appear in a residual signature, binding, call, ownership fact, or backend artifact. at rejects an out-of-bounds index during static evaluation.

Evidence: static sequence requirements, static sequence source actor.

STATIC-010 — static for generates ordinary runtime work from finite static elements

Status: Confirmed

static for <binding> in <expression> { ... } is a statement. Its iterable must evaluate to a finite static sequence or reflected field collection. The compiler re-elaborates one fresh body scope per element in deterministic order. A heterogeneous field collection gives each scope the field descriptor's concrete Field<Owner, Value> type, so one authored body can select different interfaces and generate differently typed runtime operations.

import silk.reflect { Reflect }

fn visit<Owner>(owner: &Owner) -> () {
  static for field in Reflect.fields<Owner>() {
    let value = Reflect.borrowField(owner, field)
    inspect(value)
  }
}

A zero-element loop contributes no residual statements and does not elaborate its body. If iterable evaluation, a later iteration, or a static budget fails, the whole expansion is discarded: earlier generated calls, HIR, ownership, cleanup, and instance facts do not survive.

Boundary: static for is not a runtime loop, expression, declaration container, or unbounded generator protocol. Runtime values, Effects, services, unsafe operations, host input, I/O, time, randomness, and external access cannot supply its iterable. The residual program contains only the ordinary operations produced by successful iterations.

Evidence: static iteration requirements, static iteration syntax.

STATIC-011 — Template formatting validates statically and writes borrowed fields at runtime

Status: Confirmed

silk.format.Format.format<Args>(static template: string, args: &Args) accepts one static template and one ordinary shared reference to a tuple or record argument pack. Positional {} placeholders consume every tuple position in order. Named {name} placeholders select visible record fields; they may repeat, and unrelated fields may remain unused. {{ and }} emit literal braces.

run Format.format("My name is {}, I'm {}", &("Julia", 31))
run Format.format("Hello, {name}", &.{ name: "Julia", age: 31 })

Parsing, aggregate-kind checking, field lookup, and Display selection finish during specialization. Runtime code retains only ordered Writer text operations, ordinary shared field projections, and concrete Display calls. The template, parser plan, descriptors, static loops, and argument-pack copy are absent. Display<string> writes the borrowed string's existing UTF-8 bytes through the same Writer path as other presentations and allocates no intermediate String.

Boundary: One template cannot mix positional and named placeholders. The initial grammar has no format specifiers, dynamic widths, interpolation expressions, nested fields, runtime placeholder names, variadic calling convention, or runtime parser. Malformed braces, invalid labels, wrong aggregate kinds or arity, unavailable fields, and missing Display evidence fail specialization before Writer execution is published. An uncalled invalid application remains unevaluated.

Evidence: template formatting requirements, Format source actor.

Demand, diagnostics, and the runtime boundary

STATIC-012 — Static evaluation is demand-driven and leaves one runtime program

Status: Confirmed

The compiler evaluates a static function or mixed specialization only when a concrete selected target and static application is demanded by a constant initializer or reachable executable work. Loading, indexing, importing, or navigating an uncalled declaration does not execute its body. Equal target, generic arguments, evidence, and canonical static arguments reuse one deterministic result.

After selection, static parameters, static locals, static-function frames, inactive arms, and static-only intrinsics are absent from runtime calling shapes, ownership facts, cleanup plans, runtime reachability, MIR, and backend artifacts. Runtime analysis sees only the selected residual program.

Boundary: Runtime control flow cannot defer static evaluation to an execution engine. Static functions have no runtime address or callable form, and target information cannot be preserved as a runtime query.

Diagnostics: A failed static application publishes its static diagnostic and no partial residual body. An uncalled failing declaration publishes no static diagnostic.

Evidence: residual-program requirements, residual instance requirements, residual ownership requirements.

STATIC-013 — Static diagnostics are deterministic source traces

Status: Confirmed

Static diagnostics retain the primary operation, selected target, ordered static calls, canonical static arguments, and selected static if arms. Static-text diagnostics may also retain the source literal and byte offset. They do not expose host stack frames, JavaScript causes, compiler addresses, cache identities, or backend details.

CodeCondition
SEM0176An operation or value is unavailable during static evaluation
SEM0177A selected compileError requests compilation failure
SEM0178A demanded static application forms a cycle
SEM0179The deterministic evaluator-step limit is exhausted
SEM0180The logical static call-depth limit is exhausted
SEM0181The retained canonical-value byte limit is exhausted
SEM0182The residual-program growth limit is exhausted

Limit diagnostics name the exhausted resource and produce no partial value or residual program. They never masquerade as a source-requested compileError.

Boundary: The concrete limit thresholds are compiler policy, not successful-program semantics. Changing a threshold does not change the meaning of a program that remains within every limit.

Diagnostics: Repeating the same failing source, target, generic arguments, evidence, and static arguments produces the same code, semantic details, related spans, and trace encoding.

Evidence: static diagnostic requirements, diagnostic catalog.

Target information

STATIC-014 — silk.target exposes target information only as static source values

Status: Confirmed

The ordinary standard-library module silk.target exposes individual machine facts:

MemberTypeValue
Target.arch()ArchInstruction-set architecture
Target.operatingSystem()OperatingSystemOperating system
Target.abi()AbiPlatform ABI
Target.objectFormat()ObjectFormatObject-file format
Target.endianness()EndiannessByte order
pointerBitsu32Data-pointer width in bits
pointerAlignmentu32Data-pointer alignment in bytes
usizeMaxusizeLargest unsigned pointer-sized integer
isizeMaxisizeLargest signed pointer-sized integer
isizeMinisizeSmallest signed pointer-sized integer

Use silk.compilation for typed logical build choices. Source checks use the narrow domain needed for the decision. The complete compilation profile also contains resolved package parameters and controls static-evaluation identity.

import silk.target { Target, Arch }

fn targetWordBits() -> u32 {
  static if Target.arch() == Arch.Wasm32 {
    return 32
  } else {
    return 64
  }
}

The selected target is fixed before any demanded specialization executes. The result of static selection is ordinary residual source; no target-profile parameter or runtime probe remains.

Boundary: silk.target is ordinary source and receives no compiler-known name privilege. The sealed individual target and profile-fact intrinsics are static-only and have no runtime lowering. Target data cannot be changed by source, inferred from the host when an explicit target was selected, or queried dynamically at runtime. A selected cleanup-free target value may appear in residual code only as an embedded ordinary value.

Diagnostics: A call that would require the sealed target-profile intrinsic to survive as runtime work reports SEM0176 and creates no runtime intrinsic inventory entry. Comparing target enums from different nominal types uses the ordinary cross-enum diagnostic.

Evidence: static target requirements, sealed target primitive requirements, intrinsic catalog.

On this page