Document the public API in libakgl's Doxygen style

Adds a Doxyfile in the shape libakgl uses -- fifteen deliberate lines, including
WARN_IF_UNDOCUMENTED and WARN_AS_ERROR=FAIL_ON_WARNINGS -- and fills in the 70
public declarations that had no doc block, bringing include/akbasic to 114 of
114.

libakgl is the model rather than libakstdlib. Measured before starting: libakgl
documents 122 of 122 public declarations and libakstdlib 3 of 25, and
libakstdlib's Doxyfile is the unedited doxygen default, PROJECT_NAME = "My
Project" and an empty INPUT. So the house standard is libakgl's, down to the
boilerplate phrasing for the recurring parameters -- "Object to initialize,
inspect, or modify", "Output destination populated by the function", "`NULL` on
success, otherwise an error context owned by the caller".

Worth knowing what the gate actually gates. EXTRACT_ALL=YES suppresses
doxygen's undocumented-entity warnings, so the rule it enforces is that a
*partial* block is an error: document one @param and you must document them all.
Verified by deleting a @param and confirming a non-zero exit, then restoring it.
Full coverage is therefore a convention this commit adopts rather than something
the tool made me do.

Where a contract is non-obvious the block says so rather than restating the
signature: math_plus explains why it alone mutates its left operand, new_unary
notes that hanging the operand on .right is what makes the parser miscount a
negative literal argument, leaf_to_string warns that an assignment renders with
an empty operator, and stop_waiting records that a verb nobody is waiting for is
tolerated. Each cross-references its TODO.md section 6 item.

Also records in TODO.md that this repository has no CI, which libakgl and
libakstdlib both have -- so ctest, the sanitizer build, coverage and this new
doxygen gate are all run by hand today.

ctest 61/61; doxygen Doxyfile exits 0; no warnings under -Wall -Wextra.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 07:00:16 -04:00
parent e51b9a1694
commit 134ade9392
9 changed files with 783 additions and 15 deletions

14
Doxyfile Normal file
View File

@@ -0,0 +1,14 @@
PROJECT_NAME = libakbasic
PROJECT_BRIEF = "Commodore/Dartmouth BASIC interpreter, embeddable in C"
OUTPUT_DIRECTORY = build/docs
INPUT = include/akbasic src examples
FILE_PATTERNS = *.h *.c
RECURSIVE = YES
EXTRACT_ALL = YES
EXTRACT_STATIC = YES
WARN_IF_UNDOCUMENTED = YES
WARN_IF_DOC_ERROR = YES
WARN_AS_ERROR = FAIL_ON_WARNINGS
GENERATE_HTML = YES
GENERATE_LATEX = NO
QUIET = YES

View File

@@ -13,8 +13,17 @@ cmake --build build --parallel
# The test suite: unit tests plus the Go version's own corpus, byte-compared # The test suite: unit tests plus the Go version's own corpus, byte-compared
ctest --test-dir build --output-on-failure ctest --test-dir build --output-on-failure
# API documentation, into build/docs/html
doxygen Doxyfile
``` ```
The `Doxyfile` is configured the way `libakgl`'s is, including
`WARN_AS_ERROR = FAIL_ON_WARNINGS` — a doc block that documents some of a function's
parameters but not all of them fails the run, so `doxygen Doxyfile` is a gate rather than a
convenience. Every one of the 114 public declarations under `include/akbasic/` carries a
`@brief`, a `@param` per parameter, a `@return` and its `@throws`.
# Why rewrite it in C? # Why rewrite it in C?
Three reasons, in the order they matter. Three reasons, in the order they matter.

View File

@@ -620,6 +620,7 @@ entire test corpus and passes clean under ASan and UBSan.
| Line coverage | 92.3% (2823/3058) — above the 90% gate | | Line coverage | 92.3% (2823/3058) — above the 90% gate |
| Function coverage | 96.9% (221/228) | | Function coverage | 96.9% (221/228) |
| Warnings | none under `-Wall -Wextra` | | Warnings | none under `-Wall -Wextra` |
| `doxygen Doxyfile` | clean; 114/114 public declarations documented |
Branch coverage reads 17.3% and is not a target, for the reason `libakgl/TODO.md` and Branch coverage reads 17.3% and is not a target, for the reason `libakgl/TODO.md` and
`libakstdlib/TODO.md` both give: the akerror control-flow macros expand into large branch trees `libakstdlib/TODO.md` both give: the akerror control-flow macros expand into large branch trees
@@ -634,6 +635,11 @@ Dependency baseline:
| `deps/libakstdlib` | 0.1.0 | soname `libakstdlib.so.0.1`. `AKSL_VERSION_CHECK()` asserted in `tests/version_check.c`. Its `aksl_ato*` family is banned here — see §1.9. | | `deps/libakstdlib` | 0.1.0 | soname `libakstdlib.so.0.1`. `AKSL_VERSION_CHECK()` asserted in `tests/version_check.c`. Its `aksl_ato*` family is banned here — see §1.9. |
| `deps/libakgl` | 0.1.0 | Migrated to the 1.0.0 registry and owns 256260. Not yet linked: `AKBASIC_WITH_AKGL` defaults OFF and `src/sink_akgl.c` does not exist. | | `deps/libakgl` | 0.1.0 | Migrated to the 1.0.0 registry and owns 256260. Not yet linked: `AKBASIC_WITH_AKGL` defaults OFF and `src/sink_akgl.c` does not exist. |
**This repository has no CI.** `libakgl` and `libakstdlib` both carry
`.gitea/workflows/ci.yaml`; akbasic does not, so every gate here — ctest, the sanitizer build,
coverage, and `doxygen Doxyfile` with `WARN_AS_ERROR` — is currently run by hand. Copying
`deps/libakgl/.gitea/workflows/ci.yaml` and adjusting the option prefixes is the whole job.
What remains, in priority order: What remains, in priority order:
1. **§3 — the akgl text sink.** Blocked on `libakgl` having no text-measurement call; that gap 1. **§3 — the akgl text sink.** Blocked on `libakgl` having no text-measurement call; that gap

