mux-lang 0.1.2

The Mux Programming Language Compiler
Documentation

MuxLang Version  Rust  LLVM  crates.io  Docs  Status

Mux - A Programming Language For The People

By Derek Corniello

Why Mux?

  • Simple yet powerful: Combines Go-like minimalism with Rust-inspired safety.
  • Strong static typing: Helps catch errors early and ensures safer code.
  • LLVM-powered: Fast compilation and native performance.
  • Flexible memory management: Ease of use through reference counting.
  • Extensible: Designed to evolve with features like traits, concurrency, and a standard library.

Quick Start Guide

Check out the docs!

Runtime Setup

Mux builds a small runtime library the first time you compile or run a program. If you want to do this up front, run:

mux doctor

This verifies dependencies and builds the runtime if it is missing.

Repository Structure

  • mux-compiler is the compiler and CLI crate published as mux-lang. It installs the mux binary.
  • mux-runtime is the runtime library crate published as mux-runtime. The compiler links against it when producing executables.
  • mux-website is the website and docs content.
  • test_scripts holds sample programs used during development and manual testing.

Wanna Contribute?

Mux is an open-source project, and I welcome contributions from the community! Whether you're interested in adding new features, fixing bugs, improving documentation, or helping with testing, your contributions are valuable. Please check out the CONTRIBUTING.md file for guidelines on how to get started.

Important Notes!

While I take pride in this project, please be aware that Mux is still in its early stages of development. The language specification, compiler, and tooling are actively evolving. Expect breaking changes and incomplete features as I work towards a stable release.

I also want to acknowlege that I am aware that there are likely far better ways to do some of the things I have done here. This is a personal project and learning experience for me, and I appreciate your understanding as I continue to improve Mux.

Finally, I want to acknowledge that I have also been using this project as a way to experiment with AI tools to help me write, review, test and document code. While I have made every effort to ensure the accuracy and quality of the content, there may be occasional "bad code", errors, or inconsistencies. I appreciate your understanding as I continue to refine both the language and my use of these tools.

Mux Language Specification

1. Overview

Mux (fully "MuxLang") is a statically-typed, reference-counted language that combines:

  • Java-style explicit typing with local type inference
  • Python-style collection literals
  • Rust-style pattern-matching with guards
  • Curly-brace syntax and no semicolons
  • Minimal trait/Class model (use is instead of implements like Java)
  • Built-in Result<T,E> and Optional<T> for error handling

2. Lexical Structure

  • Case-sensitive identifiers: letters, digits, _, not starting with a digit
  • Whitespace (spaces, tabs, newlines) separates tokens
  • Comments:
    • Single-line: // comment
    • Multi-line: /* comment */
  • Statement termination: by end-of-line only (no semicolons)
  • Underscore placeholder: _ can be used for unused parameters, variables, or pattern matching wildcards
  • Keywords: func, returns, const, auto, class, interface, enum, match, if, else, for, while, break, continue, return, import, is, as, in, true, false, common, None, Optional, Result, Ok, Err

3. Types

Type System: Mux uses strict static typing with NO implicit type conversions. All type conversions must be explicit using conversion methods.

3.1 Primitive Types

int      // 64-bit signed integer
float    // 64-bit IEEE-754
bool     // true | false
char     // Unicode code point
string   // UTF-8 sequence

3.2 Type Conversions

Mux requires explicit type conversions for all operations. There are no implicit conversions between types.

3.2.1 Numeric Conversions

// Integer conversions
auto x = 42
auto x_float = x.to_float()     // int -> float
auto x_str = x.to_string()      // int -> str
auto x_same = x.to_int()        // int -> int (identity)

// Float conversions
auto pi = 3.14
auto pi_int = pi.to_int()       // float -> int (truncates: 3)
auto pi_str = pi.to_string()    // float -> str
auto pi_same = pi.to_float()    // float -> float (identity)

// Boolean conversions
auto flag = true
auto flag_int = flag.to_int()   // bool -> int (true=1, false=0)
auto flag_float = flag.to_float() // bool -> float (true=1.0, false=0.0)
auto flag_str = flag.to_string() // bool -> string ("true" or "false")

// Char conversions
auto ch = 'A'
auto ch_str = ch.to_string()    // char -> str

// Method calls on literals require parentheses
auto num = (3).to_string()      // Valid
auto val = (42).to_float()      // Valid
// auto bad = 3.to_string()     // ERROR: parsed as float 3.0

3.2.2 String Parsing (Fallible Conversions)

String and char parsing methods return Result<T, string> because they can fail:

// String to number (returns Result)
auto num_str = "42"
auto result = num_str.to_int()
match result {
    Ok(value) {
        print("Parsed: " + value.to_string())  // "Parsed: 42"
    }
    Err(error) {
        print("Parse error: " + error)
    }
}

// String to float
auto float_str = "3.14159"
auto float_result = float_str.to_float()
match float_result {
    Ok(value) { print(value.to_string()) }
    Err(msg) { print("Error: " + msg) }
}

// Char to digit (only works for '0'-'9')
auto digit_char = '5'
auto digit_result = digit_char.to_int()
match digit_result {
    Ok(digit) { print(digit.to_string()) }  // "5"
    Err(msg) { print(msg) }
}

auto letter = 'A'
auto letter_result = letter.to_int()
match letter_result {
    Ok(_) { print("Unexpected success") }
    Err(msg) { print(msg) }  // "Character is not a digit (0-9)"
}

3.2.3 No Implicit Conversions

The following operations are compile-time errors:

// Type mismatches in binary operations
auto bad1 = 1 + 1.0        // ERROR: cannot add int and float
auto bad2 = "hello" + 3    // ERROR: cannot add string and int
auto bad3 = true + false   // ERROR: cannot add bool and bool

// Type mismatches in comparisons
auto bad4 = 1 < 1.0        // ERROR: cannot compare int and float
auto bad5 = "a" == 1       // ERROR: cannot compare string and int

// Function argument type mismatches
func takes_string(string s) returns void { }
takes_string(123)          // ERROR: expected string, got int
// Correct usage requires explicit conversion
auto good1 = 1 + (1.0).to_int()           // OK: 2
auto good2 = "hello" + (3).to_string()    // OK: "hello3"
auto good3 = 1.to_float() < 1.0           // OK: true
auto good4 = (true).to_int() + (false).to_int()  // OK: 1

