1 Commits

Author SHA1 Message Date
1f3b9f5cf6 Report prescan errors on their source lines
All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m28s
akbasic CI Build / sanitizers (push) Successful in 4m47s
akbasic CI Build / coverage (push) Successful in 3m58s
akbasic CI Build / akgl_build (push) Successful in 8m50s
akbasic CI Build / mutation_test (push) Successful in 23m38s
2026-08-05 15:53:33 -04:00
22 changed files with 159 additions and 163 deletions

View File

@@ -12,4 +12,3 @@ WARN_AS_ERROR = FAIL_ON_WARNINGS
GENERATE_HTML = YES GENERATE_HTML = YES
GENERATE_LATEX = NO GENERATE_LATEX = NO
QUIET = YES QUIET = YES
DOT_GRAPH_MAX_NODES = 100

View File

@@ -9,7 +9,6 @@ so a call with the wrong number is a syntax error rather than a surprise.
| Function | Args | Form | What it gives | | Function | Args | Form | What it gives |
|---|---|---|---| |---|---|---|---|
| `ABS` | 1 | `ABS(n)` | The absolute value of an integer or float. | | `ABS` | 1 | `ABS(n)` | The absolute value of an integer or float. |
| `ASC` | 1 | `ASC(A$)` | The Unicode code point of a string's first character. |
| `ATN` | 1 | `ATN(n)` | Arctangent, in radians. | | `ATN` | 1 | `ATN(n)` | Arctangent, in radians. |
| `BUMP` | 1 | `BUMP(1)` | Which sprites have collided, as a bitmask. **Reading clears it.** | | `BUMP` | 1 | `BUMP(1)` | Which sprites have collided, as a bitmask. **Reading clears it.** |
| `CHR` | 1 | `CHR(n)` | The character for a Unicode code point, as a string. | | `CHR` | 1 | `CHR(n)` | The character for a Unicode code point, as a string. |
@@ -30,7 +29,6 @@ so a call with the wrong number is a syntax error rather than a surprise.
| `RGR` | 1 | `RGR(f)` | The `GRAPHIC` mode (0), the drawing surface's width (1) or height (2) in pixels, or a character cell's width (3) or height (4). | | `RGR` | 1 | `RGR(f)` | The `GRAPHIC` mode (0), the drawing surface's width (1) or height (2) in pixels, or a character cell's width (3) or height (4). |
| `RIGHT` | 2 | `RIGHT(A$, n)` | The rightmost `n` characters. Clamped. | | `RIGHT` | 2 | `RIGHT(A$, n)` | The rightmost `n` characters. Clamped. |
| `RMENU` | 2 | `RMENU(n, f)` | A menu's state: field 0 the highlighted entry, field 1 whether it has been confirmed. **Reading field 1 clears it.** | | `RMENU` | 2 | `RMENU(n, f)` | A menu's state: field 0 the highlighted entry, field 1 whether it has been confirmed. **Reading field 1 clears it.** |
| `RND` | 1 | `RND(n)` | A random integer from 0 up to but not including `n`. |
| `RWINDOW` | 1 | `RWINDOW(f)` | The current text window's rows (0) or columns (1). Field 2 is a C128 screen mode and is refused. | | `RWINDOW` | 1 | `RWINDOW(f)` | The current text window's rows (0) or columns (1). Field 2 is a C128 screen mode and is refused. |
| `RSPCOLOR` | 1 | `RSPCOLOR(n)` | One of `SPRCOLOR`'s two shared registers, 1 or 2. | | `RSPCOLOR` | 1 | `RSPCOLOR(n)` | One of `SPRCOLOR`'s two shared registers, 1 or 2. |
| `RSPHIT` | 2 | `RSPHIT(n, f)` | One of `SPRHIT`'s settings for sprite `n`, in `SPRHIT`'s own argument order: 0 the kind, 1 to 4 the two corners. | | `RSPHIT` | 2 | `RSPHIT(n, f)` | One of `SPRHIT`'s settings for sprite `n`, in `SPRHIT`'s own argument order: 0 the kind, 1 to 4 the two corners. |

View File

@@ -50,7 +50,7 @@ nothing would say which type it is.
``` ```
```output ```output
? 40 : PARSE ERROR TYPE POINT: POINT is a reserved word and cannot name a type ? 10 : PARSE ERROR TYPE POINT: POINT is a reserved word and cannot name a type
``` ```

