basic — Luau Binding

The require("basic") API: read and edit the AntBASIC program, run it, inspect its variables, save/load in either form, and spawn parallel BASIC contexts. Every function here is [antos] — the whole library is an AntOS addition with no standard-Lua equivalent.

Status: design. Not built yet. Signatures below are the intended surface, settled against the AntBASIC model; expect refinement during implementation. The library reference / native core is on antos_basic.

local basic = require("basic")

All calls act on the global main space unless given a context handle (see Contexts). All are [antos].


Program store

The tokenised line list — what LIST, RENUMBER, DELETE and an editor work over. Luau sees text by default; raw tokens are available via tokens.

Call Returns Does
basic.tokenise(text) tokens tokenise a source line (no store change) — resolves p.-style abbreviations too, so the console and scripts get shorthand for free
basic.check(text) status, err validate a line without storing it. status is "ok", "incomplete" (unfinished but not yet wrong — an open string, a half-typed statement), or "invalid"; for "invalid", err is { col, message, line? } with a character col for cursor placement. See validating a line
basic.detokenise(tokens, style?) text source from a token stream. style is "canonical" (default, full keywords — what LIST shows), "tight" (shortest p.-style abbreviations + optional spaces dropped — the crunch rendering), or "indented" (canonical + block depth)
basic.insert(line, text) ok, err tokenise text and store it as program line line (replacing any existing)
basic.remove(line) ok delete a program line (a bare line number at the console)
basic.get(line) text? the source of one line, or nil
basic.tokens(line) tokens? the raw token bytes of one line — for tracers, editors, highlighters
basic.lines(style?) iterator yields { line = n, text = "…" } in line-number order; style renders the text as in basic.detokenise ("canonical" / "tight" / "indented")
basic.line_count() number how many program lines
basic.clear() — empty the program (NEW)
-- renumber.lua  (renumber [start] [step])   — pure script over the library
local basic = require("basic")
local a, done = os.arguments{ name = "renumber",
    { "+Start", type = "number", default = 10 },
    { "+Step",  type = "number", default = 10 } }
if done then return end

