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:
Ris 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
typedeclarations 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