(rontolisp) docs
← Functions

rontolisp:wit-export

(rontolisp:wit-export "world.wit" :world name)

Declares that the program implements a WIT world. The world's export items are checked against the program's top-level defuns at compile time and lowered into the rontolisp:wasm-export directives they stand for, so the boundary types are never written by hand and the .wit file and the compiled component cannot drift apart. The WIT is the single source of truth: the world is the program's export list (a hand-written rontolisp:wasm-export alongside it is an error), and the emitted component is byte-identical to the one those hand-written directives would have produced. It is a compile-time directive, not an ordinary function: on the interpreter and JVM backends it runs the same contract check and then returns nil, so the same source runs on every backend. See Implementing a WIT World for the full guide.

Because the directive reads a .wit file from disk, the example is shown statically:

// wit/greeter.wit
package example:greeter;

world greeter {
  /// Greet someone by name.
  export greet: func(who: string) -> string;
}
;;; greet.lisp -- the directive comes last: on the interpreter it sees only the
;;; functions defined so far.
(defun greet (who)
  (concatenate 'string "Hello, " who "!"))

(rontolisp:wit-export "wit/greeter.wit" :world greeter)
rontolisp greet.lisp --component -o greet.wasm
wasmtime run -W gc=y --invoke 'greet("world")' greet.wasm
# "Hello, world!"

Arguments

  • The WIT file path, as a string. A relative path resolves against the directory of the source file that names it, like load.
  • :world — the world to implement, as a bare symbol (spelled the way WIT spells it) or a string. It may be omitted when the file declares exactly one world; when the file declares several, one must be named.

Everything else comes from the world: rontolisp:wasm-export's :params, :param-names, :returns and :async are all filled in from it, so the defuns carry no boundary types at all.

Supported WIT types

WIT typeBoundary typeLisp value
s8 s16 s32:s8 :s16 :s32an integer
u8 u16 u32:u8 :u16 :u32an integer
s64 u64:s64 :u64an integer; a u64 value of 2^63 or more traps at the boundary
f64:floata float
bool:boolt or nil
string:stringa string
(no result):voidthe function's value is discarded

The whole fixed-width integer family crosses, so the canonical component-model tutorial world compiles unedited:

// wit/adder.wit
package docs:adder@0.1.0;

interface add {
  add: func(x: u32, y: u32) -> u32;
}

world adder {
  export add;
}

Each type carries its own range exactly, or the call traps — a negative returned through u32 is refused rather than delivered as 4294967295. The component model has no integer subtyping, so this is not cosmetic: a component that lifted u32 as s32 would be rejected against its own world by wasm-tools component targets, by jco, and by any bindgen-based host.

An async func in the world lifts the export with :async t, so blocking is always legal inside it: I/O inside a sync export usually works too (the asynchronous built-ins complete without blocking when the host accepts immediately), but a host that reports BLOCKED would make it trap, and the async lift removes that residual risk — the WIT states which exports are async rather than leaving it to be guessed. Every other WIT type (record, list, option, result, resources, ...) is a compile error at the export boundary today; the error names the rontolisp representation the type is settled to have, once marshalling it lands.

What it checks

Every violation is a compile error naming the WIT file and the line of the offending export:

  • an export the world declares with no matching defunwit/greeter.wit:5: export 'greet' has no matching (defun greet ...) in the program
  • an arity mismatch — wit/greeter.wit:5: export 'greet' declares 1 parameter(s), but (defun greet ...) takes 2 (an exported function takes required parameters only: &optional / &rest / &key are rejected)
  • a WIT type the export boundary does not carry (the whole fixed-width integer family plus f64 / bool / string crosses; a record, list, ... does not yet)
  • an async func under --no-gc --component, whose adapter-free reactor has no async machinery
  • an export name that is not a component-model label (lower-kebab-case words), a duplicate export, or the reserved name run (the component's wasi:cli/run entry point)
  • a world with no exports, a :world the file does not declare, or an omitted :world when the file declares several

Because the world is the export list, so is mixing it with the hand-written form: a rontolisp:wasm-export in a program that also has a rontolisp:wit-export, and a rontolisp:http-handler together with a world (a serve-mode component exports only wasi:http/handler@0.3.0).

Limitations

  • Only the world's export side is a contract. import items are ignored (a component's WASI imports come from the build, not from the world), and an inline import name: func(...) is rejected rather than silently dropped — the functions a program calls are bound from an interface with rontolisp:wit-import, or declared by hand with rontolisp:wasm-import (both Preview 1 only). The component you get therefore has a much larger type than the world you wrote: the 6-line world above compiles to a 149-line component type (ten wasi:* imports plus export wasi:cli/run), and calling rontolisp:fetch inside greet silently adds five more. --emit-wit is how you see it.
  • A world exports freestanding functions or an interface defined in the same file: export add; referencing an in-file interface add { ... }, or an inline export ops: interface { ... }, is implemented member by member and produces a real docs:adder/add instance export (see Exporting an interface). An export naming an interface the file does not define (a bare wasi:* reference) is still an error.
  • :s-expr has no WIT spelling, so an export carrying an arbitrary s-expression still needs a hand-written rontolisp:wasm-export — and therefore a program without a world.
  • On the interpreter the directive is an ordinary form evaluated in order, so it sees only the functions defined so far: put it at the end of the file. (The compile path collects every top-level defun first, so there the position does not matter.)
  • Adding --emit-wit writes the component's real type back out, and its export lines reproduce the world handed in, parameter names included — but by construction, not by coincidence: the world is what those lines are derived from, so they cannot disagree with it. Emitting is worth it for the import side, not as a check on your program (that is wit-export's own job, on every backend). Two deliberate differences from the input file: the /// doc comments are gone (a component's type does not store them), and the emitted world is always package root:component; world root.