Static copy
Copied from docs/language/tour.md in https://github.com/poppy-team/ori-lang (MIT). Pinned to revision 42e1817227fbc96e8e4a4a85fac78b3fec0a0e0f, blob 2a6b04b97c4cd087bf433e45116feea420c92ac4. The source repository remains canonical; this copy is refreshed through a sync pull request, not live.
Audience: people learning to read and write Ori
Portuguese: tour.pt-BR.md
Normative detail: ../spec/01-overview.md
Surface: S30.3.0+ inference B0.3.1· result ctorsok/err
This tour matches what the compiler accepts today. Pre-S3 forms are hard errors (ori migrate-syntax rewrites many of them).
module app.hello
import ori.io as io
main()
io.println("Hello, Ori!")
const answer: int = 21 * 2
io.println(f"The answer is {answer}")
endori run main.orl| Idea | Form |
|---|---|
| File belongs to a namespace | module app.hello first line |
| Import with short name | import ori.io as io (or = io) |
| Entry point | main() — no func keyword |
| Blocks | end with end |
| Types | explicit on bindings when public / when not inferred |
Three import forms:
import ori.fs (read_text, write_text) -- selective
import ori.string as str -- alias (`as` or `=`)
import ori.math -- whole module: only ori.math.…import ori.io does not create a local name io.ori.fs, ori.string (not ori.fs.utils in new code).import ori.fs (TextResult).| Type | Meaning |
|---|---|
int, float, bool, string, bytes | primitives / text / binary |
list[T], map[K, V], set[T] | collections |
simd[float32, 4] | fixed-width SIMD vector (see advanced types) |
optional[T] | presence or none (not null) |
result[T, E] | success ok(T) or failure err(E) |
void | no useful value (side effects) |
Composite types always use [], never <> or list of T.
alias names an existing type (transparent); newtype creates a distinct one with the same representation and no runtime cost:
alias UserMap = map[int, string] -- interchangeable with its target
newtype UserId = int -- NOT an int: UserId(7) in, int(id) outconst names: list[string] = ["ada", "grace"]
const maybe: optional[int] = none
const file: result[string, string] = err("missing")Bind several struct fields at once with a destructuring binding:
const Point { x, y } = get_pos() -- type written
const { x, y } = get_pos() -- type inferred
const Point { x: px } = get_pos() -- renamedStruct fields only — tuples are not destructured.
You may omit the type on a local const/var when the right-hand side is:
|>const n = ori.list.len(names) -- call
const first = names[0] -- index
const upper = "hi" |> str.to_upper -- pipe (if str imported)You still annotate public API and cases the checker cannot decide.
if n > 0
io.println("positive")
elif n == 0
io.println("zero")
else
io.println("negative")
end
while i < 10
i = i + 1
end
for item in names
io.println(item)
end
match value
case ok(text):
io.println(text)
case err(msg):
io.eprintln(msg)
end
match score
case n if n >= 90:
io.println("A")
case n if n >= 80:
io.println("B")
case else:
io.println("C")
end
-- `match` also works as an expression: each arm is a single value
const grade: string = match score
case n if n >= 90: "A"
case else: "C"
endelif, not else if.case Variant / case Variant(fields) — no leading ..case pattern if cond: guards an arm: a false guard falls through to the next case. case else: is the explicit fallback (guarded cases do not count toward exhaustiveness).case a or b: groups alternatives (the word or, not |). Alternatives cannot bind values, and together they count as full coverage.load(path: string) -> result[string, string]
return ori.fs.read_text(path)
end
main() -> result[void, string]
const text: string = try load("notes.txt")
io.println(text)
return ok()
end| Construct | Role |
|---|---|
ok(v) / err(e) | build a result |
some(v) / none | build an optional |
try expr | propagate err or none |
match | exhaustive handling |
if some(x) = expr | branch on presence, binding the value |
if ok(v) = expr / if err(e) = expr | branch on a result, binding either side |
if some(user) = find_user(id)
greet(user)
else
io.println("not found")
end
if ok(value) = divide(10, 2)
io.println(string(value))
end
if err(message) = divide(1, 0)
io.println(message) -- taken when the result is NOT ok
endPostfix expr? was removed in S3.
struct Point
x: int
y: int
end
enum Color
Red
Green
Rgb(r: int, g: int, b: int)
end
const p: Point = Point { x: 1, y: 2 }
const c: Color = Color.Rgb(r: 1, g: 2, b: 3)
-- update expression: a new value derived from `p`; `p` is untouched
const moved: Point = p with { x: 10 } endTraits use apply Type: Trait (or apply Type use Trait). Import the trait module first (import ori.core as core).
import ori.core as core
import ori.io as io
struct Point
x: int
y: int
end
apply Point: core.Displayable
display(self) -> string
return f"({self.x}, {self.y})"
end
end
main()
const p: Point = Point { x: 1, y: 2 }
io.println(string(p)) -- Displayable via string(value)
endMost code needs no manual memory handling. For the uncommon case where a value owns something outside Ori's managed heap, it may implement core.Destructor:
apply NativeHandle: core.Destructor
mut destroy(self)
close_external(self.id)
end
enddestroy runs automatically when the last reference dies. Its timing may be delayed by a reference cycle, so use using + core.Disposable when a file, socket, or similar resource must close at the end of a specific scope.
add(a: int, b: int) -> int
return a + b
end
greet(name: string) -> void
io.println("hi " + name)
end
-- expression body
double(n: int) -> int => n * 2(x: int) => x + 1 or (x: int) … end. The parameter type may be omitted when an annotated binding supplies it: const double: func(int) -> int = (x) => x * 2.value |> pure_fn is typed as pure_fn(value).Generators — iter + suspend:
iter counter(stop: int) -> int
var i: int = 0
while i < stop
suspend i -- hand one value to the loop, resume here next step
i = i + 1
end
end
for n in counter(4)
io.println(f"{n}") -- 0, 1, 2, 3
endThe body is inlined into the for — no allocation, no state machine. An iterator can only be consumed by a for loop (free functions, same module, no generics for now).
identity[T](value: T) -> T
return value
end
struct Pair[A, B]
first: A
second: B
endBounds use a for clause instead of a bracket list — the two never combine:
max for T: Comparable (a: T, b: T) -> T
if a.compare(b) > 0
return a
end
return b
endConst generics let a compile-time number be part of a type. The argument is named, because a bare Buffer[8] would read like the index fruits[8]:
struct Buffer[const size: int]
used: int
end
const small: Buffer[size: 8] = Buffer { used: 0 }
const large: Buffer[size: 16] = Buffer { used: 0 } -- a different typeTwo limits worth knowing: a generic trait can be declared but not applied, and higher-kinded types (a parameter standing for list/optional itself) are deliberately out of scope. See spec/11-generics.md.
my_app/
ori.proj -- required
main.orl -- recommended entry
docs/ -- optional .oridoc sidecarsori new my_app
cd my_app
ori check main.orl
ori run main.orl
ori test main.orlSee First project and spec/17-project-and-docs.md.
| Avoid | Use instead |
|---|---|
namespace | module |
func name() | name() |
import path only (…) | import path (…) / (item as alias) |
list of T / Foo<T> | list[T] / Foo[T] |
success / error | ok / err |
else if | elif |
expr? | try expr |
implement Trait for T | apply T: Trait |
ori migrate-syntax path/to/sourcesmodule app.main
import ori.io as io
import ori.task as task
async main()
await task.sleep(10)
io.println("done")
endasync main() is the async entry point; use await only inside async functions.fs.read_text_async, net.connect_async, …examples/async_demo.Native AOT/JIT supports the documented async subset; unsupported shapes are rejected explicitly.
| Goal | Doc |
|---|---|
| Install package (Linux primary) | ../install.md |
| Recipes | ../guides/cookbook.md |
| Errors mental model | ../guides/errors-null-void.md |
| Runnable samples | ../../examples/ |
| Full grammar / types | ../spec/ |
| ABI / runtime | ../spec/19-abi.md |
| Advanced types and generics | advanced.md |
| Async, tasks, and channels | concurrency.md |
| C ABI and FFI | interop.md |