3.2.4 Available Conversion Methods

From Type Method Returns Notes
int .to_string() string Converts to string representation
int .to_float() float Converts to floating-point
int .to_int() int Identity function
float .to_string() string Converts to string representation
float .to_int() int Truncates decimal part
float .to_float() float Identity function
bool .to_string() string Returns "true" or "false"
bool .to_int() int Returns 1 or 0
bool .to_float() float Returns 1.0 or 0.0
char .to_string() string Converts char to string
char .to_int() Result<int, string> Digit value for '0'-'9' only
string .to_string() str Identity function
string .to_int() Result<int, string> Parses string as integer
string .to_float() Result<float, string> Parses string as float

3.2.5 Technical Design: Uniform Value Representation

Mux uses a single unified type (Value enum) to represent all runtime values, enabling uniform handling in collections and generics.

The Value Enum

pub enum Value {
    Bool(bool),
    Int(i64),
    Float(OrderedFloat<f64>),
    String(String),
    List(Vec<Value>),
    Map(BTreeMap<Value, Value>),
    Set(BTreeSet<Value>),
    Tuple(Box<Tuple>),
    Optional(Option<Box<Value>>),
    Result(Result<Box<Value>, String>),
    Object(ObjectRef),
}

Boxing Strategy

All primitives are boxed into *mut Value pointers:

  1. Allocation: mux_rc_alloc(value) allocates RefHeader + Value
  2. Storage: Pointers stored in variables, collections, and function returns
  3. Extraction: Typed accessors (mux_value_get_int, etc.) unwrap values

This design enables:

  • Generic collections: list<T> works uniformly for all types
  • Polymorphic functions: Same function can handle any type
  • Simple FFI: C code receives consistent void* pointers

Type Representations Through Compilation

Mux maintains three distinct type representations:

Representation Purpose
TypeNode AST representation, source locations
Type Semantic analysis, type resolution
BasicTypeEnum LLVM IR generation

The separation enables error reporting with source locations while keeping semantic analysis LLVM-independent.

3.3 Built-in Functions

Mux provides essential built-in functions for output and utility operations. These are always available without imports.

Output Functions

Design Note: print is a direct runtime call that outputs to stdout. The runtime handles string formatting and newline appending.

Utility Functions

range(int start, int end) -> list<int> - Returns a list of integers from start (inclusive) to end (exclusive). The result is always a list<int>.

// Generate indices for iteration
for i in range(0, 5) {
    print(i.to_string())  // Prints 0, 1, 2, 3, 4
}

// Create a list of numbers
auto numbers = range(10, 15)  // [10, 11, 12, 13, 14]

Design Note: range() is the primary way to create numeric sequences for iteration, as Mux does not support C-style for (int i = 0; i < n; i++) loops.

Input Functions

read_line() -> string - Reads a line from standard input and returns it as a string (excluding the newline).

print("Enter your name: ")
auto name = read_line()
print("Hello, " + name)

3.4 Composite Types

Optional<T>
Result<T, E>
list<T>
map<K,V>
set<T>
tuple<T, U>

### 3.4.1 Tuples

Tuples are fixed size pairs. A tuple always has exactly two elements.

```mux
auto pair = (1, "one")
tuple<int, string> typed = (2, "two")

print(pair.left.to_string())   // "1"
print(pair.right.to_string())  // "one"

Tuples also support to_string() and a default constructor:

auto empty = tuple<int, string>.new()  // (0, "")

### 3.5 Generics

Mux supports Go/Rust-style generics with type parameters and interface bounds using the `is` keyword:

```mux
// Generic function with type constraints
func max<T is Comparable>(T a, T b) returns T {
    if a > b {
        return a
    }
    return b
}

// Generic function with Stringable bound for to_string()
func greet<T is Stringable>(T value) returns string {
    return "Hello, " + value.to_string()
}

// Generic function with Add bound for + operator
func add<T is Add>(T a, T b) returns T {
    return a.add(b)
}

// Generic class
class Stack<T> {
    list<T> items
    
    func push(T item) returns void {
        self.items.push_back(item)
    }
    
    func pop() returns Optional<T> {
        if self.items.is_empty() { return None }
        return self.items.pop_back()
    }
}

3.5.1 Technical Design: Monomorphization

Mux uses compile-time monomorphization for generics, generating specialized code for each type instantiation.

Monomorphization Process

  1. Type inference: Determine concrete types from function arguments
  2. Name generation: Create unique identifier: FunctionName$int$string$
  3. Type substitution: Replace type parameters with concrete types
  4. Code generation: Emit specialized function body
  5. Caching: Store generated methods to avoid regeneration

Example

func identity<T>(T value) returns T {
    return value
}

auto a = identity(42)        // Generates: identity$int
auto b = identity("hello")   // Generates: identity$string

The compiler substitutes types in the AST before code generation:

// Original generic function
func identity<T>(T value) returns T { ... }

// After substitution for T = int
func identity$int(int value) returns int { ... }

Why Monomorphization?

  • Zero runtime cost: No boxing, no vtables, no type checks
  • Static dispatch: Methods resolved at compile time
  • LLVM optimization: Each specialization can be fully optimized

The tradeoff is increased code size (one copy per type combination).

3.6 Built-in Interfaces

Mux provides built-in interfaces for common operations on generic types:

Interface Methods Description
Stringable to_string() -> string Types that can be converted to string
Add add(Self) -> Self Types that support + operator
Sub sub(Self) -> Self Types that support - operator
Mul mul(Self) -> Self Types that support * operator
Div div(Self) -> Self Types that support / operator
Arithmetic add, sub, mul, div Types that support all arithmetic operators
Equatable eq(Self) -> bool Types that support == and != operators
Comparable cmp(Self) -> int Types that support <, <=, >, >= operators

Operator Mapping:

  • a + b uses Add.add() when type doesn't natively support +
  • a > b uses Comparable.cmp() returning -1, 0, or 1
  • a == b uses Equatable.eq()

