OMG is a small programming language made for fun and for learning how languages get put together end-to-end.
OMG has the usual stuff every language has: variables, math, strings, lists, dictionaries, conditionals, loops, functions, files etc. There's nothing flashy or industrial about it. It exists so I could see what it takes to build a working programming language, all the way up to writing OMG's own compiler in OMG.
- Get it running
- A tour of the language
- Built-in functions
- The REPL
- Compile to a native binary
- Editor support (VS Code)
- What's in this repo
- A small piece of trivia
- More reading
- License
You need Rust installed. Then:
git clone https://github.com/sentrychris/omglang
cd omglang
cargo build --release --manifest-path runtime/Cargo.tomlThat builds the OMG runtime as runtime/target/release/omg. From here
on this README assumes you have it on your PATH as omg. Either
copy/symlink the binary somewhere on your PATH, or alias it:
alias omg=$(pwd)/runtime/target/release/omgMake a file called hello.omg:
;;;omg
emit "Hello, world!"
Run it:
omg hello.omgYou should see:
Hello, world!
A few things to know up front:
- OMG files conventionally start with
;;;omgon the first non-empty line. The lexer strips it if present. It's optional but recommended — editors and tools key off it. emitprints something to the screen.#starts a comment. Comments run to the end of the line.
Each section is a few lines you can paste into a .omg file and run with omg yourfile.omg.
You introduce a brand-new variable with alloc. You change an existing
one with :=:
;;;omg
alloc name := "Chris" # introduce a new variable
emit name # → Chris
name := "Bob" # update it (no `alloc` the second time)
emit name # → Bob
If you forget the alloc for a brand-new name, OMG complains with a
Python-style traceback:
Traceback (most recent call last):
File "/path/to/script.omg", line 4, in <top-level>
UndefinedIdentError: name
The full rules — they're worth reading once because they save you from typos and accidental shadowing:
- Every new binding needs
alloc.:=only works on names that already exist somewhere in scope.cont := 5when you meantcount := 5is an error, not a silent new variable. - Re-assignment uses
:=— noallocthe second time:alloc x := 1 x := 2 # fine - Bindings declared inside a function are local to that function.
They aren't visible outside:
proc round2(n) { alloc r := 2 } round2(0) emit r # UndefinedIdentError: r - Globals can be read and updated from inside a function with
plain
:=:If you instead want a fresh local that shadows an outer name, usealloc r := 1 proc bump() { r := r + 1 } bump() emit r # → 2allocagain inside the function.alloc r := 2would create a new local without touching the global. alloctwice for the same name at the top level is an error:alloc count := 0 alloc count := 5 # → SyntaxError: 'count' is already declared at the top level
OMG has two kinds of numbers: integers (whole numbers) and floats
(decimals). Most integer math returns integers, but / is true
division and always returns a float — use // when you want integer
(floor) division:
;;;omg
emit 1 + 2 # 3
emit 10 - 3 # 7
emit 4 * 5 # 20
emit 10 / 3 # 3.3333… ← `/` is true division, always a float
emit 10 // 3 # 3 ← `//` is integer (floor) division
emit 10 % 3 # 1 ← remainder ("modulo")
emit -7 // 2 # -4 ← floor rounds toward minus infinity
emit 7 // -2 # -4 ← still floors when the divisor is negative
Bitwise operators exist (integers only):
;;;omg
emit 6 & 3 # 2 bitwise AND
emit 6 | 3 # 7 bitwise OR
emit 6 ^ 3 # 5 bitwise XOR
emit ~1 # -2 bitwise NOT
emit 1 << 3 # 8 left shift
emit 16 >> 2 # 4 right shift
emit 0b1010 # 10 binary literal
Write a float by including a decimal point or an exponent:
;;;omg
emit 1.5 # 1.5
emit 2.0 + 3.5 # 5.5
emit 1.0e3 # 1000.0 (scientific notation)
emit 6.022e23 # 6.022e23
/ is always true division and always returns a float, matching
Python 3. // is floor division — it keeps the integer type when both
operands are integers, and rounds toward minus infinity for floats:
;;;omg
emit 10 / 3 # 3.3333… ← `/` always returns a float
emit 10 / 3.0 # 3.3333…
emit 10 // 3 # 3 ← `//` keeps the integer type
emit 10.5 // 3 # 3.0 ← floor div on float still rounds toward -∞
emit 7 % -2 # -1 ← `%` carries the sign of the divisor
Other things to know:
5 == 5.0istrue. Cross-type numeric comparisons compare values.- Bitwise operators (
&,|,^,~,<<,>>) reject floats with a TypeError. Same for indexing a list with a float (xs[1.5]). - Float math is IEEE-754 double precision, so
0.1 + 0.2 == 0.3isfalse. That's not an OMG bug — it's how floats work everywhere. int(x)truncates toward zero;float(x)widens an int to a float.
The standard math kit is built in: floor, ceil, round (banker's
rounding), abs, sqrt, pow, log (natural), sin, cos, tan.
See the built-ins table for the full list.
Strings are written between double quotes:
;;;omg
alloc greeting := "Hello"
alloc name := "world"
emit greeting + ", " + name + "!" # Hello, world!
emit length(greeting) # 5
emit greeting[0] # H
emit greeting[1:4] # ell ← slice from 1 up to (not including) 4
emit greeting[-1] # o ← negative index counts from the end
Inside a string these escapes work: \n (newline), \t (tab), \r,
\\, \", \0.
;;;omg
emit 5 > 3 # true
emit 5 == 5 # true
emit 5 != 4 # true
emit "abc" < "abd" # true ← strings compare alphabetically
emit true and false # false
emit true or false # true
Things considered falsy (treated as false in if and loop):
false0(the integer zero)""(the empty string)[](the empty list){}(the empty dictionary)
Everything else is truthy.
;;;omg
alloc score := 75
if score >= 90 {
emit "A"
} elif score >= 80 {
emit "B"
} elif score >= 70 {
emit "C"
} else {
emit "F"
}
elif is short for "else if". You can have any number of elif
branches, and else is optional.
OMG has one looping construct: loop <condition> { ... }. It keeps
running the body as long as the condition is truthy. Same idea as a
while loop in other languages:
;;;omg
alloc i := 0
loop i < 5 {
emit i
i := i + 1
}
Output:
0
1
2
3
4
break exits the innermost loop early:
;;;omg
alloc n := 1
loop true {
if n > 100 {
break
}
n := n * 2
}
emit n # 128: the first power of 2 greater than 100
There's no for loop. Use loop with a counter, like the example above.
;;;omg
alloc xs := [10, 20, 30]
emit xs # [10, 20, 30]
emit length(xs) # 3
emit xs[0] # 10
emit xs[-1] # 30 ← negative indices count from the end
emit xs[1:3] # [20, 30] ← slicing also works on lists
xs[0] := 99 # replace one element
emit xs # [99, 20, 30]
xs := xs + [40] # append (creates a new list)
emit xs # [99, 20, 30, 40]
Two variables holding the "same" list share it, changing one is visible through the other:
;;;omg
alloc a := [1, 2, 3]
alloc b := a
b[0] := 99
emit a # [99, 2, 3]: `a` and `b` point at the same list
A dictionary maps keys to values. Keys are strings. You can read or
write entries either with dot notation (d.name) or with brackets
(d["name"]):
;;;omg
alloc person := {name: "Chris", age: 32}
emit person.name # Chris
emit person["age"] # 32
person.age := 33 # change a value
person["job"] := "engineer" # add a new key
emit person # {name: Chris, age: 33, job: engineer}
emit length(person) # 3
proc (short for "procedure") defines a function. return returns a
value:
;;;omg
proc square(x) {
return x * x
}
emit square(4) # 16
emit square(7) # 49
Functions are first class, you can pass them as arguments, store them in variables, and return them from other functions:
;;;omg
proc apply_twice(f, x) {
return f(f(x))
}
proc inc(n) {
return n + 1
}
emit apply_twice(inc, 5) # 7 (5 → 6 → 7)
A function defined inside another function remembers the variables it saw at the time it was defined. That's called a closure:
;;;omg
proc make_adder(n) {
proc add(x) {
return x + n # `n` was captured from `make_adder`
}
return add
}
alloc add5 := make_adder(5)
alloc add100 := make_adder(100)
emit add5(10) # 15
emit add100(7) # 107
Each call to make_adder produces its own add that remembers its
own n. The capture is by reference, not a snapshot — if the
inner proc mutates the captured name, the next call sees the new
value:
;;;omg
proc make_counter() {
alloc n := 0
proc tick() {
n := n + 1
return n
}
return tick
}
alloc c := make_counter()
emit c() # 1
emit c() # 2
emit c() # 3
Same semantics as Python and JavaScript — closures share storage with their enclosing scope, not just the values that were there at definition time.
Save this as mathlib.omg:
;;;omg
proc square(x) {
return x * x
}
proc cube(x) {
return x * x * x
}
Then in another file in the same folder:
;;;omg
import "mathlib.omg" as math
emit math.square(5) # 25
emit math.cube(3) # 27
The path inside the quotes is relative to the importing file's
directory. The name after as is what you'll call it from the other
file. Imports run the imported file once, and capture its top-level
proc and alloc definitions under that name.
Read a file as text in one shot:
;;;omg
alloc text := read_file("notes.txt")
if text == false {
emit "couldn't read notes.txt"
} else {
emit "got " + length(text) + " characters"
}
Or open a handle for finer control:
;;;omg
# write text to a file
alloc h := file_open("greeting.txt", "w")
file_write(h, "hi from OMG\n")
file_close(h)
# and read it back
alloc h2 := file_open("greeting.txt", "r")
emit file_read(h2) # hi from OMG
file_close(h2)
Binary mode ("rb", "wb", "ab") reads and writes lists of bytes
(integers 0–255):
;;;omg
alloc h := file_open("photo.jpg", "rb")
alloc bytes := file_read(h)
file_close(h)
emit length(bytes) # how many bytes long the file is
emit bytes[0] # the first byte, as an integer
Relative paths in read_file and file_open are resolved against
your shell's current directory, the same way cat, wc,
python, etc. behave.
Things sometimes go wrong. A bad index or a missing key, dividing by zero... By default the program prints a Python-style traceback to stderr and exits non-zero — for example:
Traceback (most recent call last):
File "/path/to/main.omg", line 17, in <top-level>
File "/path/to/main.omg", line 13, in outer
File "/path/to/main.omg", line 4, in inner
IndexError: index 5 out of range for length 0
To recover, wrap the risky bit in try / except — the traceback is
suppressed when the error is caught:
;;;omg
try {
alloc xs := [1, 2, 3]
emit xs[99] # IndexError: index 99 out of range
} except err {
emit "oops: " + err
}
emit "still running"
You can raise your own errors with panic:
;;;omg
proc divide(a, b) {
if b == 0 {
panic("division by zero")
}
return a / b
}
try {
emit divide(10, 0)
} except err {
emit "caught: " + err # caught: RuntimeError: division by zero
}
facts is shorthand for "assert this is true; if not, error out". It's
useful inside tests:
;;;omg
alloc x := 1 + 1
facts x == 2 # silent (passes)
facts x == 3 # AssertionError: assertion failed
Always available, no import needed.
| Function | What it does |
|---|---|
| Strings & chars | |
length(x) |
length of a list or string |
chr(n) |
one-character string for byte value n |
ascii(c) |
code point of a one-character string c |
string_bytes(s) |
UTF-8 byte values of s as a list of integers |
bytes_to_string(bytes) |
inverse of string_bytes: list of bytes → string |
| Numeric & math | |
int(x) / float(x) |
convert between int and float (or parse from string) |
floor(x) / ceil(x) |
round a float toward -∞ / +∞, returns int |
round(x) |
round-half-to-even (banker's rounding), returns int |
abs(x) |
absolute value (preserves int/float type) |
sqrt(x) |
square root, returns float |
pow(a, b) |
a to the power of b (int^int stays int) |
log(x) |
natural log, returns float |
sin(x) / cos(x) / tan(x) |
trigonometry in radians, returns float |
| Formatting | |
hex(n) |
lowercase hex string for integer n |
binary(n) / binary(n, w) |
binary string for n, optionally w bits wide |
float_bits(s) |
parse a float literal to its IEEE-754 i64 bit pattern |
bits_to_float(i) |
inverse: i64 bits → float |
| Collections | |
freeze(d) |
turn a dict into a read-only one |
dict_keys(d) |
list of a dict's keys (strings, in insertion order) |
has_key(d, k) |
true iff d has key k (non-dict d → false) |
| File I/O | |
read_file(path) |
read a text file in one shot, or false on error |
file_open(path, mode) |
open and return a handle (r, rb, w, wb, a, ab) |
file_read(handle) |
read everything remaining from a handle |
file_write(handle, data) |
write to a handle |
file_close(handle) |
close a handle |
file_exists(path) |
does the file exist? |
is_dir(path) |
is the path a directory? |
read_dir(path) |
list of entry names (sorted, no ./..) |
make_dir(path) |
create directories (mkdir -p semantics) |
| Standard input | |
stdin_readline() |
read one line from stdin, or false on EOF |
stdin_read() |
slurp all of stdin to EOF as a string (empty on no input) |
stdin_read_bytes() |
slurp all of stdin to EOF as a list of bytes (0–255) |
| Real-time terminal I/O | |
time_ms() |
current time in ms (epoch-based; suitable for elapsed-time) |
sleep_ms(n) |
pause the process for n milliseconds |
stdin_set_raw(on) |
toggle cbreak / no-echo mode (Linux TTY only) |
stdin_read_key() |
non-blocking one-byte read; returns char or false |
| Errors | |
panic(msg) / raise(msg) |
raise a runtime error (catchable with try/except) |
exit_with_error(msg) |
print msg to stderr verbatim and exit 1 (uncatchable) |
| Process control | |
subprocess([cmd, args...]) |
run a child process; returns its exit code |
exit(code) |
exit the current process with the given status |
getpid() |
current process ID (useful for unique tempfile names) |
| Reflection | |
call_builtin(name, args) |
call a builtin by name (advanced) |
The runtime also hands you three special globals every program can read:
args: a list of strings: the command-line arguments.args[0]is the script's path;args[1],args[2]… are user-supplied arguments.module_file: the path of the running script.current_dir: the directory the user ranomgfrom.
Run omg with no arguments to drop into an interactive shell:
$ omg
OMG Language Interpreter - REPL
Type `exit` or `quit` to leave.
>>> alloc x := 21
>>> emit x * 2
42
>>> proc greet(name) {
... return "Hello " + name
... }
>>> emit greet("OMG")
Hello OMG
>>> quit
Variables, functions, and imports persist across lines until you quit.
OMG can also be compiled to standalone native ELF binaries — no Rust
required at runtime. The OMG-to-C transpiler at
bootstrap/src/native-c.omg emits self-contained
C, which cc -O3 turns into a small (~30 KB) executable.
# One-time bootstrap (uses the Rust runtime to build the native toolchain)
cargo build --release --manifest-path runtime/Cargo.toml
bootstrap/build.sh
# After that: no Rust required to compile or run OMG
bootstrap/bin/omg foo.omg # compile and run
bootstrap/bin/omg --build foo.omg foo # AOT to a native ELF
./fooThe full guide — architecture, language tour, pipeline, extension guide,
runtime internals, debugging — lives in docs/native/.
To produce a slimmed-down, Rust-free distribution of just the native
toolchain (matching the layout of the standalone
omglang-native
companion repo), run bootstrap/package.sh.
See docs/native/packaging.md.
The vscode/ directory holds a VS Code extension that adds:
- syntax highlighting for
.omgfiles (a TextMate grammar invscode/syntaxes/), - file icons for
.omgsource and.omgbbytecode, - a small language server (LSP) under
vscode/server/that powers autocompletion, hover info, and go-to-definition on built-ins and user-definedprocs/allocs in the open file.
The extension isn't published on the marketplace, you build it from
this repo and install the resulting .vsix directly:
cd vscode
npm install # first time only, pulls dependencies
npm run compile # compile the TypeScript client + server
npx vsce package -o omg-language-server.vsix # package into a .vsix bundle
code --install-extension omg-language-server.vsix --forceWhat each step does:
npm install: downloads the dev dependencies (TypeScript compiler,vscepackager, the VS Code LSP libraries). Only needed the first time, or after the dependency list changes.npm run compile: runstscon the client (the bit that loads into VS Code) and the server (the bit that answers LSP requests). Outputs go toclient/out/andserver/out/.npx vsce package: bundles the compiled output, the grammar, the icons, andpackage.jsoninto a single installable.vsixarchive.code --install-extension … --force: installs that bundle into your local VS Code, replacing any previous version of the extension.
After installing, open any .omg file and you should see syntax
highlighting and the OMG file icon. Start typing proc, loop,
emit, etc. to see completions.
omglang/
├── runtime/ the Rust implementation: lexer, parser, compiler, VM, REPL
├── bootstrap/
│ ├── src/ OMG sources for the self-hosted toolchain + C runtime header
│ │ ├── compiler.omg the OMG compiler, written in OMG
│ │ ├── compiler.omgb its compiled bytecode (re-built on `cargo build`)
│ │ ├── vm.omg the OMG-in-OMG VM (used for fixed-point verification)
│ │ ├── native-c.omg OMG-to-C transpiler (used by the native build path)
│ │ ├── native-js.omg OMG-to-JS transpiler (alternative backend)
│ │ ├── omg.omg user-facing driver (run / compile / build / REPL), in OMG
│ │ ├── omg-web.omg driver bundled into the browser playground
│ │ ├── omg-explorer.omg driver for the in-browser compiler explorer
│ │ ├── omg_rt.h C runtime header inlined into native binaries
│ │ └── omg_rt.js JS runtime inlined into transpiled JavaScript
│ ├── bin/ Rust-free toolchain (4 native ELFs + omg_rt.h + omg_rt.js)
│ ├── build.sh builds bootstrap/bin/ from bootstrap/src/
│ ├── build-web.sh builds the web/ bundle from bootstrap/src/omg-web.omg
│ └── package.sh spins out a slim native-only distribution into dist/
├── examples/ small standalone programs (one language feature each)
├── games/ larger interactive terminal games (snake, tetris)
├── tests/ shell-driven test suite (parity, regression, REPL, builtins)
├── tools/ command-line utilities written in OMG (wc, grep, sort, omgdb, etc.)
├── web/ browser playground + compiler explorer (static site)
├── docs/ documentation; see docs/native/ for the native compilation path
└── vscode/ VS Code extension (syntax highlighting + LSP completion)
Some interesting starting points:
examples/prime_sieve.omg: finds primes up to 100.examples/maze_solver.omg: breadth-first search over a grid.examples/higher_order.omg: closures + first-class functions.examples/donut.omg: Andy Sloane's spinning 3D ASCII donut, ported to OMG. Exercises the float kit and ANSI-escape animation — AOT-compile it for a smooth ~90 FPS spin.games/: interactive terminal games (Snake, Pong, Tetris, a small roguelike) driven bytime_ms/sleep_ms/stdin_set_raw/stdin_read_key. Seegames/README.md.tools/unix/wc.omg,tools/unix/grep.omg,tools/json.omg: Unix-style utilities, written in OMG. Seetools/README.mdfor the full list.tools/db/: omgdb, a small SQL database written in OMG — a paged on-disk format, a recursive-descent SQL parser, and a SQLite-style REPL. Seetools/db/README.md.
OMG's compiler is itself written in OMG. The file
bootstrap/src/compiler.omg reads OMG source
code and produces the bytecode the runtime executes, which is exactly
what the Rust frontend in runtime/ does.
By default, omg <script> runs your code through that OMG-written
compiler — the language compiles itself end-to-end on every run. If you
want the faster Rust frontend (e.g. while iterating, or to avoid the
~1 second compile overhead on larger programs), pass --rust:
omg foo.omg # self-hosted (default)
omg --rust foo.omg # Rust frontendTo verify both compilers agree byte-for-byte on the compiler's own source — the fixed-point check — run:
omg --verify-self-hosted bootstrap/src/compiler.omgThe runtime compiles compiler.omg two different ways, once with the
Rust frontend, once with the OMG-written compiler running on the VM,
and confirms the two byte streams are identical.
There's a stronger version too. The runtime ships an OMG-written VM
at bootstrap/src/vm.omg that interprets .omgb
bytecode. With it, you can run the OMG compiler on the OMG VM — three
levels of meta — and ask whether that still produces the same bytes:
omg --verify-omg-vm bootstrap/src/compiler.omgIf it passes, every act of language-level interpretation in the chain happened inside OMG; the Rust runtime is just the substrate at the bottom.
docs/native/: the native compilation path — turning.omgsource into standalone ELF binaries with no Rust runtime needed. Covers the architecture, language, pipeline, runtime header, and how to add new features in lockstep across Rust + OMG + C.docs/compilation-pipeline.md: howomg foo.omgactually runs your script — the two-stage compiler, the VM-on-VM dance, what--rustdoes, and the fixed-point check.runtime/README.md: runtime architecture and CLI flags.tools/README.md: the OMG-in-OMG tools.web/README.md: the browser playground and compiler explorer.vscode/README.md: VS Code extension.
MIT.
Educational project, not intended for production use.