View File

@@ -1005,48 +1005,20 @@ IF NUDGE# = 1 THEN GOSUB UNSTICK
LABEL UNSTICK LABEL UNSTICK
NUDGE# = 0 NUDGE# = 0
STALL# = 0 STALL# = 0
BVX# = (RND(4) * 3) - 6 RMAX# = 4
GOSUB RANDOM
BVX# = (RND# * 3) - 6
IF BVX# = 0 THEN BVX# = 3 IF BVX# = 0 THEN BVX# = 3
RETURN RETURN
``` ```
### Random numbers are built in ### You have to write your own random numbers
There is no `INT`, `SQR` or `TIMER` in this dialect, but **There is no `RND` in this dialect**, and no `INT`, `SQR`, `ASC` or `TIMER` either. A
`RND(n)` returns an integer from zero through `n - 1`. It seeds itself linear congruential generator is nine tokens and does the job. Put the number of possible
from the host clock the first time it is called, so a program only needs the bound: answers in `RMAX#` and read the result from `RND#`:
```basic ```basic
I# = 0
FOR I# = 1 TO 5
PRINT "ROLL " + (RND(6) + 1)
NEXT I#
END
```
Use `RND` for the serve, too, so the ball does not always leave in the same direction:
```basic norun
LABEL SERVE
PX# = (SCW# - PW#) / 2
HELD# = 1
BX# = PX# + ((PW# / 2) - 4)
BY# = PY# - 10
BVX# = BSPD#
IF RND(2) = 0 THEN BVX# = 0 - BSPD#
BVY# = 0 - BSPD#
PDEC# = 0
GOSUB SHOWSPR
RETURN
```
<details>
<summary>Historical aside: the LCG this chapter used to teach</summary>
Before `RND` existed, this nine-token linear congruential generator was copied into
every program. It remains a useful from-scratch PRNG example:
```basic norun
SEED# = 12345 SEED# = 12345
RMAX# = 6 RMAX# = 6
RND# = 0 RND# = 0
@@ -1063,11 +1035,43 @@ RND# = MOD((SEED# / 65536), RMAX#)
RETURN RETURN
``` ```
The multiplication stays inside a 64-bit integer for any seed below 2147483648. The ```output
answer is taken from the middle bits because the low bits of a power-of-two modulus ROLL 1
barely change from one call to the next. This used to be required; it is now built in. ROLL 5
ROLL 2
ROLL 1
ROLL 2
```
</details> The multiplication stays inside a 64-bit integer for any seed below 2147483648, which is
why the modulus is that number. The answer is taken from the middle bits — `SEED# / 65536`
— because the low bits of a power-of-two modulus barely change from one call to the next.
Integer division truncating for free is the `INT` you do not have.
Seed it from the clock at startup. `TI#` is the host's uptime in sixtieths of a second,
which is different every time the game is run:
```basic norun
SEED# = TI#
```
Use `RANDOM` for the serve, too, so the ball does not always leave in the same direction:
```basic norun
LABEL SERVE
PX# = (SCW# - PW#) / 2
HELD# = 1
BX# = PX# + ((PW# / 2) - 4)
BY# = PY# - 10
RMAX# = 2
GOSUB RANDOM
BVX# = BSPD#
IF RND# = 0 THEN BVX# = 0 - BSPD#
BVY# = 0 - BSPD#
PDEC# = 0
GOSUB SHOWSPR
RETURN
```
`HELD#` is the flag Step 6's loop tests: while it is 1 the ball sits on the paddle, and `HELD#` is the flag Step 6's loop tests: while it is 1 the ball sits on the paddle, and
`HOLDBAL` keeps it there: `HOLDBAL` keeps it there:
@@ -1418,7 +1422,9 @@ PX# = PX# + D#
RETURN RETURN
LABEL DEMOAIM LABEL DEMOAIM
DOFF# = RND(81) - 40 RMAX# = 81
GOSUB RANDOM
DOFF# = RND# - 40
RETURN RETURN
``` ```
@@ -1495,7 +1501,7 @@ This is the shape of the whole file:
LABEL SETUP the geometry from Step 2 LABEL SETUP the geometry from Step 2
the declaration block from Step 3 the declaration block from Step 3
the brick faces from Step 5 the brick faces from Step 5
RND(n) seeds itself from the host clock SEED# = TI#
the ceiling from Step 9 the ceiling from Step 9
GOSUB MKSPR Step 4 GOSUB MKSPR Step 4
GOSUB SNDPROBE Step 14 GOSUB SNDPROBE Step 14
@@ -1570,7 +1576,10 @@ BB# = 0
RX# = 0 RX# = 0
N# = 0 N# = 0
MROW# = 0 MROW# = 0
RMAX# = 2
RND# = 0
SND# = 0 SND# = 0
SEED# = 0
P$ = "" P$ = ""
H$ = "" H$ = ""
S$ = "" S$ = ""

