.grl

A systems language with no garbage collector
Documentation v0.1 · Built 2026-09-03

Getting Started

What is .grl?

.grl is a systems programming language with no garbage collector. It uses regions with owners, generational handles, actors, and capabilities. It runs hosted on Windows, Linux, and macOS from v0.1; Sografe-native later.

Install = copy. One folder, zero installers. Copy it wherever you want, add it to your PATH, and you're done.

Quick start

# Linux/macOS
cp -r grl-sdk-*/  ~/grl/
export PATH="$HOME/grl:$PATH"

# Windows
:: copy the folder somewhere, add to PATH in System Settings

Verify:

grl --version

Your first program:

print("hello, grl")

Save as hello.grl, then:

grl run hello.grl

Commands

Command What it does
grl run <file.grl> Compiles and executes (JIT)
grl check <file.grl> Parse + typecheck — prints "ok" or first error
grl build <file.grl> Parse + check + lower — writes <file>.grl.ir
grl fmt <file.grl> Formatter (2 spaces, = alignment, idempotent)
grl lsp Language Server (stdin/stdout, for editor integration)
grl --version Version and toolchain info

Editor support

grl lsp speaks the standard Language Server Protocol over stdio. Any LSP-capable editor (VS Code, Neovim, Helix, etc.) gets:

VS Code extension configuration (.vscode/settings.json):

{
  "grl.languageServer.path": "/path/to/grl",
  "[grl]": {
    "editor.formatOnSave": true,
    "editor.defaultFormatter": "grl"
  }
}

What's in the SDK folder

grl                  # the compiler binary (or grl.exe on Windows)
lib/                 # stdlib source (empty in v0.1 — stdlib is builtin)
docs/                # language documentation (you are here)
smoke.grl            # quick smoke test
smoke-divzero.grl    # division-by-zero test (should error, not crash)
run-smoke.sh         # runs the smoke test

What works in v0.1

What's coming

Language Guide

This guide walks through .grl's syntax and semantics with examples. It is not a formal specification — the EBNF (docs/EBNF.md) is the conformance contract. This is the readable tour.

1. Hello world

print("hello, grl")

That's a complete program. No main, no imports, no boilerplate. The file-level expressions run top to bottom.

2. Functions

# single expression — no block needed
sum(a, b) = a + b

# block body — last expression is the return value
total(values) =
  t = 0
  for v in values:
    t += v
  t

# with return type annotation
dist(a: Point, b: Point) -> Float =
  dx = a.x - b.x
  dy = a.y - b.y
  math.sqrt(dx*dx + dy*dy)

# early return
open(path) -> Res[Str] =
  if not fs.exists(path):
    return err("not found: " + path)
  ok(fs.read(path))

Rules: - = opens a definition body; : opens a control-flow body. - No end, no {}. Indentation defines blocks (like Python). - Last expression in a block is the return value. - return exits early. - One-line bodies are forbiddenif cond: stmt does not parse.

3. Variables and types

# declaration by first use
x = 5                    # Int (64-bit)
y: Int = 5              # explicit type annotation
name = "grl"            # Str
flag = true             # Bool
items = [1, 2, 3]       # List[Int]
pi = 3.14               # Float (64-bit)
mask = 0b0000_1111      # Int, binary literal
port = 0xFF             # Int, hex literal
count = 1_000_000       # Int, underscore separator

Types are inferred. An annotation is optional and acts as a compile-time assertion — if the inferred type doesn't match, it's a compile error.

Integer suffixes

a = 5u8     # 8-bit unsigned
b = 3u16    # 16-bit unsigned
c = 10u32   # 32-bit unsigned
d = 100u64  # 64-bit unsigned

Without a suffix, integer literals are Int (64-bit signed). Float is always 64-bit — there is no float suffix.

4. Naming conventions (enforced)

Naming is a compile error, not a style warning:

Convention Used for Example
PascalCase Types Point, FrameBuffer
snake_case Functions, variables, parameters read_all, total_count
UPPER_SNAKE Constants MAX_SIZE, BUFFER_LEN

Wrong casing = compile error. The formatter won't fix it — you fix it.

5. Control flow

if / else if / else

api(req: http.Request) -> http.Response =
  if req.path == "/photos":
    http.response(200, json.write(photos))
  else if req.path == "/health":
    http.response(200, "ok")
  else:
    http.response(404, "nothing")

An if with else can be the last expression in a block — its value is the taken branch's value. An if without else is a statement only.

for

# simple iteration
for v in values:
  print(v)

# flat destructuring
for k, v in m.items():
  print(k, v)

while

counter(rx) =
  n = 0
  while true:
    msg = rx.recv()
    if msg.is_err():
      break
    n += msg.val()
    print(n)

