Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m27s
akbasic CI Build / coverage (push) Failing after 3m44s
akbasic CI Build / sanitizers (push) Failing after 4m43s
akbasic CI Build / mutation_test (push) Failing after 3m45s
akbasic CI Build / akgl_build (push) Failing after 4m51s
akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
286 lines
13 KiB
C
286 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 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_
|