Language Features
Marina is a strictly-typed subset of TypeScript, compiled ahead of time to LLVM IR. Programs use the .tsm extension and file names must be kebab-case.
Types & inference
| Type | Example |
|---|---|
| Int32 | let a = 10 |
| Float64 | let b = 20.5 |
| string | let s = "hi" |
| bool | let ok = true |
| Promise<T> | return value of an async function |
| Array<T> | let xs = [1, 2, 3] |
| interface | structural data contracts |
| generic T | type parameters on functions and classes |
Annotations are available (let a: Int32 = 10) and required for function parameters. There is no any. The only implicit conversion is Int32 → Float64 in mixed arithmetic. String arithmetic is rejected — use template literals:
let a = 10 // Int32
let b = 20.5 // Float64
let result = a + b // Float64 (a is widened)
console.log(`The result of ${a} + ${b} is ${result}`)
Variables: let vs const
let declares a mutable variable; const declares an immutable one. Assigning to a const in any form (=, ++, --, +=, -=) is a compile-time error, and const declarations must be initialized. const freezes the binding, not the object — array contents can still be mutated through it.
Top-level variables are globals: they live for the whole program and are visible inside every function and lambda. Lambdas capture them by reference to the global — reads see the current value, and writes update the shared global.
Arrays
Typed lists with bounds-checked indexing (out-of-bounds panics at runtime) and reference counting. Element types are inferred, or taken from an annotation on an empty literal.
let xs = [1, 2, 3, 4, 5] // Array<Int32>
xs.push(6); xs.pop(); xs.unshift(0); xs.splice(2, 1)
console.log(xs.length)
xs[0] = 10 // index assignment
let strs: Array<string> = []
strs.push("hello"); strs.push("world")
console.log(strs.join("-")) // hello-world
xs.forEach((item) => { console.log(item) })
let doubled = xs.map((item) => { return item * 2 })
let big = xs.filter((item) => { return item > 3 })
let sum = xs.reduce((acc, item) => { return acc + item }, 0)
Supported: push, pop, shift, unshift, splice, slice, concat, join, reverse, sort, length, plus the higher-order forEach / map / filter / reduce. Nested arrays work too.
Objects & classes
Objects are struct-of-fields values, ARC-managed. Two ways to get one:
Ghost classes — an object literal infers its shape at compile time. The checker mints an anonymous class from the literal; field types come from the values. No class definition needed, and the shape is still fully static:
let abc = {
a: 5, // Int32
b: 6, // Int32
c: "Test" // string
}
console.log(abc.a) // 5
abc.b = 10 // field write
Accessing abc.d is a compile-time error. Declared classes define a named shape, instantiated with new. Fields are public by default; classes are pure data (no methods):
class Point {
public x: Float64
public y: Float64
label: string
}
let origin = new Point()
origin.x = 0.0
With a class annotation on a let, an object literal must match the class field-for-field.
Generics
Functions and classes take type parameters, monomorphized at compile time — every distinct instantiation generates its own specialized copy, so generics cost nothing at runtime.
function pick<T>(a: T, b: T, takeFirst: bool): T {
if takeFirst { return a }
return b
}
class Box<T> { value: T, label: string }
let n = pick(10, 20, true) // inferred: Int32
let box = new Box<Int32>() // explicit type argument
Interfaces
interface declares a structural data contract: any object — a ghost class (object literal) or a declared class — that has the interface's fields with matching types conforms to it, and extra fields are allowed. Conformance is checked at compile time; interfaces are erased at runtime (field access is resolved by name through the runtime, so the same code works on any conforming shape).
interface Point { x: Int32, y: Int32, name: string }
interface Named { name: string }
function describe(pt: Point): string {
return `${pt.name}: ${pt.x}, ${pt.y}`
}
let p = { x: 1, y: 2, name: "origin" } // object literal conforms
let shapes: Array<Named> = [p, q, car] // different concrete shapes
Async / await
JS-style promises. Each async function compiles to an entry function plus a resume state machine — one basic block per continuation segment. await registers the frame with the runtime; when the promise resolves, the runtime resumes it on a worker thread. The program exits when the event loop reaches quiescence.
async function fetchData() {
return new Promise((resolve) => {
setTimeout(() => { resolve("Data fetched") }, 1000)
})
}
async function main() {
let data = await fetchData()
console.log(data)
}
Control flow
if/else, while, C-style for, with ++, --, +=, -=. Loop conditions must be bool (no truthiness); the for init variable is scoped to the loop. Entry point: define main (sync or async).
Standard libraries
Bring them in with a top-level use statement (use http — the JS-style import is not supported).
| Module | What it provides |
|---|---|
net | raw TCP: net.connect(host, port) → Stream with write / read_all / close |
http | client (http.get/http.post), low-level http.listen server, and an express-style API: new http.Server, server.router, router.get/post, http.JsonValidator with chainable field validators |
fs | readFile, writeFile, exists, readDir, mkdir, remove |
crypto | pure-C++ sha256, base64Encode/Decode, randomHex, uuid |
sql | SQLite via prepared statements: new sql.Connection, sql.stmt`...` tagged templates, db.execute / db.query — values are always bound as parameters, never string-concatenated |
How a program compiles
Lex & parse → strict type check → LLVM IR codegen (with ARC retain/release insertion and async state machines) → clang links libmarina_runtime.a into a native binary. The runtime exposes a stable C ABI (marina_retain, marina_release, marina_promise_create, marina_await, …) that the generated IR calls directly.