Skip to content

Language Reference

← Back to Documentation

Complete syntax and semantics reference for Sushi Lang. For a gentler introduction, see the Language Guide.

Table of Contents

Program Structure

Every Sushi program must have a main function that returns i32:

fn main() i32:
    # Program entry point
    return Result.Ok(0)

Types

Primitive Types

Integers (signed): - i8 - 8-bit signed integer (-128 to 127) - i16 - 16-bit signed integer (-32,768 to 32,767) - i32 - 32-bit signed integer (-2,147,483,648 to 2,147,483,647) - i64 - 64-bit signed integer (-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807)

Integers (unsigned): - u8 - 8-bit unsigned integer (0 to 255) - u16 - 16-bit unsigned integer (0 to 65,535) - u32 - 32-bit unsigned integer (0 to 4,294,967,295) - u64 - 64-bit unsigned integer (0 to 18,446,744,073,709,551,615)

Floating-point: - f32 - 32-bit IEEE 754 floating-point - f64 - 64-bit IEEE 754 floating-point

Other: - bool - Boolean (true or false) - string - UTF-8 null-terminated string - ~ - Blank type (only for return types)

Numeric Literals

Decimal literals (default):

let i32 dec = 42
let i32 large = 1000000

Hexadecimal literals (base 16, prefix 0x or 0X):

let i32 hex = 0xFF           # 255
let i32 addr = 0xDEAD_BEEF   # underscores allowed
let i32 mask = 0xFF00

Binary literals (base 2, prefix 0b or 0B):

let i32 bin = 0b1111         # 15
let i32 flags = 0b1010_1010  # underscores allowed
let i32 byte = 0b11111111

Octal literals (base 8, prefix 0o or 0O):

let i32 oct = 0o755          # 493 (Unix permissions)
let i32 perm = 0o644         # 420

Note: C-style octals with leading zeros (e.g., 077) are not supported and will cause a compilation error. Use the explicit 0o prefix instead.

Common features: - All literal formats support underscore separators for readability - Prefixes are case insensitive (0xFF == 0xff, 0B1111 == 0b1111) - A literal is context-typed: it takes its type from context (annotation, argument, field, operand). With no numeric context it defaults to i32.

Context typing: a bare literal is typed by its expected type and range-checked at compile time, so no cast is needed to write a literal of a non-i32 type. A decimal literal uses value ranges (signed/unsigned per type); a hex/binary/octal literal uses the target's bit-pattern width (so 0xFF is a valid i8 — the pattern -1); an f32 rejects overflow to infinity (precision loss on f64->f32 is silently rounded). An out-of-range literal is CE2073. This is literal typing, not value coercion — converting an already-typed value still needs as (see Type Conversion).

let i64 big   = 40000000000                # context-typed i64, no cast needed
let u64 max   = 18446744073709551615       # context-typed u64
let u32 mask  = 0x01 | 0x02 | 0x04         # operands typed u32
let i8  small = 200                        # CE2073: out of range for i8

No-context default (CE2070): a literal with no numeric context defaults to i32, and a bare decimal above the signed range (or a radix literal above the 32-bit pattern) is a compile error. A literal cast directly with as is exempt and materializes at the target width:

println(40000000000)              # CE2070: context-free, defaults to i32 and overflows
let i64 x = 40000000000 as i64    # exempt: materializes at i64 width

Type Conversion

All type conversions must be explicit using the as keyword:

let i32 x = 42
let f64 y = x as f64        # int to float
let i16 small = y as i16    # float to int (truncates)
let u32 unsigned = x as u32 # signed to unsigned

Rules: - Only numeric types can be cast - Float-to-integer truncates toward zero - No implicit conversions - No casting to/from strings or arrays

Array Types

Fixed arrays:

let i32[5] fixed = [1, 2, 3, 4, 5]

Dynamic arrays:

let i32[] dynamic = from([1, 2, 3])
let string[] empty = new()

Function Types

A function type describes a first-class function value (a bare function pointer). The return type is mandatory; the optional | E names the error type (defaults to StdError).

fn(i32) -> i32                 # takes i32, returns i32 (error type StdError)
fn(i32, string) -> bool        # two parameters
fn() -> ~                      # no parameters, blank return
fn(i32) -> i32 | MathError     # explicit custom error type