local snapshot = {}                       -- read old lines first
for _, l in basic.lines() do snapshot[#snapshot+1] = l end

basic.clear()
local n = a.start
for _, l in ipairs(snapshot) do
    basic.insert(n, l.text)               -- (a real renumber also fixes GOTO/GOSUB targets)
    n += a.step
end

Renumbering that rewrites GOTO/GOSUB/THEN line targets is the script's job, not the engine's — walk basic.tokens(line) to find line-number references. This is exactly the kind of policy AntBASIC keeps out of the core.

A one-line style change turns the same iterator into a cruncher — writing the most compact .bas the program can be typed as (crunched listing), a nice demonstration of the "tight" rendering:

-- crunch.lua   (crunch <file.bas>)   — save the program in its shortest typed form
local basic = require("basic")
local a, done = os.arguments{ name = "crunch", { "*File", required = true } }
if done then return end

local f = io.open(a.file, "w")
for _, l in basic.lines("tight") do          -- shortest abbreviations, spaces dropped
    f:write(`{l.line} {l.text}\n`)
end
f:close()
-- re-tokenises to the identical program; the stored tokens were never touched

Validating a line

basic.check(text) parses a line the way the tokeniser does but stores nothing, so the console's entry handler, an editor, a linter, or a paste-importer all validate through one path (see AntBASIC → errors caught at entry).

It returns a three-way status, which is what lets live syntax colouring stay calm rather than flashing red on every half-typed line:

local status, err = basic.check(line)
if status == "invalid" then
    -- err = { col = 12, message = "unterminated string", line = <n>? }
    print(`? {err.message} at column {err.col}`)   -- reason + place the cursor at err.col
elseif status == "incomplete" then
    -- unfinished but not yet wrong (open string, half-typed statement) — render pending, not red
end

col is a character column (1-based) — exactly what positions the cursor on the offending character, and the same column where live colouring turns red. err.line is present only when the text carried its own line number (a program line, or a line from a bulk load). The check is line-local: it catches lexical and single-statement errors, not cross-line ones like NEXT without FOR, which surface at RUN.


Syntax spans

basic.spans(text) returns the colour projection of a line — a list of { col, len, class } spans (1-based col) — so one call feeds both LIST colouring and the live per-keystroke editor. class is "keyword", "variable", "string", "number", "comment", "operator", "label", … — or "error" for the trailing span from the first un-parseable column to end of line when the line is "invalid". See AntBASIC → syntax colouring.

-- colour one line: the editor maps class -> theme colour and applies terminal attributes
for _, sp in basic.spans(line) do
    term.colour(theme[sp.class] or theme.default)
    term.write(line:sub(sp.col, sp.col + sp.len - 1))
end

The engine emits spans; the terminal applies the attributes, and the class→colour map is a Settings theme — so colouring turns off cleanly on a mono console with the listing still readable.


Keyword help

The grammar entry behind each keyword is queryable, so an editor hint, a completion popup, and the HELP command all read the one source the checker validates against.

Call Returns Does
basic.help(name) { template, brief, abbrev }? the syntax entry for a keyword or command — template is the grammar rendered ("FOR var = start TO end [STEP n] …"), brief the one-line description, abbrev the shortest form ("f."). Resolves abbreviations and folds case, so basic.help("p.") → PRINT; nil if unknown
basic.keywords() iterator yields { name, abbrev } for every keyword in token-table order — the set bare HELP lists, and a completion source
basic.suggest(text, col, ctx?) { kind, items, insert_at }? valid completions at column col: kind is "token" (one mandatory keyword/literal — insert it), "menu" (an ALT — arrow through items), or "names" (variables / CONSTs / PROC / FN in scope); insert_at is the column to replace from. Reads the same grammar basic.check walks; the optional context handle lets a paused program complete its own names. nil when nothing is expected
-- help.lua   (help [name])   — the HELP command, over keywords and commands
local basic = require("basic")
local a, done = os.arguments{ name = "help", { "Name" } }
if done then return end
if a.name then
    local h = basic.help(a.name)
    if h then print(`{h.template}   {h.brief}   ({h.abbrev})`)
    else print(`? no help for {a.name}`) end
else
    for k in basic.keywords() do print(k.name) end       -- commands listed too
end

Because basic.help renders the same grammar basic.check validates against, the help can never drift from what the interpreter accepts.


Execution

Call Returns Does
basic.run(from?) ok, err run the program (optionally from a line number)
basic.stop() — halt a running program (as STOP/break)
basic.cont() ok, err resume after a stop (CONT)
basic.eval(text) value?, err run a single statement/expression on demand in the live context — the primitive behind a leading : at the prompt (:PRINT a), and callable by any tool or command script; bare, un-prefixed input is an AntOS command, not BASIC
basic.lua_eval(src, ctx?) value, err evaluate a Luau string in the context's persistent Luau env (lazily created), the inverse of this library — backs BASIC's LUA(). Compiled chunks are cached by string; a Luau error returns as err. Values marshal Luau↔BASIC types; passing is explicit (no auto variable bridge — Luau reaches back via require("basic")). Runs at full script capability
basic.asm(src, type?, address?) len, err assemble a string at run time — type is "arm" (default), "ee" or "ezx"; address is where to write (default: current P%, then advanced). Returns bytes assembled; a bad source returns err (message + column). Backs BASIC's ASM() and the deferred-block path; FPGA targets are sent to FPGA memory
basic.state() state, line "ready" / "running" / "stopped", plus the current line
-- run.lua
local basic = require("basic")
local ok, err = basic.run()
if not ok then print(`? {err}`) end

Variables

Inspect and poke the running / last-run environment — the basis of a vars command or a debugger.

Call Returns Does
basic.vars() { [name] = value } all variables of the space
basic.get_var(name) value? one variable (case-insensitive; $/% suffix respected)
basic.set_var(name, v) ok, err set one variable
-- let a Luau script read a BASIC result
basic.eval("LET RESULT = 6 * 7")
print(basic.get_var("result"))            --> 42

Storage — SAVE / CSAVE / LOAD

Both forms round-trip through DBFS and are auto-catalogued; see AntBASIC → storage.

Call Returns Does
basic.save(path) ok, err write detokenised text (.bas source) — the SAVE verb
basic.csave(path, protect?) ok, err write the tokenised image, stamped with the current token-table version — the CSAVE verb. A .bax path, or protect = true, writes a protected image (name table + REM text stripped; stripped REMs kept as empty placeholder lines so jump targets survive). protect = true forces the .bax extension; a .bas path with protect = true is renamed to .bax so the name matches the content. Runnable but not listable — basic.lines / basic.detokenise on a protected program return an error, not synthetic text
basic.load(path) ok, err auto-detect: tokenised image loaded verbatim (old versions detokenised via the retained table), text tokenised on entry — the LOAD verb. A tokenised image with no name table is recognised as protected, loaded, and runs; a later basic.csave keeps it protected
basic.include(path, name?) handle, err load another program as a module — with name, its PROC / FN / CONST / variable names are reached as name.x; without, they import flat. Runs the module's top-level lines (its init). Path-keyed dedup, cycles refused. The include verb; the RUN pre-pass calls it for each fixed-path include, the program itself for a computed one
-- save.lua / csave.lua / load.lua are three-line wrappers:
local basic = require("basic")
local a, done = os.arguments{ name = "save", { "*File", required = true } }
if done then return end
local ok, err = basic.save(a.file)
if not ok then print(`? {err}`) end

Building executables

Export an assembly-only program (nothing but [ ] / { } blocks) as a standalone native executable. See AntBASIC → building executables.

Call Returns Does
basic.build(path, target?) ok, err assemble the program to a standalone executable. target is "arm" (Linux ELF, from the [ ] code), "ee" (RV64, .ee), or "ezx" (RV64 EE for the eZX machine, .ezx); if omitted it is inferred from the extension. Errors if the program contains BASIC statements
basic.targets() list the build targets this build supports
-- build.lua   (build <file>)   — target inferred from the extension
local basic = require("basic")
local a, done = os.arguments{ name = "build", { "*File", required = true } }
if done then return end
local ok, err = basic.build(a.file)
if not ok then print(`? {err}`) end

Workspace and versioning

Call Returns Does
basic.free() number bytes free in the workspace arena — feeds the boot banner
basic.workspace() { size, used, free } arena totals
basic.version() "1.00.00" the AntBASIC version, for the banner
basic.token_version() number the current token-table version — compare against a csaved file's stamp to find stale images
-- the banner line, composed exactly as the console does it
local basic = require("basic")
local function human(n)
    if n >= 2^30 then return `{string.format("%.2f", n/2^30)} GB`
    elseif n >= 2^20 then return `{string.format("%.2f", n/2^20)} MB`
    elseif n >= 2^10 then return `{string.format("%.2f", n/2^10)} KB`
    else return `{n} bytes` end
end
print(`AntBASIC v{basic.version()}   {human(basic.free())} free`)

Contexts

One global main space, plus parallel contexts on worker threads — isolated program store and variables each. See AntBASIC → contexts.

Call Returns Does
basic.spawn(path_or_source) handle, err load a program into a new context on its own worker thread and run it; returns a handle
basic.contexts() list active context handles (main + spawned)

Every store / execution / variable call above accepts an optional context handle as its last argument, defaulting to main:

local job = basic.spawn("worker.bas")     -- runs in parallel
print(basic.state(job))                   -- inspect the spawned context
for _, l in basic.lines(job) do print(l.line, l.text) end

So list, renumber, a monitor or a debugger all work against a spawned context by passing its handle — the same script, the same verbs.


Tracing and debugging

The primitives a trace or debugger tool is built from — each takes an optional context handle, defaulting to the main space.

Call Returns Does
basic.trace(mode, ctx?) — "on" / "off" prints [line] per line; "step" single-steps; a line number traces up to it (BBC TRACE)
basic.break(ctx?) ok stop a running context — the Escape path; halts a { } FPGA core, interrupts or resets a [ ] DeMon context
basic.step(ctx?) line run one line (or statement) in a paused context, return where it stopped
basic.breakpoint(line, ctx?) / basic.clearbreak(line, ctx?) ok set / clear a breakpoint, resolved through the line table
basic.vars(ctx?) list the context's variables (names + values from the slot table); basic.getvar(name, ctx?) / basic.setvar(name, value, ctx?) read and write one live
basic.stack(ctx?) list the call stack — GOSUB / PROC / loop frames of a paused context
basic.watch(expr, ctx?) value, err evaluate an expression against a paused context (a watch)

Because the tool sits in one context and steps another, a breakpoint or a runaway loop pauses the target, never the debugger. For the machine code inside a program, drop to the soft core's own hardware debug (see the EE CPU doc — one RV64 core on either machine).


Where to go next

Important: The Ant64 family of home computers are at early design/prototype stage, everything you see here is subject to change.