Skip to content

Type System

StarLang is a statically typed language. All variables, parameters, and return values require type annotations.

Primitive Types

Type Description Example
nil Null value nil
bool Boolean true, false
number 64-bit double 3.14, -1.5
i32 32-bit signed integer 42, -10
u8 8-bit unsigned integer 0..255
string UTF-8 text "hello"
array Dynamic array (untyped) [1, "a", true]
dict Key-value map (untyped) {"port": 8080}
function Function reference fn add(a:i32, b:i32):i32
struct User-defined composite type struct Point { x:number, y:number }
chan Channel for task communication chan(4), chan(0)
value Dynamic type — holds any value, narrowed on read queue.pop(q)
result<T> Error result from try result<number>, result<string>
void For functions that return nothing only used as return type

value (dynamic type)

value holds any StarLang value regardless of its concrete type. It is used where a container must accept heterogeneous payloads — most notably queue, whose items are typed value.

Assigning a value to a concrete type performs checked narrowing: the runtime verifies the actual type matches the target and raises a type fault (code 2) on mismatch, catchable with try.

var q: i32 = queue.new(8)
queue.push(q, 42)

var n: number = queue.pop(q)   # checked narrow value -> number

String Escape Sequences

String literals support the following escape sequences:

Escape Character
\n Newline (0x0A)
\t Tab (0x09)
\r Carriage return (0x0D)
\\ Backslash
\" Double quote
\0 Null byte
var line: string = "hello\tworld\n"
var path: string = "C:\\Users\\star"
var quote: string = "she said \"hi\""

Numeric Literals

Integer literals (no decimal point) are number by default. When assigned to an i32 or u8 variable or passed to a function expecting an integer type, the compiler auto-converts them:

var x:i32 = 42          # 42 is number literal, auto-converted to i32
var y:number = 42       # 42 stays as number — both work
var f:number = 3.14     # only number, cannot be i32

Integer Types

StarLang supports two integer types alongside number:

var x:i32 = 42          # 32-bit signed integer
var b:u8 = 200           # 8-bit unsigned (0-255)
var f:number = 3.14      # 64-bit floating point

Integer arithmetic uses dedicated opcodes and does not lose precision to floating point.

Arithmetic

var a:i32 = 10
var b:i32 = 3
var sum:i32 = a + b      # 13
var diff:i32 = a - b     # 7
var prod:i32 = a * b     # 30
var quot:i32 = a / b     # 3 (integer division)
var rem:i32 = a % b      # 1
var neg:i32 = -a          # -10

Integer Division

Integer division truncates toward zero (same as C):

var x:i32 = -7
var y:i32 = 2
var r:i32 = x / y        # -3 (not -4)
var m:i32 = x % y        # -1

-7 / 2 = -3 (truncation toward zero), not -4 (floor division as in Python).

u8 Promotion

u8 values are promoted to i32 for all arithmetic operations:

var a:u8 = 200
var b:u8 = 55
var result:i32 = a + b   # 255 (i32 result)

u8 Overflow

Casting to u8 wraps modulo 256 (same as C uint8_t cast):

var x:i32 = 256
var b:u8 = u8(x)         # 0 (wraps)

var y:i32 = -1
var c:u8 = u8(y)          # 255 (wraps)

No runtime error is raised — this is a silent wrap. If you need range checking, validate before casting.

No Implicit Integer/Float Mixing

Arithmetic between integers and floats is a compile error:

var x:i32 = 10
var y:number = 3.14
var z = x + y             # ERROR: cannot mix i32 with number

Use explicit type casts instead (see below).

Comparison Auto-Coercion

Comparisons between integers and numbers are allowed — the compiler inserts implicit conversion:

var x:i32 = 42
assert(x == 42, "works")  # number literal auto-converted to i32
assert(x > 10, "works")   # number literal auto-converted to i32

Type Casting

Explicit casts between numeric types:

var x:i32 = 42
var f:number = number(x)   # i32 -> number: 42.0

var pi:number = 3.14
var n:i32 = i32(pi)        # number -> i32: 3 (truncates toward zero)

var b:u8 = u8(x)           # i32 -> u8: 42
var w:i32 = i32(b)         # u8 -> i32: 42

Generic Types

Arrays and dicts can be parameterized with element types:

Typed Arrays

var nums:array<i32> = [10, 20, 30]
var names:array<string> = ["alice", "bob"]
var flags:array<bool> = [true, false, true]

Generic type parameters are enforced at both compile time and runtime. The compiler checks element types during subscript assignment and inserts implicit numeric conversions where applicable. The VM provides a runtime safety net — assigning a wrong-typed value raises an error catchable with try.

Subscript Assignment

Array and dict elements can be assigned via subscript notation:

var nums:array<i32> = [10, 20, 30]
nums[0] = i32(99)    # OK: i32 matches
nums[1] = 42         # OK: number literal auto-converted to i32

var cfg:dict<string, i32> = {"port": 8080}
cfg["port"] = i32(9090)   # OK: updates existing key
cfg["timeout"] = 30       # OK: inserts new key

var names:array<string> = ["alice", "bob"]
names[0] = "charlie"  # OK: string matches

Type mismatches produce compile-time errors when the element type is known:

var nums:array<i32> = [10, 20, 30]
nums[0] = "hello"    # ERROR: cannot assign string to array<i32>

Numeric auto-coercion rules apply:

  • array<i32> accepts number literals (auto-converted to i32)
  • array<number> accepts i32/u8 values (auto-converted to number)
  • array<u8> accepts i32 values (truncated to u8)

Typed Dicts

var config:dict<string, i32> = {"port": 8080, "timeout": 30}
var lookup:dict<string, string> = {"name": "star", "version": "0.2"}

Untyped Collections

Omitting the type parameter gives a mixed-type collection:

var mixed:array = [1, "hello", true]
var data:dict = {"name": "star", "version": 1}

Type Annotations

Variables use : to specify the type:

var x:i32 = 42
var name:string = "stardyn"
var active:bool = true
var items:array<number> = [1.5, 2.7, 3.9]
var config:dict<string, i32> = {"port": 8080}

Type Checking

Type mismatch produces a compile-time error:

var x:number = "hello"    # ERROR: expected number, got string
var y:string = 42         # ERROR: expected string, got number

Missing type annotation is a compile error:

var x = 42                # ERROR: type annotation required

void vs nil

Type Usage In variables As return type
void Non-returning fn No Yes
nil Null value Yes Yes
fn greet(name:string):void {
    console.log(name)
}

fn find(key:string):nil {
    return nil
}

Result Type

The try keyword returns a type-safe result<T> value. The inner type T matches the return type of the called function.

fn divide(a:number, b:number):number {
    if (b == 0) { raise "division by zero" }
    return a / b
}

var res:result<number> = try divide(10, 0)
assert(res.failed == true)
assert(res.error == "division by zero")

Fields: .failed (bool), .code (i32), .error (string), .value (T). See Error Handling for details.

Bytecode Type Tags

Each value is stored in memory with a 1-byte type tag:

Tag Hex Type
NIL 0x00 nil
BOOL 0x01 bool
NUMBER 0x02 number
STRING 0x03 string
ARRAY 0x04 array
DICT 0x05 dict
FUNCTION 0x06 function
I32 0x07 i32
U8 0x08 u8
STRUCT 0x09 struct
CHANNEL 0x0A chan
RESULT 0x0B result<T>