Skip to main content

REPL

nebra repl is an interactive prompt that compiles each input on the fly and runs it in a persistent Lua state. Use it for scratch experiments, exploring a new library, debugging a tricky expression, or learning the language.

$ nebra repl
Nebra REPL 0.X.Y (target Lua54). Type :help for commands, :quit to exit.
Note: top-level `local` declarations don't persist between inputs - use `name = ...` for globals.
nebra> 2 + 2
4
nebra> function greet(n: string): string
...> return "hi " .. n
...> end
nebra> greet("world")
hi world

How it works

Each non-blank line (or multi-line chunk) goes through:

  1. Token-balance check - the lexer counts function/do/if/for/while/match/class/interface/enum/module/repeat against end/until, plus ()/[]/{}. If the totals don't balance, the REPL prompts again (...>) instead of evaluating.
  2. Dual compile attempt -
    1. First, the input is wrapped as return <input> and run through the full compiler. If it compiles, the chunk is run as an expression and its return value is printed (unless it's nil).
    2. If the expression form fails to parse, the raw input is compiled as a statement list and run. Statement output (e.g. print calls) flows through.
  3. Eval - the resulting Lua chunk is executed via RunChunk on the persistent NebraRuntime. Globals (and function foo() ... end-style declarations) persist across inputs.

So a bare 2 + 2 prints 4 (expression path), and function greet(n) ... end defines a Lua global that's reachable from the next input (statement path).


What persists, what doesn't

Lua semantics decide this, not the REPL:

FormPersists between inputs?
local x = 5❌ chunk-scoped local
x = 5 (no local)✅ global
function foo() … end✅ global function
local function foo() … end❌ chunk-scoped
class Foo … end✅ (codegen emits a global table)
enum E { A, B }
interface I … end✅ (compile-time only, no runtime artifact)
import { x } from "..."❌ creates a chunk-local; re-import per input

If you want a value to survive, drop the local. The REPL's header reminds you of this.

In strict-mode projects, x = 5 would normally fail a static check (undeclared symbol). Inside the REPL, the compiler flips to REPL mode: undeclared names are auto-treated as any-typed globals so you can iterate without fighting the type checker. The Lua runtime then resolves them naturally via globals.


REPL commands

Lines that start with : are interpreted by the REPL, not the compiler:

CommandEffect
:help, :hShow inline help with commands and tips
:quit, :qExit the REPL (also Ctrl+D / EOF on stdin)
:clearClear the screen
:resetDrop all globals; create a fresh NebraRuntime
:load <path>Read a .neb file and evaluate it in this session

:load is the quickest way to bring a bunch of definitions into the session: write them in a file, then iterate at the prompt. Example:

nebra> :load examples/scratch.neb
loaded
nebra> Vec2(3, 4):length()
5.0

Multi-line input

The token-counter detects unbalanced openers and keeps reading until they balance. Continuation lines use a different prompt (...>) so it's obvious you're still in the same chunk.

nebra> if x > 0 then
...> print("positive")
...> else
...> print("non-positive")
...> end
positive

To abort a multi-line input, press Ctrl+C (the buffer is dropped; you can also type :reset to clear it explicitly).


Tips

  • Print intermediate values by typing the variable name: nebra> x runs return x and prints the result.
  • Define a function once, iterate at call sites: write the function with function foo() … end (global), then bounce values at it.
  • Skip the runtime cost of repeated compilation by collecting definitions in a .neb file and using :load. The REPL re-compiles each input from scratch (no incremental cache yet) - about 50-200ms per input depending on project size.
  • Don't expect inline import { x } to survive: imports use local, which doesn't persist. Either :load a file that does the imports + uses them, or call require("mod").x directly.
  • CI smoke: pipe a heredoc into nebra repl for a quick "does this still work" check:
    printf '2 + 2\n:quit\n' | nebra repl

Known limitations (v1)

  • No history or line editing. Console.ReadLine is plain line-based input - arrow keys are passed verbatim.
  • No tab completion.
  • Each input goes through the full compiler pipeline. Acceptable for interactive work, but you'll feel the latency on a slow machine with a large project.
  • Multi-line input cannot be aborted with a single keystroke other than Ctrl+C (which terminates the process). A planned :abort command will clear just the buffer.

These rough edges are tracked in compiler/TODO.md - contributions welcome.