Some checks failed
akbasic CI Build / cmake_build (push) Successful in 3m2s
akbasic CI Build / sanitizers (push) Successful in 3m51s
akbasic CI Build / coverage (push) Failing after 3m24s
akbasic CI Build / akgl_build (push) Failing after 20s
akbasic CI Build / mutation_test (push) Has been cancelled
README.md was 577 lines and answered four different questions at once: what
the project is, how to build it, every verb and function in the language, and
how to maintain the test harness. The verb and function lists had already been
written a second time in docs/11 and docs/12, which is how a list of that size
goes stale -- there is no way to notice the two have drifted apart.
README.md is now 150 lines and holds only what somebody evaluating the project
needs: what it is, the quickstart, why it was rewritten in C, the five rules
embedding imposes on the design, the two ways to use it, and where everything
else lives. Technical detail goes to docs/, maintenance to MAINTENANCE.md.
The akbasic_TextSink struct moved to docs/10-embedding.md rather than being
deleted. It was the corpus's only `c excerpt=` block -- the check that caught
the stale struct two commits ago -- so dropping it with the README would have
quietly retired a test. docs/10 also stopped claiming README.md carries the
full API surface and the pool limits, which the trim made false.
CLAUDE.md went from 458 lines to 62, because almost none of it was
agent-specific. The project goals, the Go reference and its architecture, the
dependency version and ABI rules, the four ways an embedded build collides,
the libakerror convention, the error-code range map and the style rules are
all things a maintainer needs, and they are now in MAINTENANCE.md with one
copy to keep true. CLAUDE.md points there and keeps only the rules no test
enforces: tests in the same commit asserting the correct contract, file a
missing dependency capability upstream, do not edit generated output or
tests/reference/, co-author your commits.
Four claims did not survive the move, having gone stale where nothing could
notice:
- "The repository is currently empty apart from its submodules -- no
commits, no source tree, no build files." There are 43 commits.
- libakgl's target_compile_definitions(akerror PUBLIC AKERR_MAX_ERR_VALUE)
at deps/libakgl/CMakeLists.txt:44, described as inert but present. It is
gone; only a historical mention in a comment remains.
- "akbasic_init() claims 512-767." There is no akbasic_init. It is
akbasic_error_register(), called from akbasic_runtime_init().
- Time-relative phrasing ("libakgl hit two of them in the last week").
Five places pointed at CLAUDE.md for the range map or the file-it-upstream
rule and now point at MAINTENANCE.md: include/akbasic/error.h,
src/runtime_disk.c and three entries in TODO.md. Both source changes are
comments. deps/libakgl/TODO.md cites it too and is left alone; it is a
submodule, and the rule it quotes is still reachable from CLAUDE.md.
ctest is green at 95 of 95, docs_examples included: 36 programs, 9
transcripts, 44 output comparisons, 3 C snippets, 1 excerpt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
756 lines
28 KiB
C
756 lines
28 KiB
C
/**
|
|
* @file runtime_disk.c
|
|
* @brief The group F verbs: files, channels, and the ones that need a 1541.
|
|
*
|
|
* Split three ways, and the split is the whole design.
|
|
*
|
|
* - **Verbs that mean something on a filesystem** are implemented against
|
|
* `aksl_f*`: DOPEN, DCLOSE, APPEND, RECORD, SCRATCH, RENAME, COPY, CONCAT,
|
|
* DIRECTORY and the binary pair BSAVE/BLOAD.
|
|
* - **Verbs that are spellings of ones already here** are aliases: SAVE and
|
|
* LOAD are DSAVE and DLOAD, CATALOG is DIRECTORY, DVERIFY is VERIFY.
|
|
* - **Verbs that need the hardware** are refused by name, with the reason:
|
|
* HEADER, COLLECT, BACKUP, BOOT and DCLEAR. Formatting a disk, validating
|
|
* one, duplicating one and resetting a drive have no filesystem meaning that
|
|
* is not a lie.
|
|
*
|
|
* Channel numbers are the program's, written after `#`. A leading `#` is
|
|
* optional everywhere -- `DOPEN #1, "F"` and `DOPEN 1, "F"` are the same
|
|
* statement -- because the scanner reads `#` as its own token and a verb name
|
|
* cannot carry one.
|
|
*/
|
|
|
|
#include <inttypes.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include <akerror.h>
|
|
#include <akstdlib.h>
|
|
|
|
#include <akbasic/args.h>
|
|
#include <akbasic/error.h>
|
|
#include <akbasic/runtime.h>
|
|
|
|
#include "verbs.h"
|
|
|
|
/* Most verbs answer "did something happen"; this is that answer. */
|
|
#define SUCCEED_TRUE(__obj, __dest) \
|
|
do { \
|
|
*(__dest) = &(__obj)->staticTrueValue; \
|
|
} while ( 0 )
|
|
|
|
akerr_ErrorContext *akbasic_disk_state_init(akbasic_DiskState *obj)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL disk state in init");
|
|
memset(obj, 0, sizeof(*obj));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_disk_close_all(akbasic_DiskState *obj)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
int i = 0;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL disk state in close_all");
|
|
/*
|
|
* A loop, so CATCH and the _BREAK macros are banned in here. A close that
|
|
* fails is reported, but the rest are still closed: leaking the others
|
|
* because one of them was already gone would be a poor trade.
|
|
*/
|
|
for ( i = 0; i < AKBASIC_MAX_CHANNELS; i++ ) {
|
|
if ( obj->channels[i].fp != NULL ) {
|
|
IGNORE(aksl_fclose(obj->channels[i].fp));
|
|
obj->channels[i].fp = NULL;
|
|
obj->channels[i].name[0] = '\0';
|
|
obj->channels[i].writing = false;
|
|
}
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief Check a channel number and hand back its slot. */
|
|
static akerr_ErrorContext *channel_slot(akbasic_Runtime *obj, int64_t number, akbasic_Channel **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
FAIL_ZERO_RETURN(errctx, (number >= 0 && number < AKBASIC_MAX_CHANNELS),
|
|
AKBASIC_ERR_BOUNDS, "Channel %" PRId64 " is outside 0..%d",
|
|
number, AKBASIC_MAX_CHANNELS - 1);
|
|
*dest = &obj->disk_state.channels[number];
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief The slot for an already-open channel, or an error naming the number. */
|
|
static akerr_ErrorContext *open_channel(akbasic_Runtime *obj, int64_t number, akbasic_Channel **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
PASS(errctx, channel_slot(obj, number, dest));
|
|
FAIL_ZERO_RETURN(errctx, ((*dest)->fp != NULL), AKBASIC_ERR_STATE,
|
|
"Channel %" PRId64 " is not open", number);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief Evaluate one argument as a string. */
|
|
static akerr_ErrorContext *string_arg(akbasic_Runtime *obj, akbasic_ASTLeaf *arg, const char *verb, char *dest, size_t len)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Value *value = NULL;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX, "%s expected a file name", verb);
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
|
|
FAIL_NONZERO_RETURN(errctx, (value->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
|
|
"%s expected a file name", verb);
|
|
snprintf(dest, len, "%s", value->stringval);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief Evaluate one argument as an integer. */
|
|
static akerr_ErrorContext *int_arg(akbasic_Runtime *obj, akbasic_ASTLeaf *arg, const char *verb, int64_t *dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Value *value = NULL;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX, "%s expected a number", verb);
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
|
|
FAIL_NONZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
|
|
"%s expected a number", verb);
|
|
*dest = (value->valuetype == AKBASIC_TYPE_FLOAT)
|
|
? (int64_t)value->floatval : value->intval;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* --------------------------------------------------------- DOPEN / APPEND -- */
|
|
|
|
/** @brief Shared by DOPEN and APPEND, which differ only in the mode. */
|
|
static akerr_ErrorContext *open_file(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, const char *verb, const char *mode)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *arg = NULL;
|
|
akbasic_Channel *channel = NULL;
|
|
char name[AKBASIC_MAX_STRING_LENGTH];
|
|
int64_t number = 0;
|
|
bool writing = (mode[0] != 'r');
|
|
|
|
arg = akbasic_leaf_first_argument(expr);
|
|
PASS(errctx, int_arg(obj, arg, verb, &number));
|
|
PASS(errctx, channel_slot(obj, number, &channel));
|
|
FAIL_NONZERO_RETURN(errctx, (channel->fp != NULL), AKBASIC_ERR_STATE,
|
|
"Channel %" PRId64 " is already open on \"%s\"", number, channel->name);
|
|
PASS(errctx, string_arg(obj, (arg != NULL ? arg->next : NULL), verb, name, sizeof(name)));
|
|
|
|
/*
|
|
* DOPEN's third argument is the C128's `W` for write. It arrives as a bare
|
|
* identifier, which is a label to this dialect and so cannot be evaluated --
|
|
* so what is read is the *leaf*, not its value.
|
|
*/
|
|
if ( arg != NULL && arg->next != NULL && arg->next->next != NULL ) {
|
|
akbasic_ASTLeaf *modeleaf = arg->next->next;
|
|
|
|
if ( modeleaf->leaftype == AKBASIC_LEAF_IDENTIFIER &&
|
|
(modeleaf->identifier[0] == 'W' || modeleaf->identifier[0] == 'w') ) {
|
|
mode = "w";
|
|
writing = true;
|
|
}
|
|
}
|
|
|
|
PASS(errctx, aksl_fopen(name, mode, &channel->fp));
|
|
snprintf(channel->name, sizeof(channel->name), "%s", name);
|
|
channel->writing = writing;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_cmd_dopen(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in DOPEN");
|
|
PASS(errctx, open_file(obj, expr, "DOPEN", "r"));
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_cmd_append(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in APPEND");
|
|
PASS(errctx, open_file(obj, expr, "APPEND", "a"));
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_cmd_dclose(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *arg = NULL;
|
|
akbasic_Channel *channel = NULL;
|
|
int64_t number = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in DCLOSE");
|
|
arg = akbasic_leaf_first_argument(expr);
|
|
if ( arg == NULL ) {
|
|
/* Bare DCLOSE closes everything, which is what a C128 does. */
|
|
PASS(errctx, akbasic_disk_close_all(&obj->disk_state));
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
PASS(errctx, int_arg(obj, arg, "DCLOSE", &number));
|
|
PASS(errctx, open_channel(obj, number, &channel));
|
|
PASS(errctx, aksl_fclose(channel->fp));
|
|
channel->fp = NULL;
|
|
channel->name[0] = '\0';
|
|
channel->writing = false;
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* ----------------------------------------------------------------- RECORD -- */
|
|
|
|
akerr_ErrorContext *akbasic_cmd_record(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *arg = NULL;
|
|
akbasic_Channel *channel = NULL;
|
|
int64_t number = 0;
|
|
int64_t record = 0;
|
|
int64_t offset = 1;
|
|
int64_t size = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in RECORD");
|
|
arg = akbasic_leaf_first_argument(expr);
|
|
PASS(errctx, int_arg(obj, arg, "RECORD", &number));
|
|
PASS(errctx, open_channel(obj, number, &channel));
|
|
PASS(errctx, int_arg(obj, (arg != NULL ? arg->next : NULL), "RECORD", &record));
|
|
if ( arg != NULL && arg->next != NULL && arg->next->next != NULL ) {
|
|
PASS(errctx, int_arg(obj, arg->next->next, "RECORD", &offset));
|
|
}
|
|
|
|
/*
|
|
* A C128's RECORD positions within a *relative* file, whose record length was
|
|
* fixed when the file was created. There is no such thing on a filesystem, so
|
|
* a record is taken to be a line: the file is rewound and read forward. That
|
|
* is slower than a seek and it is the only definition that does not require
|
|
* inventing a record length the file does not have. TODO.md section 5.
|
|
*/
|
|
FAIL_ZERO_RETURN(errctx, (record >= 1), AKBASIC_ERR_VALUE,
|
|
"RECORD numbers records from 1, not %" PRId64, record);
|
|
PASS(errctx, aksl_fseek(channel->fp, 0, SEEK_SET));
|
|
for ( size = 1; size < record; size++ ) {
|
|
char line[AKBASIC_MAX_STRING_LENGTH];
|
|
size_t length = 0;
|
|
akerr_ErrorContext *got = aksl_fgets(line, sizeof(line), channel->fp, &length);
|
|
|
|
if ( got != NULL ) {
|
|
got->handled = true;
|
|
IGNORE(akerr_release_error(got));
|
|
break;
|
|
}
|
|
}
|
|
if ( offset > 1 ) {
|
|
PASS(errctx, aksl_fseek(channel->fp, (long)(offset - 1), SEEK_CUR));
|
|
}
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* -------------------------------------------------------- file management -- */
|
|
|
|
akerr_ErrorContext *akbasic_cmd_scratch(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
char name[AKBASIC_MAX_STRING_LENGTH];
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in SCRATCH");
|
|
PASS(errctx, string_arg(obj, akbasic_leaf_first_argument(expr), "SCRATCH", name, sizeof(name)));
|
|
FAIL_ZERO_RETURN(errctx, (remove(name) == 0), AKERR_IO,
|
|
"SCRATCH could not delete \"%s\"", name);
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_cmd_rename(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *arg = NULL;
|
|
char from[AKBASIC_MAX_STRING_LENGTH];
|
|
char to[AKBASIC_MAX_STRING_LENGTH];
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in RENAME");
|
|
arg = akbasic_leaf_first_argument(expr);
|
|
PASS(errctx, string_arg(obj, arg, "RENAME", from, sizeof(from)));
|
|
PASS(errctx, string_arg(obj, (arg != NULL ? arg->next : NULL), "RENAME", to, sizeof(to)));
|
|
FAIL_ZERO_RETURN(errctx, (rename(from, to) == 0), AKERR_IO,
|
|
"RENAME could not rename \"%s\" to \"%s\"", from, to);
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/** @brief Shared by COPY and CONCAT, which differ only in the destination mode. */
|
|
static akerr_ErrorContext *copy_file(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, const char *verb, const char *mode)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *arg = NULL;
|
|
FILE *in = NULL;
|
|
FILE *out = NULL;
|
|
char from[AKBASIC_MAX_STRING_LENGTH];
|
|
char to[AKBASIC_MAX_STRING_LENGTH];
|
|
char buffer[4096];
|
|
|
|
arg = akbasic_leaf_first_argument(expr);
|
|
PASS(errctx, string_arg(obj, arg, verb, from, sizeof(from)));
|
|
PASS(errctx, string_arg(obj, (arg != NULL ? arg->next : NULL), verb, to, sizeof(to)));
|
|
|
|
PASS(errctx, aksl_fopen(from, "rb", &in));
|
|
ATTEMPT {
|
|
CATCH(errctx, aksl_fopen(to, mode, &out));
|
|
for ( ;; ) {
|
|
size_t got = 0;
|
|
size_t put = 0;
|
|
bool done = false;
|
|
akerr_ErrorContext *read = aksl_fread(buffer, 1, sizeof(buffer), in, &got);
|
|
|
|
if ( read != NULL ) {
|
|
/*
|
|
* A short read is how a file *ends*: aksl_fread reports fewer
|
|
* items than asked for as an error, and `got` still holds what
|
|
* it managed. Writing that out before stopping is the whole
|
|
* copy for any file smaller than the buffer -- the first
|
|
* version broke here and produced an empty destination every
|
|
* time.
|
|
*/
|
|
read->handled = true;
|
|
IGNORE(akerr_release_error(read));
|
|
done = true;
|
|
}
|
|
if ( got > 0 ) {
|
|
/*
|
|
* PASS rather than CATCH: this is a loop, and CATCH expands to
|
|
* a break that would leave the rest of the ATTEMPT running with
|
|
* an error pending.
|
|
*/
|
|
PASS(errctx, aksl_fwrite(buffer, 1, got, out, &put));
|
|
}
|
|
if ( done || got == 0 ) {
|
|
break;
|
|
}
|
|
}
|
|
} CLEANUP {
|
|
IGNORE(aksl_fclose(in));
|
|
if ( out != NULL ) {
|
|
IGNORE(aksl_fclose(out));
|
|
}
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_cmd_copy(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in COPY");
|
|
PASS(errctx, copy_file(obj, expr, "COPY", "wb"));
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_cmd_concat(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in CONCAT");
|
|
/* Appends rather than replaces, which is the whole difference from COPY. */
|
|
PASS(errctx, copy_file(obj, expr, "CONCAT", "ab"));
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* -------------------------------------------------------------- DIRECTORY -- */
|
|
|
|
akerr_ErrorContext *akbasic_cmd_directory(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
(void)expr; (void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in DIRECTORY");
|
|
/*
|
|
* Refused rather than half-built. Listing a directory needs opendir/readdir,
|
|
* which `libakstdlib` does not wrap -- and this project's rule is that a
|
|
* missing capability gets filed upstream rather than worked around here
|
|
* (MAINTENANCE.md). Filed in deps/libakstdlib/TODO.md.
|
|
*
|
|
* The alternative was shelling out to `ls`, which a library has no business
|
|
* doing, or calling readdir directly and stepping outside the error
|
|
* convention every other call in this file follows.
|
|
*/
|
|
FAIL_RETURN(errctx, AKBASIC_ERR_DEVICE,
|
|
"DIRECTORY is not implemented: libakstdlib has no directory-reading wrapper yet");
|
|
}
|
|
|
|
/* ------------------------------------------------------------ BSAVE/BLOAD -- */
|
|
|
|
akerr_ErrorContext *akbasic_cmd_bsave(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *arg = NULL;
|
|
FILE *fp = NULL;
|
|
char name[AKBASIC_MAX_STRING_LENGTH];
|
|
int64_t from = 0;
|
|
int64_t to = 0;
|
|
size_t put = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in BSAVE");
|
|
arg = akbasic_leaf_first_argument(expr);
|
|
PASS(errctx, string_arg(obj, arg, "BSAVE", name, sizeof(name)));
|
|
PASS(errctx, int_arg(obj, (arg != NULL ? arg->next : NULL), "BSAVE", &from));
|
|
PASS(errctx, int_arg(obj, (arg != NULL && arg->next != NULL ? arg->next->next : NULL),
|
|
"BSAVE", &to));
|
|
FAIL_ZERO_RETURN(errctx, (from != 0 && to > from), AKBASIC_ERR_VALUE,
|
|
"BSAVE needs a start address and a higher end address");
|
|
|
|
PASS(errctx, aksl_fopen(name, "wb", &fp));
|
|
ATTEMPT {
|
|
CATCH(errctx, aksl_fwrite((const void *)(uintptr_t)from, 1, (size_t)(to - from), fp, &put));
|
|
} CLEANUP {
|
|
IGNORE(aksl_fclose(fp));
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_cmd_bload(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *arg = NULL;
|
|
FILE *fp = NULL;
|
|
char name[AKBASIC_MAX_STRING_LENGTH];
|
|
int64_t at = 0;
|
|
int64_t limit = 0;
|
|
size_t got = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in BLOAD");
|
|
arg = akbasic_leaf_first_argument(expr);
|
|
PASS(errctx, string_arg(obj, arg, "BLOAD", name, sizeof(name)));
|
|
PASS(errctx, int_arg(obj, (arg != NULL ? arg->next : NULL), "BLOAD", &at));
|
|
/*
|
|
* The length is required, unlike a C128's BLOAD which reads until the file
|
|
* ends. A file longer than the caller expected would otherwise write past
|
|
* whatever it was pointed at, and there is no way to notice: a BASIC integer
|
|
* here is a real address. Refusing to guess is the only safe answer -- and
|
|
* checked before it is read, so a missing one says *that* rather than
|
|
* "expected a number".
|
|
*/
|
|
FAIL_ZERO_RETURN(errctx,
|
|
(arg != NULL && arg->next != NULL && arg->next->next != NULL),
|
|
AKBASIC_ERR_SYNTAX,
|
|
"BLOAD needs an address and a maximum length");
|
|
PASS(errctx, int_arg(obj, arg->next->next, "BLOAD", &limit));
|
|
FAIL_ZERO_RETURN(errctx, (at != 0 && limit > 0), AKBASIC_ERR_VALUE,
|
|
"BLOAD needs an address and a maximum length");
|
|
|
|
PASS(errctx, aksl_fopen(name, "rb", &fp));
|
|
ATTEMPT {
|
|
akerr_ErrorContext *read = aksl_fread((void *)(uintptr_t)at, 1, (size_t)limit, fp, &got);
|
|
|
|
if ( read != NULL ) {
|
|
/* A short read is the ordinary case: the file was smaller than the cap. */
|
|
read->handled = true;
|
|
IGNORE(akerr_release_error(read));
|
|
}
|
|
} CLEANUP {
|
|
IGNORE(aksl_fclose(fp));
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* ------------------------------------------------------------ channel I/O -- */
|
|
|
|
akerr_ErrorContext *akbasic_disk_write(akbasic_Runtime *obj, int64_t number, const char *text)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Channel *channel = NULL;
|
|
size_t put = 0;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && text != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in disk_write");
|
|
PASS(errctx, open_channel(obj, number, &channel));
|
|
FAIL_ZERO_RETURN(errctx, channel->writing, AKBASIC_ERR_STATE,
|
|
"Channel %" PRId64 " was opened for reading", number);
|
|
PASS(errctx, aksl_fwrite(text, 1, strlen(text), channel->fp, &put));
|
|
PASS(errctx, aksl_fwrite("\n", 1, 1, channel->fp, &put));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_disk_readline(akbasic_Runtime *obj, int64_t number, char *dest, size_t len, bool *eof)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_Channel *channel = NULL;
|
|
akerr_ErrorContext *got = NULL;
|
|
char *newline = NULL;
|
|
size_t length = 0;
|
|
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL && eof != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in disk_readline");
|
|
PASS(errctx, open_channel(obj, number, &channel));
|
|
FAIL_NONZERO_RETURN(errctx, channel->writing, AKBASIC_ERR_STATE,
|
|
"Channel %" PRId64 " was opened for writing", number);
|
|
|
|
*eof = false;
|
|
dest[0] = '\0';
|
|
got = aksl_fgets(dest, len, channel->fp, &length);
|
|
if ( got != NULL ) {
|
|
/*
|
|
* End of file is not an error here, the same way it is not one for the
|
|
* sink's readline: running out of input is a state a program handles.
|
|
*/
|
|
got->handled = true;
|
|
IGNORE(akerr_release_error(got));
|
|
*eof = true;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
newline = strchr(dest, '\n');
|
|
if ( newline != NULL ) {
|
|
*newline = '\0';
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_cmd_print_channel(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *arg = NULL;
|
|
akbasic_Value *value = NULL;
|
|
char text[AKBASIC_MAX_STRING_LENGTH];
|
|
int64_t number = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in PRINT#");
|
|
arg = akbasic_leaf_first_argument(expr);
|
|
PASS(errctx, int_arg(obj, arg, "PRINT#", &number));
|
|
FAIL_ZERO_RETURN(errctx, (arg != NULL && arg->next != NULL), AKBASIC_ERR_SYNTAX,
|
|
"Expected PRINT #(channel), (expression)");
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, arg->next, &value));
|
|
PASS(errctx, akbasic_value_to_string(value, text, sizeof(text)));
|
|
PASS(errctx, akbasic_disk_write(obj, number, text));
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_cmd_input_channel(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akbasic_ASTLeaf *arg = NULL;
|
|
akbasic_ASTLeaf *identifier = NULL;
|
|
akbasic_ASTLeaf literal;
|
|
akbasic_ASTLeaf assign;
|
|
akbasic_Value *unused = NULL;
|
|
char text[AKBASIC_MAX_STRING_LENGTH];
|
|
int64_t number = 0;
|
|
bool eof = false;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in INPUT#");
|
|
arg = akbasic_leaf_first_argument(expr);
|
|
PASS(errctx, int_arg(obj, arg, "INPUT#", &number));
|
|
identifier = (arg != NULL ? arg->next : NULL);
|
|
FAIL_ZERO_RETURN(errctx, akbasic_leaf_is_identifier(identifier), AKBASIC_ERR_SYNTAX,
|
|
"Expected INPUT #(channel), (variable)");
|
|
|
|
PASS(errctx, akbasic_disk_readline(obj, number, text, sizeof(text), &eof));
|
|
/*
|
|
* End of file leaves the variable empty rather than raising. A program reads
|
|
* until it gets nothing back, which is what it can check -- and a numeric
|
|
* variable reading an empty line gets zero, the same as a C128.
|
|
*/
|
|
PASS(errctx, akbasic_leaf_init(&literal, AKBASIC_LEAF_LITERAL_STRING));
|
|
snprintf(literal.literal_string, sizeof(literal.literal_string), "%s", text);
|
|
if ( akbasic_leaf_identifier_type(identifier) == AKBASIC_TYPE_INTEGER ) {
|
|
long long converted = 0;
|
|
|
|
literal.leaftype = AKBASIC_LEAF_LITERAL_INT;
|
|
literal.literal_int = 0;
|
|
if ( !eof && text[0] != '\0' ) {
|
|
PASS(errctx, aksl_atoll(text, &converted));
|
|
literal.literal_int = (int64_t)converted;
|
|
}
|
|
} else if ( akbasic_leaf_identifier_type(identifier) == AKBASIC_TYPE_FLOAT ) {
|
|
literal.leaftype = AKBASIC_LEAF_LITERAL_FLOAT;
|
|
literal.literal_float = 0.0;
|
|
if ( !eof && text[0] != '\0' ) {
|
|
PASS(errctx, aksl_atof(text, &literal.literal_float));
|
|
}
|
|
}
|
|
PASS(errctx, akbasic_leaf_init(&assign, AKBASIC_LEAF_BINARY));
|
|
assign.left = identifier;
|
|
assign.right = &literal;
|
|
assign.operator_ = AKBASIC_TOK_ASSIGNMENT;
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, &assign, &unused));
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- VERIFY -- */
|
|
|
|
akerr_ErrorContext *akbasic_cmd_dverify(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
FILE *fp = NULL;
|
|
char name[AKBASIC_MAX_STRING_LENGTH];
|
|
char line[AKBASIC_MAX_LINE_LENGTH];
|
|
int64_t lineno = 0;
|
|
int64_t mismatch = 0;
|
|
|
|
(void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
|
"NULL argument in VERIFY");
|
|
PASS(errctx, string_arg(obj, akbasic_leaf_first_argument(expr), "VERIFY", name, sizeof(name)));
|
|
|
|
/*
|
|
* A C128 verifies the program in memory against the one on disk byte for
|
|
* byte, because a 1541 write could fail silently. Here the comparison is
|
|
* line by line against the file's text, which is the same question asked of
|
|
* a filesystem: is what I would save what is already there?
|
|
*/
|
|
PASS(errctx, aksl_fopen(name, "r", &fp));
|
|
ATTEMPT {
|
|
for ( lineno = 0; lineno < AKBASIC_MAX_SOURCE_LINES; lineno++ ) {
|
|
size_t length = 0;
|
|
akerr_ErrorContext *got = NULL;
|
|
char *newline = NULL;
|
|
|
|
if ( obj->source[lineno].code[0] == '\0' ) {
|
|
continue;
|
|
}
|
|
got = aksl_fgets(line, sizeof(line), fp, &length);
|
|
if ( got != NULL ) {
|
|
got->handled = true;
|
|
IGNORE(akerr_release_error(got));
|
|
mismatch += 1;
|
|
break;
|
|
}
|
|
newline = strchr(line, '\n');
|
|
if ( newline != NULL ) {
|
|
*newline = '\0';
|
|
}
|
|
/*
|
|
* The stored line has had its number stripped, so the file's copy is
|
|
* compared from past its own number. PASS rather than CATCH: this is
|
|
* a loop.
|
|
*/
|
|
{
|
|
const char *filetext = line;
|
|
|
|
while ( *filetext == ' ' ) {
|
|
filetext += 1;
|
|
}
|
|
while ( *filetext >= '0' && *filetext <= '9' ) {
|
|
filetext += 1;
|
|
}
|
|
while ( *filetext == ' ' ) {
|
|
filetext += 1;
|
|
}
|
|
if ( strcmp(filetext, obj->source[lineno].code) != 0 ) {
|
|
mismatch += 1;
|
|
}
|
|
}
|
|
}
|
|
} CLEANUP {
|
|
IGNORE(aksl_fclose(fp));
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
|
|
FAIL_NONZERO_RETURN(errctx, (mismatch != 0), AKBASIC_ERR_VALUE,
|
|
"VERIFY: \"%s\" does not match the program in memory (%" PRId64 " lines differ)",
|
|
name, mismatch);
|
|
PASS(errctx, akbasic_runtime_println(obj, "OK"));
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
/* --------------------------------------------------- needs a real 1541 --- */
|
|
|
|
/**
|
|
* @brief Refuse a verb that has no meaning without the drive, and say why.
|
|
*
|
|
* Five of them, and the reasoning is the same each time: formatting a disk,
|
|
* validating one, duplicating one, resetting a drive and booting from one are
|
|
* operations on a physical device. A filesystem has no equivalent that is not a
|
|
* lie -- `HEADER` would have to mean "delete everything in this directory",
|
|
* which is a spectacularly bad thing to do to somebody who typed a C128 verb.
|
|
*/
|
|
static akerr_ErrorContext *no_drive(const char *verb, const char *what)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
FAIL_RETURN(errctx, AKBASIC_ERR_DEVICE,
|
|
"%s %s, and there is no disk drive here -- only a filesystem", verb, what);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_cmd_header(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)obj; (void)expr; (void)lval; (void)rval; (void)dest;
|
|
PASS(errctx, no_drive("HEADER", "formats a disk"));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_cmd_collect(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)obj; (void)expr; (void)lval; (void)rval; (void)dest;
|
|
PASS(errctx, no_drive("COLLECT", "validates a disk's block allocation map"));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_cmd_backup(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)obj; (void)expr; (void)lval; (void)rval; (void)dest;
|
|
PASS(errctx, no_drive("BACKUP", "duplicates one disk onto another"));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_cmd_boot(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
(void)obj; (void)expr; (void)lval; (void)rval; (void)dest;
|
|
PASS(errctx, no_drive("BOOT", "loads and runs a boot sector"));
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akbasic_cmd_dclear(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
|
|
(void)expr; (void)lval; (void)rval;
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in DCLEAR");
|
|
/*
|
|
* The one of the five with a defensible meaning. DCLEAR resets the drive and
|
|
* closes every channel it had open; the second half is exactly
|
|
* akbasic_disk_close_all(), so that is what it does. The first half has
|
|
* nothing to reset.
|
|
*/
|
|
PASS(errctx, akbasic_disk_close_all(&obj->disk_state));
|
|
SUCCEED_TRUE(obj, dest);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|