22.6 The REPL
hanki repl is the bytecode tier with a prompt in front of it. It is the same evaluator hanki run uses, not a smaller dynamic one beside it, so a line typed at the prompt is type-checked and effect-checked exactly as a line in a file is and reports the same coded diagnostics against a <repl> span:
> def bad(a: int) -> string
a
end
<repl>:2:3: error[H0201]: type mismatch: expected string, found int
What a line may be. A top-level item - def, actor, type, struct, trait, impl, use/open - goes through the type checker and is acknowledged (defined: add). Anything else is an expression, evaluated and printed as = VALUE. Both may span several lines; the prompt waits for the construct to close.
A binding is an expression (x = f()), including of an action's result. A binding that carries a type annotation (x: int = f!()) is currently checked as a pure def and so cannot call an action - a known limitation, not the intended rule.
The session is live, not replayed. Bindings and the session heap persist from one prompt to the next, and an actor spawned at the prompt keeps running under a resident scheduler, so a later line sends it a message and gets an answer back:
> actor Counter
state n: i32 = 0i32
on increment() -> ()
n += 1i32
end
on get() -> i32
n
end
end
defined: Counter
> c = spawn Counter
> c.increment()
> c.get()
= 1
sys.actors!() (§6) reports that scheduler's roster from the prompt, the session itself included.
Replacing a definition takes a command. Retyping a top-level name is a redefinition and is rejected (H0515: 'f' is already defined) rather than silently shadowing the earlier one - a session where the same name means different things at different depths is the thing a typed REPL is trying not to be. Two commands cover the cases: :reload PATH re-reads a file and replaces the session's same-name declarations with that file's, which is the edit-and-reload loop; :reset clears the session outright.
A loaded module hot-reloads here too, and it is not :reload. The two are unrelated despite the names. :reload PATH re-reads a file into the session, replacing same-name declarations, and concerns the session's own source. module.reload! (§14, §15) swaps a loaded module's implementation for freshly compiled code, and works at a prompt as it does anywhere:
> open module
> trait Greeter
def greet(self, name: string) -> string
end
> m: Module<Greeter> = module.load!("greeter.hk")
> m.greet("before")
= "v1 before"
greeter.hk is edited between these two prompts
> m2: Module<Greeter> = module.reload!(m, "greeter.hk")
> m2.greet("after")
= "v2 after"
module.unload!(m2) ends it. The annotation is not decoration: it is what pins the trait, and a Module<T> has no other way to receive it.
Commands. :help lists them; the rest of the input is Hanki.
| Command | What it does | |
|---|---|---|
:type EXPR, :t | the type of EXPR, without evaluating it | |
:show NAME, :s | the source of a defined item | |
:load PATH, :l | read PATH as if its contents were typed | |
:reload PATH, :r | re-read PATH, replacing same-name declarations | |
:doc TARGET, help(TARGET) | documentation for a name, module, type, or method | |
:stdlib | the embedded stdlib modules, grouped by tier | |
| `:hints on\ | off` | inline type hints (§22, analyzer-backed) |
:reset | clear all session state | |
:quit, :q | leave (Ctrl-D also) |
The per-keystroke surface - type-aware completion, inline ghost hints, and what help resolves - is described under The REPL is analyzer-backed in §22.
The prompt is held to the same parity requirement as the compiler. crates/hanki-cli/tests/repl_parity.rs is the third tier of the requirement stated in §22: it feeds a corpus program's items to a real hanki repl subprocess, invokes its entry point, and requires the output hanki run produces. A program that behaves differently at the prompt is a failing test, not a REPL quirk.