Status
Accepted
Context
Slug now has struct schemas and struct values as schema-backed immutable records (ADR-020). Structs provide a fixed field set, typo resistance, and ergonomic pattern matching, while maps remain the primary construct for dynamic data (JSON, headers, user input, etc.).
As structs become common in Slug libraries and user code, we need a small, explicit, and reliable way to:
- introspect struct instances (enumerate fields)
- perform dynamic field access (when the field name is computed)
- keep the critical distinction between structs and maps intact
- avoid “stringly-typed” field access that undermines typo resistance
We also want a clean foundation for future “symbols” (identifier-like keys) that can be used for struct fields and optionally in user code and libraries.
Decision
1) Introduce symbol values with : literal syntax
Slug adds a symbol runtime type.
Symbol literals use ::
:age
:contentType
:"weird-key"
Symbols are intended to represent identifier-like keys and are suitable for use as keys and discriminants. (Exact runtime representation is not part of the language contract.)
2) keys() supports structs and schemas, returning schema-order symbols
keys() is extended to support struct values and struct schemas.
keys(structValue) -> list<symbol>
keys(structSchema) -> list<symbol>
Rules:
- For structs,
keys()returns the declared field names as symbols. - The returned list is in schema definition order (stable and deterministic).
- Calling
keys()on a struct value is equivalent to calling it on the value’s schema.
3) keys(map) returns the map’s actual keys
Maps remain dynamic and may be keyed by any hashable value. Therefore:
keys(map) -> list<any>
Rules:
- For maps,
keys()returns the actual key objects present in the map (no coercion). - Key order is defined by map iteration order (if maps are ordered), otherwise unspecified. (Implementation-defined.)
4) Dynamic access: structs require symbol indexing; maps accept any key
To enable dynamic field access without collapsing structs into maps:
- Struct indexing requires
symbolkeys. - Map indexing accepts any key value according to map semantics.
u[:age] // valid: dynamic struct field access
u["age"] // error: struct indexes must be symbols
m["age"] // valid: map lookup
m[:age] // valid: map lookup if that key exists
m[anyKey] // valid if the key is present / comparable per map rules
Additional rules for structs:
- Indexing a struct with a symbol that is not a declared field is an error.
- This preserves typo resistance and makes the “safe path” the default.
5) type() returns the schema value for struct instances
type(x) returns a descriptor suitable for match.
Rules:
- For struct instances,
type(instance)returns the struct schema value itself. - For struct schemas,
type(schema)returns:struct. - For non-struct values,
type(x)returns a well-known tag (e.g.:map,:list,:number, …).
Example:
val User = struct { name, age }
val u = User { name: "Slug", age: 42 }
type(u) == User // true
type(User) == :struct // true
This design avoids string-based type names and allows direct, explicit matching on schema values.
6) Non-goals
keys()does not turn structs into maps; it exposes schema metadata only.
Consequences
Positive
- Enables ergonomic and safe introspection of structs without introducing a general reflection system.
- Provides a clean, explicit solution for dynamic struct field access (
u[:field]) while preserving typo resistance. -
Keeps a clear conceptual boundary:
- structs = schema-backed records (symbol field keys only)
- maps = dynamic dictionaries (any key type)
type()becomes match-friendly and schema-oriented (no string registries).- Schema-order
keys()is deterministic and supports stable formatting, serialization, diffs, and generic tooling.
Negative
- Introduces a new runtime type (
symbol) and literal syntax (:). - Developers must learn that structs require symbol indexing (strings are rejected for struct lookup).
- Map
keys()returninglist<any>may be surprising to developers expecting string keys everywhere (mitigated by docs).
Neutral
- Map key ordering and iteration semantics remain an implementation choice unless separately specified.
- Symbols may later become more broadly useful (e.g., tagged unions, protocol keys), but this ADR only relies on them for struct field identity and safe dynamic access.