Smilium 2.1 · official language documentation · CLI & editor extension in preparation

The Smilium language.

A programming language simple enough for a kid's first line of code — and powerful enough to keep up with them. Born inside SmileyOS, Smilium has the features modern languages have — structs with methods, enums, maps, pattern matching, reactivity, persistence, named arguments — spelled in plain words, with errors that teach instead of scold.

This page is the official reference: every feature explained, with examples from first-step to advanced. Hit ▶ Try on any snippet — it opens in a playground tab and runs right in your browser, on the real interpreter.

hello.smy
@core
@shell
struct Player {
string name;
number score;
}
[persist]
number best = 0; // remembered across runs
string name = ask("What is your name? ");
need(not name.empty, "A hero needs a name!");
Player p = { name: name, score: name.length * 3 };
print("Hi {p.name}, you scored {p.score}!", Colors.green);
if(p.score over best or best is 0) {
best = p.score;
print("NEW BEST: {best}", Colors.yellow);
}
nullable by defaultstructs + methodsenumsmapswhen matchingreactive watch[persist] across runsJSON + CSVnamed argumentserrors that teach198 interpreter tests
Playground00

Try Smilium right here.

The REAL interpreter — the same kernel source that runs inside SmileyOS, compiled to WebAssembly. Press Run (or F5), answer ask() prompts, and event programs stay live. The + tab creates extra .smy / .json files that load() can read — and every file your program save()s opens as a tab.

playground.smysnake.smy
@core
@shell
@system

print("Welcome to Smilium!", Colors.yellow);

string name = ask("What is your name? ");
print("Hi {name}!", Colors.green);

// a list, a loop and some luck
number[] rolls = [];
repeat(3) {
    rolls.add(random(1, 6));
}
print("Your dice: {rolls}  (sum {rolls.sum()})");

// watch a value - the block runs on every change
number score = 0;
score.watch(old, new) {
    print("score: {old} -> {new}", Colors.magenta);
}
score = rolls.sum() * 10;

save("best", score);   // remembered - opens a save.json tab!

// match the result - first true case wins
when(score) {
    (over 100)   { print("CHAMPION!", Colors.green); }
    (50 to 100)  { print("Nice roll!", Colors.cyan); }
    ()           { print("Try again!"); }
}
ready

Press Run (or F5 in the editor) — output appears here. Files your program saves open as tabs; the + tab creates .smy / .json files that load() can read.

Smilium 2.1Ln 1, Col 1Spaces: 2UTF-8
Meet the language01

The whole language fits in your head.

This is ALL of Smilium — every word it knows, on one card. Single English words, each with one job. Hover a word to see what it does and how it looks in code, then dive into the sections below.

Directives4

Keywords21

Word operators8

Types5

Attributes5

Builtins13

Value methods10

Namespaces3

when

Keywords

Match ONE value against cases — first true case wins, no fall-through, empty () catches the rest. Over an enum it checks you covered every value.

in code
when(score) {
(over 90) { print("A"); }
(80 to 89) { print("B"); }
() { print("ok"); }
}

Hover any word — or tap on touch screens. The deep dives with runnable examples start right below. ↓

Basics02

Directives, types and null.

A program starts with directives: @core is always on, @shell makes it a text program, @system unlocks helpers like random(). Three simple types — and one honest rule: a variable you never set is null (“no value yet”), not a sneaky zero.

