/**
 * @file runtime.h
 * @brief Declares the interpreter: source, pools, scanner, parser and evaluator.
 *
 * Ported from basicruntime.go, minus SDL and minus the for{} loop that owned the
 * process. The reference's run() does not return until MODE_QUIT; here
 * akbasic_runtime_step() executes exactly one iteration of that loop and returns,
 * and akbasic_runtime_run() bounds it. A host game must be able to step or bound
 * execution rather than surrender control, so the loop belongs to the caller.
 *
 * Nothing in this library terminates the process. Errors propagate out as
 * akerr_ErrorContext * for the host to handle; FINISH_NORETURN belongs only to
 * the driver's main().
 */

#ifndef _AKBASIC_RUNTIME_H_
#define _AKBASIC_RUNTIME_H_

#include <akerror.h>

#include <akbasic/audio.h>
#include <akbasic/console.h>
#include <akbasic/data.h>
#include <akbasic/disk.h>
#include <akbasic/environment.h>
#include <akbasic/format.h>
#include <akbasic/grammar.h>
#include <akbasic/graphics.h>
#include <akbasic/input.h>
#include <akbasic/sink.h>
#include <akbasic/sprite.h>
#include <akbasic/structtype.h>
#include <akbasic/symtab.h>
#include <akbasic/types.h>
#include <akbasic/ui.h>
#include <akbasic/value.h>
#include <akbasic/variable.h>

/** @brief The BASIC-visible error classes, used to build the "? n : CLASS msg" line. */
typedef enum
{
    AKBASIC_ERRCLASS_NONE = 0,
    AKBASIC_ERRCLASS_IO,
    AKBASIC_ERRCLASS_PARSE,
    AKBASIC_ERRCLASS_SYNTAX,
    AKBASIC_ERRCLASS_RUNTIME
} akbasic_ErrorClass;

/**
 * @brief One stored program line.
 *
 * `numbered` records whether the program itself wrote this line's number or the
 * loader assigned one because the line arrived without. Both are real lines and
 * both execute identically; the difference matters only to
 * akbasic_runtime_check_targets(), which refuses `GOTO 100` when line 100 is a
 * number nobody wrote.
 */
typedef struct
{
    char code[AKBASIC_MAX_LINE_LENGTH];
    int64_t lineno;
    bool numbered;
} akbasic_SourceLine;

/**
 * @brief What can interrupt a running program and send it into a handler.
 *
 * The numbering is COLLISION's own argument minus one, so `COLLISION 1` arms
 * slot 0. The two collision types this interpreter refuses still hold slots:
 * dropping them would make the arithmetic a lookup table for no gain, and a
 * later release that implements them wants the same numbers.
 */
typedef enum
{
    AKBASIC_INTERRUPT_SPRITE = 0,	/** COLLISION 1 -- sprite met sprite */
    AKBASIC_INTERRUPT_BACKGROUND,	/** COLLISION 2 -- sprite met background; refused */
    AKBASIC_INTERRUPT_LIGHTPEN,		/** COLLISION 3 -- light pen; refused */
    AKBASIC_INTERRUPT_ERROR,		/** TRAP -- a BASIC error; group C, not yet armed by anything */
    AKBASIC_MAX_INTERRUPTS
} akbasic_InterruptSource;

/**
 * @brief One armed interrupt: where its handler is, and whether it is due.
 *
 * The target is held as a line number *or* a label name, and a label is resolved
 * when the interrupt fires rather than when it is armed. A label may be re-filed
 * by its own LABEL statement at any point, so resolving late is what lets the
 * later spelling win.
 */
typedef struct
{
    bool armed;
    bool pending;
    int64_t line;			/* the numeric target; 0 when a label was given	*/
    char label[AKBASIC_SYMTAB_MAX_KEY];	/* the label target; empty when a number was given */
} akbasic_Interrupt;

/** @brief A user-defined subroutine or single-expression function. */
typedef struct
{
    char name[AKBASIC_SYMTAB_MAX_KEY];
    akbasic_ASTLeaf *arglist;
    akbasic_ASTLeaf *expression;
    int64_t lineno;
    /*
     * Set by akbasic_parse_gen(), left false by akbasic_parse_def(). GEN and
     * DEF share this table (TODO.md's namespace decision for generators), so
     * this is what lets akbasic_runtime_call_function() refuse to run a GEN
     * called like an ordinary function -- cleanly, before anything is pushed,
     * rather than relying on EMIT to fail deep inside a call whose BASIC-level
     * error a caller driving its own step loop would not see raised.
     */
    bool isGenerator;
    /*
     * There is deliberately no environment here. It used to be owned by the
     * funcdef and reset on every call, which made a function not re-entrant --
     * two calls in one expression shared one slot, and recursion never came
     * back. A call takes one from the pool now, exactly as GOSUB does.
     */
    /* Deep copies of the arglist and expression need storage that outlives the line. */
    akbasic_ASTLeaf leafstorage[AKBASIC_MAX_LEAVES * 2];
    akbasic_LeafPool leafpool;
    bool used;
} akbasic_FunctionDef;

