Simple is a general-purpose programming language with clean syntax emphasizing composition over inheritance.
- Artifacts: Unified construct for data structures, enumerations, and namespaces
- Procedures: Functions with explicit return types
- Procedure References: Pass and store references to procedures
- Pointers and References: C-style pointers with read-only references
- Minimal conditionals: Expression-based with
|>chain operator for short-circuiting - Strict typing: All variables must have explicit type declarations
- Minimal syntax: No unnecessary keywords, clear and readable
File Extension: .s
An Artifact is Simple's unified construct that serves multiple purposes:
- Data structures with methods (similar to classes/structs in other languages)
- Enumerations (constant value definitions)
- Namespaces (static member collections)
All artifacts use the same syntax: Identifier { body } with no keyword prefix.
A Procedure is a callable function defined with no keyword prefix:
identifier(params): return_type { body }
Procedures can be referenced and passed around using the fn type, but cannot be defined inline.
Conditionals are expression-based with no keywords:
- Standalone:
expr { body }- executes if expression is true - Chained:
|> expr { body }- short-circuit chain, first match wins
Example:
|> score > 90 { return "A" }
|> score > 80 { return "B" }
|> true { return "F" }
while, break, return, skip
int, float, string, bool, fn
i8, i16, i32, i64, i128
u8, u16, u32, u64, u128
f32, f64
Mod
true, false, null
+, -, *, /, %, ++, --
&, |, ^, <<, >>
==, !=, <, >, <=, >=
&&, ||, !
= (assignment and mutable declaration), :: (immutable declaration)
. (property access), [] (index access)
@ (address-of), ^ (dereference)
.. (iteration)
|> (conditional chain)
From highest to lowest precedence:
++,--(postfix),[],.(member access)!,-(unary),++,--(prefix),@,^(address-of/deref)*,/,%+,-<<,>><,>,<=,>===,!=&(bitwise AND)^(bitwise XOR)|(bitwise OR)&&(logical AND)||(logical OR)|>(conditional chain)=,::(assignment/declaration)
Examples:
2 + 3 * 4 // 14 (multiplication first)
x && y || z // (x && y) || z
a & b == c // a & (b == c)
Integer: 123, -456, 0
Float: 3.14, -0.5, 0.0
String: "hello", 'world'
Boolean: true, false
Null: null
i8 // 8-bit signed integer
i16 // 16-bit signed integer
i32 // 32-bit signed integer
i64 // 64-bit signed integer
i128 // 128-bit signed integer
int // defaults to i64
u8 // 8-bit unsigned integer
u16 // 16-bit unsigned integer
u32 // 32-bit unsigned integer
u64 // 64-bit unsigned integer
u128 // 128-bit unsigned integer
f32 // 32-bit floating point
f64 // 64-bit floating point
float // defaults to f64
string // text
bool // true or false
fn // procedure reference
intis an alias fori64floatis an alias forf64
Declaration Pattern:
::(double colon with=) → immutable:(single colon with=) → mutable- All declarations require explicit types
| Pattern | Mutability | Example |
|---|---|---|
ident :: type = value |
Immutable | x :: int = 10 |
ident : type = value |
Mutable | y : int = 10 |
ident = value |
Assignment | y = 20 |
// Immutable
<ident> :: <type> = <expr>
// Mutable
<ident> : <type> = <expr>
// Assignment
<ident> = <expr>
// Integer types
small :: i8 = 127
medium :: i32 = 100000
large :: i64 = 9223372036854775807
huge :: i128 = 170141183460469231731687303715884105727
// Unsigned types
byte :: u8 = 255
count :: u32 = 4294967295
// Default int (i64)
x :: int = 42
// Floating point types
precise :: f32 = 3.14159
precise_double :: f64 = 3.141592653589793
// Default float (f64)
pi :: float = 3.14159
// Other types
name :: string = "Alice"
active :: bool = true
callback :: fn = add
Pointers provide full read-write access with explicit syntax:
ptr : *int = @x // mutable pointer, can reassign and dereference
ptr :: *int = @x // immutable pointer, can't reassign but can dereference
Operations:
@x- get address of x^ptr- dereference pointerptr = @y- reassign pointer (only if mutable)^ptr = 10- modify value at address
References provide read-only access:
ref : &int = @x // read-only reference, can reassign ref
ref :: &int = @x // read-only reference, can't reassign ref
Operations:
@x- get reference to x^ref- dereference reference (explicit)ref = @y- reassign reference (only if mutable)- Cannot write through reference (read-only)
increment(value: *int): void {
^value = ^value + 1
}
calculate(value: &int): int {
return ^value * 2
}
swap(a: *int, b: *int): void {
temp :: int = ^a
^a = ^b
^b = temp
}
main(): void {
x : int = 10
y : int = 20
// Pointers
ptr : *int = @x
^ptr = 15
ptr = @y
// References (explicit deref)
ref : &int = @x
result :: int = ^ref * 2
// Function calls
increment(@x)
value :: int = calculate(@x)
swap(@x, @y)
}
- Pointers (
*T): Explicit dereferencing with^, read-write access - References (
&T): Explicit dereferencing with^, read-only (cannot write)
@- No confusion with bitwise AND (&)^- No confusion with multiplication (*)- Clear visual distinction from other operators
- Consistent: always explicit
^for dereference
<ident>(<params>): <type> { <body> }
All procedures must have explicit return types. Use void for procedures that don't return a value.
<params> ::= ε
| <ident>: <type>
| <params>, <params>
All parameters must have explicit types.
greet(name: string): string {
return "Hello " + name
}
add(a: int, b: int): int {
return a + b
}
print_message(msg: string): void {
}
greet(name: string = "World"): string {
return "Hello " + name
}
greet() // "Hello World"
greet("Jae") // "Hello Jae"
create_point(x: float = 0.0, y: float = 0.0): Point {
return Point(x, y)
}
Procedures can be assigned to variables using the fn type:
<ident> : fn = <procedure_name>
<ident> :: fn = <procedure_name>
add(a: int, b: int): int {
return a + b
}
subtract(a: int, b: int): int {
return a - b
}
// Assign procedure to variable
callback :: fn = add
// Use it
result :: int = callback(5, 3)
// Reassign to different procedure (if mutable)
operation : fn = add
operation = subtract
// Pass as parameter
apply(operation: fn, x: int, y: int): int {
return operation(x, y)
}
result = apply(add, 10, 5)
Note: The fn type is inferred from the referenced procedure. Type checking ensures the procedure signature matches usage.
<ident> {
<property>*
<method>*
}
// Immutable
<property> ::= <ident> :: <type> = <expr>
// Mutable
<property> ::= <ident> : <type> = <expr>
All properties must have explicit types and default values.
<method> ::= <ident>(<params>): <type> { <body> }
All methods must have explicit return types.
Use . prefix to reference artifact properties within methods:
.<property>
Person {
name :: string = ""
age : int = 0
title :: string = "Mr."
new(name: string, age: int): Person {
return { .name = name, .age = age, .title = "Mr." }
}
greet(): string {
return "Hello, " + .name + "! Age: " + str(.age)
}
birthday(): void {
.age = .age + 1
}
}
// Constructor method instantiation
p :: Person = Person.new("Jae", 25)
// Or struct-style instantiation
p2 : Person = { .name = "Alice", .age = 30, .title = "Ms." }
p.greet()
p.birthday()
Artifacts support composition only. No inheritance.
Artifacts are instantiated through constructor methods that return the artifact type:
<instance> : <ArtifactType> = <ArtifactName>.<constructor>(<args>) // mutable
<instance> :: <ArtifactType> = <ArtifactName>.<constructor>(<args>) // immutable
Alternatively, using struct-style initialization:
<instance> : <ArtifactType> = { .prop = value, .prop = value }
<instance> :: <ArtifactType> = { .prop = value, .prop = value }
Artifacts can also be used as namespaces without instantiation:
Math {
PI :: float = 3.14159
E :: float = 2.71828
abs(x: float): float {
|> x < 0.0 { return -x }
|> true { return x }
}
sqrt(x: float): float {
// implementation
}
}
value :: float = Math.PI
result :: float = Math.abs(-5.0)
<ident> {
<ident> = <literal>,
<ident>,
...
}
Colors {
Red = 0,
Blue,
Green
}
Status {
Pending = 1,
Active = 2,
Completed = 3
}
Artifacts can represent enumerated values by defining constant members.
Arrays:
- Fixed-size, stack-allocated
- Size must be known at compile time
- Syntax:
[size]type - Faster access, contiguous memory
Lists:
- Dynamic-size, heap-allocated
- Can grow and shrink at runtime
- Syntax:
[type] - Flexible but slower than arrays
<ident> : [<size>]<type> = [<expr>, ...] // mutable array
<ident> :: [<size>]<type> = [<expr>, ...] // immutable array
<ident> : [<type>] = [<expr>, ...] // mutable list
<ident> :: [<type>] = [<expr>, ...] // immutable list
<ident>[<index>] // 1-based indexing
// Fixed-size arrays (stack-allocated)
numbers : [5]int = [10, 20, 30, 40, 50]
coords :: [3]float = [1.0, 2.0, 3.0]
// Dynamic lists (heap-allocated)
items : [int] = [1, 2, 3]
names :: [string] = ["Alice", "Bob", "Charlie"]
// Array access
first :: int = numbers[1]
numbers[2] = 25 // OK: mutable array
coords[1] = 5.0 // ERROR: immutable array
// List operations
items.push(4) // OK: mutable list can grow
items.pop() // OK: mutable list can shrink
names.push("Dave") // ERROR: immutable list
Note: Simple uses 1-based indexing. The first element is at index 1.
<expr> { <body> }
Executes body if expression evaluates to true.
|> <expr> { <body> }
|> <expr> { <body> }
|> <expr> { <body> }
Each condition prefixed with |> forms a short-circuit chain. Only the first true condition executes.
age > 18 {
print("Adult")
}
count > 0 {
print("Has items")
}
// Chained conditionals (short-circuit, first match only)
|> x > 10 {
return "large"
}
|> x > 5 {
return "medium"
}
|> true {
return "small"
}
// In a procedure
grade(score: int): string {
|> score > 90 { return "A" }
|> score > 80 { return "B" }
|> score > 70 { return "C" }
|> score > 60 { return "D" }
|> true { return "F" }
}
// Nested conditionals
process(value: int): void {
value > 0 {
|> value > 100 { print("very large") }
|> value > 50 { print("large") }
|> true { print("positive") }
}
}
while <expr> { <body> }
count : int = 0
while count < 10 {
count = count + 1
}
<ident>, <start> .. <end> { <body> } // literal end value
<ident>, <start> .. <collection> { <body> } // collection.length at compile time
i, 1 .. 10 {
// i goes from 1 to 10 (inclusive)
}
i, 5 .. 20 {
// i goes from 5 to 20 (inclusive)
}
i, 1 .. items {
// i goes from 1 to items.length
value :: int = items[i]
}
i, 3 .. names {
// i goes from 3 to names.length
// skips first 2 elements
}
Note: Ranges are inclusive and use 1-based indexing.
Exit the current loop immediately.
Exit the current function, optionally returning a value.
Skip to the next iteration of the loop (continue).
i, 1 .. 10 {
i == 5 {
skip
}
i == 8 {
break
}
}
find(items: [int], target: int): int {
i, 1 .. items {
items[i] == target {
return i
}
}
return -1
}
Mod <path>;
<path> ::= <ident> // looks for <ident>.s in current directory
| <path>/<ident> // looks for <ident>.s in specified directory
Mod utils; // imports utils.s
Mod src/game; // imports src/game.s
Mod lib/math/vec; // imports lib/math/vec.s
Imports all declarations from the specified module.
// This is a comment
int / int = int // 10 / 3 = 3
float / float = float // 10.0 / 3.0 = 3.333...
int / float = float // 10 / 3.0 = 3.333...
float / int = float // 10.0 / 3 = 3.333...
Any type concatenated with a string becomes a string:
"Age: " + 25 // "Age: 25"
"Value: " + 3.14 // "Value: 3.14"
"Status: " + true // "Status: true"
int + int = int
float + float = float
int + float = float
float + int = float
Same rules apply for -, *, %
int(x) // convert to integer
float(x) // convert to float
str(x) // convert to string
bool(x) // convert to boolean
float(10) / 3 // 3.333... (force float division)
int(3.14) // 3
str(42) // "42"
bool(0) // false
- Block scope: Variables declared in
{}blocks are local to that block - Global access: Procedures and blocks can access global variables
- Shadowing allowed: Variables can be redeclared with
:or::to shadow existing names - Assignment:
=assigns to existing variables; requires mutable variable
x :: int = 10 // global (immutable)
example(): void {
x = 5 // ERROR: cannot assign to immutable x
x : int = 5 // OK: declares new mutable x (shadows global)
x = 10 // OK: assigns to the mutable local x
y : int = 20 // local mutable
y = 30 // OK: assigns to mutable y
z :: int = 40 // local immutable
z = 50 // ERROR: cannot assign to immutable z
true {
x :: int = 100 // OK: declares new immutable x (shadows local x)
w : int = 60 // local to block
}
// w not accessible here (out of scope)
}
- Primitives (
int,float,bool) → pass by value - Artifacts, Arrays, Lists → pass by reference
- Pointers (
*T) → explicit^to dereference, read-write access - References (
&T) → explicit^to dereference, read-only access - Mutability:
:creates mutable references,::creates immutable references
Programs execute from main() if present, otherwise execute top-level statements in order.
Mod math/utils;
Status {
Idle = 0,
Running,
Stopped
}
Vec2 {
x :: float = 0.0
y :: float = 0.0
new(x: float, y: float): Vec2 {
return { .x = x, .y = y }
}
length(): float {
return sqrt(.x * .x + .y * .y)
}
add(other: Vec2): Vec2 {
return { .x = .x + other.x, .y = .y + other.y }
}
}
Math {
PI :: float = 3.14159
sqrt(x: float): float {
// implementation
}
}
classify(value: int): string {
|> value > 100 { return "very large" }
|> value > 50 { return "large" }
|> value > 10 { return "medium" }
|> value > 0 { return "small" }
|> true { return "non-positive" }
}
main(): void {
pos :: Vec2 = Vec2.new(3.0, 4.0)
vel :: Vec2 = Vec2.new(1.0, 0.0)
status :: int = Status.Running
status == Status.Running {
newPos :: Vec2 = pos.add(vel)
distance :: float = newPos.length()
}
items :: [int] = [10, 20, 30, 40, 50]
sum : int = 0
i, 1 .. items {
sum = sum + items[i]
}
i, 1 .. 10 {
i % 2 == 0 {
skip
}
}
count : int = 0
while count < 5 {
count++
}
radius :: float = 5.0
area :: float = Math.PI * radius * radius
// Pointers and references
x : int = 100
ptr : *int = @x
^ptr = 200
ref : &int = @x
doubled :: int = ^ref * 2
}
<program> ::= <statement>*
<statement> ::= <var_decl>
| <proc_decl>
| <artifact_decl>
| <mod_decl>
| <conditional_stmt>
| <while_stmt>
| <for_stmt>
| <return_stmt>
| <break_stmt>
| <skip_stmt>
| <expr_stmt>
<var_decl> ::= <ident> "::" <type> "=" <expr>
| <ident> ":" <type> "=" <expr>
| <ident> "=" <expr>
<proc_decl> ::= <ident> "(" <params> ")" ":" <type> <block>
<params> ::= ε
| <param> ("," <param>)*
<param> ::= <ident> ":" <type>
| <ident> ":" <type> "=" <expr>
<artifact_decl> ::= <ident> "{" <artifact_body> "}"
<artifact_body> ::= (<property_decl> | <proc_decl> | <enum_member>)*
<property_decl> ::= <ident> "::" <type> "=" <expr>
| <ident> ":" <type> "=" <expr>
<enum_member> ::= <ident>
| <ident> "=" <literal>
<mod_decl> ::= "Mod" <path> ";"
<path> ::= <ident> ("/" <ident>)*
<conditional_stmt> ::= <expr> <block>
| "|>" <expr> <block> ("|>" <expr> <block>)*
<while_stmt> ::= "while" <expr> <block>
<for_stmt> ::= <ident> "," <expr> ".." <expr> <block>
<return_stmt> ::= "return" <expr>?
<break_stmt> ::= "break"
<skip_stmt> ::= "skip"
<expr_stmt> ::= <expr>
<block> ::= "{" <statement>* "}"
<expr> ::= <literal>
| <ident>
| <expr> <binary_op> <expr>
| <unary_op> <expr>
| <expr> "." <ident>
| <expr> "[" <expr> "]"
| <expr> "(" <args> ")"
| "(" <expr> ")"
| "[" <list> "]"
| <ident> "." <ident> "(" <args> ")" // Artifact.method() call
| "{" <struct_init> "}" // struct-style initialization
<args> ::= ε
| <expr> ("," <expr>)*
<list> ::= ε
| <expr> ("," <expr>)*
<struct_init> ::= "." <ident> "=" <expr> ("," "." <ident> "=" <expr>)*
<binary_op> ::= "+" | "-" | "*" | "/" | "%"
| "==" | "!=" | "<" | ">" | "<=" | ">="
| "&&" | "||"
| "&" | "|" | "^" | "<<" | ">>"
<unary_op> ::= "-" | "!" | "++" | "--" | "@" | "^"
<type> ::= "int" | "float" | "string" | "bool" | "fn"
| "i8" | "i16" | "i32" | "i64" | "i128"
| "u8" | "u16" | "u32" | "u64" | "u128"
| "f32" | "f64"
| <ident>
| "[" <type> "]" // dynamic list
| "[" <int> "]" <type> // fixed-size array
| "*" <type> // pointer type
| "&" <type> // reference type
<literal> ::= <int> | <float> | <string> | <bool> | "null"
<ident> ::= [a-zA-Z_][a-zA-Z0-9_]*