Primitives and Interfaces:

  • int: Implements Stringable, Add, Sub, Mul, Div, Arithmetic, Equatable, Comparable
  • float: Implements Stringable, Add, Sub, Mul, Div, Arithmetic, Equatable, Comparable
  • string: Implements Stringable, Add, Equatable, Comparable
  • bool: Implements Stringable, Equatable

Example: Custom Type Implementing Interfaces

interface Add {
    func add(Self) returns Self
}

class Point {
    int x
    int y
    
    func add(Point other) returns Point {
        return Point.new(self.x + other.x, self.y + other.y)
    }
}

func sum_points<T is Add>(list<T> points) returns T {
    auto result = points[0]
    for i in range(1, points.size()) {
        result = result.add(points[i])
    }
    return result
}

auto points = [Point.new(1, 2), Point.new(3, 4), Point.new(5, 6)]
auto total = sum_points(points)  // Point(9, 12)

3.7 Generic Type Constraints

// Using built-in interfaces
func process<T is Stringable>(list<T> items) returns void {
    for item in items {
        print(item.to_string())
    }
}

// Multiple bounds (AND semantics - type must implement all)
func combine<T is Add & Stringable>(T a, T b) returns string {
    return (a.add(b)).to_string()
}

// Type parameters must be explicitly specified
auto max_int = max<int>(3, 7)           // T = int, Comparable bound satisfied
auto names = ["apple", "banana"]
print(greet<string>(names[0]))          // T = string, Stringable bound satisfied

3.8 User-Defined Types

  • Structs: simple aggregates
  • Enums: tagged unions (see §8)
  • Classes: with fields + methods (see §9)

4. Variable & Constant Declarations

4.1 Explicit Typing

const int MAX = 100

// Variables (explicit type required for declarations without inference)
int x = 5
bool flag = true
string name = "MuxLang"

4.2 Variable Declarations

Mux supports both explicit types and type inference with auto:

// Type inferred with 'auto'
auto x = 42          // inferred as int
auto pi = 3.14159    // inferred as float
auto name = "Mux"    // inferred as str

// Explicit type annotation
int count = 0
list<string> names = []
map<string, string | int> user = {"name": "Alice", "age": 30}

// Valid inference
auto value = someFunction()
auto numbers = [1, 2, 3]
map<string, string> userMap = {"key": "value"}

// Invalid - no initializer with 'auto'
auto x  // ERROR: cannot infer type without initializer

// Function parameters must be explicitly typed
func process(auto item) returns void { }  // ERROR
func process(int item) returns void { }   // Valid

// Unused parameter
func process(int item, int _) returns void { }  // second parameter unused

All declarations require either an explicit type or auto with an initializer; semicolons are not used.

4.3 Constants

Constants are immutable values that cannot be reassigned or modified after initialization:

// Function-level constants
func calculate() returns int {
    const int MULTIPLIER = 10
    const float TAX_RATE = 0.08
    int value = 100
    return value * MULTIPLIER
}

// Constants in classes
class Config {
    const int MAX_RETRIES
    int current_retry
    
    func increment() returns void {
        self.current_retry++  // OK - mutable field
        // self.MAX_RETRIES++  // ERROR: Cannot modify const field 'MAX_RETRIES'
    }
}

auto cfg = Config.new()
cfg.current_retry = 1  // OK - mutable field
// cfg.MAX_RETRIES = 5  // ERROR: Cannot assign to const field 'MAX_RETRIES'

Const Enforcement:

  • Cannot reassign: const_var = new_value → ERROR
  • Cannot use compound assignment: const_var += 1 → ERROR
  • Cannot increment/decrement: const_var++ or const_var-- → ERROR
  • Applies to both identifiers and class fields
  • Use const when you want a value that won't change after initialization

5. Functions

func add(int a, int b) returns int {
    return a + b
}

func greet(string name, int times = 1) returns void {
    for i in range(0, times) {
        print("Hello, " + name)
    }
}

func processData() returns map<string, int> {
    map<string, int> results = {"processed": 100, "skipped": 5}
    auto total = results["processed"] + results["skipped"]
    results["total"] = total
    return results
}

// Function with unused parameters
func callback(string event, int timestamp, string _) returns void {
    print("Event: " + event + " at " + timestamp)
    // third parameter ignored
}
  • Keyword func
  • Parameter list with explicit types; default values optional
  • returns clause for return type (explicit, no inference)
  • Body enclosed in {…}; no semicolons
  • Local variables within functions can use auto inference
  • Use _ for unused parameters

6. Operators

6.1 Arithmetic Operators

Mux supports standard arithmetic operations with strict type requirements (no implicit conversions):

Operator Description Types Example
+ Addition int, float, string 5 + 3, "a" + "b"
- Subtraction int, float 10 - 4
* Multiplication int, float 6 * 7
/ Division int, float 15 / 3
% Modulo int, float 10 % 3
** Exponentiation int, float 2 ** 3

Exponentiation Operator (**):

  • Right-associative: 2 ** 3 ** 2 equals 2 ** (3 ** 2) = 512
  • Higher precedence than * and /: 2 * 3 ** 2 equals 2 * 9 = 18
  • Works on both int and float types
auto squared = 5 ** 2        // 25
auto cubed = 2.0 ** 3.0      // 8.0
auto complex = 2 ** 3 ** 2   // 512 (right-associative)

6.2 Increment and Decrement Operators

Mux provides postfix increment (++) and decrement (--) operators with specific design constraints:

Design Constraints:

  • Postfix only: counter++ is valid, ++counter is not supported
  • Standalone only: Must appear on their own statement line, not within expressions
  • Only on mutable variables: Cannot be used on const declarations or literals
  • Type preservation: Operates on int types only
auto counter = 0
counter++         // Valid: counter is now 1
counter--         // Valid: counter is now 0

// INVALID - cannot use in expressions:
// auto x = counter++    // ERROR: ++ cannot be used in expressions
// auto y = (counter++) + 5  // ERROR: standalone only

// INVALID - prefix not supported:
// ++counter            // ERROR: prefix increment not supported

// INVALID - cannot modify const:
const int MAX = 100
// MAX++               // ERROR: cannot modify const

Rationale: The postfix-only, standalone-only design prevents ambiguity and side-effect confusion that can occur with prefix operators or expression-embedded increments.

