Files
akbasic/src/grammar.c
Tachikoma 4e98dba423 Give argument lists their own link, not each argument's .right
An identifier's subscript list, a unary's operand and a binary's right-hand
side all lived in the same field the argument chain used, so ABS(-9),
MOD(A#, B# + 12) and any array reference in a parameter list were counted as
extra arguments and refused. Subscript lists move to .expr and arguments now
chain through a dedicated .next.

This is the third time the same collision was fixed; the first two moved it
along rather than removing it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-07-31 11:58:15 -04:00

401 lines
14 KiB
C

/**
* @file grammar.c
* @brief Implements token and AST leaf construction.
*/
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <akerror.h>
#include <akstdlib.h>
#include <akbasic/error.h>
#include <akbasic/grammar.h>
/* Copy into a leaf's inline identifier/literal buffer, refusing truncation. */
static akerr_ErrorContext *copy_bounded(char *dest, const char *src, const char *what)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (src != NULL), AKERR_NULLPOINTER, "NULL %s", what);
FAIL_ZERO_RETURN(errctx, (strlen(src) < AKBASIC_MAX_STRING_LENGTH),
AKBASIC_ERR_VALUE,
"%s of %zu characters exceeds the %d character limit",
what, strlen(src), AKBASIC_MAX_STRING_LENGTH - 1);
strncpy(dest, src, AKBASIC_MAX_STRING_LENGTH - 1);
dest[AKBASIC_MAX_STRING_LENGTH - 1] = '\0';
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_token_init(akbasic_Token *obj)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL token in init");
obj->tokentype = AKBASIC_TOK_UNDEFINED;
obj->lineno = 0;
obj->lexeme[0] = '\0';
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_leaf_init(akbasic_ASTLeaf *obj, akbasic_LeafType leaftype)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL leaf in init");
obj->leaftype = leaftype;
obj->parent = NULL;
obj->left = NULL;
obj->right = NULL;
obj->expr = NULL;
obj->next = NULL;
obj->identifier[0] = '\0';
obj->literal_int = 0;
obj->literal_float = 0.0;
obj->literal_string[0] = '\0';
obj->operator_ = AKBASIC_TOK_UNDEFINED;
SUCCEED_RETURN(errctx);
}
/*
* Take one leaf from the pool. Recursion in clone_into() is bounded by the pool
* capacity, so a cyclic tree exhausts the pool and errors rather than running
* off the stack.
*/
static akerr_ErrorContext *pool_take(akbasic_LeafPool *pool, akbasic_ASTLeaf **dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (pool != NULL), AKERR_NULLPOINTER, "NULL leaf pool");
FAIL_ZERO_RETURN(errctx, (pool->leaves != NULL), AKERR_NULLPOINTER, "Leaf pool has no storage");
FAIL_ZERO_RETURN(errctx, (pool->next < pool->capacity), AKBASIC_ERR_BOUNDS,
"No more leaves available");
*dest = &pool->leaves[pool->next];
pool->next += 1;
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *clone_into(akbasic_ASTLeaf *self, akbasic_LeafPool *pool, akbasic_ASTLeaf **dest)
{
PREPARE_ERROR(errctx);
akbasic_ASTLeaf *copy = NULL;
if ( self == NULL ) {
*dest = NULL;
SUCCEED_RETURN(errctx);
}
PASS(errctx, pool_take(pool, &copy));
PASS(errctx, akbasic_leaf_init(copy, self->leaftype));
copy->parent = self->parent;
copy->literal_int = self->literal_int;
copy->literal_float = self->literal_float;
memcpy(copy->literal_string, self->literal_string, sizeof(copy->literal_string));
memcpy(copy->identifier, self->identifier, sizeof(copy->identifier));
copy->operator_ = self->operator_;
PASS(errctx, clone_into(self->left, pool, &copy->left));
PASS(errctx, clone_into(self->right, pool, &copy->right));
PASS(errctx, clone_into(self->expr, pool, &copy->expr));
PASS(errctx, clone_into(self->next, pool, &copy->next));
*dest = copy;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_leaf_clone(akbasic_ASTLeaf *self, akbasic_LeafPool *pool, akbasic_ASTLeaf **dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER, "NULL destination in clone");
PASS(errctx, clone_into(self, pool, dest));
SUCCEED_RETURN(errctx);
}
akbasic_ASTLeaf *akbasic_leaf_first_argument(akbasic_ASTLeaf *self)
{
if ( self == NULL ||
self->right == NULL ||
self->right->leaftype != AKBASIC_LEAF_ARGUMENTLIST ||
self->right->operator_ != AKBASIC_TOK_FUNCTION_ARGUMENT ) {
return NULL;
}
return self->right->right;
}
akbasic_ASTLeaf *akbasic_leaf_first_subscript(akbasic_ASTLeaf *self)
{
if ( self == NULL ||
self->expr == NULL ||
self->expr->leaftype != AKBASIC_LEAF_ARGUMENTLIST ||
self->expr->operator_ != AKBASIC_TOK_ARRAY_SUBSCRIPT ) {
return NULL;
}
return self->expr->right;
}
bool akbasic_leaf_is_identifier(akbasic_ASTLeaf *self)
{
return (self != NULL &&
(self->leaftype == AKBASIC_LEAF_IDENTIFIER ||
self->leaftype == AKBASIC_LEAF_IDENTIFIER_INT ||
self->leaftype == AKBASIC_LEAF_IDENTIFIER_FLOAT ||
self->leaftype == AKBASIC_LEAF_IDENTIFIER_STRING));
}
bool akbasic_leaf_is_literal(akbasic_ASTLeaf *self)
{
return (self != NULL &&
(self->leaftype == AKBASIC_LEAF_LITERAL_INT ||
self->leaftype == AKBASIC_LEAF_LITERAL_FLOAT ||
self->leaftype == AKBASIC_LEAF_LITERAL_STRING));
}
akerr_ErrorContext *akbasic_leaf_new_comparison(akbasic_ASTLeaf *obj, akbasic_ASTLeaf *left, akbasic_TokenType op, akbasic_ASTLeaf *right)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL leaf in new_comparison");
FAIL_ZERO_RETURN(errctx, (left != NULL && right != NULL), AKERR_NULLPOINTER,
"nil pointer arguments");
PASS(errctx, akbasic_leaf_init(obj, AKBASIC_LEAF_COMPARISON));
obj->left = left;
obj->right = right;
switch ( op ) {
case AKBASIC_TOK_LESS_THAN:
case AKBASIC_TOK_LESS_THAN_EQUAL:
case AKBASIC_TOK_NOT_EQUAL:
case AKBASIC_TOK_GREATER_THAN:
case AKBASIC_TOK_GREATER_THAN_EQUAL:
SUCCEED_RETURN(errctx);
default:
FAIL_RETURN(errctx, AKBASIC_ERR_SYNTAX, "Invalid operator %d for comparison", (int)op);
}
}
akerr_ErrorContext *akbasic_leaf_new_binary(akbasic_ASTLeaf *obj, akbasic_ASTLeaf *left, akbasic_TokenType op, akbasic_ASTLeaf *right)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL leaf in new_binary");
FAIL_ZERO_RETURN(errctx, (left != NULL && right != NULL), AKERR_NULLPOINTER,
"nil pointer arguments");
PASS(errctx, akbasic_leaf_init(obj, AKBASIC_LEAF_BINARY));
obj->left = left;
obj->right = right;
obj->operator_ = op;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_leaf_new_unary(akbasic_ASTLeaf *obj, akbasic_TokenType op, akbasic_ASTLeaf *operand)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL leaf in new_unary");
FAIL_ZERO_RETURN(errctx, (operand != NULL), AKERR_NULLPOINTER, "nil pointer arguments");
PASS(errctx, akbasic_leaf_init(obj, AKBASIC_LEAF_UNARY));
/*
* .left, not .right. An argument list chains its arguments through .right,
* so an operand there is indistinguishable from a second argument: ABS(-9)
* counted as two and was refused. See the note on the declaration.
*/
obj->left = operand;
obj->operator_ = op;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_leaf_new_function(akbasic_ASTLeaf *obj, const char *fname, akbasic_ASTLeaf *right)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL leaf in new_function");
PASS(errctx, akbasic_leaf_init(obj, AKBASIC_LEAF_FUNCTION));
obj->right = right;
obj->operator_ = AKBASIC_TOK_COMMAND;
PASS(errctx, copy_bounded(obj->identifier, fname, "function name"));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_leaf_new_command(akbasic_ASTLeaf *obj, const char *cmdname, akbasic_ASTLeaf *right)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL leaf in new_command");
PASS(errctx, akbasic_leaf_init(obj, AKBASIC_LEAF_COMMAND));
obj->right = right;
obj->operator_ = AKBASIC_TOK_COMMAND;
PASS(errctx, copy_bounded(obj->identifier, cmdname, "command name"));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_leaf_new_immediate_command(akbasic_ASTLeaf *obj, const char *cmdname, akbasic_ASTLeaf *right)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL leaf in new_immediate_command");
PASS(errctx, akbasic_leaf_init(obj, AKBASIC_LEAF_COMMAND_IMMEDIATE));
obj->right = right;
obj->operator_ = AKBASIC_TOK_COMMAND_IMMEDIATE;
PASS(errctx, copy_bounded(obj->identifier, cmdname, "command name"));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_leaf_new_branch(akbasic_ASTLeaf *obj, akbasic_ASTLeaf *expr, akbasic_ASTLeaf *trueleaf, akbasic_ASTLeaf *falseleaf)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL leaf in new_branch");
FAIL_ZERO_RETURN(errctx, (expr != NULL), AKERR_NULLPOINTER, "nil pointer arguments");
PASS(errctx, akbasic_leaf_init(obj, AKBASIC_LEAF_BRANCH));
obj->expr = expr;
obj->left = trueleaf;
obj->right = falseleaf;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_leaf_new_grouping(akbasic_ASTLeaf *obj, akbasic_ASTLeaf *expr)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL leaf in new_grouping");
FAIL_ZERO_RETURN(errctx, (expr != NULL), AKERR_NULLPOINTER, "nil pointer arguments");
PASS(errctx, akbasic_leaf_init(obj, AKBASIC_LEAF_GROUPING));
obj->expr = expr;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_leaf_new_literal_int(akbasic_ASTLeaf *obj, const char *lexeme)
{
PREPARE_ERROR(errctx);
long long value = 0;
int base = 10;
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL leaf in new_literal_int");
FAIL_ZERO_RETURN(errctx, (lexeme != NULL), AKERR_NULLPOINTER, "NULL lexeme in new_literal_int");
FAIL_ZERO_RETURN(errctx, (lexeme[0] != '\0'), AKBASIC_ERR_VALUE, "Empty integer literal");
/*
* Base 10 unless the lexeme is prefixed `0x`. The reference selects base 8
* for *any* lexeme starting with '0' (basicgrammar.go:224), so `PRINT 010`
* printed 8 and `PRINT 08` was a parse error -- TODO.md section 6 item 10.
* Commodore BASIC has no octal literals, and a leading zero in a listing is
* padding, not a radix.
*/
if ( strlen(lexeme) > 2 && strncmp(lexeme, "0x", 2) == 0 ) {
base = 16;
}
PASS(errctx, akbasic_leaf_init(obj, AKBASIC_LEAF_LITERAL_INT));
PASS(errctx, aksl_strtoll(lexeme, NULL, base, &value));
obj->literal_int = (int64_t)value;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_leaf_new_literal_float(akbasic_ASTLeaf *obj, const char *lexeme)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL leaf in new_literal_float");
PASS(errctx, akbasic_leaf_init(obj, AKBASIC_LEAF_LITERAL_FLOAT));
PASS(errctx, aksl_atof(lexeme, &obj->literal_float));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_leaf_new_literal_string(akbasic_ASTLeaf *obj, const char *lexeme)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL leaf in new_literal_string");
PASS(errctx, akbasic_leaf_init(obj, AKBASIC_LEAF_LITERAL_STRING));
PASS(errctx, copy_bounded(obj->literal_string, lexeme, "string literal"));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_leaf_new_identifier(akbasic_ASTLeaf *obj, akbasic_LeafType leaftype, const char *lexeme)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL leaf in new_identifier");
PASS(errctx, akbasic_leaf_init(obj, leaftype));
PASS(errctx, copy_bounded(obj->identifier, lexeme, "identifier"));
SUCCEED_RETURN(errctx);
}
static const char *operator_to_str(akbasic_TokenType op)
{
switch ( op ) {
case AKBASIC_TOK_EQUAL: return "=";
case AKBASIC_TOK_LESS_THAN: return "<";
case AKBASIC_TOK_GREATER_THAN: return ">";
case AKBASIC_TOK_LESS_THAN_EQUAL: return "<=";
case AKBASIC_TOK_GREATER_THAN_EQUAL: return ">=";
case AKBASIC_TOK_NOT_EQUAL: return "<>";
case AKBASIC_TOK_PLUS: return "+";
case AKBASIC_TOK_MINUS: return "-";
case AKBASIC_TOK_STAR: return "*";
case AKBASIC_TOK_LEFT_SLASH: return "/";
case AKBASIC_TOK_CARAT: return "^";
case AKBASIC_TOK_NOT: return "NOT";
case AKBASIC_TOK_AND: return "AND";
case AKBASIC_TOK_OR: return "OR";
default: return "";
}
}
akerr_ErrorContext *akbasic_leaf_to_string(akbasic_ASTLeaf *self, char *dest, size_t len)
{
PREPARE_ERROR(errctx);
char sub1[AKBASIC_MAX_STRING_LENGTH];
char sub2[AKBASIC_MAX_STRING_LENGTH];
FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER, "NULL leaf in to_string");
FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER, "NULL destination in to_string");
FAIL_ZERO_RETURN(errctx, (len > 0), AKBASIC_ERR_BOUNDS, "Zero-length destination in to_string");
switch ( self->leaftype ) {
case AKBASIC_LEAF_LITERAL_INT:
snprintf(dest, len, "%" PRId64, self->literal_int);
break;
case AKBASIC_LEAF_LITERAL_FLOAT:
snprintf(dest, len, "%f", self->literal_float);
break;
case AKBASIC_LEAF_LITERAL_STRING:
snprintf(dest, len, "%s", self->literal_string);
break;
case AKBASIC_LEAF_IDENTIFIER_INT:
case AKBASIC_LEAF_IDENTIFIER_FLOAT:
case AKBASIC_LEAF_IDENTIFIER_STRING:
case AKBASIC_LEAF_IDENTIFIER:
snprintf(dest, len, "%s", self->identifier);
break;
case AKBASIC_LEAF_IDENTIFIER_STRUCT:
snprintf(dest, len, "NOT IMPLEMENTED");
break;
case AKBASIC_LEAF_UNARY:
PASS(errctx, akbasic_leaf_to_string(self->left, sub1, sizeof(sub1)));
snprintf(dest, len, "(%s %s)", operator_to_str(self->operator_), sub1);
break;
case AKBASIC_LEAF_BINARY:
PASS(errctx, akbasic_leaf_to_string(self->left, sub1, sizeof(sub1)));
PASS(errctx, akbasic_leaf_to_string(self->right, sub2, sizeof(sub2)));
snprintf(dest, len, "(%s %s %s)", operator_to_str(self->operator_), sub1, sub2);
break;
case AKBASIC_LEAF_GROUPING:
PASS(errctx, akbasic_leaf_to_string(self->expr, sub1, sizeof(sub1)));
snprintf(dest, len, "(group %s)", sub1);
break;
case AKBASIC_LEAF_COMMAND:
case AKBASIC_LEAF_COMMAND_IMMEDIATE:
case AKBASIC_LEAF_FUNCTION:
/*
* The reference falls through to Go's %+v struct dump here, which has no
* useful C equivalent. Print something a test can assert on instead.
*/
snprintf(dest, len, "(%s)", self->identifier);
break;
default:
snprintf(dest, len, "(leaf %d)", (int)self->leaftype);
break;
}
SUCCEED_RETURN(errctx);
}