Skip to content

Commit 0d59ec5

Browse files
omiqcursoragent
andauthored
Feature/development environment setup cd8c (#15)
* docs(AGENTS.md): add Raylib and emsdk Cloud Agent setup notes - Document Raylib installation (from source, gcc/g++ cmake workaround) - Document emsdk location (~/.bashrc sourcing) and WASM build/test targets - Note that basic-gfx cannot run headlessly in Cloud Agent VMs - Add Playwright WASM test commands reference Co-authored-by: Chris Garrett <chris@chrisg.com> * Add multi-dimensional array WASM freeze reproduction test Adds a minimal BASIC program that triggers a WASM stack overflow in the canvas (GFX_VIDEO) build due to eval_factor() allocating struct value args[MAX_UDF_PARAMS] (~65KB) on the stack, exceeding the default Emscripten STACK_SIZE of 64KB. Root cause: eval_factor() speculatively parses A(I,0) as a potential UDF call, allocating a 65KB args array. The 2D array index evaluation recursively calls eval_factor, compounding stack usage beyond 64KB. Fix needed: increase STACK_SIZE in Makefile WASM targets and/or heap-allocate args in eval_factor(). Co-authored-by: Chris Garrett <chris@chrisg.com> * fix(wasm): heap-allocate UDF args to prevent stack overflow on 2D arrays Root cause: eval_factor() and execute_statement() declared struct value args[MAX_UDF_PARAMS] on the stack — 16 × 4108 = ~65KB, which exceeds Emscripten's default 64KB stack. When evaluating 2D array expressions like A(I,J), recursive eval_factor calls compounded the overflow, causing the WASM interpreter to freeze. Fix (defense in depth): - Heap-allocate args via calloc() in both eval_factor() and execute_statement(), freeing on all exit paths - Increase WASM STACK_SIZE from 64KB to 512KB in all three Makefile targets (basic-wasm, basic-wasm-modular, basic-wasm-canvas) Added tests/dim2d_wasm_test.bas regression test. Co-authored-by: Chris Garrett <chris@chrisg.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent 7a2bf0b commit 0d59ec5

4 files changed

Lines changed: 48 additions & 4 deletions

File tree

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ basic-wasm:
6666
-s EXPORTED_RUNTIME_METHODS='["ccall","cwrap","FS","HEAPU8","wasmMemory","getValue"]' \
6767
-s FORCE_FILESYSTEM=1 -s NO_EXIT_RUNTIME=1 \
6868
-s INITIAL_MEMORY=33554432 \
69+
-s STACK_SIZE=524288 \
6970
-s ASYNCIFY=1 -s ASYNCIFY_IMPORTS='["emscripten_sleep"]' \
7071
-o web/basic.js basic.c petscii.c -lm
7172
@echo "Built web/basic.js and web/basic.wasm"
@@ -79,6 +80,7 @@ basic-wasm-modular:
7980
-s EXPORTED_RUNTIME_METHODS='["ccall","cwrap","FS","HEAPU8","wasmMemory","getValue"]' \
8081
-s FORCE_FILESYSTEM=1 -s NO_EXIT_RUNTIME=1 \
8182
-s INITIAL_MEMORY=33554432 \
83+
-s STACK_SIZE=524288 \
8284
-s ASYNCIFY=1 -s ASYNCIFY_IMPORTS='["emscripten_sleep"]' \
8385
-o web/basic-modular.js basic.c petscii.c -lm
8486
@echo "Built web/basic-modular.js and web/basic-modular.wasm"
@@ -91,6 +93,7 @@ basic-wasm-canvas:
9193
-s EXPORTED_RUNTIME_METHODS='["ccall","cwrap","FS","HEAPU8","wasmMemory","getValue"]' \
9294
-s FORCE_FILESYSTEM=1 -s NO_EXIT_RUNTIME=1 \
9395
-s INITIAL_MEMORY=67108864 \
96+
-s STACK_SIZE=524288 \
9497
-s ASYNCIFY=1 -s ASYNCIFY_IMPORTS='["emscripten_sleep"]' \
9598
-o web/basic-canvas.js basic.c petscii.c gfx/gfx_video.c gfx/gfx_charrom.c gfx/gfx_canvas.c gfx/gfx_software_sprites.c -lm
9699
@echo "Built web/basic-canvas.js and web/basic-canvas.wasm"

basic.c

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5582,21 +5582,23 @@ static struct value eval_factor(char **p)
55825582
char namebuf[VAR_NAME_MAX];
55835583
char *q;
55845584
int i, arg_count, udf_idx, uf_index;
5585-
struct value args[MAX_UDF_PARAMS];
55865585
q = *p;
55875586
read_identifier(&q, namebuf, sizeof(namebuf));
55885587
for (i = 0; namebuf[i]; i++) {
55895588
namebuf[i] = (char)toupper((unsigned char)namebuf[i]);
55905589
}
55915590
skip_spaces(&q);
55925591
if (*q == '(') {
5592+
struct value *args = (struct value *)calloc(MAX_UDF_PARAMS, sizeof(struct value));
5593+
if (!args) { runtime_error("Out of memory"); return make_num(0.0); }
55935594
char *saved_p = *p;
55945595
*p = q + 1;
55955596
skip_spaces(p);
55965597
arg_count = 0;
55975598
if (**p != ')') {
55985599
for (;;) {
55995600
if (arg_count >= MAX_UDF_PARAMS) {
5601+
free(args);
56005602
runtime_error("Too many arguments");
56015603
return make_num(0.0);
56025604
}
@@ -5605,6 +5607,7 @@ static struct value eval_factor(char **p)
56055607
skip_spaces(p);
56065608
if (**p == ')') break;
56075609
if (**p != ',') {
5610+
free(args);
56085611
runtime_error("Expected ',' or ')'");
56095612
return make_num(0.0);
56105613
}
@@ -5615,7 +5618,9 @@ static struct value eval_factor(char **p)
56155618
if (**p == ')') (*p)++;
56165619
udf_idx = udf_lookup(namebuf, arg_count);
56175620
if (udf_idx >= 0) {
5618-
return invoke_udf(udf_idx, args, arg_count);
5621+
struct value ret = invoke_udf(udf_idx, args, arg_count);
5622+
free(args);
5623+
return ret;
56195624
}
56205625
if (arg_count == 1) {
56215626
uf_index = user_func_lookup(namebuf);
@@ -5629,7 +5634,7 @@ static struct value eval_factor(char **p)
56295634
strncpy(pname_buf, uf->param_name, sizeof(pname_buf) - 1);
56305635
pname_buf[sizeof(pname_buf) - 1] = '\0';
56315636
param_var = find_or_create_var(pname_buf, uf->param_is_string, 0, 0, NULL, 0);
5632-
if (!param_var) return make_num(0.0);
5637+
if (!param_var) { free(args); return make_num(0.0); }
56335638
saved_scalar = param_var->scalar;
56345639
if (uf->param_is_string) {
56355640
ensure_str(&args[0]);
@@ -5642,10 +5647,12 @@ static struct value eval_factor(char **p)
56425647
}
56435648
{ char *body_p = uf->body; result = eval_expr(&body_p); }
56445649
param_var->scalar = saved_scalar;
5650+
free(args);
56455651
return result;
56465652
}
56475653
}
56485654
/* No UDF or DEF FN matched; treat as array/variable */
5655+
free(args);
56495656
*p = saved_p;
56505657
}
56515658
{
@@ -8472,18 +8479,20 @@ static void execute_statement(char **p)
84728479
char namebuf[VAR_NAME_MAX];
84738480
char *q = *p;
84748481
int arg_count, udf_idx, uf_index, i;
8475-
struct value args[MAX_UDF_PARAMS];
84768482
read_identifier(&q, namebuf, sizeof(namebuf));
84778483
for (i = 0; namebuf[i]; i++) namebuf[i] = (char)toupper((unsigned char)namebuf[i]);
84788484
skip_spaces(&q);
84798485
if (*q == '(') {
8486+
struct value *args = (struct value *)calloc(MAX_UDF_PARAMS, sizeof(struct value));
8487+
if (!args) { runtime_error("Out of memory"); return; }
84808488
char *saved_p = *p;
84818489
*p = q + 1;
84828490
skip_spaces(p);
84838491
arg_count = 0;
84848492
if (**p != ')') {
84858493
for (;;) {
84868494
if (arg_count >= MAX_UDF_PARAMS) {
8495+
free(args);
84878496
runtime_error("Too many arguments");
84888497
return;
84898498
}
@@ -8492,6 +8501,7 @@ static void execute_statement(char **p)
84928501
skip_spaces(p);
84938502
if (**p == ')') break;
84948503
if (**p != ',') {
8504+
free(args);
84958505
runtime_error("Expected ',' or ')'");
84968506
return;
84978507
}
@@ -8503,6 +8513,7 @@ static void execute_statement(char **p)
85038513
udf_idx = udf_lookup(namebuf, arg_count);
85048514
if (udf_idx >= 0) {
85058515
(void)invoke_udf(udf_idx, args, arg_count);
8516+
free(args);
85068517
return;
85078518
}
85088519
if (arg_count == 1) {
@@ -8529,10 +8540,12 @@ static void execute_statement(char **p)
85298540
{ char *body_p = uf->body; (void)eval_expr(&body_p); }
85308541
param_var->scalar = saved_scalar;
85318542
}
8543+
free(args);
85328544
return;
85338545
}
85348546
}
85358547
/* No match; restore and fall through to implicit LET */
8548+
free(args);
85368549
*p = saved_p;
85378550
}
85388551
}

tests/dim2d_wasm_test.bas

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
10 REM 2D array test — previously caused WASM stack overflow (eval_factor args[])
2+
20 DIM A(2,3)
3+
30 FOR P=0 TO 2:A(P,0)=0:A(P,1)=1:A(P,2)=2:A(P,3)=3: NEXT P
4+
40 FOR I=0 TO 1
5+
50 FOR J=0 TO 2
6+
60 A(I,J)=I*10+J
7+
70 NEXT J
8+
80 NEXT I
9+
90 FOR I=0 TO 1
10+
100 FOR J=0 TO 2
11+
110 IF A(I,J)<>I*10+J THEN PRINT "FAIL":END
12+
120 NEXT J
13+
130 NEXT I
14+
140 PRINT "OK"
15+
150 END

tests/multi_dim_array_test.bas

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
10 DIM A(2,3)
2+
15 FOR P=0 TO 2:A(P,0)=0:A(P,1)=1:A(P,2)=2:A(P,3)=3: NEXT P
3+
20 FOR I=0 TO 1
4+
30 FOR J=0 TO 2
5+
40 A(I,J)=I*10+J
6+
50 NEXT J
7+
60 NEXT I
8+
70 FOR I=0 TO 1
9+
80 FOR J=0 TO 2
10+
90 PRINT I;",";J;"=";A(I,J)
11+
100 NEXT J
12+
110 NEXT I
13+
120 END

0 commit comments

Comments
 (0)