6.3 Comparison and Logical Operators

Operator Description Types Example
== Equality All comparable types a == b
!= Inequality All comparable types a != b
< Less than int, float, string 5 < 10
<= Less than or equal int, float, string x <= 100
> Greater than int, float, string y > 0
>= Greater than or equal int, float, string age >= 18
&& Logical AND bool a && b (short-circuit)
|| Logical OR bool a || b (short-circuit)
! Logical NOT bool !flag

Short-circuit Evaluation:

  • && only evaluates right side if left is true
  • || only evaluates right side if left is false

6.3.1 Technical Design: Short-Circuit Logical Operators

The && and || operators use LLVM control flow for short-circuit evaluation, not simple boolean operations.

Phi Nodes for Result Merging

Phi nodes select a value based on which predecessor block executed:

%result = phi i1 [ 0, %left_block ], [ %b_value, %right_block ]
  • From left_block: constant 0 (left was false)
  • From right_block: computed %b_value (left was true)

Why This Approach?

If LLVM generated a && b as a single expression:

  1. Both a and b would always be evaluated (no short-circuit)
  2. No opportunity for branch prediction
  3. Can't exploit constant operands

The basic block approach preserves semantics while enabling LLVM optimizations (dead code elimination, inlining, vectorization).

6.4 The in Operator

The in operator tests for membership/containment with strict type requirements:

Left Operand Right Operand Description
T list<T> Check if value exists in list
T set<T> Check if value exists in set
string string Check if substring exists
char string Check if character exists in string

Type Constraints:

  • Both operands must have compatible element types
  • No implicit type conversions allowed
  • Returns bool
// List containment
auto nums = [1, 2, 3, 4, 5]
auto hasThree = 3 in nums     // true
auto hasTen = 10 in nums      // false

// Set containment
auto tags = {"urgent", "important"}
auto isUrgent = "urgent" in tags    // true

// String containment
auto msg = "hello world"
auto hasWorld = "world" in msg      // true
auto hasFoo = "foo" in msg          // false

// Character in string
auto hasO = 'o' in msg              // true
auto hasZ = 'z' in msg              // false

// INVALID - type mismatch:
// auto bad = "1" in nums           // ERROR: string not in list<int>
// auto bad2 = 1 in msg             // ERROR: int not in string

6.5 Collection Operators

Concatenation with +

The + operator is overloaded for collection types with type-specific semantics:

Types Operation Result
list<T> + list<T> Concatenation Combined list
map<K,V> + map<K,V> Merge Combined map (latter overwrites former on key collision)
set<T> + set<T> Union Set containing all unique elements
string + string Concatenation Combined string

6.5.1 Technical Design: Operator Overloading

Operators map to interface methods, enabling user-defined operator behavior.

Operator to Method Mapping

Operator Interface Method
+ Add add(Self) -> Self
- Sub sub(Self) -> Self
* Mul mul(Self) -> Self
/ Div div(Self) -> Self
== Equatable eq(Self) -> bool
< Comparable cmp(Self) -> int

Semantic Validation

The semantic analyzer checks operator types:

// For a + b
let left_type = analyzer.get_expression_type(left_expr)?;
let right_type = analyzer.get_expression_type(right_expr)?;

if !type_supports_addition(&left_type) {
    return Err(format!("Type {} does not support +", left_type));
}

Code Generation

For primitive types, direct LLVM operations:

%result = add i64 %a, %b

For interface types, method call:

%result = call i8* @Add.add(i8* %a_ptr, i8* %b_ptr)

Type Constraints:

  • Both operands must be the exact same collection type
  • No mixing of collection types (e.g., list + set is an error)
  • No implicit element type conversions
// List concatenation
auto list1 = [1, 2]
auto list2 = [3, 4]
auto combined = list1 + list2    // [1, 2, 3, 4]

// Map merge (latter wins on key collision)
auto map1 = {"a": 1, "b": 2}
auto map2 = {"b": 3, "c": 4}     // Note: key "b" exists in both
auto merged = map1 + map2        // {"a": 1, "b": 3, "c": 4}

// Set union
auto set1 = {1, 2, 3}
auto set2 = {3, 4, 5}
auto unioned = set1 + set2       // {1, 2, 3, 4, 5}

// String concatenation
auto greeting = "Hello, " + "World"  // "Hello, World"

// INVALID - type mismatch:
// auto bad = [1, 2] + {3, 4}       // ERROR: cannot add list and set
// auto bad2 = [1, 2] + [3.0, 4.0]  // ERROR: list<int> + list<float>

7. Lambdas & Closures

// Block-form lambda with explicit types and return type
auto square = func(int n) returns int {
    return n * n
}

auto doubler = func(float x) returns float {
    return x * 2.0
}

// Passing lambdas to functions
auto result = apply(10, func(int x) returns int {
    return x + 5
})

// Lambda with unused parameters
auto processFirst = func(int first, int _) returns int {
    return first * 2  // second parameter ignored
}

// Block-form lambda with mixed explicit/inferred types
auto filter = func(list<int> nums, func(int) returns bool cond) returns list<int> {
    list<int> out = []
    for n in nums {
        if cond(n) {
            out.push_back(n)
        }
    }
    return out
}
  • All lambdas use block syntax with func(params) { ... }
  • Lambda parameters can use auto when type can be inferred from context
  • Use _ for unused lambda parameters
  • Optional capture list in […] form [needs more clarification]

8. Control Flow

8.1 If / Else

if x > 0 {
    print("positive")
} else if x < 0 {
    print("negative")
} else {
    print("zero")
}

// With type inference
auto message = if x > 0 { "positive" } else { "non-positive" }

8.2 Match with Guards

match (value) {
    Some(v) if v > 10 {
        auto msg = "large: " + v  // local inference
        print(msg)
    }
    Some(v) {
        print("small: " + v)
    }
    None {
        print("no value")
    }
    _ {
        print("unexpected case")  // wildcard pattern
    }
}

8.2.1 Match as Switch Statement

Match statements can be used as switch statements for any type:

// Match on int literals (like a switch)
auto status = 200
match status {
    200 { print("OK") }
    404 { print("Not Found") }
    500 { print("Server Error") }
    _ { print("Unknown status") }
}

