Files
akbasic/include/akbasic/value.h
Tachikoma 4e7d2cff6c Finish the language: every remaining verb group, and the defects that blocked them
Closes groups A, C, D, E, F, H and J of TODO.md section 4, plus RESTORE and
RENUMBER, and closes section 6 -- all seventeen reference defects. Seven of
those turned out to have been fixed or never ported and nobody had written it
down; the audit records the evidence for each.

Two of the seventeen were real. math_plus mutated its left operand when the
operand was mutable, so A# + 1 could modify A#; it was gated on FOR/NEXT
coverage because NEXT relied on the mutation, so tests/for_next.c came first
and NEXT now writes the counter back itself. And the binary operators summed
both numeric fields of their right operand, which no BASIC program can reach
-- that one needed a test written against the value API.

Writing the tests turned up eight defects nobody had listed. Seven are fixed:

  IF A = 2 THEN was a parse error; only == worked
  IF ... AND ... was a parse error, because a condition parsed as one relation
  IF A = 1 OR B = 2 THEN was silently always false, and so was IF A THEN
  EXIT before any NEXT restarted the program and exhausted the variable pool
  READ never found a DATA line above it, and swallowed the lines between
  PRINT 2 + 2 at the prompt was filed as program text instead of answering
  a short read discarded its bytes, so COPY produced empty files
  every verb taking an argument list said "peek() returned nil token!" on none

The eighth is not fixed and cannot be quietly: a FOR whose step overshoots
runs its body one extra time, and FOR I = 1 TO 1 runs it zero times. The two
errors cancel for a step of 1, which is why neither was noticed. Correcting
them changes the expected output of a checked-in acceptance file, and
tests/reference/README.md forbids editing one to suit this interpreter. It is
tests/for_semantics.c in AKBASIC_KNOWN_FAILING_TESTS, asserting the correct
contract, and TODO.md items 19 and 20.

Sprites are real libakgl actors with a renderfunc of their own, because
akgl_actor_render draws every sprite square and an actor has no per-axis
scale. Both are filed upstream. SPRSAV takes an image file, an SSHAPE handle
or a 63-element integer array -- a string here cannot hold a zero byte.

Verbs that need hardware that does not exist are refused by name with the
reason rather than faked: SYS, HEADER, COLLECT, BACKUP, BOOT, FILTER, and
DIRECTORY, which is refused for a missing libakstdlib wrapper filed upstream.

94 tests in the default build, 93 with SDL, 94 under ASan and UBSan, doxygen
clean. The Go acceptance corpus stayed green throughout.

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

285 lines
15 KiB
C

/**
* @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);
/**
* @brief True when the value is nonzero, whatever numeric type it carries.
*
* What a condition tests. Commodore BASIC has no boolean type -- a comparison
* yields -1 or 0, `AND` and `OR` are the bitwise operators, and `IF A THEN` is
* legal for any numeric A -- so "is this a boolean holding true" is the wrong
* question for a branch to ask. Strings are never true.
*
* @param self Value to test; NULL is not true.
* @return `true` when the value is a nonzero number or a true truth value.
*/
bool akbasic_value_is_truthy(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);
/**
* @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);
/**
* @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);
/**
* @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);
/**
* @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);
/**
* @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);
/**
* @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);
/**
* @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);
/**
* @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);
/**
* @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);
/**
* @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);
/**
* @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);
/**
* @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);
/**
* @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);
/**
* @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);
/**
* @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);
/**
* @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);
#endif // _AKBASIC_VALUE_H_