Port the BASIC interpreter from Go to C

Reproduces deps/basicinterpret in C, in the idiom of the ak* libraries. All 41
.bas files in the reference's corpus produce byte-identical stdout, including
the trailing double newline on an error line -- that comes from basicError
building a string ending in \n and handing it to Println, and
array_outofbounds.txt encodes it.

The corpus is driven in place from the submodule as 41 individual CTest cases
rather than copied, so it cannot drift from upstream. Eighteen unit tests cover
what the corpus cannot reach.

Three structural changes carry most of the work. Go's three reflection lookups
(Command*, Function*, ParseCommand*) become one sorted dispatch table in
src/verbs.c searched with bsearch; adding a verb is a row and two functions. The
five Go maps become one fixed open-addressed table over aksl_strhash_djb2. And
run(), which owned the process until MODE_QUIT, splits into step() plus a
bounded run() -- goal 3 requires a host game to be able to bound execution, and
nothing in the library now terminates the process or touches SDL.

Output goes through an akbasic_TextSink vtable. src/sink_stdio.c is what makes
the corpus runnable with no SDL present; the akgl-backed sink is still to come
and is blocked on libakgl having no text-measurement call.

src/convert.c exists because libakstdlib's aksl_ato* family cannot report a
conversion failure (its TODO.md 2.1.5). The reference checks strconv's error at
four sites and turns it into a BASIC error; routing those through aksl_atoi
would have turned four diagnosable errors into wrong answers, with VAL("garbage")
quietly returning 0. TODO.md 1.9 records which libakstdlib calls are cleared for
use here and which are not.

Reference defects are reproduced, not fixed: the golden files encode the observed
behaviour and a silent correction is a behaviour change. TODO.md section 6 lists
sixteen, and tests/known_reference_defects.c asserts the *correct* contract for
six of them under AKBASIC_KNOWN_FAILING_TESTS, so a fix shows up as
"unexpectedly passed". Five of the sixteen were found by this port and are new:
subtraction stops after one operator so 1-2-3 computes 1-2 and abandons the rest
of the line (a wrong answer, not a refused one); a unary-minus argument inflates
a function's arity so ABS(-9) is rejected; a comparison operator in a line's
final column is dropped; hex literals never survive the scanner; and the
"Reserved word in variable name" check is dead code.

Where the reference reaches undefined behaviour by a route that is defined in Go
-- an out-of-range shift, a negative string multiplier, integer division by zero
-- this raises instead of inheriting the UB. No golden case exercises any of
them.

The top-level CMakeLists shadows add_test, set_tests_properties and
add_custom_target around all three add_subdirectory calls. Without it libakerror's
tests land in our suite as Not Run, and its un-namespaced `coverage` target stops
a coverage build from configuring at all. Test targets are akbasic_test_<name>:
bare test_<name> collides with libakstdlib's, which is what broke libakgl's
configure in c2b16d3.

ctest 59/59; ASan+UBSan 59/59; 92.3% line and 96.9% function coverage; no
warnings under -Wall -Wextra. Branch coverage is not a target, for the reason
libakstdlib and libakgl both record: the akerror macros expand into large branch
trees at every call site.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-30 23:53:56 -04:00
commit f8c15c2f2c
60 changed files with 10192 additions and 0 deletions

46
include/akbasic/convert.h Normal file
View File