Reference a plain top-level function by name to get a value of that type, then store, pass, or call through it:

let fn(i32) -> i32 f = add_one     # `add_one` used as a value
let i32 r = f(41)??                # call through it -> Result, like a direct call

Function types are invariant (arity, parameters, return, and error type must match exactly). A plain top-level function is referenceable as above; a closure — a capturing lambda literal (|i32 x| x + n) — is also a fn(...)-typed value and shares the same call syntax. A generic function is referenceable when the expected function type is explicit (let fn(i32) -> i32 g = identity); otherwise it is CE2093.

You can also call through any expression that evaluates to a function value, not just a bare name — a fn-typed struct field (obj.handler(x), when no method of that name exists), a container get-out (fns.get(0)??(x)), or a parenthesized expression ((e)(x)). See the First-Class Functions guide and the Closures guide.

Variables

Declaration

Variables must be declared with let:

let i32 x = 42
let string name = "Arthur"
let bool flag = true

Rebinding

Use := to rebind variables (must be declared first):

let i32 x = 10
x := 20     # OK
x := 30     # OK

# ERROR: Cannot rebind without prior declaration
# y := 5    # CE1003: Undefined variable 'y'

Scope

Variables are block-scoped:

fn main() i32:
    let i32 x = 1

    if (true):
        let i32 y = 2  # y scoped to if block
        x := 3         # OK: x from outer scope

    # ERROR: y not in scope
    # println(y)

    return Result.Ok(0)

Functions

Declaration

fn function_name(param1_type param1_name, param2_type param2_name) return_type:
    # Function body
    return Result.Ok(value)

Example:

fn add(i32 a, i32 b) i32:
    return Result.Ok(a + b)

fn greet(string name) ~:
    println("Hello, {name}!")
    return Result.Ok(~)

Return Types

All functions implicitly return Result<T, E>:

fn divide(i32 a, i32 b) i32:  # Actually returns Result<i32, StdError>
    if (b == 0):
        return Result.Err(StdError.Error)
    return Result.Ok(a / b)

Parameters

By value:

fn modify(i32 x) i32:
    x := x + 1
    return Result.Ok(x)

Borrowed (by reference):

Sushi has two borrow modes: - &peek T - Read-only borrow (multiple allowed) - &poke T - Read-write borrow (exclusive access)

fn increment(&poke i32 counter) ~:
    counter := counter + 1
    return Result.Ok(~)

fn read_value(&peek i32 x) i32:
    return Result.Ok(x)

Operators

Arithmetic

  • + - Addition
  • - - Subtraction
  • * - Multiplication
  • / - Division (integer division for int types)
  • % - Modulo (remainder)

Comparison

  • == - Equal
  • != - Not equal
  • < - Less than
  • <= - Less than or equal
  • > - Greater than
  • >= - Greater than or equal

Logical

  • and (or &&) - Logical AND (short-circuits)
  • or (or ||) - Logical OR (short-circuits)
  • xor (or ^^) - Logical XOR (evaluates both sides)
  • not (or !) - Logical NOT

Alternative syntax: Sushi supports both keyword (and, or, xor, not) and symbolic (&&, ||, ^^, !) forms for all logical operators.

Bitwise

  • & - Bitwise AND
  • | - Bitwise OR
  • ^ - Bitwise XOR
  • ~ - Bitwise NOT (complement)
  • << - Left shift (zero-fill)
  • >> - Right shift (type-dependent, see below)

Right shift behavior (matches Go/Rust): - Signed types (i8, i16, i32, i64): Arithmetic shift (sign-extends)

let i32 a = -16
let i32 shifted = a >> 2  # Result: -4 (preserves sign bit)
- Unsigned types (u8, u16, u32, u64): Logical shift (zero-fills)
let u32 a = 3221225472
let u32 shifted = a >> 2  # Result: 805306368 (zero-fill from left)

String

There is no + concatenation operator for strings. Build strings with interpolation instead:

let string a = "foo"
let string b = "bar"
let string combined = "{a}{b}"   # "foobar"

Other

  • as - Type casting
  • ?? - Error propagation

Control Flow

If-Elif-Else

Parentheses required around conditions:

if (condition):
    # Block
