Files
akbasic/include/akbasic/disk.h
Andrew Kesterson 9845e77a5c 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>
2026-07-31 21:50:37 -04:00

100 lines
3.7 KiB
C

/**
* @file disk.h
* @brief The group F file channels: DOPEN, DCLOSE, PRINT#, INPUT#, GET#.
*
* A Commodore's disk verbs address a 1541 over the serial bus. There is no 1541,
* so what these mean here is the nearest thing a filesystem offers, and the
* verbs that mean nothing without the hardware are refused by name rather than
* faked -- see TODO.md section 5.
*
* Channels are the interpreter's, not the host's: a program that opens a file
* and is then stepped by a game's frame loop keeps its channel across frames,
* and a `NEW` closes the lot.
*/
#ifndef _AKBASIC_DISK_H_
#define _AKBASIC_DISK_H_
#include <stdio.h>
#include <akerror.h>
#include <akbasic/types.h>
/** @brief How many files may be open at once. A C128 allows ten; this allows the same. */
#define AKBASIC_MAX_CHANNELS 10
/** @brief One open file channel. */
typedef struct
{
FILE *fp;
char name[AKBASIC_MAX_STRING_LENGTH];
bool writing;
} akbasic_Channel;
/** @brief Every open channel. Indexed by the number a program writes after `#`. */
typedef struct
{
akbasic_Channel channels[AKBASIC_MAX_CHANNELS];
} akbasic_DiskState;
struct akbasic_Runtime;
/**
* @brief Mark every channel closed. Does not close anything; nothing is open yet.
* @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_disk_state_init(akbasic_DiskState *obj);
/**
* @brief Close every open channel.
*
* Called by `DCLOSE` with no argument and by `NEW`. A program that ends without
* closing its files leaves them to this -- and to the host, which owns the
* process and outlives the 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_disk_close_all(akbasic_DiskState *obj);
/**
* @brief Write one line to a channel, terminator included.
*
* What `PRINT #n, ...` reaches. A line rather than raw bytes, because that is
* what `INPUT #n` reads back and the pair has to agree.
*
* @param obj Object to initialize, inspect, or modify.
* @param number The channel number.
* @param text What to write.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` or `text` is NULL.
* @throws AKBASIC_ERR_BOUNDS When the channel number is out of range.
* @throws AKBASIC_ERR_STATE When the channel is closed, or was opened for reading.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_disk_write(struct akbasic_Runtime *obj, int64_t number, const char *text);
/**
* @brief Read one line from a channel, without its terminator.
*
* What `INPUT #n, VAR` reaches. End of file is reported through @p eof rather
* than raised, the same way the sink's `readline` reports it: running out of
* input is a state a program handles, not a failure.
*
* @param obj Object to initialize, inspect, or modify.
* @param number The channel number.
* @param dest Output destination populated by the function.
* @param len Capacity of @p dest.
* @param eof Output destination populated by the function; true at end of file.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When any pointer argument is NULL.
* @throws AKBASIC_ERR_BOUNDS When the channel number is out of range.
* @throws AKBASIC_ERR_STATE When the channel is closed, or was opened for writing.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_disk_readline(struct akbasic_Runtime *obj, int64_t number, char *dest, size_t len, bool *eof);
#endif // _AKBASIC_DISK_H_