typedef struct akbasic_Runtime
{
    akbasic_SourceLine source[AKBASIC_MAX_SOURCE_LINES];

    /* Scratch owned by this runtime for RENUMBER and its target prescan. */
    int16_t renumber_map[AKBASIC_MAX_SOURCE_LINES];
    uint8_t renumber_visited[AKBASIC_MAX_SOURCE_LINES];
    akbasic_SourceLine renumber_line;
    char renumber_discard[AKBASIC_MAX_LINE_LENGTH * 2];

    /* Pools. Nothing here is malloc'd; everything is drawn from and returned. */
    akbasic_Environment environments[AKBASIC_MAX_ENVIRONMENTS];
    akbasic_Variable variables[AKBASIC_MAX_VARIABLES];
    akbasic_FunctionDef functions[AKBASIC_MAX_FUNCTIONS];
    akbasic_ValuePool valuepool;
    /* Every TYPE the program declared, prescanned like labels and DATA. */
    akbasic_StructTypeTable structtypes;

    akbasic_Value staticTrueValue;
    akbasic_Value staticFalseValue;

    int mode;
    int run_finished_mode;
    akbasic_ErrorClass errclass;
    int64_t autoLineNumber;

    /*
     * When false, evaluating an identifier yields the live value rather than a
     * clone. POKE and POINTER need the address of the real storage. The
     * reference declares this on both the runtime and the environment and only
     * ever reads the runtime's; the environment copy is dropped here.
     */
    bool eval_clone_identifiers;

    akbasic_Environment *environment;
    akbasic_TextSink *sink;

    /*
     * The device backends, any of which may be NULL. The standalone driver
     * supplies none of them, so a PRINT-only program must keep working; every
     * verb that needs one refuses with AKBASIC_ERR_DEVICE instead.
     */
    akbasic_GraphicsBackend *graphics;
    akbasic_AudioBackend *audio;
    akbasic_InputBackend *input;
    akbasic_SpriteBackend *sprites;
    /*
     * The fifth one, set on its own through akbasic_runtime_set_ui() rather than
     * as a fifth argument to akbasic_runtime_set_devices(). Adding a parameter
     * would have rewritten twenty-eight call sites to pass a NULL none of them
     * cares about, in tests and documentation that are about something else.
     */
    akbasic_UiBackend *ui;

    /*
     * The graphics verbs' own state -- mode, color-source bindings, pixel cursor
     * and SCALE. It lives here rather than on the backend because it is BASIC's
     * state, not the device's: a host that swaps one renderer for another does
     * not expect the program's COLOR settings to go with it.
     */
    akbasic_GraphicsState gfx;

    /*
     * The sound verbs' state, including the PLAY queue. Same reasoning as gfx:
     * TEMPO, the ENVELOPE presets and the current octave are the program's, not
     * the device's.
     */
    akbasic_AudioState audio_state;

    /* GETKEY's hold on the step loop. Same reasoning again: it is the program's. */
    akbasic_InputState input_state;

    /* Which menus are up, and GETMENU's hold on the step loop. */
    akbasic_UiState ui_state;

    /*
     * The eight sprites, their positions and their collision bits. Same
     * reasoning as gfx and audio_state, and one more: RSPPOS and RSPRITE read
     * this rather than asking the device, so they answer correctly with no
     * device attached at all.
     */
    akbasic_SpriteState sprite_state;

    /* PUDEF's fill characters, which PRINT USING pads and punctuates with. */
    akbasic_FormatState format_state;

    /* SLEEP's and WAIT's hold on the step loop, and KEY's macros. */
    akbasic_ConsoleState console_state;

    /* Every DATA item in the program, and how far READ has got along it. */
    akbasic_DataState data_state;

    /* The open file channels DOPEN hands out. */
    akbasic_DiskState disk_state;

    /*
     * Where the running program was loaded from, or empty when it did not come
     * from a file -- a REPL session, or a host that handed over a string.
     *
     * Only one thing needs it today: SPRSAV resolving an image path that does
     * not exist relative to the working directory, which is what makes a `.bas`
     * beside its art work from anywhere. Group F's disk verbs will want the
     * same, which is why it is on the runtime rather than in the sprite state.
     *
     * Sized to AKBASIC_MAX_SOURCE_PATH_LENGTH, not AKBASIC_MAX_LINE_LENGTH: a
     * directory is a filesystem path, not a line of BASIC, and the two do not
     * belong to the same budget.
     */
    char sourcepath[AKBASIC_MAX_SOURCE_PATH_LENGTH];

    /*
     * The armed interrupts, and the environment the one currently running was
     * entered through.
     *
     * `handlerenv` is doing two jobs at once. It is the "a handler is running"
     * flag -- an interrupt does not interrupt an interrupt, which is what keeps a
     * collision that persists across the handler from recursing until the
     * environment pool is gone. And it is the identity RETURN compares against,
     * so the flag clears on the RETURN that leaves *this* handler rather than on
     * the first RETURN of any GOSUB the handler itself makes.
     */
    akbasic_Interrupt interrupts[AKBASIC_MAX_INTERRUPTS];
    akbasic_Environment *handlerenv;

    /*
     * The status code of the error being reported, for `ER#` to carry into a
     * TRAP handler. Set by the reporting path, which is the only place that
     * still has the error context; akbasic_runtime_error() sees a message and a
     * class, both of which have already lost the number.
     */
    int lasterrorstatus;

    /*
     * The host's clock, in milliseconds, as of its last akbasic_runtime_settime()
     * call. The interpreter does not read a clock: it owns no loop and must not
     * block, so the caller that owns the frame owns the time. libakgl does the
     * same thing -- akgl_actor_logic_changeframe() takes curtimems as an
     * argument rather than asking the system for it.
     *
     * Left at zero, every duration expires on the step after it starts. That is
     * wrong but never a hang, which is the right way for it to fail.
     */
    int64_t timems;

    /* RND's lazy seed state. The flag distinguishes an unseeded run from a
     * legitimate LCG state of zero. */
    int64_t rndseed;
    bool rndseeded;

    /*
     * Set by a branch that has decided the remaining statements on its line
     * belong to the arm it did not take, and cleared at the top of every line.
     *
     * This exists because a line can hold several statements and BASIC 7.0
     * scopes everything after THEN to the condition -- `IF C THEN A : B` runs
     * neither A nor B when C is false. The statement loop is what knows where a
     * line ends, so the branch raises a flag and the loop acts on it. See the
     * BRANCH case in akbasic_runtime_evaluate() for the exact rule, which is not
     * quite "skip when false".
     */
    bool skiprestofline;

    /*
     * TRON/TROFF. When set, every line prints its number in brackets before it
     * runs, inline and with no newline, which is what a C128 does: a traced
     * program's output reads `[10][20]HELLO`.
     */
    bool trace;

    /*
     * CONT's two pieces. `stopped` says a STOP actually happened, so CONT can
     * refuse rather than silently starting a program that was never running;
     * `stoppedline` is where to pick up, taken before STOP hands control back to
     * the REPL and the environment's line counters move on.
     */
    bool stopped;
    int64_t stoppedline;

    /*
     * The line the last BASIC-visible error was reported on, and 0 if there has
     * not been one. HELP re-displays it, which is the whole of what HELP does.
     */
    int64_t errorline;

    /* REPL line assembly */
    char userline[AKBASIC_MAX_LINE_LENGTH];

    /*
     * Set by the scanner when the line it just read began with a line number.
     *
     * It is what tells the REPL apart from a program. A line typed *with* a
     * number is program text and is filed; a line typed *without* one is a
     * direct-mode statement and runs now. Without this the REPL could only run
     * the handful of verbs marked immediate, so `PRINT 2 + 2` at the prompt was
     * silently stored rather than answered -- see TODO.md section 5.
     */
    bool hadlinenumber;

    /* Scanner state */
    char line[AKBASIC_MAX_LINE_LENGTH];
    int current;
    int start;
    akbasic_TokenType tokentype;
    bool hasError;

    bool inputEof;
} akbasic_Runtime;