View File

@@ -81,8 +81,28 @@ typedef struct akbasic_Environment
bool used; /** Pool bookkeeping */ bool used; /** Pool bookkeeping */
} akbasic_Environment; } 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); 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); 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); akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_zero_parser(akbasic_Environment *obj);
/** @brief Take the next value from this environment's per-line pool. */ /** @brief Take the next value from this environment's per-line pool. */
@@ -91,9 +111,45 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_new_value(akbasic_Environ
/** @brief Take the next leaf from this environment's per-line pool. */ /** @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); 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); akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_wait_for_command(akbasic_Environment *obj, const char *command);
/**
* @brief True when this scope or any enclosing one is skipping forward.
* @param obj Scope to inspect; NULL is not waiting.
* @return `true` when execution is currently suppressed.
*/
bool akbasic_environment_is_waiting_for_any(akbasic_Environment *obj); bool akbasic_environment_is_waiting_for_any(akbasic_Environment *obj);
/**
* @brief True when this scope or an enclosing one is waiting for a given verb.
* @param obj Scope to inspect; NULL is not waiting.
* @param command Verb to test for.
* @return `true` when that verb is what execution is waiting on.
*/
bool akbasic_environment_is_waiting_for(akbasic_Environment *obj, const char *command); bool akbasic_environment_is_waiting_for(akbasic_Environment *obj, const char *command);
/**
* @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); akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_stop_waiting(akbasic_Environment *obj, const char *command);
/** /**
@@ -106,8 +162,37 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_stop_waiting(akbasic_Envi
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_get(akbasic_Environment *obj, const char *varname, akbasic_Variable **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_get(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); 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); 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); 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. */ /** @brief Assign into the slot an lvalue leaf names, following any subscripts. */

View File

@@ -122,8 +122,21 @@ typedef struct
akbasic_ASTLeaf *leaves; akbasic_ASTLeaf *leaves;
} akbasic_LeafPool; } akbasic_LeafPool;
/**
* @brief Reset a token to the undefined state.
* @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_token_init(akbasic_Token *obj); akerr_ErrorContext AKERR_NOIGNORE *akbasic_token_init(akbasic_Token *obj);
/**
* @brief Reset a leaf, clearing every payload and link.
* @param obj Object to initialize, inspect, or modify.
* @param leaftype Type to stamp on the leaf.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_init(akbasic_ASTLeaf *obj, akbasic_LeafType leaftype); akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_init(akbasic_ASTLeaf *obj, akbasic_LeafType leaftype);
/** /**
@@ -135,29 +148,165 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_init(akbasic_ASTLeaf *obj, akbas
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_clone(akbasic_ASTLeaf *self, akbasic_LeafPool *pool, akbasic_ASTLeaf **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_clone(akbasic_ASTLeaf *self, akbasic_LeafPool *pool, akbasic_ASTLeaf **dest);
/** @brief The first argument of a function-call leaf, or NULL. */ /**
* @brief The first argument of a function-call leaf.
* @param self Leaf to inspect.
* @return The first argument, or NULL when the leaf carries no argument list.
*/
akbasic_ASTLeaf *akbasic_leaf_first_argument(akbasic_ASTLeaf *self); akbasic_ASTLeaf *akbasic_leaf_first_argument(akbasic_ASTLeaf *self);
/** @brief The first subscript of an array-reference leaf, or NULL. */ /**
* @brief The first subscript of an array-reference leaf.
* @param self Leaf to inspect.
* @return The first subscript, or NULL when the leaf carries no subscript list.
*/
akbasic_ASTLeaf *akbasic_leaf_first_subscript(akbasic_ASTLeaf *self); akbasic_ASTLeaf *akbasic_leaf_first_subscript(akbasic_ASTLeaf *self);
/**
* @brief True when a leaf is any of the four identifier kinds.
* @param self Leaf to inspect; NULL is not an identifier.
* @return `true` when the leaf names a variable or a label.
*/
bool akbasic_leaf_is_identifier(akbasic_ASTLeaf *self); bool akbasic_leaf_is_identifier(akbasic_ASTLeaf *self);
/**
* @brief True when a leaf is an integer, float or string literal.
* @param self Leaf to inspect; NULL is not a literal.
* @return `true` when the leaf carries a literal value.
*/
bool akbasic_leaf_is_literal(akbasic_ASTLeaf *self); bool akbasic_leaf_is_literal(akbasic_ASTLeaf *self);
/**
* @brief Build a comparison leaf.
* @param obj Leaf to initialize; drawn from a pool by the caller.
* @param left Left operand.
* @param op Comparison operator; must be one of the six relational tokens.
* @param right Right operand.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `left` or `right` is NULL.
* @throws AKBASIC_ERR_SYNTAX When `op` is not a comparison operator.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_comparison(akbasic_ASTLeaf *obj, akbasic_ASTLeaf *left, akbasic_TokenType op, akbasic_ASTLeaf *right); akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_comparison(akbasic_ASTLeaf *obj, akbasic_ASTLeaf *left, akbasic_TokenType op, akbasic_ASTLeaf *right);
/**
* @brief Build a binary-operator leaf.
* @param obj Leaf to initialize; drawn from a pool by the caller.
* @param left Left operand.
* @param op Operator token.
* @param right Right operand.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `left` or `right` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_binary(akbasic_ASTLeaf *obj, akbasic_ASTLeaf *left, akbasic_TokenType op, akbasic_ASTLeaf *right); akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_binary(akbasic_ASTLeaf *obj, akbasic_ASTLeaf *left, akbasic_TokenType op, akbasic_ASTLeaf *right);
/**
* @brief Build a unary-operator leaf.
*
* The operand hangs off `.right`, which is why the arity counter in the parser
* miscounts a negative literal argument -- see TODO.md section 6 item 13.
* @param obj Leaf to initialize; drawn from a pool by the caller.
* @param op Operator token.
* @param right Operand.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `right` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_unary(akbasic_ASTLeaf *obj, akbasic_TokenType op, akbasic_ASTLeaf *right); akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_unary(akbasic_ASTLeaf *obj, akbasic_TokenType op, akbasic_ASTLeaf *right);
/**
* @brief Build a function-call leaf.
* @param obj Leaf to initialize; drawn from a pool by the caller.
* @param fname Function name as written; case is preserved for the error message.
* @param right Argument list, or NULL.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_VALUE When the name exceeds the length limit.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_function(akbasic_ASTLeaf *obj, const char *fname, akbasic_ASTLeaf *right); akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_function(akbasic_ASTLeaf *obj, const char *fname, akbasic_ASTLeaf *right);
/**
* @brief Build a verb leaf.
* @param obj Leaf to initialize; drawn from a pool by the caller.
* @param cmdname Verb name as written.
* @param right The verb's rval, or NULL for a verb that takes none.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_VALUE When the name exceeds the length limit.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_command(akbasic_ASTLeaf *obj, const char *cmdname, akbasic_ASTLeaf *right); akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_command(akbasic_ASTLeaf *obj, const char *cmdname, akbasic_ASTLeaf *right);
/**
* @brief Build a leaf for a verb a REPL may run without a line number.
* @param obj Leaf to initialize; drawn from a pool by the caller.
* @param cmdname Verb name as written.
* @param right The verb's rval, or NULL.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_VALUE When the name exceeds the length limit.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_immediate_command(akbasic_ASTLeaf *obj, const char *cmdname, akbasic_ASTLeaf *right); akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_immediate_command(akbasic_ASTLeaf *obj, const char *cmdname, akbasic_ASTLeaf *right);
/**
* @brief Build a branch leaf, which is what IF parses to.
* @param obj Leaf to initialize; drawn from a pool by the caller.
* @param expr Condition, evaluated for a BASIC boolean.
* @param trueleaf Taken when the condition is true.
* @param falseleaf Taken when it is false; may be NULL, as ELSE is optional.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `expr` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_branch(akbasic_ASTLeaf *obj, akbasic_ASTLeaf *expr, akbasic_ASTLeaf *trueleaf, akbasic_ASTLeaf *falseleaf); akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_branch(akbasic_ASTLeaf *obj, akbasic_ASTLeaf *expr, akbasic_ASTLeaf *trueleaf, akbasic_ASTLeaf *falseleaf);
/**
* @brief Build a parenthesised-expression leaf.
* @param obj Leaf to initialize; drawn from a pool by the caller.
* @param expr The grouped expression.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `expr` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_grouping(akbasic_ASTLeaf *obj, akbasic_ASTLeaf *expr); akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_grouping(akbasic_ASTLeaf *obj, akbasic_ASTLeaf *expr);
/**
* @brief Build an integer literal leaf from its lexeme.
*
* A `0x` prefix selects base 16 and a bare leading `0` selects base 8, so `010`
* is 8 and `08` will not parse. Commodore BASIC has no octal literals; this is
* reproduced from the reference and filed as TODO.md section 6 item 10.
* @param obj Leaf to initialize; drawn from a pool by the caller.
* @param lexeme Digits as the scanner captured them.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_VALUE When the lexeme is empty or not a valid number in its base.
* @throws ERANGE When the value does not fit in an int64_t.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_literal_int(akbasic_ASTLeaf *obj, const char *lexeme); akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_literal_int(akbasic_ASTLeaf *obj, const char *lexeme);
/**
* @brief Build a float literal leaf from its lexeme.
* @param obj Leaf to initialize; drawn from a pool by the caller.
* @param lexeme Digits as the scanner captured them.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_VALUE When the lexeme is not a valid float.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_literal_float(akbasic_ASTLeaf *obj, const char *lexeme); akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_literal_float(akbasic_ASTLeaf *obj, const char *lexeme);
/**
* @brief Build a string literal leaf.
* @param obj Leaf to initialize; drawn from a pool by the caller.
* @param lexeme String contents, without the surrounding quotes.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_VALUE When the literal exceeds the length limit.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_literal_string(akbasic_ASTLeaf *obj, const char *lexeme); akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_literal_string(akbasic_ASTLeaf *obj, const char *lexeme);
/**
* @brief Build an identifier leaf.
* @param obj Leaf to initialize; drawn from a pool by the caller.
* @param leaftype Which identifier type the suffix selected.
* @param lexeme Identifier name, including its type suffix.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_VALUE When the name exceeds the length limit.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_identifier(akbasic_ASTLeaf *obj, akbasic_LeafType leaftype, const char *lexeme); akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_identifier(akbasic_ASTLeaf *obj, akbasic_LeafType leaftype, const char *lexeme);
/** @brief Render a leaf as the reference's toString() does, for tests and LIST. */ /**
* @brief Render a leaf the way the reference's toString() does.
*
* Prefix form, so `1 + 2` reads `(+ 1 2)`. Note that an assignment renders with
* an empty operator -- the reference's operator table has a case for `=` meaning
* equality but none for assignment -- so `A# = 1` reads `( A# 1)`.
*
* @param self Leaf to render.
* @param dest Output destination populated by the function.
* @param len Size of `dest`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `self` or `dest` is NULL.
* @throws AKBASIC_ERR_BOUNDS When `len` is zero.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_to_string(akbasic_ASTLeaf *self, char *dest, size_t len); akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_to_string(akbasic_ASTLeaf *self, char *dest, size_t len);
#endif // _AKBASIC_GRAMMAR_H_ #endif // _AKBASIC_GRAMMAR_H_

View File

@@ -33,28 +33,162 @@ typedef struct akbasic_Parser
akbasic_Runtime *runtime; akbasic_Runtime *runtime;
} akbasic_Parser; } akbasic_Parser;
/**
* @brief Bind a parser to a runtime.
* @param obj Object to initialize, inspect, or modify.
* @param runtime The runtime whose active environment holds the token stream.
* @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_parser_init(akbasic_Parser *obj, akbasic_Runtime *runtime); akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_init(akbasic_Parser *obj, akbasic_Runtime *runtime);
/**
* @brief Reset the token and leaf cursors for a fresh 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_parser_zero(akbasic_Parser *obj); akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_zero(akbasic_Parser *obj);
/** @brief Parse one statement from the token stream. */ /**
* @brief Parse one statement from the token stream.
*
* A line can hold more than one statement, so callers loop on
* akbasic_parser_is_at_end().
*
* @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_SYNTAX When the tokens do not parse.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_parse(akbasic_Parser *obj, akbasic_ASTLeaf **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_parse(akbasic_Parser *obj, akbasic_ASTLeaf **dest);
/**
* @brief True when the token stream for this line is spent.
* @param obj Parser to inspect; a NULL or unbound parser is at the end.
* @return `true` when there is nothing left to parse.
*/
bool akbasic_parser_is_at_end(akbasic_Parser *obj); bool akbasic_parser_is_at_end(akbasic_Parser *obj);
/* --- Internal API: the grammar rules, reachable from src/parser_commands.c. --- */ /* --- Internal API: the grammar rules, reachable from src/parser_commands.c. --- */
/**
* @brief Grammar rule: a verb and its rval, or an assignment.
*
* A verb with its own parse path in the dispatch table delegates to it; anything
* else takes an expression as its rval, and a line that is not a verb at all
* falls through to assignment().
* @param obj Object to initialize, inspect, or modify.
* @param dest Output destination populated by the function; the leaf this rule produced.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_SYNTAX When the tokens do not match the rule.
* @throws AKBASIC_ERR_BOUNDS When the per-line leaf pool is exhausted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_command(akbasic_Parser *obj, akbasic_ASTLeaf **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_command(akbasic_Parser *obj, akbasic_ASTLeaf **dest);
/**
* @brief Grammar rule: `IDENTIFIER = expression`, or a bare expression.
* @param obj Object to initialize, inspect, or modify.
* @param dest Output destination populated by the function; the leaf this rule produced.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_SYNTAX When the tokens do not match the rule.
* @throws AKBASIC_ERR_BOUNDS When the per-line leaf pool is exhausted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_assignment(akbasic_Parser *obj, akbasic_ASTLeaf **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_assignment(akbasic_Parser *obj, akbasic_ASTLeaf **dest);
/**
* @brief Grammar rule: the top of the expression chain.
* @param obj Object to initialize, inspect, or modify.
* @param dest Output destination populated by the function; the leaf this rule produced.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_SYNTAX When the tokens do not match the rule.
* @throws AKBASIC_ERR_BOUNDS When the per-line leaf pool is exhausted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_expression(akbasic_Parser *obj, akbasic_ASTLeaf **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_expression(akbasic_Parser *obj, akbasic_ASTLeaf **dest);
/**
* @brief Grammar rule: a comparison between two subtractions.
* @param obj Object to initialize, inspect, or modify.
* @param dest Output destination populated by the function; the leaf this rule produced.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_SYNTAX When the tokens do not match the rule.
* @throws AKBASIC_ERR_BOUNDS When the per-line leaf pool is exhausted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_relation(akbasic_Parser *obj, akbasic_ASTLeaf **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_relation(akbasic_Parser *obj, akbasic_ASTLeaf **dest);
/**
* @brief Grammar rule: a literal, an identifier, or a parenthesised expression.
*
* An identifier with a type suffix also consumes an array subscript list when
* one follows it.
* @param obj Object to initialize, inspect, or modify.
* @param dest Output destination populated by the function; the leaf this rule produced.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_SYNTAX When the tokens do not match the rule.
* @throws AKBASIC_ERR_BOUNDS When the per-line leaf pool is exhausted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_primary(akbasic_Parser *obj, akbasic_ASTLeaf **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_primary(akbasic_Parser *obj, akbasic_ASTLeaf **dest);
/**
* @brief Grammar rule: comma-separated expressions joined through `.right`.
*
* With `requireparens` false and no opening paren this still builds a list,
* which is how DATA and READ take a bare comma-separated series.
*
* @param obj Object to initialize, inspect, or modify.
* @param arglisttype Marks the list as function arguments or array subscripts.
* @param requireparens When true, parentheses are mandatory and unbalanced ones are an error.
* @param dest Output destination populated by the function; NULL when parens were required and absent.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_SYNTAX When `arglisttype` is wrong or a paren is unbalanced.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_argument_list(akbasic_Parser *obj, akbasic_TokenType arglisttype, bool requireparens, akbasic_ASTLeaf **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_argument_list(akbasic_Parser *obj, akbasic_TokenType arglisttype, bool requireparens, akbasic_ASTLeaf **dest);
/**
* @brief Take the next leaf from the active environment's per-line 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 the line has already used all its leaves.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_new_leaf(akbasic_Parser *obj, akbasic_ASTLeaf **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_new_leaf(akbasic_Parser *obj, akbasic_ASTLeaf **dest);
/**
* @brief Consume the next token when it is any of the given types.
* @param obj Object to initialize, inspect, or modify.
* @param types Token types to accept.
* @param count How many entries `types` holds.
* @return `true` when a token was consumed.
*/
bool akbasic_parser_match(akbasic_Parser *obj, const akbasic_TokenType *types, int count); bool akbasic_parser_match(akbasic_Parser *obj, const akbasic_TokenType *types, int count);
/**
* @brief Consume the next token when it is of one given type.
* @param obj Object to initialize, inspect, or modify.
* @param type Token type to accept.
* @return `true` when a token was consumed.
*/
bool akbasic_parser_match1(akbasic_Parser *obj, akbasic_TokenType type); bool akbasic_parser_match1(akbasic_Parser *obj, akbasic_TokenType type);
/**
* @brief The next token, without consuming it.
* @param obj Parser to inspect.
* @return The next token, or NULL at the end of the line.
*/
akbasic_Token *akbasic_parser_peek(akbasic_Parser *obj); akbasic_Token *akbasic_parser_peek(akbasic_Parser *obj);
/**
* @brief The token just consumed.
* @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_SYNTAX When nothing has been consumed yet.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_previous(akbasic_Parser *obj, akbasic_Token **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_previous(akbasic_Parser *obj, akbasic_Token **dest);
/**
* @brief Raise a parse error naming the token the parser stopped on.
*
* Always returns an error; it never succeeds. The reference also dumps a stack
* trace here, which is not reproduced -- an interpreter library does not print
* the host's stack, and the akerr trace already carries it.
*
* @param obj Object to initialize, inspect, or modify.
* @param message What was expected, appended to the position.
* @return An error context owned by the caller; never `NULL`.
* @throws AKBASIC_ERR_SYNTAX Always.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_error(akbasic_Parser *obj, const char *message); akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_error(akbasic_Parser *obj, const char *message);
#endif // _AKBASIC_PARSER_H_ #endif // _AKBASIC_PARSER_H_

View File

@@ -104,32 +104,120 @@ typedef struct akbasic_Runtime
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_init(akbasic_Runtime *obj, akbasic_TextSink *sink); akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_init(akbasic_Runtime *obj, akbasic_TextSink *sink);
/** @brief Reset the per-line state without disturbing the program or variables. */ /**
* @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); 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); 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); akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_prev_environment(akbasic_Runtime *obj);
/** @brief Report a BASIC error on the current line, in the reference's format. */ /**
* @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); akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_error(akbasic_Runtime *obj, akbasic_ErrorClass errclass, const char *message);
/** @brief Write text, mirroring the reference's Write(). */ /**
* @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); akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_write(akbasic_Runtime *obj, const char *text);
/** @brief Write text and a newline, mirroring the reference's Println(). */ /**
* @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); 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); akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_set_mode(akbasic_Runtime *obj, int mode);
/** @brief Evaluate one AST leaf, drawing scratch values from the environment. */ /**
* @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); 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 command a REPL may run immediately. */ /**
* @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); 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. */ /**
* @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); akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_interpret(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value **dest);
/** @brief Call a user-defined function or subroutine. */ /**
* @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); akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_user_function(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value **dest);
/** /**
@@ -137,16 +225,38 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_user_function(akbasic_Runtime
* *
* One call reads or runs at most one source line. It never blocks beyond a * One call reads or runs at most one source line. It never blocks beyond a
* single readline on the sink, and it always returns. * 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); akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_step(akbasic_Runtime *obj);
/** /**
* @brief Step until the runtime quits or `maxsteps` steps have elapsed. * @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. * @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); akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_run(akbasic_Runtime *obj, int maxsteps);
/** @brief Point the runtime at an input stream and choose a starting mode. */ /**
* @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); akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_start(akbasic_Runtime *obj, int mode);
/** /**
@@ -173,12 +283,60 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_load(akbasic_Runtime *obj, co
/* --- Internal API: exposed for the scanner, parser and verb handlers only. --- */ /* --- Internal API: exposed for the scanner, parser and verb handlers only. --- */
/**
* @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); akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_new_variable(akbasic_Runtime *obj, akbasic_Variable **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); 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.
* @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); akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_store_line(akbasic_Runtime *obj, int64_t lineno, 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); 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); 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); 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); int64_t akbasic_runtime_find_previous_lineno(akbasic_Runtime *obj);
#endif // _AKBASIC_RUNTIME_H_ #endif // _AKBASIC_RUNTIME_H_

View File

@@ -76,26 +76,196 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_set_bool(akbasic_Value *obj, bo
/** @brief True when the value is a boolean holding AKBASIC_TRUE. */ /** @brief True when the value is a boolean holding AKBASIC_TRUE. */
bool akbasic_value_is_true(akbasic_Value *self); bool akbasic_value_is_true(akbasic_Value *self);
/**
* @brief Negate a numeric value.
*
* Negates both the integer and float payloads, so it is correct whichever one
* the value is carrying.
* @param self Operand.
* @param scratch A value drawn from the caller's pool for the result to land in.
* @param dest Output destination populated by the function; set to `scratch`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_TYPE When `self` is a string.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_invert(akbasic_Value *self, akbasic_Value *scratch, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_invert(akbasic_Value *self, akbasic_Value *scratch, akbasic_Value **dest);
/**
* @brief One's complement of an integer value.
* @param self Operand.
* @param scratch A value drawn from the caller's pool for the result to land in.
* @param dest Output destination populated by the function; set to `scratch`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_TYPE When `self` is not an integer.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_bitwise_not(akbasic_Value *self, akbasic_Value *scratch, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_bitwise_not(akbasic_Value *self, akbasic_Value *scratch, akbasic_Value **dest);
/**
* @brief Shift an integer value left.
*
* Refuses a count outside 0..63. Go defines the result for any count; in C it
* would be undefined behaviour, so this is a deliberate deviation.
* @param self Operand; must be an integer.
* @param bits Number of bit positions to shift by; must be 0..63.
* @param scratch A value drawn from the caller's pool for the result to land in.
* @param dest Output destination populated by the function; set to `scratch`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_TYPE When `self` is not an integer.
* @throws AKBASIC_ERR_VALUE When `bits` is outside 0..63.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_shift_left(akbasic_Value *self, int64_t bits, akbasic_Value *scratch, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_shift_left(akbasic_Value *self, int64_t bits, akbasic_Value *scratch, akbasic_Value **dest);
/**
* @brief Shift an integer value right.
* @param self Operand; must be an integer.
* @param bits Number of bit positions to shift by; must be 0..63.
* @param scratch A value drawn from the caller's pool for the result to land in.
* @param dest Output destination populated by the function; set to `scratch`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_TYPE When `self` is not an integer.
* @throws AKBASIC_ERR_VALUE When `bits` is outside 0..63.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_shift_right(akbasic_Value *self, int64_t bits, akbasic_Value *scratch, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_shift_right(akbasic_Value *self, int64_t bits, akbasic_Value *scratch, akbasic_Value **dest);
/**
* @brief Bitwise AND of two integer values.
* @param self Left operand, and the value whose type selects the operation.
* @param rval Right operand.
* @param scratch A value drawn from the caller's pool for the result to land in.
* @param dest Output destination populated by the function; set to `scratch`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_TYPE When `self` is not an integer.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_bitwise_and(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_bitwise_and(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest);
/**
* @brief Bitwise OR of two integer values.
* @param self Left operand, and the value whose type selects the operation.
* @param rval Right operand.
* @param scratch A value drawn from the caller's pool for the result to land in.
* @param dest Output destination populated by the function; set to `scratch`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_TYPE When `self` is not an integer.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_bitwise_or(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_bitwise_or(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest);
/**
* @brief Bitwise exclusive OR of two integer values.
* @param self Left operand, and the value whose type selects the operation.
* @param rval Right operand.
* @param scratch A value drawn from the caller's pool for the result to land in.
* @param dest Output destination populated by the function; set to `scratch`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_TYPE When either operand is not an integer.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_bitwise_xor(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_bitwise_xor(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest);
/**
* @brief Add two values, or concatenate when the left operand is a string.
*
* The one operator that does not always clone: when `self` is mutable the result
* is written into `self` and `*dest` is set to it rather than to `scratch`.
* CommandNEXT's loop increment depends on that, so it is not an oversight --
* see TODO.md section 6 item 4.
* @param self Left operand, and the value whose type selects the operation.
* @param rval Right operand.
* @param scratch A value drawn from the caller's pool for the result to land in.
* @param dest Output destination populated by the function; set to `scratch`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_TYPE When the operand types have no defined addition.
* @throws AKBASIC_ERR_VALUE When a concatenated string exceeds the length limit.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_math_plus(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_math_plus(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest);
/**
* @brief Subtract one numeric value from another.
* @param self Left operand, and the value whose type selects the operation.
* @param rval Right operand.
* @param scratch A value drawn from the caller's pool for the result to land in.
* @param dest Output destination populated by the function; set to `scratch`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_TYPE When either operand is a string.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_math_minus(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_math_minus(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest);
/**
* @brief Multiply two numeric values, or repeat a string.
*
* A string left operand is repeated `rval->intval` times, which is what makes
* `" " * 5` work.
* @param self Left operand, and the value whose type selects the operation.
* @param rval Right operand.
* @param scratch A value drawn from the caller's pool for the result to land in.
* @param dest Output destination populated by the function; set to `scratch`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_TYPE When both operands are strings.
* @throws AKBASIC_ERR_VALUE When the multiplier is negative or the result would overflow the length limit.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_math_multiply(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_math_multiply(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest);
/**
* @brief Divide one numeric value by another.
* @param self Left operand, and the value whose type selects the operation.
* @param rval Right operand.
* @param scratch A value drawn from the caller's pool for the result to land in.
* @param dest Output destination populated by the function; set to `scratch`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_TYPE When either operand is a string.
* @throws AKBASIC_ERR_VALUE On integer division by zero.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_math_divide(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_math_divide(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest);
/**
* @brief Compare two values for less-than, yielding a BASIC boolean.
* @param self Left operand, and the value whose type selects the operation.
* @param rval Right operand.
* @param scratch A value drawn from the caller's pool for the result to land in.
* @param dest Output destination populated by the function; set to `scratch`.
* @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_value_less_than(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_less_than(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest);
/**
* @brief Compare two values for less-than-or-equal, yielding a BASIC boolean.
* @param self Left operand, and the value whose type selects the operation.
* @param rval Right operand.
* @param scratch A value drawn from the caller's pool for the result to land in.
* @param dest Output destination populated by the function; set to `scratch`.
* @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_value_less_than_equal(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_less_than_equal(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest);
/**
* @brief Compare two values for greater-than, yielding a BASIC boolean.
* @param self Left operand, and the value whose type selects the operation.
* @param rval Right operand.
* @param scratch A value drawn from the caller's pool for the result to land in.
* @param dest Output destination populated by the function; set to `scratch`.
* @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_value_greater_than(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_greater_than(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest);
/**
* @brief Compare two values for greater-than-or-equal, yielding a BASIC boolean.
* @param self Left operand, and the value whose type selects the operation.
* @param rval Right operand.
* @param scratch A value drawn from the caller's pool for the result to land in.
* @param dest Output destination populated by the function; set to `scratch`.
* @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_value_greater_than_equal(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_greater_than_equal(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest);
/**
* @brief Compare two values for equality, yielding a BASIC boolean.
* @param self Left operand, and the value whose type selects the operation.
* @param rval Right operand.
* @param scratch A value drawn from the caller's pool for the result to land in.
* @param dest Output destination populated by the function; set to `scratch`.
* @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_value_is_equal(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_is_equal(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest);
/**
* @brief Compare two values for inequality, yielding a BASIC boolean.
* @param self Left operand, and the value whose type selects the operation.
* @param rval Right operand.
* @param scratch A value drawn from the caller's pool for the result to land in.
* @param dest Output destination populated by the function; set to `scratch`.
* @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_value_is_not_equal(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_is_not_equal(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest);
#endif // _AKBASIC_VALUE_H_ #endif // _AKBASIC_VALUE_H_

View File

@@ -41,7 +41,12 @@ typedef struct
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_variable_init(akbasic_Variable *obj, akbasic_ValuePool *pool, int64_t *sizes, int sizecount); akerr_ErrorContext AKERR_NOIGNORE *akbasic_variable_init(akbasic_Variable *obj, akbasic_ValuePool *pool, int64_t *sizes, int sizecount);
/** @brief Mark the variable undefined and mutable without touching its storage. */ /**
* @brief Mark the variable undefined and mutable without touching its storage.
* @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_variable_zero(akbasic_Variable *obj); akerr_ErrorContext AKERR_NOIGNORE *akbasic_variable_zero(akbasic_Variable *obj);
/** /**
@@ -55,11 +60,49 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_variable_zero(akbasic_Variable *obj);
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_variable_get_subscript(akbasic_Variable *obj, int64_t *subscripts, int subscriptcount, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_variable_get_subscript(akbasic_Variable *obj, int64_t *subscripts, int subscriptcount, akbasic_Value **dest);
/** @brief Copy a value into the slot a subscript list addresses. */ /**
* @brief Copy a value into the slot a subscript list addresses.
* @param obj Object to initialize, inspect, or modify.
* @param value Source value; cloned into the slot, not aliased.
* @param subscripts Index per dimension.
* @param subscriptcount Number of indices supplied; must equal the dimension count.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `value` is NULL.
* @throws AKBASIC_ERR_BOUNDS When the count is wrong or an index is out of range.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_variable_set_subscript(akbasic_Variable *obj, akbasic_Value *value, int64_t *subscripts, int subscriptcount); akerr_ErrorContext AKERR_NOIGNORE *akbasic_variable_set_subscript(akbasic_Variable *obj, akbasic_Value *value, int64_t *subscripts, int subscriptcount);
/**
* @brief Store an integer into the slot a subscript list addresses.
* @param obj Object to initialize, inspect, or modify.
* @param value Value to store.
* @param subscripts Index per dimension.
* @param subscriptcount Number of indices supplied; must equal the dimension count.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_BOUNDS When the count is wrong or an index is out of range.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_variable_set_integer(akbasic_Variable *obj, int64_t value, int64_t *subscripts, int subscriptcount); akerr_ErrorContext AKERR_NOIGNORE *akbasic_variable_set_integer(akbasic_Variable *obj, int64_t value, int64_t *subscripts, int subscriptcount);
/**
* @brief Store a float into the slot a subscript list addresses.
* @param obj Object to initialize, inspect, or modify.
* @param value Value to store.
* @param subscripts Index per dimension.
* @param subscriptcount Number of indices supplied; must equal the dimension count.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKBASIC_ERR_BOUNDS When the count is wrong or an index is out of range.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_variable_set_float(akbasic_Variable *obj, double value, int64_t *subscripts, int subscriptcount); akerr_ErrorContext AKERR_NOIGNORE *akbasic_variable_set_float(akbasic_Variable *obj, double value, int64_t *subscripts, int subscriptcount);
/**
* @brief Store a string into the slot a subscript list addresses.
* @param obj Object to initialize, inspect, or modify.
* @param value Value to store.
* @param subscripts Index per dimension.
* @param subscriptcount Number of indices supplied; must equal the dimension count.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `value` is NULL.
* @throws AKBASIC_ERR_VALUE When the string exceeds the length limit.
* @throws AKBASIC_ERR_BOUNDS When the count is wrong or an index is out of range.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_variable_set_string(akbasic_Variable *obj, const char *value, int64_t *subscripts, int subscriptcount); akerr_ErrorContext AKERR_NOIGNORE *akbasic_variable_set_string(akbasic_Variable *obj, const char *value, int64_t *subscripts, int subscriptcount);
#endif // _AKBASIC_VARIABLE_H_ #endif // _AKBASIC_VARIABLE_H_