Invisible link to canonical for Microformats

ADR-022 Introspection for Structs keys(), type(), and Symbol-Key Indexing


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 symbol keys.
  • 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() returning list<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.