/**
 * @brief Bring a runtime up: register status codes, build the root environment.
 * @param obj Object to initialize, inspect, or modify.
 * @param sink Where output goes and input comes from; required.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_init(akbasic_Runtime *obj, akbasic_TextSink *sink);

/**
 * @brief Attach the device backends a host is willing to lend the interpreter.
 *
 * Any argument may be NULL, which is how a host withholds a capability: a script
 * given no audio backend gets an error from SOUND rather than silence. Call it
 * after akbasic_runtime_init() and before akbasic_runtime_start(); calling it
 * again mid-run is allowed and takes effect on the next verb.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @param graphics Where DRAW, BOX, CIRCLE and PAINT land; may be NULL.
 * @param audio Where SOUND, PLAY and VOL land; may be NULL.
 * @param input Where GET and GETKEY read; may be NULL.
 * @param sprites Where SPRITE, MOVSPR and SPRSAV land; may be NULL.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` is NULL.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_set_devices(akbasic_Runtime *obj, akbasic_GraphicsBackend *graphics, akbasic_AudioBackend *audio, akbasic_InputBackend *input, akbasic_SpriteBackend *sprites);

/**
 * @brief Attach the UI backend, where MENU, DIALOG, HUD and UISTYLE land.
 *
 * Separate from akbasic_runtime_set_devices() rather than a fifth argument to
 * it, because that signature has twenty-eight call sites and none of them is
 * about the UI. NULL withholds the capability, which is what the standalone
 * stdio driver does and what every no-SDL build gets.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @param ui Where MENU, DIALOG, HUD and UISTYLE land; may be NULL.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` is NULL.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_set_ui(akbasic_Runtime *obj, akbasic_UiBackend *ui);

/**
 * @brief Tell the interpreter where the program it is running came from.
 *
 * An asset path a program writes -- `SPRSAV "ship.png", 1` -- is tried against
 * the process working directory first and against this directory second, so a
 * program stored beside its art works whether it was launched from its own
 * directory or from somewhere else. Exactly what libakgl does for a sprite
 * document naming its spritesheet.
 *
 * @p path is the program *file*, not its directory; the directory is taken from
 * it. Passing NULL, or never calling this, leaves only the working directory,
 * which is the right answer for a REPL session and for a host that handed the
 * interpreter a string.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @param path Path to the program file, or NULL for none.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` is NULL.
 * @throws AKBASIC_ERR_BOUNDS When the path is longer than AKBASIC_MAX_SOURCE_PATH_LENGTH.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_set_source_path(akbasic_Runtime *obj, const char *path);

/**
 * @brief Tell the interpreter what time the host thinks it is.
 *
 * SOUND durations, PLAY note lengths and TEMPO are all time-based, and section
 * 1.6 forbids the library blocking or reading a clock of its own. A host calls
 * this once a frame before akbasic_runtime_run(); the standalone driver calls it
 * from a monotonic clock each step.
 *
 * Time is only ever compared, never differenced against a wall clock, so any
 * monotonic millisecond source will do. It is not required to advance, and a
 * host that never calls this leaves it at zero -- durations then expire
 * immediately, which is audible but never a hang.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @param timems The host's current time in milliseconds.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` is NULL.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_settime(akbasic_Runtime *obj, int64_t timems);

/**
 * @brief Move every line onto a new number, rewriting every reference to one.
 *
 * Moving lines is a permutation of `source[]`. Rewriting `GOTO`, `GOSUB`, `RUN`,
 * `RESTORE`, `TRAP` and `COLLISION` targets to match is the work, and doing it on
 * source text means telling a line number apart from digits inside a string
 * literal -- `PRINT "GOTO 10"` must survive.
 *
 * A target naming a line that does not exist is left alone rather than
 * rewritten: `GOTO 9999` in a program with no line 9999 is already broken, and
 * inventing a destination for it would hide that. A `GOTO` to a *label* needs no
 * rewriting at all, which is the strongest argument for writing programs that
 * way.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @param newstart The number the first renumbered line takes.
 * @param increment How far apart the new numbers are; must be positive.
 * @param oldstart Renumber from this line on, leaving anything before it alone.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` is NULL.
 * @throws AKBASIC_ERR_VALUE When the increment is not positive, or a renumbered line would land on a kept one.
 * @throws AKBASIC_ERR_BOUNDS When a new number would exceed AKBASIC_MAX_SOURCE_LINES, or a rewritten line would not fit.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_renumber(akbasic_Runtime *obj, int64_t newstart, int64_t increment, int64_t oldstart);

/**
 * @brief Refuse a program that branches by number to a line it did not number.
 *
 * The fourth prescan, run beside the label, `DATA` and `TYPE` ones on every entry
 * into AKBASIC_MODE_RUN. Since a loaded line may be given its number rather than
 * carry one, `GOTO 100` in a script written without numbers finds the hundredth
 * line and branches there -- plausible, silent and wrong. This says so before any
 * line executes, which is the earliest it can be said and the only place that
 * covers all four ways a program arrives.
 *
 * It walks the same `GOTO`, `GOSUB`, `RUN`, `RESTORE`, `TRAP`, `ON ... GOTO` and
 * `COLLISION` targets akbasic_renumber() rewrites, sharing that walk rather than
 * repeating it. A target naming an *empty* line is allowed through, for the same
 * reason RENUMBER leaves one alone. A `GOTO` to a label never reaches here.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` is NULL.
 * @throws AKBASIC_ERR_SYNTAX When a numeric target names a line the program did not number.
 * @throws AKBASIC_ERR_BOUNDS When a line does not fit the walk's buffer.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_check_targets(akbasic_Runtime *obj);

/**
 * @brief File every `LABEL` in the stored program before any of it runs.
 *
 * Without this a label exists only from the moment its `LABEL` statement
 * executes, so `GOTO` and `GOSUB` reach backwards and never forwards -- and an
 * interrupt handler, which by definition sits on a line normal flow does not
 * fall into, could not be named by label at all. That is the case this exists
 * for; forward `GOTO` is the improvement that comes with it.
 *
 * The scan is textual rather than a parse: `LABEL` at the start of a statement,
 * outside a string literal, followed by an identifier. Parsing every line up
 * front would raise on lines the program would never have reached, which is a
 * worse trade than a scanner that understands one keyword.
 *
 * `LABEL` still executes normally and still files itself, so a label re-filed at
 * run time wins. A name that appears twice therefore resolves to the last one in
 * the source until one of them runs.
 *
 * Called by akbasic_runtime_set_mode() on every entry into AKBASIC_MODE_RUN,
 * which is the one point `RUN`, `CONT`, akbasic_runtime_start() and the end of a
 * RUNSTREAM load all pass through. A host has no reason to call it directly.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` is NULL or the runtime has no environment.
 * @throws AKBASIC_ERR_BOUNDS When the program holds more labels than AKBASIC_MAX_LABELS.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_scan_labels(akbasic_Runtime *obj);

/**
 * @brief Point an interrupt source at a handler, replacing whatever it had.
 *
 * Exactly one of @p line and @p label carries the target. A label is stored by
 * name and resolved when the interrupt fires; see #akbasic_Interrupt for why.
 *
 * Arming does not clear a pending event. A program that disarms and re-arms
 * around a critical section still sees the collision that happened inside it,
 * which is the behaviour that loses no events.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @param source Which interrupt to arm.
 * @param line Line number to enter, or 0 when @p label carries the target.
 * @param label Label to enter, or NULL when @p line carries the target.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` is NULL.
 * @throws AKBASIC_ERR_BOUNDS When `source` is out of range or the label is too long.
 * @throws AKBASIC_ERR_VALUE When neither or both of `line` and `label` name a target.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_arm_interrupt(akbasic_Runtime *obj, akbasic_InterruptSource source, int64_t line, const char *label);

/**
 * @brief Stop an interrupt source from entering a handler.
 *
 * Any event already pending on it is dropped: a program that has said it no
 * longer cares should not be sent into a handler it has just taken down.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @param source Which interrupt to disarm.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` is NULL.
 * @throws AKBASIC_ERR_BOUNDS When `source` is out of range.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_disarm_interrupt(akbasic_Runtime *obj, akbasic_InterruptSource source);

/**
 * @brief Record that an interrupt source fired.
 *
 * Cheap and safe to call every frame from a device backend, whether or not
 * anything is armed: an unarmed source records nothing, so a host does not have
 * to ask what the script has subscribed to. The handler is entered later, by
 * akbasic_runtime_step(), at a line boundary -- never from inside the code that
 * raised it.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @param source Which interrupt fired.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` is NULL.
 * @throws AKBASIC_ERR_BOUNDS When `source` is out of range.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_raise_interrupt(akbasic_Runtime *obj, akbasic_InterruptSource source);

/**
 * @brief Enter the handler of the first pending interrupt, if one is due.
 *
 * Called by akbasic_runtime_step() between source lines, which is the only place
 * it is safe: a handler entered mid-statement would return into the middle of a
 * line, and the parser holds no state that could resume there. That granularity
 * is the same one block skipping already works at -- see TODO.md.
 *
 * Entering a handler is a GOSUB the program did not write: a scope is pushed,
 * its return line is the line that was about to run, and the handler's RETURN
 * pops back to it. So a handler must end in RETURN, exactly as on a C128.
 *
 * Sources are tried in enum order, and only one is entered per call. Nothing is
 * entered while a handler is already running.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @param entered Output destination populated by the function; true when a handler was entered. May be NULL.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` is NULL.
 * @throws AKBASIC_ERR_UNDEFINED When the handler's label names nothing in the program.
 * @throws AKBASIC_ERR_ENVIRONMENT When the environment pool is exhausted.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_service_interrupts(akbasic_Runtime *obj, bool *entered);

/**
 * @brief Publish `ER#` and `EL#` into the global scope for a TRAP handler.
 *
 * A C128 spells these `ER` and `EL` as bare reserved names. This dialect has no
 * bare variable names -- an identifier carries its type in a suffix, and a name
 * without one is a label -- so they are ordinary global integers. See TODO.md
 * section 5.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @param status The status code of the error.
 * @param line The line it was reported on.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` is NULL.
 * @throws AKBASIC_ERR_BOUNDS When no variable slot is free.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_trap_set_error_variables(akbasic_Runtime *obj, int status, int64_t line);

/**
 * @brief Reset the per-line state without disturbing the program or variables.
 * @param obj Object to initialize, inspect, or modify.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` is NULL.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_zero(akbasic_Runtime *obj);

/**
 * @brief Push a new scope, taking one from the pool.
 * @param obj Object to initialize, inspect, or modify.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKBASIC_ERR_ENVIRONMENT When the environment pool is exhausted.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_new_environment(akbasic_Runtime *obj);
/**
 * @brief Pop the active scope and release it back to the pool.
 *
 * The reference never releases, which its garbage collector papers over. Here
 * the pool is finite, so an unreleased scope shows up as exhaustion later.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKBASIC_ERR_ENVIRONMENT When the active scope is the root.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_prev_environment(akbasic_Runtime *obj);

/**
 * @brief Return control to the active scope's parent, without releasing it.
 *
 * The half of akbasic_runtime_prev_environment() that pops; the other half,
 * akbasic_runtime_release_environment(), gives the scope's variables and its
 * pool slot back. Split for EMIT: a generator suspended between iterations
 * has to keep existing -- its own `nextline` is where NEXT resumes it -- so
 * detaching without releasing is what lets `obj->environment` move on to the
 * loop while the generator's scope stays alive, reachable through
 * `forGeneratorEnv`.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKBASIC_ERR_ENVIRONMENT When the active scope is the root.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_detach_environment(akbasic_Runtime *obj);
/**
 * @brief Give a scope's variables and its pool slot back.
 *
 * The other half of akbasic_runtime_prev_environment(): unlike that function,
 * @p env need not be `obj->environment` -- a generator environment sitting
 * detached in some loop's `forGeneratorEnv` is released this way once it is
 * exhausted or abandoned, without disturbing whatever scope is active now.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @param env The scope to release; must not be NULL and must not be the root.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `env` is NULL.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_release_environment(akbasic_Runtime *obj, akbasic_Environment *env);

/**
 * @brief Pop and release every scope from the active one up to @p target.
 *
 * The shared teardown for every path that abandons part of the environment
 * chain at once instead of popping it a verb at a time: the error unwinds in
 * akbasic_runtime_pump_generator() and akbasic_runtime_call_function(). Each
 * popped scope's suspended generator (`forGeneratorEnv`), if it still holds
 * one, is released through akbasic_runtime_release_generator() -- a suspended
 * generator is a *child* of its loop scope, so no walk up the parent chain
 * would ever reach it.
 *
 * Stops without error at the root if @p target is not on the chain: callers
 * are already cleaning up after a failure, and releasing everything is the
 * least-wrong answer to a target that has gone missing.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @param target The scope to stop at; it is left active and untouched.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When @p target is NULL.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_unwind_to_environment(akbasic_Runtime *obj, akbasic_Environment *target);

/**
 * @brief Push a GEN's environment, bind its parameters and run it to its first EMIT.
 *
 * Shared by the EACH branches of `FOR` and `DO`: both look up the same kind of
 * call expression (an `AKBASIC_LEAF_FUNCTION` leaf naming a GEN), guard
 * against invoking a GEN that is already running higher up this same parent
 * chain, evaluate the call's arguments in the caller's scope, bind them into a
 * fresh environment the way a function call does, and run that environment
 * until EMIT detaches it or END GEN ends it with nothing emitted.
 *
 * @p loopenv is left in the state a caller checks afterwards:
 * `loopenv->forGeneratorEnv` is the live generator environment when
 * something was emitted, or NULL when the GEN produced nothing at all.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @param loopenv The FOR EACH/DO EACH loop's own environment; becomes the new
 *        generator environment's parent.
 * @param callexpr The generator call, e.g. `ROOMOBJECTS(CURROOM%)`.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKBASIC_ERR_STATE When the named GEN is already running higher up
 *         this same parent chain.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_generator_invoke(akbasic_Runtime *obj, akbasic_Environment *loopenv, akbasic_ASTLeaf *callexpr);
/**
 * @brief Run a suspended generator until it next detaches.
 *
 * Shared by the EACH branches of `NEXT` and `LOOP`: `obj->environment` is
 * expected to already be the generator environment to resume (a caller sets
 * that from `loopenv->forGeneratorEnv` before calling), and this drives the
 * step loop until either EMIT detaches it back to @p loopenv with another
 * value, or END GEN really ends it -- in which case it releases the
 * generator environment itself and clears `loopenv->forGeneratorEnv`.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @param loopenv The FOR EACH/DO EACH loop's own environment, and the pump's
 *        stopping point.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_pump_generator(akbasic_Runtime *obj, akbasic_Environment *loopenv);

/**
 * @brief Release a live, abandoned generator, wherever EMIT left it suspended.
 *
 * `env` is expected to be a loop's `forGeneratorEnv` -- the resume point, not
 * necessarily the GEN's own call frame, since EMIT may have run several
 * levels below it inside a FOR/DO/GOSUB the body wrote. Walks up from there,
 * releasing every environment through the call frame itself inclusive, so
 * nothing above the resume point is left behind.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @param env The suspended resume point to release.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `env` is NULL.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_release_generator(akbasic_Runtime *obj, akbasic_Environment *env);

/**
 * @brief Report a BASIC error on the current line, in the reference's format.
 *
 * Writes `? <line> : <CLASS> <message>` through the sink. The message is expected
 * to end in a newline and the sink adds another, which is why an error line in
 * the golden corpus is followed by a blank one -- that is the contract, not an
 * accident.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @param errclass Which error class to name in the line.
 * @param message Text to append; conventionally ends in a newline.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` or `message` is NULL.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_error(akbasic_Runtime *obj, akbasic_ErrorClass errclass, const char *message);

/**
 * @brief Write text through the sink, mirroring the reference's Write().
 * @param obj Object to initialize, inspect, or modify.
 * @param text Text to emit.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` or `text` is NULL.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_write(akbasic_Runtime *obj, const char *text);
/**
 * @brief Write text and a newline, mirroring the reference's Println().
 * @param obj Object to initialize, inspect, or modify.
 * @param text Text to emit.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` or `text` is NULL.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_println(akbasic_Runtime *obj, const char *text);

/**
 * @brief Change the execution mode, announcing READY when entering the REPL.
 * @param obj Object to initialize, inspect, or modify.
 * @param mode One of the AKBASIC_MODE_* values.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` is NULL.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_set_mode(akbasic_Runtime *obj, int mode);

/**
 * @brief Forgive the last BASIC-level error, so a host may call again.
 *
 * A program run latches its first runtime error and ends -- deliberately, and
 * a host cannot un-decide that with akbasic_runtime_set_mode() alone: the
 * latch survives the mode change, every later line is skipped, and every
 * later akbasic_runtime_call_function() answers a stale value after walking
 * the whole source table doing nothing.
 *
 * A host that absorbed a script error -- reported through the sink, actor
 * marked dumb, frame preserved -- calls this beside
 * `akbasic_runtime_set_mode(obj, AKBASIC_MODE_RUN)` to put the runtime back
 * in service. It is for hosts between calls, not for verbs during a run: a
 * running program's first error still ends it, exactly once, with one line.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` is NULL.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_clear_error(akbasic_Runtime *obj);

/**
 * @brief Evaluate one AST leaf, drawing scratch values from the environment.
 * @param obj Object to initialize, inspect, or modify.
 * @param expr Leaf to evaluate.
 * @param dest Output destination populated by the function; points into the per-line value pool.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When any argument is NULL.
 * @throws AKBASIC_ERR_BOUNDS When the per-line value pool is exhausted.
 * @throws AKBASIC_ERR_UNDEFINED When the leaf names a verb, function or label that does not exist.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_evaluate(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value **dest);

/**
 * @brief Evaluate a leaf only when it is a verb a REPL may run without a line number.
 * @param obj Object to initialize, inspect, or modify.
 * @param expr Leaf to consider.
 * @param dest Output destination populated by the function; NULL when the leaf was not immediate.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When any argument is NULL.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_interpret_immediate(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value **dest);

/**
 * @brief Evaluate a leaf unless the environment is skipping forward to a verb.
 * @param obj Object to initialize, inspect, or modify.
 * @param expr Leaf to evaluate.
 * @param dest Output destination populated by the function.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When any argument is NULL.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_interpret(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value **dest);

/**
 * @brief Call a user-defined function or subroutine.
 *
 * A single-expression DEF evaluates and returns. A multi-line one hands control
 * to its own scope and runs until RETURN pops back out, which is why this can
 * execute an arbitrary number of source lines.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @param expr The call leaf, carrying the function name and its arguments.
 * @param dest Output destination populated by the function.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_KEY When no such function is defined.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_user_function(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value **dest);

/**
 * @brief Execute exactly one iteration of the reference's run() loop.
 *
 * One call reads or runs at most one source line. It never blocks beyond a
 * single readline on the sink, and it always returns.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` is NULL.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_step(akbasic_Runtime *obj);

/**
 * @brief Step until the runtime quits or `maxsteps` steps have elapsed.
 *
 * This is the entry point an embedding host calls once per frame. A bounded
 * budget is what keeps a script with an infinite loop from taking the host's
 * frame rate with it.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @param maxsteps Step ceiling; zero or negative means unbounded.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` is NULL.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_run(akbasic_Runtime *obj, int maxsteps);

/**
 * @brief Choose a starting mode and decide what happens when a program ends.
 *
 * Starting in AKBASIC_MODE_REPL returns to the REPL when a program finishes;
 * anything else quits.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @param mode One of the AKBASIC_MODE_* values.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` is NULL.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_start(akbasic_Runtime *obj, int mode);

/**
 * @brief Load a whole program from memory, filing each line under its line number.
 *
 * The entry point for an embedding host, which usually already holds its script
 * as a string and wants the sink reserved for output. The alternative --
 * AKBASIC_MODE_RUNSTREAM -- reads the program through the sink's readline, which
 * works for a file-backed driver but forces a game to point its output device at
 * its source text.
 *
 * Lines are separated by `\n`; a `\r` before it is tolerated. Blank lines are
 * skipped.
 *
 * **Line numbers are optional here.** A line that carries one is filed under it;
 * a line that does not is filed one slot after the last line filed, so a script
 * written entirely with `LABEL` and `GOTO NAME` needs no numbers at all. The two
 * can be mixed: a numbered line moves the cursor, and the unnumbered lines after
 * it continue from there. This is the rule RUNSTREAM and `DLOAD` follow too; only
 * the prompt still requires a number, because that is the one place a number is
 * what separates program text from a statement to run now.
 *
 * This does not run anything: follow it with
 * akbasic_runtime_start(obj, AKBASIC_MODE_RUN).
 *
 * @param obj Object to initialize, inspect, or modify.
 * @param source Whole program text; must not be NULL.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` or `source` is NULL.
 * @throws AKBASIC_ERR_BOUNDS When a line is longer than AKBASIC_MAX_LINE_LENGTH, its number is out of range, the program runs past AKBASIC_MAX_SOURCE_LINES, or an assigned number collides with a line already filed.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_load(akbasic_Runtime *obj, const char *source);

/**
 * @brief Find or create a variable in the script's outermost scope.
 *
 * **This is the entry point a host exchanging values with a script should use.**
 * akbasic_environment_get() lands a new variable in whatever scope is active,
 * and a script suspended part-way through a bounded akbasic_runtime_run() is
 * usually inside a `FOR` or `GOSUB` body -- so the script reads the value
 * correctly inside the loop and gets `0` immediately after it, with nothing
 * raised anywhere. Reaching for the root by hand does not help either: that
 * function only auto-creates in the *active* environment, so with a child active
 * it returns NULL through `dest` without raising and an unchecked host
 * dereferences it.
 *
 * Here the root is found by walking `obj->environment` to the environment with
 * no parent, and the variable is created there unconditionally. That is the
 * right answer inside a user function's scope too: a funcdef's environment is
 * initialized with the caller's as its parent, so the walk terminates at the
 * same root.
 *
 * Seeding before akbasic_runtime_start() and reading after the script stops both
 * still work through this, so a host has no reason to use anything else.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @param name Variable name including its type suffix -- `A#`, `B%`, `C$`.
 * @param dest Output destination populated by the function; never NULL on success.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When any argument is NULL, or the runtime has no environment.
 * @throws AKBASIC_ERR_BOUNDS When the name is too long, or no variable slot is free.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_global(akbasic_Runtime *obj, const char *name, akbasic_Variable **dest);

/* --- Internal API: exposed for the scanner, parser and verb handlers only. --- */