// Match on string literals
auto command = "start"
match command {
    "start" { print("Starting...") }
    "stop" { print("Stopping...") }
    "restart" { print("Restarting...") }
    _ { print("Unknown command") }
}

// Variable binding in patterns
auto value = 42
match value {
    1 { print("one") }
    captured { print("got: " + captured.to_string()) }
    _ { print("other") }
}

// List literal matching
auto nums = [1, 2, 3]
match nums {
    [] { print("empty") }
    [1, 2, 3] { print("three elements") }
    [first, ..rest] { print("has elements") }
}

// Switch with guards
auto score = 85
match score {
    n if n >= 90 { print("A") }
    n if n >= 80 { print("B") }
    n if n >= 70 { print("C") }
    n if n >= 60 { print("D") }
    _ { print("F") }
}

8.3 For Loops

for item in myList {
    auto processed = transform(item)  // type inferred
    print(processed)
}

// Iterator with inference
for item in collection {
    // item type inferred from collection element type
    process(item)
}

// Ignoring loop variables when not needed
for _ in range(0, 10) {
    doSomething()  // don't care about the index
}

// Destructuring in loops with unused parts
for (key, _) in keyValuePairs {
    print("Key: " + key)  // value ignored
}

8.4 While Loops

while cond {
    auto currentTime = getCurrentTime()  // local inference
    // ...
}

8.5 Break / Continue / Return

Works as in C/Java.


9. Enums (Tagged Unions)

enum Shape {
    Circle(float radius)
    Rectangle(float width, float height)
    Square(float size)
}
enum Shape {
    Circle(float radius)
    Rectangle(float width, float height)
    Square(float size)
}

// Usage with inference
auto myShape = Circle.new(5.0)  // type inferred as Shape
list<Shape> shapes = [Circle.new(1.0), Rectangle.new(2.0, 3.0)]

// Pattern matching with unused enum data
match (shape) {
    Circle(_) {
        print("It's a circle")  // radius value ignored
    }
    Rectangle(width, _) {
        print("Rectangle with width: " + width)  // height ignored
    }
    Square(size) {
        print("Square with size: " + size)
    }
}

Each variant may carry data. Pattern-match with destructuring and guards. Use _ to ignore unused enum data in patterns.


10. Classes & Traits

10.1 Traits (Interfaces)

interface Drawable {
    func draw() returns void
}

10.2 Classes with is Clause

class Circle is Drawable, ShapeLike {
    float radius  // explicit type required for fields

    func draw() returns void {
        auto message = "Circle radius=" + radius  // local inference in methods
        print(message)
    }

    func area() returns float {
        const float PI = 3.1415  // inferred as float
        return PI * radius * radius
    }
    
    // Method with unused parameters
    func resize(float newRadius, string _) returns void {
        radius = newRadius  // second parameter ignored
    }
}

// Generic class example
class Stack<T> {
    list<T> items

    func push(T item) returns void {
        items.push_back(item)
    }

    func pop() returns Optional<T> {
        if items.isEmpty() {
            return None
        }
        auto item = items.pop_back()
        return Some(item)
    }
}

// Usage with inference
auto circle = Circle.new(5.0)  // type inferred as Circle
list<Drawable> shapes = [circle]
Stack<int> intStack = Stack<int>.new()  // explicit generic instantiation with .new()

10.3 Static Methods with common

Mux uses the common keyword to declare static (class-level) methods that can be called without an instance. This is distinct from const which declares immutable values.

common vs const:

Keyword Purpose Usage Example
common Static methods and factory functions Called on the class itself, not instances ClassName.method()
const Immutable constants Values that cannot change after initialization const int MAX = 100
class Stack<T> {
    list<T> items
    
    // Instance method - called on instances
    func push(T item) returns void {
        self.items.push_back(item)
    }
    
    // Static method - called on the class
    common func who_am_i() returns string {
        return "I'm a Stack!"
    }
    
    // Factory pattern - static method that creates instances
    common func from(list<T> init_list) returns Stack<T> {
        auto new_stack = Stack<T>.new()
        new_stack.items = init_list
        return new_stack
    }
}

// Calling static methods
print(Stack.who_am_i())                    // "I'm a Stack!"
auto s = Stack<int>.from([1, 2, 3])        // Factory method

// Calling instance methods
auto stack = Stack<int>.new()
stack.push(42)                             // Instance method

Key Differences:

  • Instance methods (no keyword) operate on self and require an instance
  • Static methods (common) have no self and are called on the class
  • Const fields are immutable instance/class fields, not methods
  • Static methods cannot access instance fields (no self context)
  • Factory patterns commonly use common func from(...) to create instances with pre-populated data

10.4 Class Instantiation

Classes are instantiated using the .new() method pattern:

// Basic instantiation
auto circle = Circle.new()           // No constructor arguments
auto circle2 = Circle.new(5.0)       // With constructor arguments

// Generic class instantiation
auto int_stack = Stack<int>.new()
auto string_stack = Stack<string>.new()

// Using factory methods
auto prebuilt = Stack<int>.from([1, 2, 3])
auto pair = Pair<string, int>.from("key", 42)

10.5 Technical Design: Object System

Mux objects use Rust's Rc<Arc<ObjectData>> pattern for shared ownership with type information.

ObjectRef Structure

struct ObjectData {
    ptr: *mut c_void,      // User's object data
    type_id: TypeId,       // Runtime type identifier
    size: usize,           // Size for deallocation
    ref_count: AtomicUsize, // Reference count
}

struct ObjectRef {
    data: Rc<ObjectData>,  // Shared ownership
}

Type Registry

lazy_static::lazy_static! {
    static ref TYPE_REGISTRY: Mutex<HashMap<TypeId, ObjectType>> = ...
    static ref NEXT_TYPE_ID: AtomicUsize = ...
}

pub struct ObjectType {
    pub id: TypeId,
    pub name: String,
    pub size: usize,
    pub destructor: Option<fn(*mut c_void)>,
}

Each class registers with the runtime, receiving a unique TypeId.

Allocation

