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
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>
296 lines
13 KiB
C
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_
|