View File

@@ -296,7 +296,7 @@ typedef struct
* *
* Claimed up front rather than per scan for two reasons. The pool is shared * Claimed up front rather than per scan for two reasons. The pool is shared
* with whatever host this interpreter is embedded in -- a game with its own * with whatever host this interpreter is embedded in -- a game with its own
* shaped actors draws from the same `AKGL_MAX_HEAP_COLLISION_PROXY` -- so * shaped actors draws from the same #AKGL_MAX_HEAP_COLLISION_PROXY -- so
* running out is a real possibility, and it should be an init-time failure * running out is a real possibility, and it should be an init-time failure
* naming the pool rather than a collision scan that starts refusing halfway * naming the pool rather than a collision scan that starts refusing halfway
* through a game. And a proxy carries a *copy* of its shape, so there is * through a game. And a proxy carries a *copy* of its shape, so there is

View File

@@ -252,11 +252,6 @@ typedef struct akbasic_Runtime
*/ */
int64_t timems; int64_t timems;
/* RND's lazy seed state. The flag distinguishes an unseeded run from a
* legitimate LCG state of zero. */
int64_t rndseed;
bool rndseeded;
/* /*
* Set by a branch that has decided the remaining statements on its line * Set by a branch that has decided the remaining statements on its line
* belong to the arm it did not take, and cleared at the top of every line. * belong to the arm it did not take, and cleared at the top of every line.
@@ -832,6 +827,13 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_reserve_globals(akbasic_Runti
* @throws AKBASIC_ERR_BOUNDS When every variable slot is in use. * @throws AKBASIC_ERR_BOUNDS When every variable slot is in use.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_new_variable(akbasic_Runtime *obj, akbasic_Variable **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_new_variable(akbasic_Runtime *obj, akbasic_Variable **dest);
/**
* @brief Take an unused function definition from the runtime's pool.
* @param obj Object to initialize, inspect, or modify.
* @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 every function slot is in use.
*/
/** /**
* @brief Call a user-defined function with values a caller already has. * @brief Call a user-defined function with values a caller already has.
* *
@@ -858,13 +860,6 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_new_variable(akbasic_Runtime
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_call_function(struct akbasic_Runtime *obj, const char *name, akbasic_Value **args, int nargs, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_call_function(struct akbasic_Runtime *obj, const char *name, akbasic_Value **args, int nargs, akbasic_Value **dest);
/**
* @brief Take an unused function definition from the runtime's pool.
* @param obj Object to initialize, inspect, or modify.
* @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 every function slot is in use.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_new_function(akbasic_Runtime *obj, akbasic_FunctionDef **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_new_function(akbasic_Runtime *obj, akbasic_FunctionDef **dest);
/** /**
* @brief File one already-scanned source line under its line number. * @brief File one already-scanned source line under its line number.

View File

@@ -59,7 +59,7 @@
* with headroom. The ceiling matters because these come out of libakgl's * with headroom. The ceiling matters because these come out of libakgl's
* collision proxy pool, which is shared with whatever host this interpreter is * collision proxy pool, which is shared with whatever host this interpreter is
* embedded in: eight sprites plus sixty-four solids is seventy-two of * embedded in: eight sprites plus sixty-four solids is seventy-two of
* `AKGL_MAX_HEAP_COLLISION_PROXY`, and the rest is the host's. * #AKGL_MAX_HEAP_COLLISION_PROXY, and the rest is the host's.
*/ */
#ifndef AKBASIC_MAX_SOLIDS #ifndef AKBASIC_MAX_SOLIDS
#define AKBASIC_MAX_SOLIDS 64 #define AKBASIC_MAX_SOLIDS 64
@@ -117,7 +117,7 @@ typedef struct
int speed; /* clockwise from vertical, and 0-15 */ int speed; /* clockwise from vertical, and 0-15 */
/** /**
* SPRHIT's collision shape: one of the `AKBASIC_SHAPE_*` kinds, and a * SPRHIT's collision shape: one of the #AKBASIC_SHAPE_* kinds, and a
* rectangle measured from the sprite's top-left corner. * rectangle measured from the sprite's top-left corner.
* *
* `shapeexplicit` is what separates "the program asked for the whole frame" * `shapeexplicit` is what separates "the program asked for the whole frame"

View File

@@ -160,14 +160,21 @@ akerr_ErrorContext *akbasic_data_scan(akbasic_Runtime *obj)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
int64_t i = 0; int64_t i = 0;
int64_t entry = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in data_scan"); FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in data_scan");
FAIL_ZERO_RETURN(errctx, (obj->environment != NULL), AKERR_NULLPOINTER,
"Runtime has no environment; call akbasic_runtime_init() first");
entry = obj->environment->lineno;
PASS(errctx, akbasic_data_state_init(&obj->data_state)); PASS(errctx, akbasic_data_state_init(&obj->data_state));
for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) { for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) {
if ( obj->source[i].code[0] != '\0' ) { if ( obj->source[i].code[0] != '\0' ) {
/* Keep BASIC's error prefix on the source line being prescanned. */
obj->environment->lineno = i;
PASS(errctx, scan_line(&obj->data_state, obj->source[i].code, i)); PASS(errctx, scan_line(&obj->data_state, obj->source[i].code, i));
} }
} }
obj->environment->lineno = entry;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }

View File

@@ -1600,17 +1600,22 @@ akerr_ErrorContext *akbasic_runtime_scan_labels(akbasic_Runtime *obj)
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
akbasic_Environment *root = NULL; akbasic_Environment *root = NULL;
int64_t i = 0; int64_t i = 0;
int64_t entry = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in scan_labels"); FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in scan_labels");
FAIL_ZERO_RETURN(errctx, (obj->environment != NULL), AKERR_NULLPOINTER, FAIL_ZERO_RETURN(errctx, (obj->environment != NULL), AKERR_NULLPOINTER,
"Runtime has no environment; call akbasic_runtime_init() first"); "Runtime has no environment; call akbasic_runtime_init() first");
for ( root = obj->environment; root->parent != NULL; root = root->parent ) { for ( root = obj->environment; root->parent != NULL; root = root->parent ) {
} }
entry = obj->environment->lineno;
for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) { for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) {
if ( obj->source[i].code[0] != '\0' ) { if ( obj->source[i].code[0] != '\0' ) {
/* Keep BASIC's error prefix on the source line being prescanned. */
obj->environment->lineno = i;
PASS(errctx, scan_line_labels(root, obj->source[i].code, i)); PASS(errctx, scan_line_labels(root, obj->source[i].code, i));
} }
} }
obj->environment->lineno = entry;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }

View File