@core / @shell / @system / @datenumber · string · boolean{curly} interpolationmultiline """ strings16 print colorscommentsnull by defaultis null vs .empty
1 / 3
types.smy
@core
@shell
number score = 42; // whole or decimal
string name = "Marija"; // text
boolean happy = true; // yes / no
// {curly} interpolation inside strings:
print("{name} scored {score}!");
print("double: {score * 2}");

Six directives exist: @core, @shell, @system and @date work everywhere (including this page's playground) — while @ui (windows, buttons, inputs) and @game (sprites for Gamely2D) need the real SmileyOS.

Input03

Ask a question, get a typed answer.

ask() shows the question and waits. The declared type of the target decides how the answer is read — ask into a number and a non-number answer gets a friendly “try again”. It fills ANY target — fields, elements, whole structs — and can offer a fixed list of choices.

typed answersre-asks on bad inputchoices listsame-line answers (, false)fills fields & elementswhole-struct askenum choices auto-offered
1 / 3
ask.smy
@core
@shell
string name = ask("Your name? ");
number age = ask("How old are you? "); // re-asks until it's a number
print("Hi {name}!");
print("Next year you will be {age + 1}.");
Control flow04

Words, loops and when.

Smilium has TWO equal spellings for logic. Kids meet the word style first (is, isnt, over, under, and, or, not); the symbols (==, !=, >, <, &&, ||) are what every other language uses — so graduating to Python or JS feels familiar. And when matches one value against cases, with no fall-through.

words AND symbolsif / elserepeat · for · whilebreak / continue / exitno-hang loop guardwhen matchingranges (a to b)comma OR-cases() catch-all
1 / 3
if-else.smy
@core
@shell
number score = 87;
if(score over 50 and score isnt 100) {
print("Nice score!", Colors.green);
}
else {
print("Keep trying!");
}
// words and symbols are BOTH fine:
if(score >= 80 || score == 42) {
print("Top tier");
}
Lists & maps05

Arrays, transforms and maps.

Arrays grow as you go and answer real questions — .sum(), .max(), .contains(), .pick(). The transform family (.map / .where / .count / .all / .any) makes new lists without loops — and MAPS hold values by string key, like a phone book.

auto-growing arraysadd · sort · sum · avg · pickcontains / find / slice.map / .where / .count.all / .anymaps: number<> string<> Player<>.keys · .values · .hasmissing key -> null
1 / 3
arrays.smy
@core
@shell
@system
number[] scores = [10, 25, 3];
scores.add(40);
print(scores);
print("count: {scores.length}, sum: {scores.sum()}");
print("best: {scores.max()}, avg: {scores.avg()}");
print(scores.contains(25));
scores.sort();
print(scores);
Strings06

Text that answers back.

Strings carry their own tools — case, search, replace, split — and conversion is honest: "5" and 5 are different things, and .number() converts on purpose (with a friendly stop if it can't).

upper / lower / replace / splitcontains · starts · ends.repeat(n) banners"5".number() conversion.between(a, b).emptychar-by-char .eachexpression .each
1 / 3
strings.smy
@core
@shell
string s = "Smilium is fun";
print(s.upper());
print(s.contains("fun"));
print(s.replace("fun", "POWER"));
print(s.split(" "));
print("len: {s.length}");
Functions07

Your own commands — now with methods.

func makes a command; -> type declares what it returns; parameters can have defaults. Calls can NAME their values (spawn(speed: 5)), and your own structs can carry METHODS (func Player.reset(...) with self).

func + returnparameter defaults-> return typesnamed argumentsskip-middle defaultsstruct methods (func Player.x)self by referencemethod chainingdid-you-mean on typos
1 / 3
functions.smy
@core
@shell
func greet(string who) {
print("Hello {who}!");
}
func double(number n) -> number {
return (n * 2);
}
greet("Ana");
number d = double(21);
print(d);
Structs08

Bundle values that belong together.

A struct is a bundle of named values — no classes, no ceremony, and plenty of power: nesting, arrays inside, literal shorthand, destructuring, and real REFERENCES — = shares, .clone() copies, .same() asks.

literals & shorthandnested structsarrays inside structsdeep paths (a.b.c)table printingdestructuring { } and [ ]references: = shares.clone() copies.same() / built-in idextension methods (func Player.x)
1 / 3
structs.smy
@core
@shell
struct Player {
string name;
number score;
}
Player p = { name: "Ana", score: 10 };
print(p.name);
p.score = p.score + 5;
print(p);
// arrays of structs print as a TABLE
Player q = { name: "Bo", score: 25 };
Player[] team = [p, q];
print(team);

Struct extension methods — a closer look

func Player.reset(...) hangs a method on YOUR struct — no classes needed. Inside it, self IS the struct the method was called on (they share, per the reference rules), so field writes change the caller's data in place. A method with no explicit return gives back self, which is what makes calls CHAIN; declare -> Player and the result even works as a struct in expressions. Methods can live in imported module files too — a playerlib.smy can extend the structs your main file uses.

define-methods.smy
@core
@shell
struct Player {
string name;
number score;
}
// the method hangs on the TYPE - self is the receiver
func Player.reset(string name = "?", number score = 0) {
self.name = name;
self.score = score;
}
func Player.addPoints(number pts) -> Player {
self.score = self.score + pts;
}
func Player.total() -> number {
return (self.score * 2);
}
use-methods.smy
Player p = { name: "Ana", score: 10 };
p.reset("Bo", 5); // self changed p itself
print("{p.name} {p.score}"); // Bo 5
// methods return self - so calls CHAIN:
p.addPoints(1).addPoints(4).save("hero");
print(p.total()); // 20 - works in expressions
p.reset(score: 42); // named args work on
print(p.score); // methods too
Enums09

One value from a fixed set.

enum Direction { up, down, left, right } — a type whose values are a closed list. They print as their names, save as their names, step in order with ++ / --, and typos get a did-you-mean.

values print as namesDirection.all list.pick() random value++ / -- stepping+= n jumps.reset()when completeness checkask() offers valuesdid-you-mean
1 / 3
enums.smy
@core
@shell
enum Direction { up, down, left, right }
Direction way = Direction.up;
print(way); // enums print as their NAME
print("going {way}");
if(way == Direction.up) {
print("north it is");
}
// when over an enum must cover EVERY value (or add a () catch-all)
when(way) {
(Direction.up) { print("N"); }
(Direction.down) { print("S"); }
(Direction.left, Direction.right) { print("sideways"); }
}

Miss a case and the parser tells you exactly which values are missing — completeness checking, like Rust's match, in kid clothes.

Reactive10

Watch values change.

.watch runs a block whenever a value actually CHANGES — the heart of reactive programming, one line of it. Watch a team of variables together, and opt arrays in with [state].

.watch(old, new)fires only on real changeteam watch + changed[state] array watchingstructs watchable (any field)one watcher per variableno chain reactions
1 / 3
watch.smy
@core
@shell
number score = 0;
// run a block whenever the value CHANGES
score.watch(old, new) {
print("score: {old} -> {new}", Colors.magenta);
}
score = 10;
score = 10; // no change -> nothing fires
score = 25;

Structs are watchable too — p.watch(old, new) fires on ANY field change, and old/new are full snapshots (old.score works). Two rules keep it sane: one watcher per variable, and writes inside a watcher never fire another one — no chain reactions.

Events11

Timers and keys — programs that stay alive.

every(ms) runs a block on a timer. onKey(Key k) receives every key press — compare with Keys.space, Keys.enter, arrows, a–z, 0–9. An event program stays alive until exit; — in the playground the terminal keeps listening for your keys.

every(ms) timersonKey(Key k) handlersKeys.space / arrows / a–z / 0–9programs stay aliveexit; ends themworks with watch
reaction-game.smy
@core
@shell
@system
number ticks = 0;
print("Press SPACE as fast as you can! q quits.");
every(1000) {
ticks = ticks + 1;
print("tick {ticks}");
}
onKey(Key k) {
if(k == Keys.space){
print("jump!", Colors.green);
}
if(k == Keys.q){
print("bye!");
exit; // ends the event program too
}
}
Safety12

Errors that teach.

Nothing fails silently in Smilium. Using a null value, a bad enum name, a typo'd field — each is a friendly STOP that says what happened and how to fix it. And you choose where errors land: need() guards, a local .catch() net, or the program-wide catch.

null = friendly stopneed() guardsnearest-door exitsprogram-wide catchlocal .catch() nets[const] constants.lock() / .unlock()errors that teach
1 / 3
null-safety.smy
@core
@shell
number age;
print(age is null); // unset = null, never a sneaky 0
catch(string e) {
print("the net caught: {e}");
}
number next = age + 1; // using null = friendly stop -> the net
age = 9;
print(age + 1); // now it's fine

The error messages, verbatim

This is what Smilium actually says — collected straight from the interpreter. Every message names the line, says what went wrong in plain words, and most bring a did-you-mean or an exact fix. Parse errors stop BEFORE the program runs; runtime stops go through your catch nets first.

print("hi")

Line 3: Every line needs a ; at the end

hint: Add a semicolon ; after this line

print(scroe);

Line 4: I don't know what 'scroe' is

hint: Did you mean 'score'?

a.sortt();

Line 4: Arrays don't have a 'sortt' method

hint: Try: add, pop, remove, insert, sort, reverse, shuffle

when(d) { ...up, down... }

Line 8: when(d) is missing: left, right

hint: Add those cases, or a () catch-all at the end

spawn(sped: 5);

Line 6: spawn has no 'sped'

hint: Did you mean 'speed'?

u.nmae = "Bo";

Line 5: 'User' has no field 'nmae'

hint: Check the struct's field names

number age;
number next = age + 1;

Oops: 'age' has no value yet (null) - set it first!

lives = 5;   // lives is [const]

Line 5: 'lives' is a constant - it can't change

hint: A [const] gets its value once, at the declaration

while(true) { n = n + 1; }

Oops: this loop ran 50000 times - does it ever end?

Saving13

Persistence, JSON and CSV.

One line to remember a value, one attribute to remember it FOREVER: [persist] loads on start and auto-saves every change. Structs write real JSON — or CSV that opens straight in Excel — and a leading - hides a field from every saved file.

save() / load() one-linersstruct .save / Type.load[persist] across runsFormat.json / Format.csv[csv] / [json] defaults-field hidingauto file extensionsmaps & arrays as JSON
1 / 3
save-load.smy
@core
@shell
// one-line persistence for values
save("highscore", 420);
number best = load("highscore");
print(best);
// whole structs save as real JSON files
struct Player { string name; number score; }
Player p = { name: "Ana", score: 10 };
p.save("hero"); // writes hero.json
Player back = Player.load("hero");
print(back.name);
Builtins14

Batteries included.

Math, randomness, formatting, dates — always a single word away. @system unlocks the helpers, @date the clock and calendar.

random · round · clampabs · floor · ceil · sqrt · powmin · max · sin · cos · tanformat · pad · type · lensleep · clear · timetoday + get* date helpers
math.smy
@core
@shell
@system
print(random(1, 6));
print(round(3.7));
print(clamp(15, 0, 10));
print(abs(0 - 4));
print(min(3, 8));
print(pow(2, 10));
print(sqrt(81));
format-and-date.smy
@core
@shell
@date
print(format(3.14159, 2)); // 3.14
print(pad(7, 3)); // 007
print(type(42)); // number
print("today is day {getDay()} of month {getMonth()}");

Also on values themselves: .length, .empty, .between(a, b), .number(), string methods (.upper, .split, .replace, …), array methods (.sum, .sort, .pick, .map, …), map methods (.keys, .has, …) and struct methods (.clone, .same, .save).

The full table

print(text, color?)one output line — {curly} interpolation, optional Colors.x
ask(q, choices?|false)question -> typed answer; choices list re-asks; false = same-line answer
save(key, value)remember a value (also structs/arrays/maps) in save.json
load(key)read a saved value back — typed by the variable
random(a, b) / random(list)whole number a..b, or one element of a list
round(x, decimals?)round — optionally to n decimals
clamp(v, min, max)keep a value inside limits
abs(x) · floor(x) · ceil(x)distance from zero · round down · round up
sqrt(x) · pow(b, e)square root · powers
min(a, b) · max(a, b)the smaller / bigger of the two
sin(x) · cos(x) · tan(x)trigonometry, in radians
format(x, decimals)number -> text with fixed decimals
pad(n, width)zero-pad: pad(7, 3) -> "007"
type(x)the type name as text
len(x)length of text or list (also the .length property)
sleep(ms)pause — 1000 is one second
clear()wipe the output console
time()milliseconds since the program started
today()today's date as text (@date)
getYear() … getSecond()calendar & clock pieces; getDayOfWeek(): 1 = Monday (@date)
Modules15

Split code into files.

Mark a file as a library with #name on its first line, and pull it in with @name. In the playground the tabs ARE the filesystem — make a second tab and import it.

#name exports a library@name imports itfunctions & structs travelstruct methods tooplayground tabs = files
mathlib.smy
#mathlib
@core
func double(number n){
return (n * 2);
}
func triple(number n){
return (n * 3);
}
main.smy
@core
@shell
@mathlib // <- import the library
print("double 5 = {double(5)}"); // 10
print("triple 5 = {triple(5)}"); // 15

Struct METHODS (func Player.reset(...)) are allowed in imported files too — so a playerlib.smy can extend the structs your main file uses.

Tooling16

One interpreter, every home.

The exact same interpreter source runs in SmileyOS, in this page's playground, in the terminal and in your editor — no dialects, no “works on my machine”.

Shipped

SmileyOS Code Sandbox

The visual home: step through programs, watch variables as memory boxes, autocomplete with kid-friendly hint cards — in English and Macedonian. Try SmileyOS in the browser.

In preparation

Official CLI

smilium run file.smy in any terminal — macOS and Linux — compiled from the very same kernel sources. ANSI-colored print, a smilium check mode that lists errors with hints, and the full 198-case test suite (smilium test). Being polished for release now.

In preparation

VS Code / Cursor extension

Full editor support for .smy files: syntax highlighting, IntelliSense with kid-friendly parameter docs, snippets, live error squiggles as you type (with the same teaching hints), and a Run button / F5. Ships together with the CLI.

Using Cursor, Copilot or another AI editor? Point it at the machine-readable language reference — the canonical Smilium 2.1 spec, also listed in this site's llms.txt. Paste the link into your project rules and the AI writes valid Smilium.

Roadmap17

What's next — 2.2 and 3.0.

Smilium 2.1 is the language documented on this page. The next two versions are in the DESIGN phase — syntax may still change, and your vote genuinely steers what gets built first. The kid principles stay law: single-word names, English keywords, errors that teach.

Designv2.22.2-a

Encrypted saves

Saved files are plain text today - anyone can open the high-score file. An optional Encryption.x argument protects it, same shape as Format.x.
Today (2.1)
p.save("player");
// player.json - readable
// (and editable!) by anyone
The idea
secret("my-key");
p.save("player", Format.json,
Encryption.secret);
// scrambled on disk
Would you use this?
Designv2.22.2-b

Async — later / .then / wait

Promises, the Smilium way: a later block runs without blocking, .then and .err are the same block shape kids already know from .each and .catch.
Today (2.1)
sleep(2000);
// the WHOLE program
// stands still and waits
The idea
later fetchScores() {
return (Player.load("team"));
}
fetchScores().then(team) {
print("got {team.length}!");
}
print("this prints FIRST");
Would you use this?
Designv2.22.2-c

Choose where files go

Every save lands in one fixed folder today. savePath() points a whole program somewhere else - a backup folder, the desktop, a shared spot.
Today (2.1)
team.save("team");
// always the sandbox
// folder - no choice
The idea
savePath("~/Documents/mygame");
team.save("team");
// -> ~/Documents/mygame/team.csv
Would you use this?
Designv2.22.2-d

Interpolation everywhere

Today {x} only works in print() and ask() - stored strings keep the braces literally. In 2.2 EVERY string interpolates, plus formatting right inside the braces.
Today (2.1)
list.add("run {n}");
// stores literally:
// "run {n}" (!)
The idea
list.add("run {n}"); // "run 3"
string msg = "Hi {name}";
print("price: {price:2}");
print("lap {i:pad3}"); // 007
Would you use this?
Designv2.22.2-e

Block-powered sort / find / sum

The transform family grows up: sort a struct list BY a block, pull the best element out with .max, total a field with .sum - no loops.
Today (2.1)
team.sort();
// sort by WHAT?
// structs can't say
The idea
team.sort(p){ return (p.score); };
Player top = team.max(p){ return (p.score); };
number total = team.sum(p){ return (p.score); };
Would you use this?
Designv2.22.2-f

Range loops - 1 to 10

The a to b range kids already know from when becomes a walkable thing - loop it, map it, pick from it. Counts down when a is bigger.
Today (2.1)
for(i = 1; i <= 10; i = i + 1) {
print(i);
}
The idea
each(1 to 10, i) {
print(i);
}
number[] sq = (1 to 5).map(n){ return (n * n); };
Would you use this?
Designv2.22.2-g

ask() with a CHECK block

Typed re-asking already exists - this adds YOUR rule: a one-line block validates the answer, a false means a friendly "try again" and the question repeats.
Today (2.1)
number age = ask("Age? ");
// any number passes -
// even 9999
The idea
number age = ask("Age? ", a){ return (a.between(5, 120)); };
// wrong value -> re-asks
// with a friendly nudge
Would you use this?
Designv2.22.2-h

Null-safe reads - ?. and ??

For the "it might be missing and that is fine" moments: ?. reads through null without stopping, ?? supplies the fallback. Plain . stays the default everywhere.
Today (2.1)
if(u.address isnt null) {
print(u.address.city);
}
The idea
string city = u?.address?.city;
print(city ?? "no city yet");
Would you use this?
Designv2.22.2-i

test blocks - tiny unit tests

Real testing habits, kid-sized: test blocks with check() inside, run by smilium test file.smy - green dots for passes, teaching errors for fails.
Today (2.1)
// testing today = printing
// and eyeballing the
// output by hand
The idea
test "double doubles" {
check(double(2) is 4);
check(double(0) is 0);
}
Would you use this?
Designv2.22.2-j

debug() + [debug] variables

See inside without string glue: debug(x) prints name, value, type and line in one call - and a [debug] attribute makes a variable narrate every change it goes through.
Today (2.1)
print("score is {score}");
// name it yourself,
// glue it yourself
The idea
debug(score);
// score = 7 (number) @ line 12
[debug]
number score = 0;
score = 5; // "score: 0 -> 5"
Would you use this?
Designv2.22.2-m

Array & struct function params

Parameters are scalars-only today - func f(Contact[] c) is a parse error, discovered live by the playground AI. In 2.2 functions take lists and structs like everything else.
Today (2.1)
func f(Player[] team) { }
// "not a valid type" -
// only number/string/boolean
The idea
func best(Player[] team) -> Player {
return (team.max(p){ return (p.score); });
}
Would you use this?
Designv2.22.2-n

Math pack - 10K, 2^10, solve

Numbers you can READ (10K, 2.5M, 1B), powers you can WRITE (2^10), roots, PI, degrees<->radians - and a first taste of algebra with solve in backticks.
Today (2.1)
number users = 2500000;
number n = pow(2, 10);
// count the zeros and hope
The idea
number users = 2.5M;
number n = 2^10;
number x = solve(`2x + 4 = 10`);
print(deg(PI)); // 180
Would you use this?
Plannedv3.03.0-a

Compile to JavaScript

Your .smy program as real, readable JavaScript with real import lines - plus a tree-shakeable smilium npm library for the builtins. The transpiler writes the imports itself.
Today (2.1)
// today: the interpreter
// runs your program
// (wasm / CLI / SmileyOS)
The idea
// smilium build game.smy
import { clamp, random }
from "smilium";
// kids never type that line -
// the compiler emits it
Would you use this?
Support18

Help Smilium grow.

Smilium, SmileyOS, this playground, the CLI and the editor extension are built by a tiny team in Macedonia — free, for kids learning to code. If it made you (or a kid you know) smile, a coffee keeps the lights on and the batches shipping.

Buy us a coffee, power a language.

No ads, no accounts, no paywall — Smilium and this playground stay free for every kid who wants to code. Coffee turns directly into:

  • The CLI release

    smilium in every terminal

  • 2.2 & 3.0

    async, encryption, JS compile

  • Free forever

    playground, docs & SmileyOS

Support us on Buy Me a Coffee

One-time or monthly — every single coffee counts. 💛

Your turn19

Suggest a feature.

Smilium is being shaped in the open — many features on this page grew from ideas exactly like yours. Have one — a method, a keyword, a whole concept? Sketch it below (with a code snippet if you like) and it lands straight on the maintainer's desk.

snippet.smy · optional

One friendly language, a whole OS behind it.

Smilium is born inside SmileyOS — built in Macedonia to teach kids real programming. Try the OS in your browser, or scroll back up and keep playing.