elif (other_condition):
    # Block
else:
    # Block

While Loops

while (condition):
    # Loop body
    if (done):
        break
    if (skip):
        continue

For-Each Loops

foreach(element in iterable.iter()):
    # Use element

Type annotation optional:

foreach(i32 element in array.iter()):
    println(element)

Arrays

See Standard Library for complete array API.

Fixed Arrays

Stack-allocated, compile-time size:

let i32[5] arr = [1, 2, 3, 4, 5]
let i32 first = arr.get(0)??  # .get returns Maybe<i32>; ?? unwraps it

Dynamic Arrays

Heap-allocated, runtime size:

let i32[] arr = from([1, 2, 3])
let i32[] empty = new()

arr.push(4)
let i32 last = arr.pop()

Structs

Definition

struct Name:
    type1 field1
    type2 field2

Example:

struct Person:
    string name
    i32 age
    bool active

Instantiation

Structs support both positional and named parameter construction:

Positional (traditional):

let Person p = Person("Arthur", 42, true)

Named (order-independent):

let Person p1 = Person(name: "Arthur", age: 42, active: true)
let Person p2 = Person(age: 42, active: true, name: "Arthur")  # Order doesn't matter

Rules: - Named parameters provide clarity and prevent argument order mistakes - All fields must be provided (no partial construction) - Cannot mix positional and named arguments (all-or-nothing) - Named parameters are resolved at compile-time (zero-cost abstraction)

Field Access

println(p.name)
p.age := 43

Nested Structs

struct Point:
    i32 x
    i32 y

struct Rectangle:
    Point top_left
    Point bottom_right

let Rectangle rect = Rectangle(
    top_left: Point(x: 0, y: 0),
    bottom_right: Point(x: 10, y: 10)
)

println(rect.top_left.x)

Enums

Definition

enum Name:
    Variant1()
    Variant2(type1)
    Variant3(type1, type2)

Example:

enum Status:
    Idle()
    Running(i32)
    Error(string)

Enum variant fields are positional (type-only); they are bound by position in pattern matching, not by field name.

Construction

let Status s1 = Status.Idle()
let Status s2 = Status.Running(42)
let Status s3 = Status.Error("Failed")

Pattern Matching

Required to access enum data:

match s2:
    Status.Idle() ->
        println("Idle")
    Status.Running(task_id) ->
        println("Running task {task_id}")
    Status.Error(msg) ->
        println("Error: {msg}")

Pattern Matching

Basic Match

match expression:
    Pattern1 -> statement
    Pattern2 -> statement

Wildcard

match value:
    Status.Running(_) -> println("Running")
    _ -> println("Other")

Nested Patterns

match result:
    FileResult.Err(FileError.NotFound()) ->
        println("File not found")
    FileResult.Err(_) ->
        println("Other file error")
    FileResult.Ok(f) ->
        println("File opened")

Exhaustiveness

The compiler enforces that all variants are matched:

enum Color:
    Red()
    Green()
    Blue()

# ERROR: Non-exhaustive match (missing Blue)
match color:
    Color.Red() -> println("Red")
    Color.Green() -> println("Green")

Module System

Units

Sushi uses a unit system where each source file is a unit:

# file: math.sushi
unit math

fn add(i32 a, i32 b) i32:
    return Result.Ok(a + b)

Visibility

Functions are private by default. Use public for external access:

unit utils

public fn helper() i32:
    return Result.Ok(private_helper())

fn private_helper() i32:
    return Result.Ok(42)

Standard Library

Import stdlib modules with use:

# List<T> is built-in (no import needed)
# HashMap requires explicit import:
use <collections/hashmap>
use <collections/strings> # String utilities
use <io/stdio>           # stdio functions

Comments

Single-line comments only:

# This is a comment
let i32 x = 42  # Inline comment

Keywords

Reserved keywords:

  • fn - Function declaration
  • let - Variable declaration
  • struct - Struct definition
  • enum - Enum definition
  • if, elif, else - Conditionals
  • while - Loop
  • foreach, in - For-each loop
  • break, continue - Loop control
  • match - Pattern matching
  • return - Function return
  • and, or, not - Logical operators
  • true, false - Boolean literals
  • as - Type casting
  • unit - Unit declaration
  • public - Visibility modifier
  • use - Module import
  • extend - Extension method
  • self - Extension method receiver