@@ -0,0 +1,46 @@
/**
* @file convert.h
* @brief Declares string-to-number conversion that can actually report failure.
*
* This exists because libakstdlib's aksl_atoi/atol/atoll/atof family cannot:
* atoi("not a number") returns success with 0, and an overflowing literal
* returns success with a wrapped value (deps/libakstdlib/TODO.md 2.1.5). The Go
* reference checks the conversion error at every numeric site and turns it into
* a BASIC error, so porting onto that family would convert four diagnosable
* errors into wrong answers.
*
* Delete this file and switch the call sites over when libakstdlib grows the
* stricter contract its TODO.md already specifies.
*/
#ifndef _AKBASIC_CONVERT_H_
#define _AKBASIC_CONVERT_H_
#include <stdint.h>
#include <akerror.h>
/**
* @brief Convert a complete string to a 64-bit integer.
* @param str Source string; must be entirely consumed.
* @param base Numeric base, or 0 to let strtoll infer it from a 0x/0 prefix.
* @param dest Output destination populated by the function.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `str` or `dest` is NULL.
* @throws AKBASIC_ERR_VALUE When no digits were consumed or trailing junk remains.
* @throws ERANGE When the value does not fit in an int64_t.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_str_to_int64(const char *str, int base, int64_t *dest);
/**
* @brief Convert a complete string to a double.
* @param str Source string; must be entirely consumed.
* @param dest Output destination populated by the function.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `str` or `dest` is NULL.
* @throws AKBASIC_ERR_VALUE When no digits were consumed or trailing junk remains.
* @throws ERANGE When the value is out of range for a double.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_str_to_double(const char *str, double *dest);
#endif // _AKBASIC_CONVERT_H_

View File

@@ -0,0 +1,116 @@
/**
* @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>
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;
/* Loop bounds */
int64_t loopFirstLine;
int64_t loopExitLine;
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;
akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_init(akbasic_Environment *obj, struct akbasic_Runtime *runtime, akbasic_Environment *parent);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_zero(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. */
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);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_wait_for_command(akbasic_Environment *obj, const char *command);
bool akbasic_environment_is_waiting_for_any(akbasic_Environment *obj);
bool akbasic_environment_is_waiting_for(akbasic_Environment *obj, const char *command);
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.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_get(akbasic_Environment *obj, const char *varname, akbasic_Variable **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_get_label(akbasic_Environment *obj, const char *label, int64_t *dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_set_label(akbasic_Environment *obj, const char *label, int64_t value);
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_

71
include/akbasic/error.h Normal file
View File

@@ -0,0 +1,71 @@
/**
* @file error.h
* @brief Declares akbasic's status codes and the registry claim for them.
*/
#ifndef _AKBASIC_ERROR_H_
#define _AKBASIC_ERROR_H_
#include <akerror.h>
/*
* libakerror 1.0.0 is the floor. That release moved the status-name table into a
* private registry -- AKERR_MAX_ERR_VALUE and __AKERR_ERROR_NAMES are gone, the
* registry entry points raise akerr_ErrorContext * instead of returning int, and
* the library gained an soname -- so a translation unit that pairs this header
* with a pre-1.0.0 akerror.h is an ABI mismatch, not just a compile problem.
*
* libakerror publishes no version macro, so this feature-tests on
* AKERR_FIRST_CONSUMER_STATUS, which that release introduced. Same guard
* libakstdlib and libakgl already carry.
*/
#ifndef AKERR_FIRST_CONSUMER_STATUS
#error "libakbasic requires libakerror >= 1.0.0: the akerror.h on the include path predates the status registry. Rebuild and reinstall libakerror."
#endif
/*
* libakerror reserves 0-255 for the host's errno values and its own AKERR_*
* codes. libakgl claims 256-260. akbasic claims 512-767, leaving 261-511 as
* headroom for libakgl to grow into -- see CLAUDE.md for the coordinated range
* map, which is the only coordination there is: libakerror can enumerate its
* consumers no better than we can.
*
* These are absolute constants, never offsets from AKERR_LAST_ERRNO_VALUE, so a
* libc that grows an errno cannot move them. An enum rather than a chain of
* #defines so the values stay compile-time integer constants -- HANDLE expands
* to case labels, which require that.
*
* Add a code here and you must name it in akbasic_error_register(), or it prints
* as "Unknown Error" in every stack trace that carries it.
*/
#define AKBASIC_OWNER "akbasic"
enum {
AKBASIC_ERR_BASE = 512, /** Start of akbasic's reserved status range */
AKBASIC_ERR_SYNTAX = AKBASIC_ERR_BASE,
/** Parse-time grammar violation */
AKBASIC_ERR_TYPE, /** Incompatible types in an operation */
AKBASIC_ERR_UNDEFINED, /** Reference to an undefined verb, function or label */
AKBASIC_ERR_BOUNDS, /** Array subscript or pool index out of range */
AKBASIC_ERR_ENVIRONMENT, /** Environment pool exhausted or orphaned environment */
AKBASIC_ERR_VALUE, /** A value was malformed, truncated or unconvertible */
AKBASIC_ERR_STATE, /** A verb ran outside the block structure it requires */
AKBASIC_ERR_LIMIT = AKBASIC_ERR_BASE + 256
};
/**
* @brief Claim the akbasic status band and register a name for every code in it.
*
* Reserves the whole 256-value range in one call -- libakerror treats only an
* identical reservation as a repeat, so a subset or superset raises -- and then
* names each code through the owned entry point. Call it before anything else in
* the library. Repeating the call is a no-op.
*
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_STATUS_RANGE_OVERLAP When another component already owns part of the band.
* @throws AKERR_STATUS_RANGE_FULL When libakerror has no reservation slots left.
* @throws AKERR_STATUS_NAME_FULL When libakerror's name registry is full.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_error_register(void);
#endif // _AKBASIC_ERROR_H_

163
include/akbasic/grammar.h Normal file
View File

@@ -0,0 +1,163 @@
/**
* @file grammar.h
* @brief Declares the token and AST leaf types the scanner and parser share.
*
* The reference splits these across basicscanner.go (BasicTokenType),
* basicparser.go (BasicToken) and basicgrammar.go (BasicASTLeafType,
* BasicASTLeaf). They are one header here because a leaf's operator *is* a token
* type and separating them buys nothing in C. The numeric values are preserved
* from the reference so a debugging session against either implementation reads
* the same.
*/
#ifndef _AKBASIC_GRAMMAR_H_
#define _AKBASIC_GRAMMAR_H_
#include <akerror.h>
#include <akbasic/types.h>
typedef enum
{
AKBASIC_TOK_UNDEFINED = 0,
AKBASIC_TOK_EQUAL, /* 1 */
AKBASIC_TOK_LESS_THAN, /* 2 */
AKBASIC_TOK_LESS_THAN_EQUAL, /* 3 */
AKBASIC_TOK_GREATER_THAN, /* 4 */
AKBASIC_TOK_GREATER_THAN_EQUAL, /* 5 */
AKBASIC_TOK_COMMA, /* 6 */
AKBASIC_TOK_HASH, /* 7 */
AKBASIC_TOK_NOT_EQUAL, /* 8 */
AKBASIC_TOK_LEFT_PAREN, /* 9 */
AKBASIC_TOK_RIGHT_PAREN, /* 10 */
AKBASIC_TOK_PLUS, /* 11 */
AKBASIC_TOK_MINUS, /* 12 */
AKBASIC_TOK_LEFT_SLASH, /* 13 */
AKBASIC_TOK_STAR, /* 14 */
AKBASIC_TOK_CARAT, /* 15 */
AKBASIC_TOK_LITERAL_STRING, /* 16 */
AKBASIC_TOK_LITERAL_INT, /* 17 */
AKBASIC_TOK_LITERAL_FLOAT, /* 18 */
AKBASIC_TOK_IDENTIFIER, /* 19 */
AKBASIC_TOK_IDENTIFIER_STRING, /* 20 */
AKBASIC_TOK_IDENTIFIER_FLOAT, /* 21 */
AKBASIC_TOK_IDENTIFIER_INT, /* 22 */
AKBASIC_TOK_COLON, /* 23 */
AKBASIC_TOK_AND, /* 24 */
AKBASIC_TOK_NOT, /* 25 */
AKBASIC_TOK_OR, /* 26 */
AKBASIC_TOK_REM, /* 27 */
AKBASIC_TOK_EOL, /* 28 */
AKBASIC_TOK_EOF, /* 29 */
AKBASIC_TOK_LINE_NUMBER, /* 30 -- an integer literal in token position 0 */
AKBASIC_TOK_COMMAND, /* 31 */
AKBASIC_TOK_COMMAND_IMMEDIATE, /* 32 */
AKBASIC_TOK_FUNCTION, /* 33 */
AKBASIC_TOK_ASSIGNMENT, /* 34 */
AKBASIC_TOK_LEFT_SQUAREBRACKET, /* 35 */
AKBASIC_TOK_RIGHT_SQUAREBRACKET, /* 36 */
AKBASIC_TOK_ARRAY_SUBSCRIPT, /* 37 */
AKBASIC_TOK_FUNCTION_ARGUMENT, /* 38 */
AKBASIC_TOK_ATSYMBOL, /* 39 */
AKBASIC_TOK_IDENTIFIER_STRUCT /* 40 */
} akbasic_TokenType;
typedef enum
{
AKBASIC_LEAF_UNDEFINED = 0,
AKBASIC_LEAF_LITERAL_INT, /* 1 */
AKBASIC_LEAF_LITERAL_FLOAT, /* 2 */
AKBASIC_LEAF_LITERAL_STRING, /* 3 */
AKBASIC_LEAF_IDENTIFIER, /* 4 */
AKBASIC_LEAF_IDENTIFIER_INT, /* 5 */
AKBASIC_LEAF_IDENTIFIER_FLOAT, /* 6 */
AKBASIC_LEAF_IDENTIFIER_STRING, /* 7 */
AKBASIC_LEAF_UNARY, /* 8 */
AKBASIC_LEAF_BINARY, /* 9 */
AKBASIC_LEAF_GROUPING, /* 10 */
AKBASIC_LEAF_EQUALITY, /* 11 */
AKBASIC_LEAF_COMPARISON, /* 12 */
AKBASIC_LEAF_TERM, /* 13 */
AKBASIC_LEAF_PRIMARY, /* 14 */
AKBASIC_LEAF_COMMAND, /* 15 */
AKBASIC_LEAF_COMMAND_IMMEDIATE, /* 16 */
AKBASIC_LEAF_FUNCTION, /* 17 */
AKBASIC_LEAF_BRANCH, /* 18 */
AKBASIC_LEAF_ARGUMENTLIST, /* 19 */
AKBASIC_LEAF_IDENTIFIER_STRUCT /* 20 */
} akbasic_LeafType;
typedef struct
{
akbasic_TokenType tokentype;
int64_t lineno;
char lexeme[AKBASIC_MAX_LINE_LENGTH];
} akbasic_Token;
typedef struct akbasic_ASTLeaf
{
akbasic_LeafType leaftype;
int64_t literal_int;
char literal_string[AKBASIC_MAX_STRING_LENGTH];
double literal_float;
char identifier[AKBASIC_MAX_STRING_LENGTH];
akbasic_TokenType operator_;
struct akbasic_ASTLeaf *parent;
struct akbasic_ASTLeaf *left;
struct akbasic_ASTLeaf *right;
struct akbasic_ASTLeaf *expr;
} akbasic_ASTLeaf;
/**
* @brief A pool of leaves a deep clone can draw from.
*
* The reference's BasicASTLeaf.clone() recurses and allocates. Here the caller
* supplies storage and a deep clone that would exceed it fails rather than
* growing without bound.
*/
typedef struct
{
int next;
int capacity;
akbasic_ASTLeaf *leaves;
} akbasic_LeafPool;
akerr_ErrorContext AKERR_NOIGNORE *akbasic_token_init(akbasic_Token *obj);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_init(akbasic_ASTLeaf *obj, akbasic_LeafType leaftype);
/**
* @brief Deep-copy a leaf and everything hanging off it into pool storage.
* @param self Source leaf.
* @param pool Storage the copies are drawn from.
* @param dest Output destination populated by the function.
* @throws AKBASIC_ERR_BOUNDS When the pool runs out of leaves.
*/
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. */
akbasic_ASTLeaf *akbasic_leaf_first_argument(akbasic_ASTLeaf *self);
/** @brief The first subscript of an array-reference leaf, or NULL. */
akbasic_ASTLeaf *akbasic_leaf_first_subscript(akbasic_ASTLeaf *self);
bool akbasic_leaf_is_identifier(akbasic_ASTLeaf *self);
bool akbasic_leaf_is_literal(akbasic_ASTLeaf *self);
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_binary(akbasic_ASTLeaf *obj, akbasic_ASTLeaf *left, akbasic_TokenType op, akbasic_ASTLeaf *right);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_unary(akbasic_ASTLeaf *obj, akbasic_TokenType op, akbasic_ASTLeaf *right);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_function(akbasic_ASTLeaf *obj, const char *fname, akbasic_ASTLeaf *right);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_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);
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_grouping(akbasic_ASTLeaf *obj, akbasic_ASTLeaf *expr);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_literal_int(akbasic_ASTLeaf *obj, const char *lexeme);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_literal_float(akbasic_ASTLeaf *obj, const char *lexeme);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_new_literal_string(akbasic_ASTLeaf *obj, 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. */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_leaf_to_string(akbasic_ASTLeaf *self, char *dest, size_t len);
#endif // _AKBASIC_GRAMMAR_H_

60
include/akbasic/parser.h Normal file
View File

@@ -0,0 +1,60 @@
/**
* @file parser.h
* @brief Declares the recursive-descent parser.
*
* The hierarchy is as-per "Commodore 128 Programmer's Reference Guide" page 23,
* copied verbatim from the reference because it is the specification:
*
* program -> line*
* line -> (line_number ( command | expression )) (immediate_command expression)
* command -> command (expression)
* expression -> logicalandor
* logicalandor -> logicalnot ( "OR" "AND" ) logicalnot
* logicalnot -> "NOT" relation
* relation -> subtraction* [ < <= = <> >= > ] subtraction*
* subtraction -> addition* "-" addition*
* addition -> multiplication* "+" multiplication*
* multiplication -> division* "*" division*
* division -> unary* "/" unary*
* unary -> "-" exponent
* primary -> IDENTIFIER | LITERAL_INT | LITERAL_FLOAT | LITERAL_STRING | "(" expression ")"
*/
#ifndef _AKBASIC_PARSER_H_
#define _AKBASIC_PARSER_H_
#include <akerror.h>
#include <akbasic/grammar.h>
#include <akbasic/runtime.h>
typedef struct akbasic_Parser
{
akbasic_Runtime *runtime;
} akbasic_Parser;
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_init(akbasic_Parser *obj, akbasic_Runtime *runtime);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_zero(akbasic_Parser *obj);
/** @brief Parse one statement from the token stream. */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_parse(akbasic_Parser *obj, akbasic_ASTLeaf **dest);
bool akbasic_parser_is_at_end(akbasic_Parser *obj);
/* --- Internal API: the grammar rules, reachable from src/parser_commands.c. --- */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_command(akbasic_Parser *obj, akbasic_ASTLeaf **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_assignment(akbasic_Parser *obj, akbasic_ASTLeaf **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_expression(akbasic_Parser *obj, akbasic_ASTLeaf **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_relation(akbasic_Parser *obj, akbasic_ASTLeaf **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_primary(akbasic_Parser *obj, akbasic_ASTLeaf **dest);
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_new_leaf(akbasic_Parser *obj, akbasic_ASTLeaf **dest);
bool akbasic_parser_match(akbasic_Parser *obj, const akbasic_TokenType *types, int count);
bool akbasic_parser_match1(akbasic_Parser *obj, akbasic_TokenType type);
akbasic_Token *akbasic_parser_peek(akbasic_Parser *obj);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_previous(akbasic_Parser *obj, akbasic_Token **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parser_error(akbasic_Parser *obj, const char *message);
#endif // _AKBASIC_PARSER_H_

162
include/akbasic/runtime.h Normal file
View File

@@ -0,0 +1,162 @@
/**
* @file runtime.h
* @brief Declares the interpreter: source, pools, scanner, parser and evaluator.
*
* Ported from basicruntime.go, minus SDL and minus the for{} loop that owned the
* process. The reference's run() does not return until MODE_QUIT; here
* akbasic_runtime_step() executes exactly one iteration of that loop and returns,
* and akbasic_runtime_run() bounds it. A host game must be able to step or bound
* execution rather than surrender control, so the loop belongs to the caller.
*
* Nothing in this library terminates the process. Errors propagate out as
* akerr_ErrorContext * for the host to handle; FINISH_NORETURN belongs only to
* the driver's main().
*/
#ifndef _AKBASIC_RUNTIME_H_
#define _AKBASIC_RUNTIME_H_
#include <akerror.h>
#include <akbasic/environment.h>
#include <akbasic/grammar.h>
#include <akbasic/sink.h>
#include <akbasic/types.h>
#include <akbasic/value.h>
#include <akbasic/variable.h>
/** @brief The BASIC-visible error classes, used to build the "? n : CLASS msg" line. */
typedef enum
{
AKBASIC_ERRCLASS_NONE = 0,
AKBASIC_ERRCLASS_IO,
AKBASIC_ERRCLASS_PARSE,
AKBASIC_ERRCLASS_SYNTAX,
AKBASIC_ERRCLASS_RUNTIME
} akbasic_ErrorClass;
typedef struct
{
char code[AKBASIC_MAX_LINE_LENGTH];
int64_t lineno;
} akbasic_SourceLine;
/** @brief A user-defined subroutine or single-expression function. */
typedef struct
{
char name[AKBASIC_SYMTAB_MAX_KEY];
akbasic_ASTLeaf *arglist;
akbasic_ASTLeaf *expression;
int64_t lineno;
akbasic_Environment *environment;
/* Deep copies of the arglist and expression need storage that outlives the line. */
akbasic_ASTLeaf leafstorage[AKBASIC_MAX_LEAVES * 2];
akbasic_LeafPool leafpool;
bool used;
} akbasic_FunctionDef;
typedef struct akbasic_Runtime
{
akbasic_SourceLine source[AKBASIC_MAX_SOURCE_LINES];
/* Pools. Nothing here is malloc'd; everything is drawn from and returned. */
akbasic_Environment environments[AKBASIC_MAX_ENVIRONMENTS];
akbasic_Variable variables[AKBASIC_MAX_VARIABLES];
akbasic_FunctionDef functions[AKBASIC_MAX_FUNCTIONS];
akbasic_ValuePool valuepool;
akbasic_Value staticTrueValue;
akbasic_Value staticFalseValue;
int mode;
int run_finished_mode;
akbasic_ErrorClass errclass;
int64_t autoLineNumber;
/*
* When false, evaluating an identifier yields the live value rather than a
* clone. POKE and POINTER need the address of the real storage. The
* reference declares this on both the runtime and the environment and only
* ever reads the runtime's; the environment copy is dropped here.
*/
bool eval_clone_identifiers;
akbasic_Environment *environment;
akbasic_TextSink *sink;
/* REPL line assembly */
char userline[AKBASIC_MAX_LINE_LENGTH];
/* Scanner state */
char line[AKBASIC_MAX_LINE_LENGTH];
int current;
int start;
akbasic_TokenType tokentype;
bool hasError;
bool inputEof;
} akbasic_Runtime;
/**
* @brief Bring a runtime up: register status codes, build the root environment.
* @param obj Object to initialize, inspect, or modify.
* @param sink Where output goes and input comes from; required.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_init(akbasic_Runtime *obj, akbasic_TextSink *sink);
/** @brief Reset the per-line state without disturbing the program or variables. */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_zero(akbasic_Runtime *obj);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_new_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. */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_error(akbasic_Runtime *obj, akbasic_ErrorClass errclass, const char *message);
/** @brief Write text, mirroring the reference's Write(). */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_write(akbasic_Runtime *obj, const char *text);
/** @brief Write text and a newline, mirroring the reference's Println(). */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_println(akbasic_Runtime *obj, const char *text);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_set_mode(akbasic_Runtime *obj, int mode);
/** @brief Evaluate one AST leaf, drawing scratch values from the environment. */
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. */
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. */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_interpret(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value **dest);
/** @brief Call a user-defined function or subroutine. */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_user_function(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value **dest);
/**
* @brief Execute exactly one iteration of the reference's run() loop.
*
* One call reads or runs at most one source line. It never blocks beyond a
* single readline on the sink, and it always returns.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_step(akbasic_Runtime *obj);
/**
* @brief Step until the runtime quits or `maxsteps` steps have elapsed.
* @param maxsteps Step ceiling; zero or negative means unbounded.
*/
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. */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_start(akbasic_Runtime *obj, int mode);
/* --- Internal API: exposed for the scanner, parser and verb handlers only. --- */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_new_variable(akbasic_Runtime *obj, akbasic_Variable **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_new_function(akbasic_Runtime *obj, akbasic_FunctionDef **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_store_line(akbasic_Runtime *obj, int64_t lineno, const char *code);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_process_line_run(akbasic_Runtime *obj);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_process_line_runstream(akbasic_Runtime *obj);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_process_line_repl(akbasic_Runtime *obj);
int64_t akbasic_runtime_find_previous_lineno(akbasic_Runtime *obj);
#endif // _AKBASIC_RUNTIME_H_

39
include/akbasic/scanner.h Normal file
View File

@@ -0,0 +1,39 @@
/**
* @file scanner.h
* @brief Declares the line tokenizer.
*
* Ported from basicscanner.go. The scanner's own cursor state lives on the
* runtime rather than in a separate struct -- the reference's BasicScanner holds
* a back-pointer to the runtime and reaches through it for the token array
* anyway, so a second object bought nothing.
*
* Its three keyword maps are gone: the token type for a name comes from the one
* dispatch table in verbs.h.
*/
#ifndef _AKBASIC_SCANNER_H_
#define _AKBASIC_SCANNER_H_
#include <akerror.h>
#include <akbasic/runtime.h>
/** @brief Reset the scanner cursor. */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_scanner_zero(akbasic_Runtime *obj);
/**
* @brief Tokenize one source line into the active environment's token array.
*
* Returns the line as the scanner left it through `dest`: an integer literal in
* token position 0 is consumed as a line number rather than emitted as a token,
* and the line is rewritten to everything after it with leading spaces stripped.
* The REPL depends on that rewrite, which is why the line comes back out.
*
* @param obj Object to initialize, inspect, or modify.
* @param line Source line to scan.
* @param dest Output destination for the possibly-rewritten line; may be NULL.
* @param len Size of `dest`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_scanner_scan(akbasic_Runtime *obj, const char *line, char *dest, size_t len);
#endif // _AKBASIC_SCANNER_H_

54
include/akbasic/sink.h Normal file
View File

@@ -0,0 +1,54 @@
/**
* @file sink.h
* @brief Declares the text sink: where PRINT goes and where INPUT comes from.
*
* The reference's Write() and Println() mirror every line to stdout *and* to an
* SDL surface, and that mirror is the only reason its golden-file suite works.
* Reproducing it as a hardcoded pair of calls would drag SDL into the core and
* make the corpus unrunnable without a display, so it becomes a record of
* function pointers instead -- the house pattern for anything that varies.
*
* akbasic_sink_init_stdio() is in the library. akbasic_sink_init_akgl() is in the
* separate akbasic_akgl target and draws through whatever renderer the host game
* already initialized; the interpreter never owns a window.
*/
#ifndef _AKBASIC_SINK_H_
#define _AKBASIC_SINK_H_
#include <stdio.h>
#include <akerror.h>
#include <akbasic/types.h>
typedef struct akbasic_TextSink
{
void *self;
akerr_ErrorContext AKERR_NOIGNORE *(*write)(struct akbasic_TextSink *self, const char *text);
akerr_ErrorContext AKERR_NOIGNORE *(*writeln)(struct akbasic_TextSink *self, const char *text);
/**
* Read one line. Sets *eof true at end of input rather than raising, because
* running off the end of a stream is how RUNSTREAM mode finishes normally.
*/
akerr_ErrorContext AKERR_NOIGNORE *(*readline)(struct akbasic_TextSink *self, char *dest, size_t len, bool *eof);
akerr_ErrorContext AKERR_NOIGNORE *(*clear)(struct akbasic_TextSink *self);
} akbasic_TextSink;
/** @brief State for the stdio-backed sink. */
typedef struct
{
FILE *out;
FILE *in;
} akbasic_StdioSink;
/**
* @brief Point a sink at a pair of stdio streams.
* @param obj Object to initialize, inspect, or modify.
* @param state Storage for the stream pair; must outlive the sink.
* @param out Where write/writeln go; NULL selects stdout.
* @param in Where readline reads; NULL selects stdin.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_sink_init_stdio(akbasic_TextSink *obj, akbasic_StdioSink *state, FILE *out, FILE *in);
#endif // _AKBASIC_SINK_H_

87
include/akbasic/symtab.h Normal file
View File

@@ -0,0 +1,87 @@
/**
* @file symtab.h
* @brief Declares a fixed-capacity open-addressed string-keyed table.
*
* The Go reference used five maps. Three of them -- an environment's variables,
* functions and labels -- become this: one implementation, capacity supplied by
* the caller, keyed by aksl_strhash_djb2 with linear probing. A full table is an
* error, not a resize.
*/
#ifndef _AKBASIC_SYMTAB_H_
#define _AKBASIC_SYMTAB_H_
#include <stdbool.h>
#include <stdint.h>
#include <akerror.h>
#include <akbasic/types.h>
/*
* Slots are sized for the largest table any caller needs, so one slot array type
* serves variables, functions and labels. Capacity is a member; the table is
* kept below a 75% load factor by construction because `capacity` counts slots
* and the caller's logical maximum is smaller.
*/
#define AKBASIC_SYMTAB_MAX_SLOTS 256
#define AKBASIC_SYMTAB_MAX_KEY 64
typedef struct
{
bool used;
char key[AKBASIC_SYMTAB_MAX_KEY];
void *value;
int64_t ivalue;
} akbasic_SymbolSlot;
typedef struct
{
int capacity;
int count;
akbasic_SymbolSlot slots[AKBASIC_SYMTAB_MAX_SLOTS];
} akbasic_SymbolTable;
/**
* @brief Symbol table initialize.
* @param obj Object to initialize, inspect, or modify.
* @param capacity Number of slots to use; must be positive and no larger than AKBASIC_SYMTAB_MAX_SLOTS.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
* @throws AKBASIC_ERR_BOUNDS When `capacity` is out of range.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_symtab_init(akbasic_SymbolTable *obj, int capacity);
/**
* @brief Store a pointer and an integer against a key, replacing any existing entry.
* @param obj Object to initialize, inspect, or modify.
* @param key Lookup key; copied into the slot.
* @param value Pointer payload, may be NULL.
* @param ivalue Integer payload, used by the label table.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` or `key` is NULL.
* @throws AKBASIC_ERR_BOUNDS When the key is too long or the table is full.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_symtab_set(akbasic_SymbolTable *obj, const char *key, void *value, int64_t ivalue);
/**
* @brief Look a key up.
* @param obj Object to initialize, inspect, or modify.
* @param key Lookup key.
* @param value Output destination for the pointer payload; may be NULL.
* @param ivalue Output destination for the integer payload; may be NULL.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` or `key` is NULL.
* @throws AKERR_KEY When the key is not present.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_symtab_get(akbasic_SymbolTable *obj, const char *key, void **value, int64_t *ivalue);
/**
* @brief Drop every entry.
* @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_symtab_clear(akbasic_SymbolTable *obj);
#endif // _AKBASIC_SYMTAB_H_

74
include/akbasic/types.h Normal file
View File

@@ -0,0 +1,74 @@
/**
* @file types.h
* @brief Declares the interpreter's fixed budget and its primitive value type.
*/
#ifndef _AKBASIC_TYPES_H_
#define _AKBASIC_TYPES_H_
#include <stdbool.h>
#include <stdint.h>
/*
* The budget, transcribed from the Go reference's main.go:14-30. Everything the
* interpreter uses is drawn from a pool sized by one of these; nothing calls
* malloc.
*/
/* Per-environment pools */
#define AKBASIC_MAX_LEAVES 32 /* ~16 operations per source line */
#define AKBASIC_MAX_TOKENS 32
#define AKBASIC_MAX_VALUES 64
#define AKBASIC_MAX_VARIABLES 128
/* Whole-runtime pools */
#define AKBASIC_MAX_SOURCE_LINES 9999
#define AKBASIC_MAX_LINE_LENGTH 256
#define AKBASIC_MAX_ARRAY_DEPTH 64 /* dimensions per array */
#define AKBASIC_MAX_ARRAY_ELEMENTS 1024 /* elements in one array */
#define AKBASIC_MAX_ARRAY_VALUES 4096 /* array elements across all variables */
#define AKBASIC_MAX_STRING_LENGTH 256 /* see TODO.md 1.2 */
#define AKBASIC_MAX_ENVIRONMENTS 32 /* new: Go allocated these unbounded */
#define AKBASIC_MAX_FUNCTIONS 64 /* new: Go used an unbounded map */
#define AKBASIC_MAX_LABELS 64 /* new: Go used an unbounded map */
/* Commodore convention: true is -1, not 1. */
#define AKBASIC_TRUE -1
#define AKBASIC_FALSE 0
/** @brief Runtime execution modes. */
#define AKBASIC_MODE_REPL 1
#define AKBASIC_MODE_RUN 2
#define AKBASIC_MODE_RUNSTREAM 3
#define AKBASIC_MODE_QUIT 4
/** @brief The type carried by a value, taken from an identifier's suffix. */
typedef enum
{
AKBASIC_TYPE_UNDEFINED = 0,
AKBASIC_TYPE_INTEGER, /* 1 -- identifier suffix '#' */
AKBASIC_TYPE_FLOAT, /* 2 -- identifier suffix '%' */
AKBASIC_TYPE_STRING, /* 3 -- identifier suffix '$' */
AKBASIC_TYPE_BOOLEAN /* 4 */
} akbasic_Type;
/*
* A value. The string lives inline rather than behind a pointer so that a copy
* is a struct assignment with no allocator, refcount or lifetime question --
* see TODO.md 1.2 for the size tradeoff that buys.
*
* The reference's BasicValue carries a `name` field. It is dead: nothing ever
* writes it a non-empty string, and its only reader is BasicEnvironment.update(),
* which has no callers. Both are dropped here.
*/
typedef struct akbasic_Value
{
akbasic_Type valuetype;
char stringval[AKBASIC_MAX_STRING_LENGTH];
int64_t intval;
double floatval;
int64_t boolvalue;
bool mutable_;
} akbasic_Value;
#endif // _AKBASIC_TYPES_H_

101
include/akbasic/value.h Normal file
View File

@@ -0,0 +1,101 @@
/**
* @file value.h
* @brief Declares the strongly-typed BASIC value and its operators.
*
* Ported from the reference's basicvalue.go. The arithmetic is reproduced
* exactly, including the parts that look wrong -- see TODO.md section 12 for the
* catalogue and the reason they are not fixed yet.
*
* Every binary operator takes a `scratch` value the caller has drawn from an
* environment's pool and an out-parameter `dest`. Most operators set `*dest` to
* `scratch` and fill it; akbasic_value_math_plus sets `*dest` to `self` when
* `self` is mutable, because the reference mutates in place there and
* CommandNEXT's increment relies on it.
*/
#ifndef _AKBASIC_VALUE_H_
#define _AKBASIC_VALUE_H_
#include <akerror.h>
#include <akbasic/types.h>
/*
* Backing store for array variables. Scalars and arrays alike draw their storage
* from here rather than from malloc, and a variable holds a pointer into it. The
* runtime owns exactly one; it is passed explicitly rather than kept at file
* scope, because the interpreter must be embeddable and may not own global
* mutable state.
*
* The allocator is a bump: releasing is not supported, because nothing in BASIC
* destroys a variable. Re-DIMming a variable to the same size or smaller reuses
* its existing slice; growing it takes fresh slots and abandons the old ones. A
* program that re-DIMs the same array larger in a loop will exhaust the pool and
* get AKBASIC_ERR_BOUNDS -- bounded and diagnosable, which is the point.
*/
typedef struct
{
int next;
akbasic_Value values[AKBASIC_MAX_ARRAY_VALUES];
} akbasic_ValuePool;
/** @brief Reset the pool to empty. */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_valuepool_init(akbasic_ValuePool *obj);
/**
* @brief Take `count` contiguous zeroed values from the pool.
* @param obj Object to initialize, inspect, or modify.
* @param count Number of elements required.
* @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 pool has too few slots left.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_valuepool_take(akbasic_ValuePool *obj, int count, akbasic_Value **dest);
/** @brief Reset a value to a usable empty state. */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_init(akbasic_Value *obj);
/** @brief Clear a value to undefined, zero, immutable and unnamed. */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_zero(akbasic_Value *obj);
/**
* @brief Copy the payload of one value onto another.
*
* Copies name, type, string, integer, float and boolean payloads. It does *not*
* copy `mutable_`: the destination keeps its own, matching the reference, where
* clone() writes every field except that one.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_clone(akbasic_Value *self, akbasic_Value *dest);
/** @brief Render a value the way PRINT does. */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_to_string(akbasic_Value *self, char *dest, size_t len);
/** @brief Set a value to a BASIC boolean (-1 true, 0 false). */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_set_bool(akbasic_Value *obj, bool result);
/** @brief True when the value is a boolean holding AKBASIC_TRUE. */
bool akbasic_value_is_true(akbasic_Value *self);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_value_invert(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);
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_right(akbasic_Value *self, int64_t bits, 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);
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_xor(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);
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_multiply(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);
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_equal(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);
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_is_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_

View File

@@ -0,0 +1,65 @@
/**
* @file variable.h
* @brief Declares a named, strongly-typed, optionally multi-dimensional slot.
*
* Ported from basicvariable.go. Type comes from the identifier's last character:
* `$` string, `#` integer, `%` float. Note that `%` meaning *float* inverts the
* Commodore convention where `%` is integer; the reference does it that way and
* its README documents it, so it is kept.
*/
#ifndef _AKBASIC_VARIABLE_H_
#define _AKBASIC_VARIABLE_H_
#include <akerror.h>
#include <akbasic/types.h>
#include <akbasic/value.h>
typedef struct
{
char name[AKBASIC_MAX_STRING_LENGTH];
akbasic_Type valuetype;
akbasic_Value *values; /** Points into the runtime's value pool */
int valuecount;
int64_t dimensions[AKBASIC_MAX_ARRAY_DEPTH];
int dimensioncount;
bool mutable_;
bool used; /** Pool bookkeeping */
} akbasic_Variable;
/**
* @brief Give a variable storage and a type.
* @param obj Object to initialize, inspect, or modify.
* @param pool Backing store the elements are drawn from.
* @param sizes Dimension sizes; every one must be positive.
* @param sizecount Number of dimensions.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` or `pool` is NULL.
* @throws AKBASIC_ERR_VALUE When the name is empty or a dimension is not positive.
* @throws AKBASIC_ERR_BOUNDS When the array is larger than the pool can serve.
*/
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. */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_variable_zero(akbasic_Variable *obj);
/**
* @brief Resolve a subscript list to the value it addresses.
* @param obj Object to initialize, inspect, or modify.
* @param subscripts Index per dimension.
* @param subscriptcount Number of indices supplied; must equal the dimension count.
* @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 count is wrong or an index is out of range.
*/
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. */
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_integer(akbasic_Variable *obj, int64_t value, int64_t *subscripts, int subscriptcount);
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_string(akbasic_Variable *obj, const char *value, int64_t *subscripts, int subscriptcount);
#endif // _AKBASIC_VARIABLE_H_

56
include/akbasic/verbs.h Normal file
View File

@@ -0,0 +1,56 @@
/**
* @file verbs.h
* @brief Declares the one dispatch table that replaces the reference's reflection.
*
* The Go runtime resolves a verb with reflect.MethodByName("Command" + NAME), a
* function with "Function" + NAME, and a special parse path with "ParseCommand" +
* NAME. C has no reflection and none is being added. All three collapse into the
* table in src/verbs.c: sorted by name, searched with bsearch, one row per verb.
*
* Adding a verb is adding one row and up to two functions. A NULL parse handler
* means "parse the rval as a plain expression", which is exactly what
* commandByReflection returning (nil, nil) means in the reference. A NULL exec
* handler means the token is consumed by another verb's parser and never
* evaluated on its own -- THEN, ELSE, TO and STEP are all like that.
*/
#ifndef _AKBASIC_VERBS_H_
#define _AKBASIC_VERBS_H_
#include <akerror.h>
#include <akbasic/grammar.h>
#include <akbasic/types.h>
struct akbasic_Parser;
struct akbasic_Runtime;
typedef akerr_ErrorContext AKERR_NOIGNORE *(*akbasic_ParseHandler)(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
typedef akerr_ErrorContext AKERR_NOIGNORE *(*akbasic_ExecHandler)(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
typedef struct
{
const char *name;
akbasic_TokenType tokentype;
int arity; /** Argument count for a function; -1 when not applicable */
akbasic_ParseHandler parse;
akbasic_ExecHandler exec;
} akbasic_Verb;
/**
* @brief Find a verb, function or reserved word by name, case-insensitively.
*
* Verbs and function names are case-insensitive in this dialect; variable names
* are not. That asymmetry lives here.
*
* @param name Name to look up; matched without regard to case.
* @param dest Output destination populated by the function; set to NULL on a miss.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `name` or `dest` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_verb_lookup(const char *name, const akbasic_Verb **dest);
/** @brief The table itself, exposed so tests can assert it is sorted. */
const akbasic_Verb *akbasic_verb_table(int *count);
#endif // _AKBASIC_VERBS_H_