@@ -183,69 +183,6 @@ akerr_ErrorContext *akbasic_fn_chr(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *akbasic_fn_asc(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_Value *arg = NULL;
akbasic_Value *out = NULL;
const unsigned char *text = NULL;
int64_t codepoint = 0;
(void)lval; (void)rval;
PASS(errctx, first_arg(obj, expr, "ASC", NULL, &arg, &out));
FAIL_NONZERO_RETURN(errctx, (arg->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
"ASC expected a string");
FAIL_ZERO_RETURN(errctx, (arg->stringval[0] != '\0'), AKBASIC_ERR_BOUNDS,
"ASC expected a non-empty string");
/* Decode the first UTF-8 code point, the inverse of CHR's encoder. */
text = (const unsigned char *)arg->stringval;
if ( text[0] < 0x80 ) {
codepoint = text[0];
} else if ( (text[0] & 0xE0) == 0xC0 ) {
codepoint = ((int64_t)(text[0] & 0x1F) << 6) |
(text[1] & 0x3F);
} else if ( (text[0] & 0xF0) == 0xE0 ) {
codepoint = ((int64_t)(text[0] & 0x0F) << 12) |
((int64_t)(text[1] & 0x3F) << 6) |
(text[2] & 0x3F);
} else {
codepoint = ((int64_t)(text[0] & 0x07) << 18) |
((int64_t)(text[1] & 0x3F) << 12) |
((int64_t)(text[2] & 0x3F) << 6) |
(text[3] & 0x3F);
}
out->valuetype = AKBASIC_TYPE_INTEGER;
out->intval = codepoint;
*dest = out;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_fn_rnd(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
PREPARE_ERROR(errctx);
akbasic_Value *arg = NULL;
akbasic_Value *out = NULL;
const int64_t modulus = 2147483648;
(void)lval; (void)rval;
PASS(errctx, first_arg(obj, expr, "RND", NULL, &arg, &out));
FAIL_NONZERO_RETURN(errctx, (arg->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
"RND expected an integer");
FAIL_ZERO_RETURN(errctx, (arg->intval > 0), AKBASIC_ERR_VALUE,
"RND count %" PRId64 " must be positive", arg->intval);
if ( !obj->rndseeded ) {
obj->rndseed = obj->timems % modulus;
obj->rndseeded = true;
}
obj->rndseed = (obj->rndseed * 1103515245 + 12345) % modulus;
out->valuetype = AKBASIC_TYPE_INTEGER;
out->intval = (obj->rndseed / 65536) % arg->intval;
*dest = out;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_fn_hex(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest) akerr_ErrorContext *akbasic_fn_hex(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);

View File

@@ -48,7 +48,6 @@ static akerr_ErrorContext *is_at_end(akbasic_Runtime *obj, bool *dest)
/** /**
* @brief The character under the cursor. * @brief The character under the cursor.
* @param obj The runtime whose scan cursor is being read.
* @param[out] dest The character. Untouched when there is none. * @param[out] dest The character. Untouched when there is none.
* @param[out] got Whether there was one. The old `bool` return. * @param[out] got Whether there was one. The old `bool` return.
*/ */
@@ -71,7 +70,6 @@ static akerr_ErrorContext *peek(akbasic_Runtime *obj, char *dest, bool *got)
/** /**
* @brief The character one past the cursor. * @brief The character one past the cursor.
* @param obj The runtime whose scan cursor is being read.
* @param[out] dest The character. Untouched when there is none. * @param[out] dest The character. Untouched when there is none.
* @param[out] got Whether there was one. The old `bool` return. * @param[out] got Whether there was one. The old `bool` return.
*/ */
@@ -147,10 +145,6 @@ static akerr_ErrorContext *add_token(akbasic_Runtime *obj, akbasic_TokenType tok
/** /**
* @brief Consume one more character when it matches, choosing between two token types. * @brief Consume one more character when it matches, choosing between two token types.
* @param obj The runtime whose scan cursor is being advanced.
* @param cm The character that must be next for the match to succeed.
* @param truetype The token type to report when @p cm matches.
* @param falsetype The token type to report when it does not.
* @param[out] matched Whether the character was consumed. The old `bool` return. * @param[out] matched Whether the character was consumed. The old `bool` return.
* *
* On the chain below `peek`, so it reports the same way. See libakstdlib #38. * On the chain below `peek`, so it reports the same way. See libakstdlib #38.

View File

@@ -875,7 +875,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *run_scan(akbasic_AkglSprites *state)
* proxy carries the owner only so a resolver can push something. Nothing here * proxy carries the owner only so a resolver can push something. Nothing here
* resolves anything, so the field stays empty and the shape is what matters. * resolves anything, so the field stays empty and the shape is what matters.
* *
* Layers are the other half. A wall sits on `AKGL_COLLISION_LAYER_STATIC` and * Layers are the other half. A wall sits on #AKGL_COLLISION_LAYER_STATIC and
* responds to nothing, which is the asymmetry libakgl's masks exist for: the * responds to nothing, which is the asymmetry libakgl's masks exist for: the
* sprite's own `collidemask` includes STATIC, so a sprite finds a wall and two * sprite's own `collidemask` includes STATIC, so a sprite finds a wall and two
* walls never test against each other. Sixty-four motionless rectangles * walls never test against each other. Sixty-four motionless rectangles

View File

@@ -260,11 +260,13 @@ static akerr_ErrorContext *scan_names(akbasic_Runtime *obj)
size_t namelen = 0; size_t namelen = 0;
bool matched = false; bool matched = false;
const akbasic_Verb *verb = NULL; const akbasic_Verb *verb = NULL;
int64_t entry = obj->environment->lineno;
for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) { for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) {
if ( obj->source[i].code[0] == '\0' ) { if ( obj->source[i].code[0] == '\0' ) {
continue; continue;
} }
obj->environment->lineno = i;
cursor = next_word(skip_lineno(obj->source[i].code), word, sizeof(word)); cursor = next_word(skip_lineno(obj->source[i].code), word, sizeof(word));
PASS(errctx, word_is(word, "END", &matched)); PASS(errctx, word_is(word, "END", &matched));
@@ -327,6 +329,7 @@ static akerr_ErrorContext *scan_names(akbasic_Runtime *obj)
FAIL_NONZERO_RETURN(errctx, (open >= 0), AKBASIC_ERR_SYNTAX, FAIL_NONZERO_RETURN(errctx, (open >= 0), AKBASIC_ERR_SYNTAX,
"TYPE %s is never closed with END TYPE", table->types[open >= 0 ? open : 0].name); "TYPE %s is never closed with END TYPE", table->types[open >= 0 ? open : 0].name);
obj->environment->lineno = entry;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -440,7 +443,7 @@ static akerr_ErrorContext *parse_field(akbasic_StructTypeTable *table, akbasic_S
* each other by value, which has no finite size. That is the diagnosis rather * each other by value, which has no finite size. That is the diagnosis rather
* than a stack overflow later. * than a stack overflow later.
*/ */
static akerr_ErrorContext *resolve_sizes(akbasic_StructTypeTable *table) static akerr_ErrorContext *resolve_sizes(akbasic_Runtime *runtime, akbasic_StructTypeTable *table)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
bool progress = true; bool progress = true;
@@ -477,6 +480,7 @@ static akerr_ErrorContext *resolve_sizes(akbasic_StructTypeTable *table)
} }
for ( i = 0; i < table->count; i++ ) { for ( i = 0; i < table->count; i++ ) {
runtime->environment->lineno = table->types[i].firstline;
FAIL_NONZERO_RETURN(errctx, (table->types[i].slotcount < 0), AKBASIC_ERR_VALUE, FAIL_NONZERO_RETURN(errctx, (table->types[i].slotcount < 0), AKBASIC_ERR_VALUE,
"TYPE %s contains itself by value, so it has no size. " "TYPE %s contains itself by value, so it has no size. "
"A type may only refer to itself through PTR TO", "A type may only refer to itself through PTR TO",
@@ -493,9 +497,13 @@ akerr_ErrorContext *akbasic_structtype_scan(akbasic_Runtime *obj)
akbasic_StructTypeTable *table = NULL; akbasic_StructTypeTable *table = NULL;
int64_t i = 0; int64_t i = 0;
int t = 0; int t = 0;
int64_t entry = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in structtype scan"); FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in structtype scan");
FAIL_ZERO_RETURN(errctx, (obj->environment != NULL), AKERR_NULLPOINTER,
"Runtime has no environment; call akbasic_runtime_init() first");
table = &obj->structtypes; table = &obj->structtypes;
entry = obj->environment->lineno;
/* /*
* Drop what the *script* declared and keep what the *host* registered. * Drop what the *script* declared and keep what the *host* registered.
@@ -525,10 +533,12 @@ akerr_ErrorContext *akbasic_structtype_scan(akbasic_Runtime *obj)
if ( obj->source[i].code[0] == '\0' ) { if ( obj->source[i].code[0] == '\0' ) {
continue; continue;
} }
obj->environment->lineno = i;
PASS(errctx, parse_field(table, type, obj->source[i].code, i)); PASS(errctx, parse_field(table, type, obj->source[i].code, i));
} }
} }
PASS(errctx, resolve_sizes(table)); PASS(errctx, resolve_sizes(obj, table));
obj->environment->lineno = entry;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }

View File

@@ -37,7 +37,6 @@ static const akbasic_Verb VERBS[] = {
{ "ABS", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_abs }, { "ABS", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_abs },
{ "AND", AKBASIC_TOK_AND, -1, NULL, NULL }, { "AND", AKBASIC_TOK_AND, -1, NULL, NULL },
{ "APPEND", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_append }, { "APPEND", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_append },
{ "ASC", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_asc },
{ "ATN", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_atn }, { "ATN", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_atn },
{ "AUTO", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, NULL, akbasic_cmd_auto }, { "AUTO", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, NULL, akbasic_cmd_auto },
{ "BACKUP", AKBASIC_TOK_COMMAND, -1, akbasic_parse_optional_arglist, akbasic_cmd_backup }, { "BACKUP", AKBASIC_TOK_COMMAND, -1, akbasic_parse_optional_arglist, akbasic_cmd_backup },
@@ -147,7 +146,6 @@ static const akbasic_Verb VERBS[] = {
{ "RGR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_rgr }, { "RGR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_rgr },
{ "RIGHT", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_right }, { "RIGHT", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_right },
{ "RMENU", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_rmenu }, { "RMENU", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_rmenu },
{ "RND", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_rnd },
{ "RSPCOLOR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_rspcolor }, { "RSPCOLOR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_rspcolor },
{ "RSPHIT", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_rsphit }, { "RSPHIT", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_rsphit },
{ "RSPPOS", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_rsppos }, { "RSPPOS", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_rsppos },

View File

@@ -140,7 +140,6 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_stop(struct akbasic_Runtime *obj,
/* Function handlers -- src/runtime_functions.c */ /* Function handlers -- src/runtime_functions.c */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_abs(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_abs(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_asc(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_atn(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_atn(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_chr(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_chr(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_cos(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_cos(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
@@ -155,7 +154,6 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_peek(struct akbasic_Runtime *obj,
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_pointer(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_pointer(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_pointervar(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_pointervar(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_rad(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_rad(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_rnd(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_right(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_right(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_sgn(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_sgn(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_shl(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest); akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_shl(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);

View File

@@ -16,6 +16,8 @@
* below. * below.
*/ */
#include <stdio.h>
#include "harness.h" #include "harness.h"
/** /**
@@ -341,6 +343,28 @@ static akerr_ErrorContext AKERR_NOIGNORE *test_prescan_boundaries(void)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
/** @brief A full label table reports the line whose label could not be filed. */
static void test_label_prescan_error_line(void)
{
char source[4096] = "";
size_t used = 0;
int i = 0;
for ( i = 1; i <= AKBASIC_MAX_LABELS + 1; i++ ) {
used += (size_t)snprintf(source + used, sizeof(source) - used,
"%d LABEL L%d\n", i, i);
}
(void)snprintf(source + used, sizeof(source) - used, "100 PRINT 1\n");
TEST_REQUIRE_OK(harness_start(NULL));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, source));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "? 65 : PARSE ERROR") != NULL,
"a full label table should report its source line, got \"%s\"",
HARNESS_OUTPUT);
harness_stop();
}
int main(void) int main(void)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
@@ -355,6 +379,7 @@ int main(void)
CATCH(errctx, test_undefined_label_is_reported()); CATCH(errctx, test_undefined_label_is_reported());
CATCH(errctx, test_arm_refusals()); CATCH(errctx, test_arm_refusals());
CATCH(errctx, test_prescan_boundaries()); CATCH(errctx, test_prescan_boundaries());
test_label_prescan_error_line();
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE_DEFAULT(errctx) { } HANDLE_DEFAULT(errctx) {

View File

@@ -1,3 +0,0 @@
10 PRINT "97 : " + ASC("a")
20 PRINT "65 : " + ASC("A")
30 PRINT "64 : " + ASC("@")

View File

@@ -1,3 +0,0 @@
97 : 97
65 : 65
64 : 64

View File

@@ -1,2 +1,2 @@
? 90 : PARSE ERROR TYPE POINT: POINT is a reserved word and cannot name a type ? 60 : PARSE ERROR TYPE POINT: POINT is a reserved word and cannot name a type

View File

@@ -8,6 +8,7 @@
* TODO.md section 6. * TODO.md section 6.
*/ */
#include <stdio.h>
#include <string.h> #include <string.h>
#include <akbasic/error.h> #include <akbasic/error.h>
@@ -174,6 +175,34 @@ static void test_colon_ends_data(void)
harness_stop(); harness_stop();
} }
/** @brief DATA overflow reports the line where the item limit was crossed. */
static void test_data_prescan_error_line(void)
{
char source[4096] = "";
size_t used = 0;
int line = 0;
int item = 0;
for ( line = 1; line <= 34; line++ ) {
used += (size_t)snprintf(source + used, sizeof(source) - used, "%d DATA ", line);
for ( item = 0; item < 15; item++ ) {
used += (size_t)snprintf(source + used, sizeof(source) - used,
"%s1", (item == 0 ? "" : ","));
}
used += (size_t)snprintf(source + used, sizeof(source) - used, "\n");
}
used += (size_t)snprintf(source + used, sizeof(source) - used, "100 DATA 1,1\n");
(void)snprintf(source + used, sizeof(source) - used, "101 DATA 1\n200 PRINT 1\n");
TEST_REQUIRE_OK(harness_start(NULL));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, source));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "? 101 : PARSE ERROR") != NULL,
"DATA overflow should report its source line, got \"%s\"",
HARNESS_OUTPUT);
harness_stop();
}
/** @brief A float item fills a float variable with its fractional part intact. */ /** @brief A float item fills a float variable with its fractional part intact. */
static void test_float_items(void) static void test_float_items(void)
{ {
@@ -227,6 +256,7 @@ int main(void)
test_type_mismatch(); test_type_mismatch();
test_quoted_items(); test_quoted_items();
test_colon_ends_data(); test_colon_ends_data();
test_data_prescan_error_line();
test_float_items(); test_float_items();
test_negative_items(); test_negative_items();
return akbasic_test_failures; return akbasic_test_failures;

View File

@@ -162,26 +162,6 @@ int main(void)
expect_int("A# = INSTR(\"HELLO\", \"LL\")", 2); expect_int("A# = INSTR(\"HELLO\", \"LL\")", 2);
expect_int("A# = INSTR(\"HELLO\", \"ZZ\")", -1); expect_int("A# = INSTR(\"HELLO\", \"ZZ\")", -1);
/* RND auto-seeds from host time and follows the documented LCG. */
HARNESS_RUNTIME.rndseeded = false;
TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 12345));
expect_int("A# = RND(6)", 0);
expect_int("A# = RND(6)", 4);
expect_int("A# = RND(6)", 1);
expect_int("A# = RND(6)", 0);
expect_int("A# = RND(6)", 1);
expect_int("A# = RND(1)", 0);
TEST_REQUIRE_STATUS(eval_line("A# = RND(0)", &out), AKBASIC_ERR_VALUE);
TEST_REQUIRE_STATUS(eval_line("A# = RND(-1)", &out), AKBASIC_ERR_VALUE);
TEST_REQUIRE_STATUS(eval_line("A# = RND(\"x\")", &out), AKBASIC_ERR_TYPE);
/* ASC is the inverse of CHR for ASCII and non-ASCII code points. */
expect_int("A# = ASC(CHR(97))", 97);
expect_int("A# = ASC(\"A\")", 65);
expect_int("A# = ASC(CHR(8364))", 8364);
TEST_REQUIRE_STATUS(eval_line("A# = ASC(\"\")", &out), AKBASIC_ERR_BOUNDS);
TEST_REQUIRE_STATUS(eval_line("A# = ASC(65)", &out), AKBASIC_ERR_TYPE);
/* An unknown verb is diagnosed rather than silently ignored. */ /* An unknown verb is diagnosed rather than silently ignored. */
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment)); TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
TEST_REQUIRE_STATUS(akbasic_runtime_evaluate(&HARNESS_RUNTIME, NULL, &out), TEST_REQUIRE_STATUS(akbasic_runtime_evaluate(&HARNESS_RUNTIME, NULL, &out),

View File

@@ -218,6 +218,22 @@ static void test_declaration_errors_are_basic_errors(void)
} }
} }
/** @brief A field declaration error reports the field's source line. */
static void test_type_prescan_error_line(void)
{
TEST_REQUIRE_OK(harness_start(NULL));
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
"10 TYPE RECT\n"
"20 W# EXTRA\n"
"30 END TYPE\n"
"40 PRINT 1\n"));
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "? 20 : PARSE ERROR") != NULL,
"TYPE prescan should report its source line, got \"%s\"",
HARNESS_OUTPUT);
harness_stop();
}
/** /**
* @brief An over-long type or field name is refused, not silently trimmed. * @brief An over-long type or field name is refused, not silently trimmed.
* *
@@ -270,6 +286,7 @@ int main(void)
test_missing_field_lists_the_others(); test_missing_field_lists_the_others();
test_self_by_value_refused(); test_self_by_value_refused();
test_declaration_errors_are_basic_errors(); test_declaration_errors_are_basic_errors();
test_type_prescan_error_line();
test_long_names_are_refused(); test_long_names_are_refused();
return akbasic_test_failures; return akbasic_test_failures;