String Literals

Sushi supports two string literal syntaxes:

Double-quote strings ("..."): - Support interpolation with {expr} syntax - All escape sequences supported - Use for: string constants, interpolated strings

Single-quote strings ('...'): - Plain string literals, no interpolation - Same escape sequences as double-quote strings - Use for: string arguments in interpolation, literal strings

let string s1 = "double quotes"    # Supports interpolation
let string s2 = 'single quotes'    # No interpolation
let string s3 = 'can\'t'           # Escape sequences work

Both quote styles are equivalent except for interpolation support. Use whichever is more convenient.

Escape Sequences

Both quote styles support the same escape sequences:

  • \\ - Backslash
  • \" - Double quote
  • \' - Single quote
  • \n - Newline
  • \t - Tab
  • \r - Carriage return
  • \0 - Null character
  • \xNN - Hexadecimal escape (e.g., \x41 = 'A')
  • \uNNNN - Unicode escape (e.g., \u0041 = 'A')

String Interpolation

Embed expressions in double-quote strings with {expression}:

let i32 x = 42
let string name = "Arthur"

println("Hello {name}")
println("Answer: {x}")
println("Next: {x + 1}")
println("Squared: {x * x}")

Supported types: All primitives, strings

String Arguments in Interpolation

Use single-quote strings for string arguments inside interpolation expressions:

use <collections/strings>

let string text = "hello"
println("{text.pad_left(10, '*')}")       # Padding character
println("{text.find('world')}")           # Search string
println("{text.replace('old', 'new')}")   # Multiple string args
println("{','.join(parts)}")              # Separator string

Single-quote strings work naturally in nested contexts where double quotes would require escaping.

Constants

Declaration

Constants are declared with const and evaluated at compile-time:

const i32 MAX_SIZE = 100
const string VERSION = "1.0.0"
const bool DEBUG = true
const f64 PI = 3.14159

Constant Expressions

Constants support compile-time expressions with arithmetic, bitwise, logical, and comparison operators:

const i32 BASE = 10
const i32 DOUBLE = 2 * BASE              # 20
const i32 COMPLEX = (100 + 50) / 3       # 50
const u32 FLAGS = 0x01 | 0x02 | 0x04     # 7
const bool IS_VALID = (100 > 50) and true # true

Supported operations: - Arithmetic: +, -, *, /, % (numeric types) - Bitwise: &, |, ^, ~, <<, >> (integer types only) - Logical: and, or, xor, not (boolean type only) - Comparison: ==, !=, <, <=, >, >= (compatible types) - Type casts: as (between compatible types)

Constant References

Constants can reference other constants:

const i32 WIDTH = 100
const i32 HEIGHT = 50
const i32 AREA = WIDTH * HEIGHT  # 5000

const i32 BASE = 10
const i32 OFFSET = BASE * 2
const i32 TOTAL = OFFSET + BASE  # 30

The compiler detects circular dependencies:

# ERROR: Circular constant dependency
const i32 A = B + 1
const i32 B = A + 1  # CE0109: circular dependency detected

Array Constants

Fixed-size arrays with constant elements:

const i32[3] PRIMES = [2, 3, 5]
const bool[2] FLAGS = [true, false]
const i32[4] POWERS = [1, 2, 4, 8]

# Can use expressions
const i32 BASE = 10
const i32[3] VALUES = [BASE, BASE * 2, BASE * 3]  # [10, 20, 30]

Restrictions: - Array must be fixed-size (T[N]), not dynamic (T[]) - All elements must be compile-time constant expressions

Restrictions

Constants cannot use: - Function calls (including constructors) - Variable references (only other constants) - String concatenation with + (not yet supported) - Struct or enum construction - Method calls - Dynamic arrays

# ERROR: Not allowed in constants
const i32 X = get_value()     # CE0108: function calls forbidden
const i32 Y = some_variable   # CE0108: variable references forbidden
const i32[] DYNAMIC = from([1, 2])  # CE2015: dynamic arrays forbidden

See also: - Standard Library - Built-in types and functions - Error Handling - Result and Maybe - Memory Management - RAII and ownership - Generics - Generic types and functions