Files
akbasic/include/akbasic/environment.h
Logikoma 2e60e26b2f
All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m54s
akbasic CI Build / coverage (push) Successful in 4m10s
akbasic CI Build / sanitizers (push) Successful in 5m16s
akbasic CI Build / akgl_build (push) Successful in 9m43s
akbasic CI Build / mutation_test (push) Successful in 24m33s
Stop pointer parameters leaking value-pool slots
Create structure parameters before allocating their representation, then keep pointer references in the call variable's inline slot. Add an 8,000-call regression and document the remaining by-value structure escape limitation.

Co-authored-by: andrew <andrew@aklabs.net>
Co-authored-by: OpenAI Codex (GPT-5) <noreply@openai.com>
2026-08-04 23:09:42 -04:00

296 lines
13 KiB
C

/**
* @file environment.h
* @brief Declares a scope and the per-line working state that rides with it.
*
* Ported from basicenvironment.go. An environment is both a variable scope and
* the in-flight state of whatever block structure is executing: IF, FOR, GOSUB
* and READ all park their bookkeeping here.
*
* The reference allocates one with new() per FOR and per GOSUB and never frees
* it. Here they come from a pool the runtime owns and are released on pop, so a
* long-running program exhausts a bounded resource and says so rather than
* leaking quietly.
*/
#ifndef _AKBASIC_ENVIRONMENT_H_
#define _AKBASIC_ENVIRONMENT_H_
#include <akerror.h>
#include <akbasic/grammar.h>
#include <akbasic/symtab.h>
#include <akbasic/types.h>
#include <akbasic/value.h>
#include <akbasic/variable.h>
/** @brief A DO or LOOP with no condition on that end. */
#define AKBASIC_LOOPCOND_NONE 0
/** @brief `WHILE c` -- keep looping while the condition is true. */
#define AKBASIC_LOOPCOND_WHILE 1
/** @brief `UNTIL c` -- keep looping until the condition becomes true. */
#define AKBASIC_LOOPCOND_UNTIL 2
struct akbasic_Runtime;
typedef struct akbasic_Environment
{
akbasic_SymbolTable variables; /** name -> akbasic_Variable * */
akbasic_SymbolTable functions; /** name -> akbasic_FunctionDef * */
akbasic_SymbolTable labels; /** name -> line number */
/* FOR state */
akbasic_ASTLeaf *forStepLeaf;
akbasic_Value forStepValue;
akbasic_ASTLeaf *forToLeaf;
akbasic_Value forToValue;
akbasic_Variable *forNextVariable;
/*
* DO/LOOP state. The condition may sit on either end -- `DO WHILE c`,
* `LOOP UNTIL c`, both, or neither -- so each end keeps its own, and the
* leaf lives in this environment's pool because it is re-evaluated on every
* iteration long after the line that held it was scanned away.
*/
akbasic_ASTLeaf *doConditionLeaf;
/* The condition is re-evaluated every iteration, long after its line was scanned away. */
akbasic_ASTLeaf doLeafStorage[AKBASIC_MAX_CONDITION_LEAVES];
akbasic_LeafPool doLeafPool;
int doConditionKind; /** AKBASIC_LOOPCOND_* */
bool isDoLoop; /** distinguishes DO/LOOP from FOR/NEXT for EXIT */
/* Loop bounds */
int64_t loopFirstLine;
int64_t loopExitLine;
/**
* Set by `EXIT`, cleared by the `NEXT` that acts on it.
*
* `EXIT` cannot simply jump past the loop, because where the loop ends is
* not known until a `NEXT` has run at least once -- and an `EXIT` on the
* first pass is the normal case. So it skips forward to the `NEXT` using the
* same waitingForCommand machinery a zero-iteration loop body uses, and this
* is what tells that `NEXT` it is ending the loop rather than continuing it.
*/
bool exiting;
int64_t gosubReturnLine;
/* READ state. The identifier leaves are deep copies, so they need storage. */
int64_t readReturnLine;
akbasic_ASTLeaf *readIdentifierLeaves[AKBASIC_MAX_LEAVES];
int64_t readIdentifierIdx;
akbasic_ASTLeaf readLeafStorage[AKBASIC_MAX_LEAVES];
akbasic_LeafPool readLeafPool;
/*
* While this is set, no line executes until a COMMAND matching it is found.
* It is what keeps the body of a loop that should not run at all from
* running, given that the reference evaluates a loop's condition at the
* *bottom* of the structure. Any reimplementation has to reproduce it or
* restructure control flow deliberately.
*/
char waitingForCommand[AKBASIC_SYMTAB_MAX_KEY];
struct akbasic_Environment *parent;
struct akbasic_Runtime *runtime;
/* Runtime state */
int64_t lineno;
akbasic_Value values[AKBASIC_MAX_VALUES];
int nextvalue;
int64_t nextline;
akbasic_Value returnValue;
/* Parser state */
akbasic_Token tokens[AKBASIC_MAX_TOKENS];
int nexttoken;
int curtoken;
akbasic_ASTLeaf leaves[AKBASIC_MAX_LEAVES];
int nextleaf;
akbasic_Token *errorToken;
bool used; /** Pool bookkeeping */
} akbasic_Environment;
/**
* @brief Bring an environment up as a child of another, or as the root.
* @param obj Object to initialize, inspect, or modify.
* @param runtime The runtime that owns the pools this scope draws from.
* @param parent Enclosing scope, or NULL for the root.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` or `runtime` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_init(akbasic_Environment *obj, struct akbasic_Runtime *runtime, akbasic_Environment *parent);
/**
* @brief Reset the per-line value pool without touching variables or scope.
* @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_environment_zero(akbasic_Environment *obj);
/**
* @brief Reset the per-line token and leaf pools and their cursors.
* @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_environment_zero_parser(akbasic_Environment *obj);
/** @brief Take the next value from this environment's per-line pool. */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_new_value(akbasic_Environment *obj, akbasic_Value **dest);
/** @brief Take the next leaf from this environment's per-line pool. */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_new_leaf(akbasic_Environment *obj, akbasic_ASTLeaf **dest);
/**
* @brief Suppress execution until a given verb is reached.
*
* The reference panics on a second pending wait. This raises instead, but it is
* still a hard failure: two waits in one scope means the block structure is
* already corrupt.
*
* @param obj Object to initialize, inspect, or modify.
* @param command Verb to skip forward to, e.g. "NEXT" or "DATA".
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_STATE When this scope is already waiting for something.
* @throws AKBASIC_ERR_BOUNDS When the verb name is too long to record.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_wait_for_command(akbasic_Environment *obj, const char *command);
/**
* @brief Whether this scope or any enclosing one is skipping forward.
*
* The answer leaves through @p dest rather than the return value. Deciding it
* reads a recorded verb name, that read can fail, and a `bool` return has
* nowhere to report the failure -- so the signature changes rather than the
* error being swallowed. libakstdlib #38 is the ruling; `probe` in `symtab.c`
* and `loop_continues` in `runtime_structure.c` are the same shape.
*
* @param obj Scope to inspect; NULL is not waiting.
* @param[out] dest `true` when execution is currently suppressed. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When dest is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_is_waiting_for_any(akbasic_Environment *obj, bool *dest);
/**
* @brief Whether this scope or an enclosing one is waiting for a given verb.
*
* Reports through @p dest for the same reason its sibling above does.
*
* @param obj Scope to inspect; NULL is not waiting.
* @param command Verb to test for; NULL is not waiting.
* @param[out] dest `true` when that verb is what execution is waiting on. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When dest is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_is_waiting_for(akbasic_Environment *obj, const char *command, bool *dest);
/**
* @brief Clear a pending wait, searching the parent chain for it.
*
* A verb that is not being waited for is silently tolerated, matching the
* reference -- which ignores the argument entirely and clears unconditionally
* (TODO.md section 6 item 3).
*
* @param obj Object to initialize, inspect, or modify.
* @param command Verb whose wait should be cleared.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` or `command` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_stop_waiting(akbasic_Environment *obj, const char *command);
/**
* @brief Find a variable, creating it in the active environment if it is absent.
*
* Parents do not create variables on behalf of their children: only the
* runtime's currently active environment auto-creates. A lookup that misses in a
* non-active environment returns NULL through `dest` without error, matching the
* reference.
*
* **A host wanting a variable the script will still see later wants
* akbasic_runtime_global() instead.** This one lands the variable in whatever
* scope happens to be active, which during a suspended run is usually a `FOR` or
* `GOSUB` body -- and it dies when that scope pops.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_get(akbasic_Environment *obj, const char *varname, akbasic_Variable **dest);
/**
* @brief Evaluate an lvalue's subscript list, or yield {0} when it has none.
*
* A bare identifier addresses element zero, because every variable here is
* really a one-element array. Shared so that a verb taking a variable by name
* -- `SSHAPE` and `GSHAPE` do -- resolves a subscript the same way assignment
* does, rather than reading the leaf and quietly using element zero for
* everything.
*
* @param obj The environment the subscript expressions are evaluated in.
* @param lval The identifier leaf, whose `.expr` carries any subscript list.
* @param subscripts Output: one index per dimension, AKBASIC_MAX_ARRAY_DEPTH wide.
* @param count Output: how many were written; at least one.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_TYPE When a subscript does not evaluate to an integer.
* @throws AKBASIC_ERR_BOUNDS When there are more than AKBASIC_MAX_ARRAY_DEPTH.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_collect_subscripts(akbasic_Environment *obj, akbasic_ASTLeaf *lval, int64_t *subscripts, int *count);
/**
* @brief Find a variable in exactly this scope, creating it here if it is absent.
*
* No walk up the parent chain in either direction: the caller has said which
* scope it means, and no active-environment check stands in the way. This is
* what akbasic_runtime_global() calls against the root.
*
* @param obj The scope to search and, on a miss, to create in.
* @param varname Name including its type suffix.
* @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.
* @throws AKBASIC_ERR_BOUNDS When the name is too long, or no variable slot is free.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_create(akbasic_Environment *obj, const char *varname, akbasic_Variable **dest);
/**
* @brief Create a variable slot without allocating value storage.
*
* Used when the caller knows the variable's representation before its first
* initialization, such as a structure parameter. The caller must initialize
* the variable before evaluating it.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_create_empty(akbasic_Environment *obj, const char *varname,
akbasic_Variable **dest);
/**
* @brief Resolve a label to the line number it marks.
* @param obj Scope to search; the parent chain is walked.
* @param label Label name, which carries no type suffix.
* @param dest Output destination populated by the function.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_UNDEFINED When no enclosing scope defines the label.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_get_label(akbasic_Environment *obj, const char *label, int64_t *dest);
/**
* @brief Record a label against a line number.
*
* Labels are created only in the top-level scope, so one set inside a loop is
* still visible after it.
*
* @param obj Scope the request came from; the search walks up from here.
* @param label Label name.
* @param value Line number to record.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_ENVIRONMENT When no top-level scope is reachable.
* @throws AKBASIC_ERR_BOUNDS When the label table is full.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_set_label(akbasic_Environment *obj, const char *label, int64_t value);
/**
* @brief Find a user-defined function by name, case-insensitively.
* @param obj Scope to search; the parent chain is walked.
* @param fname Function name; folded to upper case before lookup.
* @param dest Output destination populated by the function; an akbasic_FunctionDef *.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_KEY When no enclosing scope defines the function.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_get_function(akbasic_Environment *obj, const char *fname, void **dest);
/** @brief Assign into the slot an lvalue leaf names, following any subscripts. */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_assign(akbasic_Environment *obj, akbasic_ASTLeaf *lval, akbasic_Value *rval, akbasic_Value **dest);
#endif // _AKBASIC_ENVIRONMENT_H_