pub fn alloc_object(type_id: TypeId) -> *mut Value {
    let obj_type = TYPE_REGISTRY.lock().get(&type_id);
    let size = obj_type.size;
    
    let ptr = std::alloc::alloc(size);
    let obj_ref = ObjectRef::new(ptr, type_id, size);
    
    mux_rc_alloc(Value::Object(obj_ref))
}

Why This Design?

  • Type information at runtime: type_id enables type checks and casts
  • Proper cleanup: size and optional destructor for cleanup
  • Shared ownership: Multiple references to same object

Design Note: Mux uses explicit .new() rather than direct constructor calls to distinguish class instantiation from function calls and enum variant construction.

10.4.1 Technical Design: Interface Dispatch (Static)

Mux uses static dispatch for interfaces - no runtime vtable lookup.

VTable Generation

VTables are generated at compile time:

@vtable_Circle = constant {
    i32,           // type tag
    void (i8*)*   // draw method pointer
} {
    i32 1,         // Circle's type ID
    void (i8*)* @Circle.draw
}

Method Name Mangling

Methods are prefixed with their class name:

class Circle {
    func draw() returns void { ... }
}

Generates LLVM function: Circle.draw

Why Static Dispatch?

  • Zero cost: No pointer indirection, direct function calls
  • Inlining: LLVM can inline interface methods
  • Optimization: Better branch prediction, no indirect jumps

Comparison to Dynamic Dispatch

// Dynamic dispatch (Python, Java)
circle.draw()  // Look up vtable, find slot, call

// Static dispatch (Mux)
Circle.draw()  // Direct call to Circle.draw

The tradeoff: interfaces cannot be added to types from other modules (no "extension traits").


11. Collections & Literals

// Explicit typing
list<int> nums = [1, 2, 3, 4]
map<string, int> scores = {"Alice": 90, "Bob": 85}

// With type inference
auto nums = [1, 2, 3, 4]           // inferred as list<int>
map<string, int> scores = {"Alice": 90, "Bob": 85}
// mixed = [1, 2.5, 3]           // ERROR: conflicting types, explicit type needed

// Nested collections
list<list<int>> matrix = [[1, 2], [3, 4]]
map<string, list<int>> lookup = {"users": [1, 2, 3], "admins": [4, 5]}

// Complex nested structures
auto users = [
    {"name": "Alice", "scores": [95, 87, 92]},
    {"name": "Bob", "scores": [78, 85, 90]}
]  // inferred as list<map<string, string | list<int>>>

auto data = {
    "numbers": [1, 2, 3, 4, 5],
    "metadata": {"version": "1.0", "count": 5}
}  // inferred as map<string, list<int> | map<string, string | int>>

// Generic collections
list<Pair<int, string>> pairs = [Pair.new(1, "one"), Pair.new(2, "two")]
list<Container<int>> containers = list<Container<int>>()

11.1 Collection Methods

All collections provide a consistent API for access, mutation, and inspection.

List Methods

Method Returns Description
.size() int Returns the number of elements in the list
.is_empty() bool Returns true if list has no elements
.get(int index) Optional<T> Safe access; returns Some(value) or None if out of bounds
[int index] T Direct access; runtime error if out of bounds
.push(T item) void Appends item to the end of the list (alias for push_back)
.push_back(T item) void Appends item to the end of the list
.pop() Optional<T> Removes and returns last item, or None if empty (alias for pop_back)
.pop_back() Optional<T> Removes and returns last item, or None if empty
.to_string() string Returns a string representation of the list
auto nums = [1, 2, 3]

// Safe access with Optional
match nums.get(0) {
    Some(first) { print(first.to_string()) }  // "1"
    None { print("Index out of bounds") }
}

// Direct access (runtime error if index invalid)
auto second = nums[1]  // 2

// Mutation
nums.push_back(4)      // [1, 2, 3, 4]
match nums.pop_back() {
    Some(last) { print(last.to_string()) }  // "4"
    None { }
}

// Inspection
print(nums.size().to_string())     // "3"
print(nums.is_empty().to_string())   // "false"

Map Methods

Method Returns Description
.size() int Returns the number of key-value pairs
.is_empty() bool Returns true if map has no entries
.get(K key) Optional<V> Safe lookup; returns Some(value) or None if key not found
[K key] V Direct access; runtime error if key not found
.put(K key, V value) void Inserts or updates a key-value pair
.contains(K key) bool Returns true if key exists in map
.remove(K key) Optional<V> Removes key and returns value, or None if key not found
.to_string() string Returns a string representation of the map
auto scores = {"Alice": 90, "Bob": 85}

// Safe access
match scores.get("Alice") {
    Some(score) { print(score.to_string()) }  // "90"
    None { print("Student not found") }
}

// Direct access
auto bobScore = scores["Bob"]  // 85

// Map entries are immutable; reassign to update
scores["Alice"] = 95  // Updates existing key

Set Methods

Method Returns Description
.size() int Returns the number of elements
.is_empty() bool Returns true if set is empty
.add(T item) void Adds an item to the set
.contains(T item) bool Returns true if item exists in set
.remove(T item) Optional<T> Removes item and returns it, or None if not found
.to_string() string Returns a string representation of the set
auto tags = {"urgent", "important", "review"}
print(tags.size().to_string())  // "3"

// Add and check membership
tags.add("priority")
if tags.contains("urgent") {
    print("Has urgent tag")
}

// Remove item
match tags.remove("review") {
    Some(removed) { print("Removed: " + removed) }
    None { print("Item not found") }
}

Design Note: Collections use consistent method naming across all types. Safe access via .get() returns Optional<T>, while direct access with [] provides unchecked access with runtime bounds checking.

11.1 Technical Design: Nested Collections

Mux's collections (list, map, set) can contain any Value, enabling arbitrary nesting.

Collection Implementations

Collection Rust Type Use Case
list<T> Vec<Value> Contiguous array, indexed access
map<K,V> BTreeMap<Value, Value> Key-value pairs, sorted keys
set<T> BTreeSet<Value> Unique elements, membership test

Why BTreeMap/BTreeSet?

Unlike HashMap/HashSet, BTree variants provide:

  • Deterministic iteration order: Always the same order
  • Ordered operations: First/last element, range queries
  • Reproducible output: to_string() produces consistent results

