Language Reference¶
Complete syntax and semantics reference for Sushi Lang. For a gentler introduction, see the Language Guide.
Table of Contents¶
- Program Structure
- Types
- Variables
- Functions
- Operators
- Control Flow
- Arrays
- Structs
- Enums
- Pattern Matching
- Module System
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)
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 declarationlet- Variable declarationstruct- Struct definitionenum- Enum definitionif,elif,else- Conditionalswhile- Loopforeach,in- For-each loopbreak,continue- Loop controlmatch- Pattern matchingreturn- Function returnand,or,not- Logical operatorstrue,false- Boolean literalsas- Type castingunit- Unit declarationpublic- Visibility modifieruse- Module importextend- Extension methodself- 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