Every unmarked Palladium block in this file is compiled and run by scripts/check-docs.sh. If
an unmarked snippet is here, the compiler accepts it. That is not a courtesy — this repository previously shipped
documentation in which 508 of 560 snippets did not compile, describing a language that did not
exist. The checker is how that stays fixed.
Blocks marked no-compile are deliberate counter-examples showing what the compiler
rejects; they are excluded from the check.
fn main() {
print("Hello, Palladium!");
}
pdc compile hello.pd -o hello
./build_output/helloprint writes a string and a newline. print_int writes an integer.
Bindings are immutable unless you write mut.
fn main() {
let x: i64 = 42;
let mut count: i64 = 0;
count = count + 1;
let name: String = "Palladium";
let ready: bool = true;
print_int(x);
print_int(count);
print(name);
if ready {
print("ready");
}
}
Type annotations on let are optional; the compiler infers literals, calls, struct and enum
values, references, field and index expressions. Where it has no rule it now says so:
error: cannot infer the type of `r`: no type rule for this range expression.
Add an explicit type annotation, e.g. `let r: i64 = ...;`
That used to be a silent default to a 64-bit integer, which produced broken C for references, enum values and string copies. Annotating is still good practice in code you intend to keep — the bootstrap compiler annotates everywhere — but it is no longer load-bearing.
The primitive types are i64 (also spelled int), i32, u32, u64, bool, and String.
There are no floating-point types and no char.
fn add(a: i64, b: i64) -> i64 {
return a + b;
}
fn greet(name: String) {
print("Hello,");
print(name);
}
fn main() {
print_int(add(10, 20));
greet("world");
}
Write return explicitly. A trailing expression does work, but being explicit costs nothing and
this language spent a year silently discarding tail expressions — and still discards the tail of
an if, so a function whose body is if … { … } else { … } returns garbage
(language-spec.md A6.6).
Functions may be called before they are defined — the compiler emits C prototypes for you. Mutual recursion works:
fn is_even(n: i64) -> bool {
if n == 0 {
return true;
}
return is_odd(n - 1);
}
fn is_odd(n: i64) -> bool {
if n == 0 {
return false;
}
return is_even(n - 1);
}
fn main() {
if is_even(10) {
print("10 is even");
}
}
Always put spaces around a binary
-. The lexer's integer rule includes the sign, son-1tokenises asnfollowed by-1and misparses.n - 1is correct.
fn classify(n: i64) -> String {
if n < 0 {
return "negative";
} else {
if n == 0 {
return "zero";
} else {
return "positive";
}
}
}
fn main() {
print(classify(-5));
print(classify(0));
print(classify(7));
let mut i: i64 = 0;
while i < 3 {
print_int(i);
i = i + 1;
}
for j in 0..3 {
print_int(j * 10);
}
}
There is no else if. Nest the if inside the else block, as above. There is also no
loop keyword — use while true. And there is no +=; write i = i + 1.
This is rejected:
fn main() {
let x: i64 = 5;
if x > 9 {
print("big");
} else if x > 1 { // error: Expected '{' after else
print("medium");
}
}
Arrays are fixed size. There is no Vec; carry an explicit count alongside the array.
fn sum_first(mut values: [i64; 8], n: i64) -> i64 {
let mut total: i64 = 0;
let mut i: i64 = 0;
while i < n {
total = total + values[i];
i = i + 1;
}
return total;
}
fn main() {
let mut values: [i64; 8] = [0; 8];
let mut count: i64 = 0;
values[count] = 10;
count = count + 1;
values[count] = 20;
count = count + 1;
values[count] = 12;
count = count + 1;
print_int(count);
print_int(sum_first(values, count));
}
Two rules make array parameters behave:
- Declare them
mut. An array parameter lowers to a C pointer, so the callee writes through to the caller's array — declaring itmutis what tells the borrow checker the same thing the code generator already believes. - Iterate a parameter with
whileand an index, notfor.forover an array parameter currently emitssizeof(arr)/sizeof(arr[0])on a pointer that has already decayed, which is wrong.forover a literal range is fine.
struct Point {
x: i64,
y: i64,
label: String,
}
fn shift(mut p: Point, dx: i64, dy: i64) {
p.x = p.x + dx;
p.y = p.y + dy;
}
fn describe(mut p: Point) -> i64 {
print(p.label);
return p.x * p.y;
}
fn main() {
let mut origin: Point = Point { x: 3, y: 4, label: "corner" };
shift(origin, 1, 1);
print_int(origin.x);
print_int(origin.y);
print_int(describe(origin));
}
A struct parameter declared mut becomes a pointer in C, so mutations are visible to the caller
— that is how shift works. Struct fields may be integers, booleans, strings, arrays, and other
structs. They may not be tuples, references, or generic types.
enum Shape {
Circle(i64),
Rect(i64, i64),
Empty,
}
fn area(s: Shape) -> i64 {
match s {
Shape::Circle(r) => {
return 3 * r * r;
}
Shape::Rect(w, h) => {
return w * h;
}
Shape::Empty => {
return 0;
}
}
}
fn main() {
let c: Shape = Shape::Circle(2);
let r: Shape = Shape::Rect(3, 4);
print_int(area(c));
print_int(area(r));
}
match works on enums, integers, strings, bools and tuples. Literal patterns (1 =>, "s" =>,
true =>), ranges (1..=9), or-patterns (A | B), @ bindings and arm guards (if cond) all
work. What is still missing: slice patterns, ref/mut bindings, field shorthand in a
struct-variant pattern (write Move { x: x, y: y }), and destructuring let.
A match must be exhaustive for EVERY scrutinee type, not only for enums — on an integer or a
string that means a _ or a binding arm, because no set of literal or range arms is complete.
The annotations on let c: Shape = ... are optional — enum construction is inferred — but they
document the intent, and match reads better when the scrutinee's type is stated nearby.
Strings are immutable handles. Build them with the builtins rather than operators.
fn main() {
let a: String = "Palla";
let b: String = "dium";
let joined: String = string_concat(a, b);
print(joined);
print_int(string_len(joined));
if string_eq(joined, "Palladium") {
print("match");
}
let first: char = string_char_at(joined, 0);
print_int(first as i64);
let head: String = string_substring(joined, 0, 5);
print(head);
print(int_to_string(42));
print_int(string_to_int("99"));
}
string_char_at returns a char (N4-04), and the classification predicates take one — so
they compose with no cast. as i64 is how you get at the code point, and how you print it:
fn count_digits(s: String) -> i64 {
let mut n: i64 = 0;
let mut i: i64 = 0;
let len: i64 = string_len(s);
while i < len {
let ch: char = string_char_at(s, i);
if char_is_digit(ch) {
n = n + 1;
}
i = i + 1;
}
return n;
}
fn main() {
print_int(count_digits("a1b22c333"));
}
+ does work on strings, but string_concat is preferred: it keeps the meaning explicit and it
is what the bootstrap compiler uses.
fn main() {
let path: String = "/tmp/palladium_tutorial.txt";
let handle: i64 = file_open(path);
file_write(handle, "written from Palladium\n");
file_close(handle);
let back: String = read_file_to_string(path);
print(back);
print_int(string_len(back));
}
Command-line arguments follow C's convention — arg_count() includes the program name, so the
first real argument is arg_at(1):
fn main() {
let n: i64 = arg_count();
if n < 2 {
print("usage: prog <name>");
} else {
print(string_concat("hello, ", arg_at(1)));
}
}
fn main() {
let x: i64 = 42;
let y: &i64 = &x;
print_int(*y);
let inferred = &x; // the annotation is optional
print_int(*inferred);
}
Note that the type checker does not distinguish &T from T — there is no reference type in it
at all. Borrow checking happens, and code generation emits real pointers, but references are not
yet part of the type system. One consequence: dereferencing a reference parameter emits a
double dereference and fails in C, so pass values or arrays rather than &T parameters for now.
A word-frequency-style counter, using every mechanism above: arrays with a count, string
scanning, a struct for state, and an explicit while loop.
struct Counter {
starts: [i64; 32],
lengths: [i64; 32],
n: i64,
}
fn add_word(mut c: Counter, start: i64, length: i64) {
if c.n < 32 {
c.starts[c.n] = start;
c.lengths[c.n] = length;
c.n = c.n + 1;
}
}
fn split_words(mut c: Counter, text: String) {
let len: i64 = string_len(text);
let mut i: i64 = 0;
while i < len {
let ch: char = string_char_at(text, i);
if char_is_whitespace(ch) {
i = i + 1;
} else {
let start: i64 = i;
while i < len {
if char_is_whitespace(string_char_at(text, i)) {
break;
}
i = i + 1;
}
add_word(c, start, i - start);
}
}
}
fn longest(mut c: Counter) -> i64 {
let mut best: i64 = 0;
let mut i: i64 = 0;
while i < c.n {
if c.lengths[i] > best {
best = c.lengths[i];
}
i = i + 1;
}
return best;
}
fn main() {
let mut c: Counter = Counter { starts: [0; 32], lengths: [0; 32], n: 0 };
let text: String = "the quick brown fox jumped over lazy dogs";
split_words(c, text);
print("words:");
print_int(c.n);
print("longest:");
print_int(longest(c));
let mut i: i64 = 0;
while i < c.n {
print(string_substring(text, c.starts[i], c.starts[i] + c.lengths[i]));
i = i + 1;
}
}
Being clear about this is the point of the document. None of the following work today, and several of them fail without an error message — see the language specification for the exact failure mode of each.
| Not available | Use instead |
|---|---|
else if |
nested if inside else |
loop |
while true |
+=, -=, *= |
i = i + 1 |
method calls x.f() |
Type::f(x) |
| traits | nothing — they parse and emit no code at all |
| generics | nothing — monomorphisation is partial and argument parsing is buggy |
| closures | top-level functions |
Vec, HashMap |
fixed-size arrays with a count |
Option, Result |
your own enum |
? operator |
explicit checks — ? is rejected: "the ? operator is not implemented" |
async / .await |
nothing — .await is rejected: ".await is not implemented" |
| tuples | a struct |
floats, char |
i64 |
bitwise & | ^ << >> |
nothing — the lexer has no such tokens |
as casts |
nothing |
| string interpolation | string_concat |
literal patterns in match |
if/else chains |
- Language specification — every construct, with its status and the source location that proves it.
- Bootstrap subset — the subset the self-hosting compiler is written in, and the self-hosting gate.
- Builtin reference — all 34 builtin functions with signatures.