Invisible link to canonical for Microformats

ADR-039 Lightweight Type System


Status

Accepted

Context

Slug is evolving from a fully dynamic language toward a lightweight optional type system intended to improve:

  • Developer experience
  • Tooling and LSP support
  • Static validation
  • AI-assisted development
  • Documentation generation
  • API clarity
  • Long-term maintainability
  • VM and compiler optimization opportunities

The goal is not to create a highly academic or maximally expressive type system.

Slug prioritizes:

  • Simplicity over complexity
  • Readability over cleverness
  • Explicitness over implicitness
  • Predictability over magic
  • Local reasoning over global inference
  • Practical enterprise software development
  • AI-friendly semantics and validation

The type system should support enterprise-scale applications while remaining approachable for scripting and exploratory development.

Slug intentionally avoids:

  • Inheritance hierarchies
  • Interface systems
  • Trait systems
  • Type-level programming
  • Macros
  • Complex generic constraints
  • Variance systems
  • Heavy inference systems
  • Non-local type reasoning

The resulting design aims to provide strong validation and clear APIs without turning the language into a type-theory-focused environment.

Decision

Slug will implement a lightweight optional type system centered around:

  • Explicit type annotations
  • Union types
  • Struct types
  • Match-based narrowing
  • Lightweight generic functions
  • A small set of generic container types
  • Type aliases

The system will remain intentionally constrained.

Type Annotation Syntax

Slug uses : for type annotations.

Examples:

val port:num = 8080

fn add(a:num, b:num):num {
  a + b
}

Type positions are intentionally consistent:

  • Variable type annotations appear after the identifier
  • Parameter type annotations appear after the identifier
  • Function return types appear after the parameter list

Primitive Types

Slug includes the following builtin primitive types:

num
str
bool
bytes
nil
any

Builtin types are lowercase.

User-defined types use PascalCase.

Examples:

User
Request
DbError

Optional Typing

Type annotations are optional in many locations.

Examples:

val x = 1

The validator may infer local types where practical.

Slug adopts the following principle:

Infer locals, require boundaries.

Type annotations are strongly encouraged for:

  • Public APIs
  • Struct fields
  • Function parameters
  • Function return values
  • Empty collection literals

Union Types

Slug supports union types using |.

Examples:

num|nil
User|DbError
Response|Timeout|nil

Union types are the canonical mechanism for:

  • Optional values
  • Error results
  • Variant results
  • Dynamic narrowing

Slug intentionally avoids dedicated nullable syntax such as ?T.

Example:

fn findUser(id:str):User|nil {
  ...
}

Match-Based Type Narrowing

match is the primary type narrowing mechanism.

Example:

match findUser(id) {
  nil => "missing"
  user => user.name
}

Within a narrowed branch, the validator may refine the known type.

Example:

user : User

This approach avoids complicated flow-sensitive type systems while still providing strong practical narrowing.

Struct Types

Slug supports named struct types.

Example:

struct User {
  id:UserId
  email:Email
}

Structs are intended to model stable domain values.

Maps remain dynamic runtime structures.

Slug intentionally favours nominal struct types over structural typing.

Generic Container Types

Slug supports a limited set of generic container types.

Initial builtin generic types are:

list<T>
map<K,V>
chan<T>
task<T>
fn<R, P1, P2>

The type system intentionally restricts generics to this small set of foundational runtime categories.

Slug does not initially support:

  • User-defined generic structs
  • Generic interfaces
  • Generic traits
  • Higher-kinded types
  • Generic constraints
  • Variance annotations

Function Type Syntax

Function types use the following syntax:

fn<R, P1, P2>

Where:

  • R is the return type
  • Remaining type parameters represent positional arguments

Examples:

fn<bool, str>
fn<num, num, num>

This format keeps function types compact while remaining consistent with Slug’s generic container model.

Generic Functions

Functions may declare generic type parameters.

Examples:

fn<T>(x:T):T {
  x
}
fn<T>(xs:list<T>):T|nil {
  match xs {
    [] => nil
    [x, ..._] => x
  }
}