/**
 * @brief Create the globals the runtime writes to, before anything needs them.
 *
 * `ER#` and `EL#`, which the `TRAP` dispatch sets. Creating a name costs a
 * variable slot, and the dispatch runs at exactly the moment the program is
 * already in trouble -- so they are made once, up front, where there is always
 * room. `akbasic_runtime_init()` calls this; so does `CLR`, which empties the
 * table it filled.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` is NULL.
 * @throws AKBASIC_ERR_BOUNDS When no variable slot is free.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_reserve_globals(akbasic_Runtime *obj);

/**
 * @brief Take an unused variable from the runtime's pool.
 * @param obj Object to initialize, inspect, or modify.
 * @param dest Output destination populated by the function.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKBASIC_ERR_BOUNDS When every variable slot is in use.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_new_variable(akbasic_Runtime *obj, akbasic_Variable **dest);
/**
 * @brief Call a user-defined function with values a caller already has.
 *
 * The half of a call that is not parsing. akbasic_runtime_user_function()
 * evaluates a parsed call site's arguments and then comes here; a **verb** that
 * wants to hand a BASIC function four numbers starts here directly, which was
 * not possible before -- the only entry point took an AST call site, so calling
 * a function required having been parsed as an expression.
 *
 * The body runs the same way either way: a scope from the environment pool, as
 * `GOSUB` takes, re-entrant, with recursion depth answering to
 * #AKBASIC_MAX_ENVIRONMENTS. A multi-line definition re-enters the line loop
 * synchronously and returns when its RETURN pops back out.
 *
 * @param obj The runtime.
 * @param name The function's name, as `DEF` spelled it.
 * @param args Values to bind, already evaluated. May be NULL when @p nargs is 0.
 * @param nargs How many. Extra arguments beyond the definition's are ignored,
 *              which is what the AST path has always done.
 * @param dest Receives the result, in the caller's scratch.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When @p obj, @p name or @p dest is NULL.
 * @throws AKBASIC_ERR_UNDEFINED When no function of that name is defined.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_call_function(struct akbasic_Runtime *obj, const char *name, akbasic_Value **args, int nargs, akbasic_Value **dest);

/**
 * @brief Take an unused function definition from the runtime's pool.
 * @param obj Object to initialize, inspect, or modify.
 * @param dest Output destination populated by the function.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKBASIC_ERR_BOUNDS When every function slot is in use.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_new_function(akbasic_Runtime *obj, akbasic_FunctionDef **dest);
/**
 * @brief File one already-scanned source line under its line number.
 *
 * Hosts should prefer akbasic_runtime_load(), which scans as well.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @param lineno Line number to file it under.
 * @param code Source text, with any line number already stripped.
 * @param numbered True when the program wrote this number, false when the loader assigned it.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKBASIC_ERR_BOUNDS When the number or the line length is out of range.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_store_line(akbasic_Runtime *obj, int64_t lineno, const char *code, bool numbered);
/**
 * @brief File a freshly-scanned line, giving it a number if it arrived without one.
 *
 * The one place the optional-line-number rule lives, shared by all three loading
 * paths -- akbasic_runtime_load(), RUNSTREAM and `DLOAD` -- so they cannot drift.
 * Reads akbasic_Runtime::hadlinenumber, which akbasic_scanner_scan() has just set,
 * and akbasic_Environment::lineno, which doubles as the loader's cursor.
 *
 * The prompt does not use this: a line typed without a number is direct mode, not
 * a line waiting for one.
 *
 * @param obj Object to initialize, inspect, or modify.
 * @param code Source text, with any line number already stripped.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` is NULL.
 * @throws AKBASIC_ERR_BOUNDS When the program runs past AKBASIC_MAX_SOURCE_LINES, or an assigned number collides with a line already filed.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_file_line(akbasic_Runtime *obj, const char *code);
/**
 * @brief Execute the next line of the stored program.
 * @param obj Object to initialize, inspect, or modify.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` is NULL.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_process_line_run(akbasic_Runtime *obj);
/**
 * @brief Read one line from the sink and file it, without executing it.
 * @param obj Object to initialize, inspect, or modify.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` is NULL.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_process_line_runstream(akbasic_Runtime *obj);
/**
 * @brief Read one line from the sink and either run it or file it.
 * @param obj Object to initialize, inspect, or modify.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER When `obj` is NULL.
 */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_process_line_repl(akbasic_Runtime *obj);
/**
 * @brief The nearest non-empty line before the current one.
 * @param obj Runtime to inspect.
 * @return That line's number, or the current one when there is nothing before it.
 */
int64_t akbasic_runtime_find_previous_lineno(akbasic_Runtime *obj);

#endif // _AKBASIC_RUNTIME_H_