break and continue work as expected.

6. Operators

Arithmetic

Operator Description
+ Add (Int/Float) or concatenate (Str)
- Subtract
* Multiply
/ Divide
% Remainder (Int/UInt only)

Integer division or remainder by zero produces err(DivByZero) — a recoverable error value, never a crash, never undefined behaviour.

Bitwise (always bits, never logic)

Operator Description
& AND
\| OR
^ XOR
~ NOT (unary)
<< Left shift
>> Right shift (arithmetic on Int, logical on UInt)

& | ^ ~ << >> are always bitwise over integers. and or not are always logic over Bool. There is no implicit truthiness — Bool & Bool is a compile error.

Precedence (tightest last)

or < and < not < comparison < bitwise < additive < multiplicative < unary < postfix

Bitwise sits between comparison and additive: x & 1 == 0 parses as (x & 1) == 0.

Comparison

Operator Description
== != Equality
< > <= >= Ordering
is Identity

Comparison is non-associative: a < b < c does not parse.

7. Strings and interpolation

name = "grl"
greeting = "hello, {name}"
print(greeting)                    # hello, grl

# escape sequences
print("tab\there")
print("quote: \"")
print("unicode: \u{00E9}")         # é

# literal braces
print("use {{ and }} for braces")  # use { and } for braces

String interpolation is lexical — the lexer finds the matching } at the correct nesting depth. Nested string literals inside interpolation are not allowed in v0.1.

+ on strings concatenates (allocates). In @freestanding code, string literals exist but concatenation does not (no heap).

8. Errors: Res and Opt

.grl has zero exceptions. Errors are values.

Res[T] — result or error

content = open("a.txt")?            # propagate error to caller
print(content)

r = open("b.txt")
if r.is_err():
  print(r.reason())
else:
  print(r.value_or(""))

? after any expression propagates the error: if the value is err(...), the function returns immediately with that error. If it's ok(...), the value is unwrapped and the expression continues.

? is not allowed inside block lambdas in v0.1 — write a named function.

Opt[T] — optional value

first = items.first()               # Opt[Int]
if first.is_some():
  print(first.value_or(0))

Built-in error helpers

Method Description
.is_err() True if Res is an error
.is_ok() True if Res is ok
.reason() Error message (Str)
.value_or(default) Unwrap with fallback
.is_some() True if Opt has a value
.val() Unwrap Opt value

9. Lambdas

# expression lambda — three parameter shapes
doubles = nums.map(x => x * 2)
evens   = nums.filter(x => x % 2 == 0)
total   = nums.reduce(0, (acc, x) => acc + x)

# block lambda — body indented, ")" at opener column
names = photos.map(f =>
  f.title + " (" + f.year.text() + ")"
)

# zero-param lambda
gui.Button("Next", () => next())

Lambdas capture by move, and only by move (BINDING B32). Once a lambda captures a variable, that variable is consumed — it cannot be used again.

? and : (if) are not allowed inside block lambda bodies in v0.1. Write a named function instead.

10. Pipelines (UFCS)

Any function f(a, b) can be called as a.f(b) — uniform function call syntax. This enables pipelines without special syntax:

photos
  .filter(f => f.year == 2026)
  .sort((a, b) => a.date - b.date)
  .take(20)
  .each(f => print(f.name))

A line starting with . continues the expression above (DOT_LEAD). A trailing binary operator also suppresses the newline:

total = prices.net() +
        taxes.compute()

11. Types (structs)

type Point =
  x: Float
  y: Float

p = Point(3, 4)
print(p.dist(Point(0, 0)))

Types are constructed by calling the type name as a function. Fields are accessed with .. Methods are defined as regular functions and called via UFCS.

Attributes

## reads a file and returns its contents
read_all(path) -> Res[Str] =
  fs.read(path)

type HandoffInfo @layout(packed) =
  magic: UInt32
  fb_base: UInt64 @align(8)
  fb_line: UInt32

## is a doc-comment — it attaches to the next function or type definition. @layout(packed) and @align(8) are layout attributes.

12. Channels and actors

counter(rx) =
  n = 0
  while true:
    msg = rx.recv()
    if msg.is_err():
      break
    n += msg.val()
    print(n)

tx, rx = chan(Int)
spawn counter(rx)
tx.send(3)?

chan(T) returns a (tx, rx) pair — a sender and receiver. Both are move-only (linear): once moved, they cannot be used again. spawn starts a new actor. tx.send(v)? sends a value.

13. @freestanding

@freestanding
@entry
boot(info: HandoffInfo) =
  fb = FrameBuffer(info.fb)
  fb.write("Sografe")
  while true:
    pause()