Generic type variables are scoped to the function declaration.

Slug intentionally limits generic type variables to functions.

Slug does not initially support user-defined generic nominal types.

Variadic Parameters

Variadic parameters use ....

Examples:

fn<T>(s:str, ...v:T):list<T> {
  v
}

Inside the function body, variadic values are represented as:

list<T>

Untyped variadic parameters default to:

list<any>

Example:

fn(s:str, ...v):list<any> {
  v
}

Generic Inference With Variadic Parameters

Generic type variables may be inferred from variadic arguments.

Examples:

f("x", 1, 2, 3)

Infers:

T = num

Mixed values may infer union types.

Examples:

f("x", 1, "a", true)

Infers:

T = num|str|bool

This allows unions and generics to compose naturally without requiring complex interface systems.

Tuple Types

Slug supports fixed-length positional tuple types.

Examples:

[str, num]
[num, num, num]

Tuple types differ from homogeneous lists.

Examples:

list<num>

Tuple types are useful for:

  • Small structured returns
  • Coordinate pairs
  • Parser results
  • Key/value pairs
  • Destructuring

Type Aliases

Slug supports lightweight type aliases using type.

Examples:

type MaybeNum = num|nil

type UserResult = User|DbError|nil

type Point = [num, num]

Initial type declarations are aliases only.

Aliases do not create distinct runtime or nominal types.

Slug intentionally avoids introducing newtype semantics initially.

Empty Collection Literals

Empty collection literals may require contextual typing.

Examples:

val xs:list<num> = []
val cfg:map<str,str> = {}

Where contextual typing is unavailable, implementations may fall back to:

list<any>
map<any,any>

This avoids unnecessary friction while preserving useful validation.

Runtime Semantics

Types are primarily intended for:

  • Validation
  • Tooling
  • Documentation
  • LSP support
  • AI feedback
  • Optimization opportunities

The runtime may initially treat most type information as advisory.

This allows the language to evolve incrementally while preserving simplicity.

Error Messages

Error messages are considered a core part of the language design.

Diagnostics should:

  • Be human-readable
  • Avoid type-theory jargon
  • Be actionable
  • Support AI-assisted repair
  • Remain local and predictable

Preferred style:

expected list<num>
got list<num|str>

value at index 1 is str

Avoid messages such as:

failed to unify T

AI-Assisted Development

The type system is intentionally designed to improve AI-assisted software development.

The combination of:

  • Explicit types
  • Unions
  • Match narrowing
  • Simple generics
  • Immutable values
  • Predictable control flow

creates strong feedback loops for:

  • Code generation
  • Repair
  • Refactoring
  • Validation
  • Documentation synthesis
  • Static analysis

Slug intentionally favors explicit semantic structure over abstraction-heavy designs.

Consequences

Positive

  • Improves API clarity and readability
  • Enables semantic validation and richer tooling
  • Greatly improves LSP capabilities
  • Supports AI-assisted development through fast feedback
  • Keeps the type system approachable and teachable
  • Avoids heavy type-system complexity
  • Provides practical enterprise-scale validation
  • Unions compose naturally with match expressions
  • Generic functions remain lightweight and predictable
  • Minimal generic surface area simplifies implementation
  • Runtime semantics remain straightforward
  • Encourages explicit handling of optional and error states
  • Strongly aligns with Slug’s readability-first philosophy

Negative

  • Lacks some advanced abstraction mechanisms found in larger type systems
  • No interface or trait system initially
  • No user-defined generic nominal types initially
  • Some APIs may require explicit unions rather than shared interfaces
  • Empty literal typing may occasionally require annotations
  • Function type syntax may be unfamiliar initially

Neutral

  • Type annotations remain optional in many locations
  • Runtime enforcement may initially remain limited
  • Future type-system expansion remains possible if practical needs emerge
  • Alias-only type declarations preserve simplicity over strict nominal safety
  • Match expressions become the primary narrowing mechanism
  • Generic type variables are intentionally restricted to function scope
  • The language remains fundamentally dynamic at runtime