Nested Example

auto nested = [
    {"name": "Alice", "scores": [95, 87, 92]},
    {"name": "Bob", "scores": [78, 85, 90]}
]
// Structure: list<map<string, list<int> | string>>

The type system tracks nesting through:

  1. Parser: Creates nested TypeNode structures
  2. Semantic Analyzer: Resolves to Type::List(Type::Map(...))
  3. Code Generator: Creates appropriate LLVM types

Reference Counting in Collections

Collections are RC-allocated and contain RC-allocated values. When freed:

  1. Collection's refcount reaches zero
  2. Collection's Vec<Value> is dropped
  3. Each contained Value has its refcount decremented
  4. Nested collections are freed recursively

12. Error Handling

12.1 Result<T, E>

func divide(int a, int b) returns Result<int, string> {
    if b == 0 {
        return Err("division by zero")
    }
    return Ok(a / b)
}

// Usage with inference
auto result = divide(10, 2)  // inferred as Result<int, string>
match result {
    Ok(value) {
        auto message = "Result: " + value  // local inference
        print(message)
    }
    Err(error) {
        print("Error: " + error)
    }
    _ {
        print("Unexpected result")  // wildcard for completeness
    }
}

// Ignoring error details when not needed
match result {
    Ok(value) {
        print("Success: " + value)
    }
    Err(_) {
        print("Some error occurred")  // error details ignored
    }
}

12.2 Optional<T>

func findEven(list<int> xs) returns Optional<int> {
    for x in xs {
        if x % 2 == 0 {
            return Some(x)
        }
    }
    return None
}

// Usage with inference
Optional<int> maybeEven = findEven([1, 3, 4, 7])  // inferred as Optional<int>

match maybeEven {
    Some(value) {
        print("Found even: " + value)
    }
    None {
        print("No even number found")
    }
    _ {
        print("Unexpected optional state")
    }
}

// Ignoring the wrapped value when you just care about presence
match maybeEven {
    Some(_) {
        print("Got a value")  // don't care what the value is
    }
    None {
        print("Got nothing")
    }
}

Use match to unpack results and optionals. Use _ to ignore unused values in patterns.

12.3 Technical Design: Result and Optional

Both Result<T, E> and Optional<T> use a uniform runtime representation.

Memory Layout

pub struct Result<T, E> {
    discriminant: i32,    // 0 = Ok, 1 = Err
    data: *mut T,        // pointer to value
}

pub struct Optional<T> {
    discriminant: i32,    // 0 = None, 1 = Some
    data: *mut T,        // pointer to value
}

Same layout enables generic code to work with either type.

Runtime Behavior

Discriminant: Determines which variant is active Data pointer: Points to the contained value (boxed like all other values)

auto opt = Some(42)      // discriminant=1, data=box(42)
auto res = Ok("error")   // discriminant=0, data=box("error")

Why This Design?

  • Single runtime representation: Collections can store either
  • No enum overhead: No runtime enum tag beyond discriminant
  • Error propagation: Easy to implement with match statements
  • Interop: Optional and Result can wrap the same types

13. Memory Model

  • Reference-counted runtime; deterministic memory management with no manual free
  • All objects and collections live on the heap
  • Primitives passed by value, objects by reference

13.1 Technical Design: Reference Counting

Mux uses atomic reference counting for deterministic memory management. Every heap-allocated value is prefixed with a reference count header.

Memory Layout

┌──────────────────┬─────────────┐
│   RefHeader      │    Value    │
│ ref_count: u64   │  (payload)  │
└──────────────────┴─────────────┘
    ^
    Allocation pointer

The RefHeader uses AtomicUsize for thread-safe atomic operations. The Value payload contains the actual data.

Reference Count Operations

Increment (mux_rc_inc): Called when creating a new reference:

  • Assigning to a new variable
  • Passing as a function argument
  • Adding to a collection

Decrement (mux_rc_dec): Called when a reference goes out of scope:

  • Variable assignment is overwritten
  • Function returns (cleanup of local variables)

When mux_rc_dec returns true, the refcount reached zero and memory is freed automatically.

Scope-Based Tracking

The compiler generates cleanup code using a scope stack:

  1. Enter scopepush_rc_scope() (function entry, if-block, loop-body, match-arm)
  2. Track variabletrack_rc_variable(name, alloca) for each RC-allocated variable
  3. Exit scopegenerate_all_scopes_cleanup() iterates through all scopes in reverse order

This ensures proper cleanup order and handles early returns.


14. References

Mux uses references for safe memory access and manipulation:

  • &T denotes a reference to type T
  • &expr creates a reference to expr
  • References are automatically dereferenced when used
  • No pointer arithmetic is allowed
  • References are non-nullable by default
  • Use Option<&T> for nullable references
// Basic reference usage
int x = 10
auto r = &x      // r is of type &int
print("ref value: " + (*r).to_string())  // 10 - explicit dereference with *

*r = 20          // Changes x to 20 via dereference
print("val after ref update: " + (*r).to_string())  // 20
print("x is now: " + x.to_string())  // 20

// References to list elements
auto numbers = [1, 2, 3, 4, 5]
auto first = &numbers[0]  // &int
print("first element: " + (*first).to_string())  // 1

// Function taking a reference
func update(&int ref) returns void {
    *ref = *ref + 1  // Must explicitly dereference to modify
}

update(&x)
print("val after update: " + x.to_string())  // 21

Reference Syntax:

  • Create reference: &variable or &expression
  • Dereference: *reference (required for both reading and writing)
  • Pass to functions: func(&int ref) declares parameter, update(&x) passes reference
  • References to references: Not supported

Design Note: Unlike some languages with automatic dereferencing, Mux requires explicit * for all reference operations. This makes memory access patterns explicit and prevents accidental mutation bugs.


15. Modules & Imports

import math
import shapes.circle as circle

// Usage with inference
float pi = math.PI         // type inferred from math module
auto c = circle.new(5.0)  // type inferred from constructor

// Import with unused alias for completeness
import utils.logger as _  // imported but not directly used in this scope
  • Python-style imports only
  • Module paths map directly to file paths
  • Imported symbols can be used with type inference
  • Use _ alias when importing for side effects only