@freestanding marks code that will run without an OS — no heap, no stdlib, no external symbols. The compiler rejects any use of stdlib functions, allocation, or string concatenation in freestanding files.

14. Import

import net.http
import net.tcp

Imports are dotted paths. Multiple consecutive imports form a single block in the formatter.

15. What's deliberately missing

These are not in v0.1 by design, not oversights:

Quick Reference

Keywords (15 active + 5 reserved)

Active

Keyword Usage
if else while for in Control flow
return break continue Early exit / loop control
and or not Logic (Bool only, short-circuit)
import Module import
type Type definition
spawn Start actor
is Identity comparison

Reserved (no meaning yet, cannot be used as identifiers)

par  region  tensor  effect  async

Not keywords (predeclared in prelude)

true, false, none — these are identifiers, not keywords (Go model).

Operators

Arithmetic

Operator Types Notes
+ Int, Float, Str Str = concatenate (hosted only)
- Int, Float
* Int, Float
/ Int, Float, UInt DivByZero → err(DivByZero)
% Int, UInt only Sign follows dividend

Bitwise

Operator Types Notes
& Int, UInt AND
\| Int, UInt OR
^ Int, UInt XOR
~ Int, UInt (unary) NOT
<< Int, UInt Left shift
>> Int, UInt Right shift (arithmetic on Int, logical on UInt)

Logic

Operator Types Notes
and Bool Short-circuit, always Bool
or Bool Short-circuit, always Bool
not Bool (unary)

Comparison (non-associative)

Operator Notes
== != Equality
< > <= >= Ordering
is Identity

Other symbols

Symbol Meaning
= Assignment / opens definition body
: Opens control-flow body / type annotation
-> Return type
=> Lambda
? Error propagation
. Field access / method call (UFCS)
@ Attribute
## Doc comment
# Line comment
+= -= *= /= Compound assignment

Precedence (loosest to tightest)

or → and → not → comparison → bitwise → additive → multiplicative → unary → postfix

Literals

Literal Type Example
Integer (no suffix) Int (64-bit signed) 42, 1_000_000
Integer with suffix UInt of that width 5u8, 3u16, 10u32, 100u64
Hex Int 0xFF, 0xDE_AD
Binary Int 0b1010, 0b0000_1111
Float (always 64-bit) Float 3.14, 1_000.5
String Str "hello", "value: {x}"

String escape sequences

Escape Meaning
\n \t \r Newline, tab, carriage return
\" \\ Quote, backslash
\0 Null
\xHH Byte (2 hex digits)
\u{HHHH} Unicode codepoint (exactly 4 hex digits)
{{ }} Literal { and }

Built-in types

Type Description
Int 64-bit signed integer
UInt 64-bit unsigned integer (or u8/u16/u32/u64)
Float 64-bit float
Bool Boolean (true / false)
Str UTF-8 string
List[T] Ordered list
Opt[T] Optional value (some(v) / none)
Res[Str] Result or error (ok(v) / err(msg))
Map[K, V] Key-value map
Set[T] Set

Built-in methods (UFCS)

List

Method Signature Notes
.at(i) -> Opt[T] Element access (no indexing [])
.first() -> Opt[T] First element
.find(k) -> Opt[T] Find by key
.set(i, v) Write element
.map(f) -> List[U] Transform
.filter(f) -> List[T] Filter by predicate
.reduce(init, f) -> U Fold
.sort(f) -> List[T] Sort by comparator
.take(n) -> List[T] First n elements
.each(f) Apply to each

Str

Method Description
.text() Convert to text representation
.count(sub) Count occurrences

Res / Opt

Method Description
.is_ok() / .is_err() Check Res state
.is_some() / .is_none() Check Opt state
.reason() Error message (Str)
.value_or(default) Unwrap with fallback
.val() Unwrap Opt value

Channel

Method Description
chan(T) Create (tx, rx) pair
tx.send(v) Send value
rx.recv() Receive (blocks)

Attributes

Attribute Applies to Description
@freestanding File No heap, no stdlib, no external symbols
@entry Function Entry point (freestanding)
@layout(packed) Type Packed memory layout
@align(n) Field Alignment in bytes

CLI commands

Command Description
grl run <file> Compile + execute (JIT)
grl check <file> Parse + typecheck
grl build <file> Compile to IR (writes <file>.grl.ir)
grl fmt <file> Format source (idempotent)
grl lsp Language Server (stdio)
grl --version Version + toolchain

Formatter rules

EBNF (Formal Grammar)

The formal grammar specification is in docs/EBNF.md (660 lines). It is the normative conformance contract for the parser — all 32 numbered examples in §4 are the parser's permanent test suite.

See EBNF.md in this folder.