Ometer Docs
Ometer is a dynamically typed, web-first programming language with a custom syntax, bytecode compiler, and its own virtual machine — built entirely in TypeScript.
Installation
Ometer requires Node.js 18+. No global installs needed — everything runs through
ts-node.
bash# Clone and enter
git clone https://github.com/Ometer/ometer.git && cd ometer
# Install deps
npm install
# Verify
npx ts-node src/cli.ts help
Hello World
hello.omfn greet(name) {
return "Hello, " + name + "!"
}
print(greet("World"))
terminalnpx ts-node src/cli.ts run hello.om
→ Hello, World!
CLI Reference
| Command | Description |
|---|---|
| ometer run <file> | Execute an Ometer source file end-to-end |
| ometer lex <file> | Print the token stream (debug) |
| ometer parse <file> | Print the AST as JSON (debug) |
| ometer compile <file> | Print bytecode disassembly (debug) |
| ometer help | Show usage information |
Syntax Overview
Ometer uses curly-brace blocks, C-style operators, and its own keyword set. Files use the .om
extension. Comments use // or /* */.
// Single-line comment
/* Multi-line
comment */
let x = 42
let name = "Ometer"
let active = true
let nothing = null
Variables
Declared with let. Dynamically typed — can hold any value and be reassigned to a different type.
let count = 0
let label = "hello"
// Compound assignment
count += 1 // 1
count -= 1 // 0
count *= 3 // 0
count /= 2 // 0
Types
| Type | Literal | Notes |
|---|---|---|
| Number | 42 / 3.14 | Integer and float, stored as JS number |
| String | "hi" / 'hi' | Single or double quotes. Escape sequences: \n \t \\ \" \' |
| Bool | true / false | Boolean literals |
| Null | null | Intentional absence of value |
| Array | [1, "x", true] | Ordered, any element types |
| Object | { key: val } | String keys, any value types |
| Function | fn(x) { ... } | First-class values |
Operators
Arithmetic
x + y // add / string concatenation
x - y // subtract
x * y // multiply
x / y // divide
x % y // modulo
x ** y // power (right-associative)
-x // unary negation
Comparison
x == y x != y // equality
x < y x > y x <= y x >= y // comparison
Logical
x && y // AND — short-circuits, returns actual value
x || y // OR — short-circuits, returns actual value
!x // NOT
Control Flow
if / else if / else
if (score >= 90) {
print("A")
} else if (score >= 70) {
print("B")
} else {
print("C")
}
while
let i = 0
while (i < 5) {
print(i)
i += 1
}
for-in
let fruits = ["apple", "banana", "cherry"]
for (fruit in fruits) {
print(fruit)
}
// Works on objects too — iterates over keys
let user = { name: "Om", age: 1 }
for (key in user) {
print(key)
}
break / continue
while (true) {
if (done) { break }
if (skip) { continue }
}
Functions
Functions are first-class values. Named with fn name(params) or anonymous as expressions.
// Named declaration
fn add(a, b) {
return a + b
}
// Anonymous expression
let double = fn(x) { return x * 2 }
// Higher-order
fn apply(f, val) {
return f(val)
}
print(apply(double, 21)) // 42
// Closures
fn counter() {
let n = 0
return fn() { n += 1; return n }
}
let c = counter()
print(c()) // 1
print(c()) // 2
Arrays
let nums = [1, 2, 3]
print(nums[0]) // 1
print(nums.length) // 3
nums.push(4)
nums.pop()
nums.reverse()
let joined = nums.join(", ")
let found = nums.includes(2)
let part = nums.slice(0, 2)
Objects
let user = {
name: "Ometer",
version: 1,
active: true
}
print(user.name) // Ometer
print(user["version"]) // 1
user.active = false
user["score"] = 99
Imports
import web // loads the web stdlib module
let app = web.create()
Built-in Modules
math
print(math.floor(3.7)) // 3
print(math.sqrt(16)) // 4
print(math.abs(-5)) // 5
print(math.random()) // 0..1
print(math.pi) // 3.14159...
str
print(str.upper("hello")) // HELLO
print(str.split("a,b,c", ",")) // [a, b, c]
print(str.replace("hi", "h", "H")) // Hi
print(str.num("42")) // 42
String methods (instance)
let s = "Hello Ometer"
s.upper() // HELLO OMETER
s.lower() // hello ometer
s.trim()
s.split(" ") // ["Hello", "Ometer"]
s.includes("Om")
s.slice(0, 5) // Hello
s.length // 12
web.create()
Creates and returns a new Ometer web application. Must import web first.
import web
let app = web.create()
Routing
app.get("/users", fn(req, res) {
res.json([])
})
app.post("/users", fn(req, res) {
res.status(201).json({ created: true })
})
app.put("/users/:id", fn(req, res) {
let id = req.params.id
res.json({ updated: id })
})
app.delete("/users/:id", fn(req, res) {
res.json({ deleted: req.params.id })
})
Middleware
Middleware receives (req, res, next). Call next() to pass to the next handler.
app.use(fn(req, res, next) {
print(req.method + " " + req.path)
next()
})
Request Object
| Property | Type | Description |
|---|---|---|
| req.method | String | HTTP method: GET, POST, PUT, DELETE |
| req.path | String | URL path (no query string) |
| req.url | String | Full URL including query string |
| req.body | Object | Parsed JSON request body |
| req.params | Object | URL parameters (e.g. :id) |
| req.query | Object | Parsed query string ?key=val |
| req.headers | Object | HTTP request headers |
Response Object
| Method | Description |
|---|---|
| res.send(text) | Send plain text (Content-Type: text/plain) |
| res.json(obj) | Send JSON response (Content-Type: application/json) |
| res.html(str) | Send HTML response (Content-Type: text/html) |
| res.status(code) | Set HTTP status code — chainable: res.status(404).json({...}) |
| res.redirect(url) | Send 302 redirect to URL |
app.listen(port)
app.listen(3000)
// → 🚀 Ometer server running → http://localhost:3000
Compilation Pipeline
.om source → Lexer → Tokens → Parser → AST → Compiler → Bytecode → VM → Output
Each stage is inspectable via the CLI debug commands. The pipeline is strictly linear — no optimization passes in v0.1.
Bytecode Opcodes
VM Architecture
The VM is stack-based with a linked call-frame system. Each function call creates a new Frame with its own instruction pointer, value stack, and Scope. Scopes are linked-list environments — variable lookup walks up the chain until the global scope.
// Call frame structure:
Frame {
chunk: Chunk // compiled bytecode
ip: number // instruction pointer
scope: Scope // linked environment
stack: Value[] // value stack
}
// Scope chain:
globalScope → functionScope → blockScope
Keywords
| Keyword | Purpose |
|---|---|
| let | Declare a variable |
| fn | Declare a named or anonymous function |
| return | Return a value from a function |
| if / else | Conditional branches |
| while | Loop while condition is truthy |
| for / in | Iterate over arrays or objects |
| break / continue | Loop control |
| import | Load a built-in module |
| Write value to stdout | |
| true / false | Boolean literals |
| null | Null value |
Error Messages
All errors include a line number. Run ometer lex or ometer parse to isolate which
stage failed.
| Error | Cause |
|---|---|
| LexerError: Unexpected character | Invalid character in source |
| LexerError: Unterminated string | Missing closing quote |
| ParseError: Expected '(' | Missing paren after if/while/fn |
| ParseError: Invalid assignment target | Left side of = is not assignable |
| CompileError: 'break' outside loop | break/continue not inside a loop |
| RuntimeError: Undefined variable | Variable used before declaration |
| RuntimeError: Division by zero | Dividing by 0 at runtime |
| RuntimeError: Cannot call non-function | Calling a non-callable value |
Grammar (BNF)
program → statement* EOF
statement → letDecl | fnDecl | returnStmt | ifStmt
| whileStmt | forInStmt | importStmt
| printStmt | breakStmt | continueStmt | exprStmt
letDecl → "let" IDENT ("=" expression)?
fnDecl → "fn" IDENT "(" params ")" block
returnStmt → "return" expression?
ifStmt → "if" "(" expr ")" block ("else" (ifStmt | block))?
whileStmt → "while" "(" expr ")" block
forInStmt → "for" "(" IDENT "in" expr ")" block
expression → assignment
assignment → logicalOr (("=" | "+=" | "-=" | "*=" | "/=") assignment)?
logicalOr → logicalAnd ("||" logicalAnd)*
logicalAnd → equality ("&&" equality)*
equality → comparison (("==" | "!=") comparison)*
comparison → addition (("<" | ">" | "<=" | ">=") addition)*
addition → multiplication (("+" | "-") multiplication)*
multiplication→ power (("*" | "/" | "%") power)*
power → unary ("**" unary)*
unary → ("!" | "-") unary | postfix
postfix → primary (call | "." IDENT | "[" expr "]")*
primary → NUMBER | STRING | BOOL | NULL | IDENT
| "fn" "(" params ")" block
| "[" (expr ("," expr)*)? "]"
| "{" (IDENT ":" expr ("," IDENT ":" expr)*)? "}"
| "(" expr ")"