15.1 Technical Design: Module System

Mux uses Python-style module imports with compile-time resolution.

Import Resolution

import math          // math.mux in same directory
import shapes.circle // shapes/circle.mux

File paths map to module paths:

  • import foofoo.mux
  • import shapes.circleshapes/circle.mux

Name Mangling for Imported Functions

Functions from imported modules use mangled names:

// math.mux
func fibonacci(int n) returns int { ... }

// main.mux
import math
auto result = math.fibonacci(10)

Generates: math!fibonacci (not fibonacci)

This prevents conflicts when multiple modules define functions with the same name.

Top-Level Statements

Top-level statements in modules become a module initialization function:

// config.mux
const int MAX_USERS = 100
auto initialized = false

func init() returns void {
    initialized = true
}

The compiler generates:

define void @config.init() { ... }

And calls it before main() executes.

Module Dependencies

The compiler:

  1. Parses all imports
  2. Builds dependency graph
  3. Processes modules in topological order
  4. Generates initialization functions for each module

15. Type Inference Guidelines

16.1 When to Use auto

Recommended:

  • Local variables with obvious initialization
  • Complex generic types that are clear from context
  • Temporary variables in calculations
  • Iterator variables in loops

16.2 Inference Limitations

// These require explicit types due to ambiguity
list<int> empty = []           // empty collection needs explicit type
auto empty = list<int>()       // or explicit constructor

Result<int, string> pending    // uninitialized variables need explicit type

// Generic instantiation may need explicit types
Stack[int] stack = Stack[int]()      // explicit generic parameter
auto pairs = zip<int, string>(numbers, names)  // when inference is ambiguous

16.3 Using Underscore Effectively

// Good uses of underscore
func process(int data, string _) { }  // ignore second parameter
for _ in range(0, 10) { }            // ignore loop counter
match result { Ok(_) { } }           // ignore success value

// Avoid overusing underscore when names would help readability
// Less clear:
func calculate(int _, int _, float _) returns float { }

// Better:
func calculate(int width, int height, float _) returns float { }

16. Example Program

import math

const float PI = 3.14159  // inferred as float

enum MaybeValue<T> { 
    Some(T) 
    None 
}

interface Shape {
    func area() returns float
}

class Circle is Shape {
    float r  // explicit type required for fields
    
    func area() returns float { 
        return PI * r * r 
    }
}

// Generic utility function
func map<T, U>(list<T> items, func(T) returns U transform) returns list<U> {
    auto result = list<U>()
    for item in items {
        result.push_back(transform(item))
    }
    return result
}

func main() returns void {
    auto shapes = [Circle.new(2.0), Circle.new(3.5)]  // inferred as list<Circle>
    
    for shape in shapes {
        float area = shape.area()  // inferred as float
        string message = "Area: " + area  // inferred as str
        print(message)
    }
    
    // Working with Results and inference
    auto results = list<Result<float, string>>()
    for shape in shapes {
        auto areaResult = Ok(shape.area())  // inferred as Result<float, string>
        results.push_back(areaResult)
    }
    
    // Using generics with inference and lambdas
    auto areas = map(shapes, func(Shape s) {
        return s.area()  // inferred as list<float>
    })
    
    auto descriptions = map(areas, func(string a) {
        return "Area: " + a  // inferred as list<string>
    })
    
    // Pattern matching with underscore
    for result in results {
        match result {
            Ok(value) {
                print("Success: " + value)
            }
            Err(_) {
                print("Error occurred")  // don't care about error details
            }
        }
    }
}

Project File Structure

mux-lang/
├── mux-compiler/
│   ├── src/
│   │   ├── ast/
│   │   │   ├── types.rs
│   │   │   ├── nodes.rs
│   │   │   ├── literals.rs
│   │   │   ├── operators.rs
│   │   │   ├── patterns.rs
│   │   │   ├── error.rs
│   │   │   └── mod.rs
│   │   ├── codegen/
│   │   │   ├── mod.rs
│   │   │   ├── expressions.rs
│   │   │   ├── statements.rs
│   │   │   ├── functions.rs
│   │   │   ├── methods.rs
│   │   │   ├── classes.rs
│   │   │   ├── constructors.rs
│   │   │   ├── operators.rs
│   │   │   ├── generics.rs
│   │   │   ├── types.rs
│   │   │   ├── memory.rs
│   │   │   └── runtime.rs
│   │   ├── semantics/
│   │   │   ├── mod.rs
│   │   │   ├── types.rs
│   │   │   ├── symbol_table.rs
│   │   │   ├── unifier.rs
│   │   │   ├── format.rs
│   │   │   └── error.rs
│   │   ├── lexer/
│   │   │   ├── mod.rs
│   │   │   ├── token.rs
│   │   │   ├── span.rs
│   │   │   └── error.rs
│   │   ├── parser/
│   │   │   ├── mod.rs
│   │   │   └── error.rs
│   │   ├── diagnostic/
│   │   │   ├── mod.rs
│   │   │   ├── emitter.rs
│   │   │   ├── files.rs
│   │   │   └── styles.rs
│   │   ├── module_resolver.rs
│   │   ├── source.rs
│   │   ├── lib.rs
│   │   └── main.rs
│   └── tests/
│       ├── lexer_integration.rs
│       ├── parser_integration.rs
│       ├── semantics_integration.rs
│       ├── executable_integration.rs
│       └── snapshots/
│
├── mux-runtime/
│   ├── src/
│   │   ├── lib.rs
│   │   ├── object.rs
│   │   ├── refcount.rs
│   │   ├── boxing.rs
│   │   ├── bool.rs
│   │   ├── int.rs
│   │   ├── float.rs
│   │   ├── string.rs
│   │   ├── list.rs
│   │   ├── map.rs
│   │   ├── set.rs
│   │   ├── optional.rs
│   │   ├── result.rs
│   │   ├── io.rs
│   │   ├── math.rs
│   │   └── std.rs
│   └── Cargo.toml
│
├── test_scripts/
│   ├── error_cases/
│   │   ├── *.mux
│   └── *.mux
│
├── Cargo.toml
├── Cargo.lock
└── AGENTS.md

18. License

Mux is licensed under the MIT license.