App Fields, Resources & Context
An app field is a value on the runtime app value produced by fn boot: config
data, dependency handlers, the per-life Context, or whatever else the
program chooses to carry. Read one as MyApp.field; use
with MyApp.field = value to rebind it for the rest of the current block.
No globals, no service locators, no DI containers — just a typed app value
and scoped overrides.
The basic shape
Section titled “The basic shape”The smallest end-to-end example. A Logger interface, a concrete Stdout
that implements it, a MyApp carrying the logger value, and a
fn boot callback that produces the app value. From there, greet reads
MyApp.logger directly:
import {
std/app: App
std/context: Context
std/io: IO
}
interface Logger {
fn log(logger: self, msg: String): Unit
}
type Stdout
impl Logger for Stdout {
fn log(logger: Stdout, msg: String): Unit {
IO.print("[log] ${msg}")
}
}
struct MyApp {
field context: Context
field logger: Logger
}
impl App for MyApp
fn boot(): App {
MyApp{context: Context.root(), logger: Stdout}
}
fn main() {
greet("World")
}
fn greet(name: String) {
Logger.log(MyApp.logger, "hello, ${name}")
}
A lot of moving parts the first time you see it. The pieces:
interface Logger— the contract. Whatever value is bound tologgermust implement this.MyApp— your project’s concrete app struct. Itsimpl Appentry is stdlib’s signal that this is the app type; the compiler infers the field set soMyApp.fieldreads andwith MyApp.field = ...overrides are type-checked.fn boot(): App— the app-entry companion tofn main. The runtime calls it once beforefn mainto produce the app value. The returned struct gets staged as the implicit app value forfn mainand everything below it.MyApp.logger— a qualified read from the active app value. There is no bareloggerbinding; the app type stays visible at the use site.
In a multi-file package, concrete app types usually carry app-specific names
such as MyApp, AdminApp, or WorkerApp.
with rebinds for a block
Section titled “with rebinds for a block”A with MyApp.field = expr statement changes an active app field for the
rest of the current block. Callees that read MyApp.field see the rebound
value. Put the with inside an ordinary nested block when you want the
rebind to roll back before the outer block continues:
import {
std/app: App
std/context: Context
std/io: IO
}
interface Logger {
fn log(logger: self, msg: String): Unit
}
type Stdout
impl Logger for Stdout {
fn log(logger: Stdout, msg: String): Unit {
IO.print("[log] ${msg}")
}
}
struct PrefixedLogger {
field tag: String
}
impl Logger for PrefixedLogger {
fn log(logger: PrefixedLogger, msg: String): Unit {
IO.print("[${logger.tag}] ${msg}")
}
}
struct MyApp {
field context: Context
field logger: Logger
}
impl App for MyApp
fn boot(): App {
MyApp{context: Context.root(), logger: Stdout}
}
fn main() {
greet("World")
{
with MyApp.logger = PrefixedLogger{tag: "audit"}
greet("Alice")
}
greet("Bob")
}
fn greet(name: String) {
Logger.log(MyApp.logger, "hello, ${name}")
}
The with statement makes app-backed dependencies testable without test
doubles or dependency injection scaffolding — just rebind. Multiple rebinds
are just multiple sequential with statements.
resource closes block-scoped values
Section titled “resource closes block-scoped values”Use resource name = expr for values that need a guaranteed close at the
end of the current block. The value’s type implements Resource, and Nomi
calls Resource.close automatically in reverse acquisition order. A function
body is a block too; use an ordinary nested block when the resource should
close before later statements in the same function continue:
import std/io: IO
struct Conn {
field name: String
}
impl Resource for Conn {
fn close(conn: Conn): Unit {
IO.print("close ${conn.name}")
}
}
fn main() {
{
resource first = Conn{name: "first"}
resource second = Conn{name: "second"}
IO.print("body")
}
IO.print("after")
}
Acquisition failure is ordinary control flow: write
resource db = try Sqlite.temp() when setup returns Result. If the
acquisition fails, db is not registered, and any earlier resources in the
same block still close while the failure returns.
App fields follow calls
Section titled “App fields follow calls”App fields are not caller-chain obligations. A function just reads the fields
it needs through the active app type. Overrides flow down the call stack, so a
deep callee reading MyApp.port sees the current scoped value:
import {
std/app: App
std/context: Context
std/io: IO
}
interface Logger {
fn log(logger: self, msg: String): Unit
}
type Stdout
impl Logger for Stdout {
fn log(logger: Stdout, msg: String): Unit {
IO.print(msg)
}
}
struct MyApp {
field context: Context
field port: Int
field logger: Logger
}
impl App for MyApp
fn boot(): App {
MyApp{context: Context.root(), port: 8080, logger: Stdout}
}
fn main() {
show_port()
}
fn show_port() {
IO.print("listening on :${MyApp.port}")
}
show_port reads MyApp.port even though fn main does not mention
port. Callers do not repeat a callee’s app needs.
Going deeper
Section titled “Going deeper”Boot and the entry frame
Section titled “Boot and the entry frame”The runtime calls app-entry fn boot(): App once before fn main. The
returned struct (any type with an impl App entry,
typically your project’s concrete app struct) becomes the active app value.
A qualified read such as MyApp.logger resolves against that value when
MyApp is the active app type.
Scoped overrides propagate down the call chain. When a function calls another
function, the callee sees any active with MyApp.field = ... values from the
caller frame.
When there’s no user-defined fn boot, Nomi provides a default root context.
Code that wants to read app fields explicitly should define an app type and
boot.
App-field access, not effect tracking
Section titled “App-field access, not effect tracking”App-field access is deliberately not impurity tracking. A field may be plain data
(port: Int) or a dependency interface (logger: Logger); the syntax is the
same either way. The compiler checks that the field exists on the active app
type.
with: rebinding, not introduction
Section titled “with: rebinding, not introduction”with MyApp.field = expr rebinds an existing active app field for the
remainder of the current block. You can’t pull a fresh app field out of thin
air with with; the target must be a field on the active app type.
Multiple with statements are evaluated sequentially — a later rebind can
reference an earlier one in scope:
with MyApp.logger = TaggedLogger{tag: "audit"} with MyApp.clock = FakeClock{at: 12345} audit_step()
After those statements, callees reading MyApp.logger see the new
TaggedLogger; callees reading MyApp.clock see the new FakeClock. When
the enclosing block exits, the rebindings revert.
Context — the per-life execution channel
Section titled “Context — the per-life execution channel”std/context’s Context is the cancellation/deadline carrier
threaded through every Nomi program. context is a regular app field:
with MyApp.context = Context.with_timeout(MyApp.context, …) correctly
threads the rebound context to every callee reached after the statement.
The v1 stdlib surface (std/context):
pub extern type Context { // Construct the root — for use in `fn boot` only. pub extern fn root(): Context // Read state. pub extern fn canceled?(c: Context): Bool pub extern fn deadline(c: Context): Maybe<Instant> pub extern fn deadline_remaining(c: Context): Maybe<Duration> // Derive a child with a new deadline / timeout. pub extern fn with_deadline(c: Context, at: Instant): Context pub extern fn with_timeout(c: Context, dur: Duration): Context // Attach and retrieve typed contextual values. pub extern fn with_value<'T>(c: Context, value: 'T): Context pub extern fn value<'T>(c: Context, value_type: Type<'T>): Maybe<'T> }
with_deadline is the primitive; with_timeout is sugar for
Context.with_deadline(c, Instant.add(Instant.now(), dur)). Every derived
Context inherits cancellation from its parent — derive a child for
a sub-task and the parent’s cancellation cancels every descendant.
User code can’t construct Context from scratch — only Context.root(),
the with_* derivers, and the runtime mint Context values. This
is what makes the cancellation tree well-formed.
Cascading cancellation across concurrent blocks
Section titled “Cascading cancellation across concurrent blocks”Concurrency layer 1’s structured-concurrency rules (concurrent { async … }) compose with Context: each concurrent block derives
its own Context from the active app value context, every async-spawned task
inherits it, and cancellation-aware ops (Timer.sleep, Channel.send /
receive, await) select on the underlying Go-side
done channel. Put
with MyApp.context = Context.with_timeout(MyApp.context, dur) in an
ordinary block before concurrent { … } to give every task in the block a
deadline — and try await(failing_task)
short-circuits the block, cancelling every sibling task at the
same point.
Embedder hooks (Go side)
Section titled “Embedder hooks (Go side)”A Go host embedding the Nomi runtime constructs the root Context itself and injects it:
deadline := time.Now().Add(30 * time.Second)
root := runtime.NewRootContext(&deadline)
rt := runtime.New(runtime.WithOutput(os.Stdout))
rt.LoadSource("main", src)
rt.RunWithOptions(runtime.RunOptions{RootContext: root})
For individual reentrant Calls into Nomi code from Go,
runtime.CallWithContext(root, fn, args…) threads a Go-side Context
into the call frame. Context is opaque at the FFI boundary —
rt.Marshal / rt.Unmarshal reject it, so it never leaks across
host/guest in a way that would let either side construct one
illegitimately.