From 7715c01656fd1060897af47feb28764c7882315b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 30 Mar 2026 10:22:23 +0000 Subject: [PATCH 1/4] Add SPRITECOLLIDE, runtime error hints, tutorial runOnEdit - Wire SPRITECOLLIDE(slot_a,slot_b) to gfx_sprite_slots_overlap_aabb; fix function_lookup length (13) and eval_factor dispatch for SPRITE* names - Non-GFX builds parse args then error with a clear message - runtime_error_hint for unknown function and type coercion hints - tutorial-embed: optional runOnEdit with debounce; clear timer on destroy - Docs: README, tutorial-embedding, CHANGELOG, to-do Co-authored-by: Chris Garrett --- CHANGELOG.md | 18 ++++++- README.md | 1 + basic.c | 100 +++++++++++++++++++++++++++++++++---- basic_api.h | 2 + docs/tutorial-embedding.md | 2 + gfx/gfx_raylib.c | 68 +++++++++++++++++++++++++ gfx/gfx_software_sprites.c | 48 ++++++++++++++++++ to-do.md | 4 +- web/tutorial-embed.js | 30 ++++++++++- 9 files changed, 259 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b741eb4..c3cdb8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ ### Unreleased +- **Sprites**: **`SPRITECOLLIDE(a, b)`** — returns **1** if two loaded, visible sprites’ axis-aligned bounding boxes overlap (basic-gfx + canvas WASM; **0** otherwise). Terminal **`./basic`** errors if used (requires **basic-gfx** or canvas WASM). **Runtime errors**: optional **`Hint:`** line for **unknown function** (shows name) and **`ensure_num` / `ensure_str`** type mismatches. **`tutorial-embed.js`**: optional **`runOnEdit`** / **`runOnEditMs`** for debounced auto-run after editing. + +- **Documentation**: **`docs/basic-to-c-transpiler-plan.md`** — design notes for a future **BASIC → C** backend (**cc65** / **z88dk**), recommended subset, and **explicit exclusions** (host I/O, `EVAL`, graphics, etc.). + +- **IF conditions with parentheses**: **`IF (J=F OR A$=" ") AND SF=0 THEN`** failed with **Missing THEN** because **`eval_simple_condition`** used **`eval_expr`** inside **`(`**, which does not parse relational **`=`**. Leading **`(`** now distinguishes **`(expr) relop rhs`** (e.g. **`(1+1)=2`**) from boolean groups **`(… OR …)`** via **`eval_condition`**. Regression: **`tests/if_paren_condition_test.bas`**. + +- **Canvas WASM `PEEK(56320+n)` keyboard**: **`key_state[]`** was never updated in the browser (only **basic-gfx** / Raylib did). **`canvas.html`** now drives **`wasm_gfx_key_state_set`** / **`wasm_gfx_key_state_clear`** on key up/down (A–Z, 0–9, arrows, Escape, Space, Enter, Tab) so **`examples/gfx_jiffy_game_demo.bas`**-style **`PEEK(KEYBASE+…)`** works. + +- **Canvas WASM `TI` / `TI$`**: **`GfxVideoState.ticks60`** was only advanced in the **Raylib** main loop, so **canvas** **`TI`** / **`TI$`** stayed frozen (especially in tight **`GOTO`** loops). **Canvas** now derives 60 Hz jiffies from **`emscripten_get_now()`** since **`basic_load_and_run_gfx`** ( **`gfx_video_advance_ticks60`** still drives **basic-gfx** each frame). **Terminal WASM** (`basic.js`, no **`GFX_VIDEO`**) still uses **`time()`** for **`TI`** / **`TI$`** (seconds + wall-clock `HHMMSS`), not C64 jiffies. + +- **WordPress**: New plugin **`wordpress/rgc-basic-tutorial-block`** — Gutenberg block **RGC-BASIC embed** with automatic script/style enqueue, **`copy-web-assets.sh`** to sync **`web/tutorial-embed.js`**, **`vfs-helpers.js`**, and modular WASM into **`assets/`**; optional **Settings → RGC-BASIC Tutorial** base URL. + +- **Canvas WASM `TAB`**: **`FN_TAB`** used **`OUTC`** for newline/spaces, which updates **`print_col`** but not the **GFX** cursor, so **`PRINT … TAB(n) …`** misaligned on the canvas. **GFX** path now uses **`gfx_newline`** + **`print_spaces`** (same as **`SPC`**). **`wasm_gfx_screen_screencode_at`** exported for **`wasm_browser_canvas_test`** regression. + +- **Canvas / basic-gfx INPUT line (`gfx_read_line`)**: Removed same-character time debounce (~80 ms) that dropped consecutive identical keys, so words like **"LOOK"** work. **Raylib `keyq_push`** no longer applies the same filter (parity with canvas). + - **WASM `run_program`**: Reset **`wasm_str_concat_budget`** each **`RUN`** (was missing; concat-yield counter could carry across runs). - **WASM canvas string concat (`Q$=LEFT$+…`)**: **`trek.bas`** **`GOSUB 5440`** builds **`Q$`** with **`+`** ( **`eval_addsub`** ) without a **`MID$`** per assignment — no prior yield path. **Canvas WASM** now yields every **256** string concatenations (**`wasm_str_concat_budget`**, separate from **`MID$`** counter). @@ -56,7 +72,7 @@ - **basic-gfx — hires bitmap (Phase 3)** - `SCREEN 0` / `SCREEN 1` (text vs 320×200 monochrome); `PSET` / `PRESET` / `LINE x1,y1 TO x2,y2`; bitmap RAM at `GFX_BITMAP_BASE` (0x2000). - **basic-gfx — PNG sprites** - - `LOADSPRITE`, `UNLOADSPRITE`, `DRAWSPRITE` (persistent per-slot pose, z-order, optional source rect), `SPRITEVISIBLE`, `SPRITEW()` / `SPRITEH()`; alpha blending over PETSCII/bitmap; `gfx_set_sprite_base_dir` from program path. + - `LOADSPRITE`, `UNLOADSPRITE`, `DRAWSPRITE` (persistent per-slot pose, z-order, optional source rect), `SPRITEVISIBLE`, `SPRITEW()` / `SPRITEH()`, `SPRITECOLLIDE(a,b)`; alpha blending over PETSCII/bitmap; `gfx_set_sprite_base_dir` from program path. - Examples: `examples/gfx_sprite_hud_demo.bas`, `examples/gfx_game_shell.bas` (+ `player.png`, `enemy.png`, `hud_panel.png`). ### 1.5.0 – 2026-03-20 diff --git a/README.md b/README.md index 7fb0e1b..c1f1b0f 100644 --- a/README.md +++ b/README.md @@ -379,6 +379,7 @@ Releases include **basic-gfx** — a full graphical version of the interpreter b - `DRAWSPRITE slot, x, y [, z [, sx, sy [, sw, sh ]]]` sets the **persistent** pose for that slot: the same image is drawn **every frame** until you call `DRAWSPRITE` again for that slot or the program exits (textures are freed when the window closes). **`x`, `y`** are **pixel** coordinates on the 320×200 framebuffer (not character rows): row *r* starts at **`y = r × 8`**. **`z`**: larger values paint **on top** (e.g. text/bitmap at 0, HUD at 200). Omit **`sx, sy`** to use the top-left of the image; omit **`sw, sh`** (or use ≤0) to use the rest of the texture from `(sx,sy)`. **Alpha** in the PNG is respected (transparency over text or bitmap). - `SPRITEVISIBLE slot, 0|1` hides or shows a loaded sprite without unloading. - `SPRITEW(slot)` / `SPRITEH(slot)` return pixel width/height after load (0 if not loaded yet). +- `SPRITECOLLIDE(a, b)` returns **1** if the axis-aligned bounding boxes of two visible, drawn sprites overlap, else **0** (empty slots or hidden sprites never collide). - Example: `./basic-gfx -petscii examples/gfx_sprite_hud_demo.bas` - **Game shell** (`examples/gfx_game_shell.bas`): tile map from `DATA` with `POKE` (walls/floor/goal), **8×8 PNG** player (`player.png`) and enemy (`enemy.png`) via `DRAWSPRITE`, `INKEY$()` loop, HUD strip (`hud_panel.png`). Run: `./basic-gfx examples/gfx_game_shell.bas` diff --git a/basic.c b/basic.c index f27489d..88252b4 100644 --- a/basic.c +++ b/basic.c @@ -1904,6 +1904,7 @@ static void do_exec(const char *cmd, char *out, size_t out_size) } static void runtime_error(const char *msg); +static void runtime_error_hint(const char *msg, const char *hint); static void load_program(const char *path); static void run_program(const char *script_path_arg, int nargs, char **args); static int find_line_index(int number); @@ -2022,14 +2023,16 @@ enum func_code { FN_JSON = 42, FN_EVAL = 43, FN_SPRITEW = 44, - FN_SPRITEH = 45 + FN_SPRITEH = 45, + FN_SPRITECOLLIDE = 46 }; /* Report an error and halt further execution. * If possible, include the BASIC line number (if present) and the * original source text with a caret pointing near the error location. + * Optional hint gives a short fix suggestion (shown on its own line). */ -static void runtime_error(const char *msg) +static void runtime_error_hint(const char *msg, const char *hint) { int line_no = 0; const char *line_text = NULL; @@ -2049,9 +2052,17 @@ static void runtime_error(const char *msg) int n; if (line_no > 0) { - n = snprintf(buf, sizeof(buf), "Error on line %d: %s\n", line_no, msg); + if (hint && hint[0]) { + n = snprintf(buf, sizeof(buf), "Error on line %d: %s\n Hint: %s\n", line_no, msg, hint); + } else { + n = snprintf(buf, sizeof(buf), "Error on line %d: %s\n", line_no, msg); + } } else { - n = snprintf(buf, sizeof(buf), "Error: %s\n", msg); + if (hint && hint[0]) { + n = snprintf(buf, sizeof(buf), "Error: %s\n Hint: %s\n", msg, hint); + } else { + n = snprintf(buf, sizeof(buf), "Error: %s\n", msg); + } } if (n < 0) { n = 0; @@ -2152,6 +2163,11 @@ static void runtime_error(const char *msg) halted = 1; } +static void runtime_error(const char *msg) +{ + runtime_error_hint(msg, NULL); +} + /* Strip trailing newline from a buffer if present. */ static void trim_newline(char *s) { @@ -2241,7 +2257,7 @@ static const char *const reserved_words[] = { "INKEY", "INPUT", "INSTR", "INT", "INDEXOF", "JSON", "LEFT", "LEN", "LET", "LINE", "LOAD", "LOADSPRITE", "LOCATE", "LOG", "LASTINDEXOF", "LCASE", "FIELD", "LTRIM", "MEMCPY", "MEMSET", "MID", "MOD", "NEXT", "OFF", "ON", "OPEN", "OR", "PEEK", "POKE", "PLATFORM", "PRESET", "PSET", "PRINT", "XOR", - "READ", "REM", "REPLACE", "RESTORE", "RETURN", "RIGHT", "RND", "RTRIM", "RVS", "SCREEN", "SCREENCODES", "SPRITEVISIBLE", + "READ", "REM", "REPLACE", "RESTORE", "RETURN", "RIGHT", "RND", "RTRIM", "RVS", "SCREEN", "SCREENCODES", "SPRITECOLLIDE", "SPRITEVISIBLE", "JOIN", "SGN", "SIN", "SLEEP", "SORT", "SPC", "SPLIT", "SPRITEH", "SPRITEW", "SQR", "STEP", "STOP", "STR", "STRING", "DRAWSPRITE", "SYSTEM", "TAB", "TAN", "TEXTAT", "THEN", "TI", "TO", "TRIM", "UCASE", "UNLOADSPRITE", "VAL", "WEND", "WHILE", @@ -2314,7 +2330,7 @@ static struct value make_str(const char *s) static void ensure_num(struct value *v) { if (v->type != VAL_NUM) { - runtime_error("Numeric value required"); + runtime_error_hint("Numeric value required", "Use a number here, or VAL(...) to convert a string."); } } @@ -2330,7 +2346,7 @@ static void ensure_str(struct value *v) *v = make_str(buf); return; } - runtime_error("String value required"); + runtime_error_hint("String value required", "Use a string expression, or STR$(...) to convert a number."); } /* Emit spaces and track current print column. */ @@ -3038,6 +3054,9 @@ static int function_lookup(const char *name, int len) name[4] == 'T' && name[5] == 'E' && name[6] == 'W') return FN_SPRITEW; if (len == 7 && name[0] == 'S' && name[1] == 'P' && name[2] == 'R' && name[3] == 'I' && name[4] == 'T' && name[5] == 'E' && name[6] == 'H') return FN_SPRITEH; + if (len == 13 && name[0] == 'S' && name[1] == 'P' && name[2] == 'R' && name[3] == 'I' && + name[4] == 'T' && name[5] == 'E' && name[6] == 'C' && name[7] == 'O' && name[8] == 'L' && + name[9] == 'L' && name[10] == 'I' && name[11] == 'D' && name[12] == 'E') return FN_SPRITECOLLIDE; return FN_NONE; case 'C': if ((len == 3 && name[0] == 'C' && name[1] == 'H' && name[2] == 'R') || @@ -4179,6 +4198,32 @@ static struct value eval_function(const char *name, char **p) return make_str(""); #endif } +#if !defined(GFX_VIDEO) + if (code == FN_SPRITECOLLIDE) { + struct value va, vb; + skip_spaces(p); + va = eval_expr(p); + (void)va; + skip_spaces(p); + if (**p != ',') { + runtime_error("SPRITECOLLIDE expects slot_a, slot_b"); + return make_num(0.0); + } + (*p)++; + skip_spaces(p); + vb = eval_expr(p); + (void)vb; + skip_spaces(p); + if (**p != ')') { + runtime_error("Missing ')'"); + return make_num(0.0); + } + (*p)++; + skip_spaces(p); + runtime_error("SPRITECOLLIDE requires basic-gfx or canvas WASM"); + return make_num(0.0); + } +#endif #ifdef GFX_VIDEO if (code == FN_SPRITEW || code == FN_SPRITEH) { struct value vs; @@ -4200,6 +4245,36 @@ static struct value eval_function(const char *name, char **p) } return make_num(0.0); } + if (code == FN_SPRITECOLLIDE) { + struct value va, vb; + int sa, sb, hit; + skip_spaces(p); + va = eval_expr(p); + ensure_num(&va); + skip_spaces(p); + if (**p != ',') { + runtime_error("SPRITECOLLIDE expects slot_a, slot_b"); + return make_num(0.0); + } + (*p)++; + skip_spaces(p); + vb = eval_expr(p); + ensure_num(&vb); + skip_spaces(p); + if (**p != ')') { + runtime_error("Missing ')'"); + return make_num(0.0); + } + (*p)++; + skip_spaces(p); + sa = (int)va.num; + sb = (int)vb.num; + if (gfx_vs) { + hit = gfx_sprite_slots_overlap_aabb(sa, sb); + return make_num((double)hit); + } + return make_num(0.0); + } #endif if (code == FN_PLATFORM) { if (**p != ')') { @@ -5180,10 +5255,13 @@ static struct value eval_function(const char *name, char **p) start = end + dlen; } } - default: - runtime_error("Unknown function"); + default: { + char ubuf[96]; + snprintf(ubuf, sizeof(ubuf), "Unknown function: %s", tmp); + runtime_error_hint(ubuf, "Check spelling; intrinsic names have no spaces."); return make_num(0.0); } + } } /* Normalize variable name to uppercase in dest, strip trailing $ and set is_string. */ @@ -5571,7 +5649,9 @@ static struct value eval_factor(char **p) starts_with_kw(*p, "ENV") || starts_with_kw(*p, "EVAL") || starts_with_kw(*p, "PLATFORM") || starts_with_kw(*p, "JSON") || starts_with_kw(*p, "ARGC") || starts_with_kw(*p, "ARG") || starts_with_kw(*p, "SYSTEM") || starts_with_kw(*p, "EXEC") || - starts_with_kw(*p, "PEEK") || starts_with_kw(*p, "INKEY")) { + starts_with_kw(*p, "PEEK") || starts_with_kw(*p, "INKEY") || + starts_with_kw(*p, "SPRITEW") || starts_with_kw(*p, "SPRITEH") || + starts_with_kw(*p, "SPRITECOLLIDE")) { char namebuf[32]; char *q; q = *p; diff --git a/basic_api.h b/basic_api.h index 179fc00..61c3b40 100644 --- a/basic_api.h +++ b/basic_api.h @@ -60,6 +60,8 @@ void gfx_sprite_enqueue_visible(int slot, int on); void gfx_sprite_enqueue_draw(int slot, float x, float y, int z, int sx, int sy, int sw, int sh); int gfx_sprite_slot_width(int slot); int gfx_sprite_slot_height(int slot); +/* Axis-aligned bounding box overlap of last DRAWSPRITE rects (basic-gfx / canvas). */ +int gfx_sprite_slots_overlap_aabb(int slot_a, int slot_b); #endif #endif /* BASIC_API_H */ diff --git a/docs/tutorial-embedding.md b/docs/tutorial-embedding.md index 9e5e0ae..e401679 100644 --- a/docs/tutorial-embedding.md +++ b/docs/tutorial-embedding.md @@ -110,6 +110,8 @@ Each embed gets its own **virtual filesystem** and **Asyncify** state; they do n | `vfsExportPath` | string | `'/out.txt'` | Default path in the download field | | `editorMinHeight` | string | `'120px'` | CSS min-height for textarea | | `outputMinHeight` | string | `'100px'` | CSS min-height for output panel | +| `runOnEdit` | boolean | `false` | If true, re-run the program after a short pause whenever the editor text changes (debounced) | +| `runOnEditMs` | number | `600` | Debounce delay in milliseconds for `runOnEdit` | ## Returned API (`Promise`) diff --git a/gfx/gfx_raylib.c b/gfx/gfx_raylib.c index 6e55808..042c43d 100644 --- a/gfx/gfx_raylib.c +++ b/gfx/gfx_raylib.c @@ -58,6 +58,8 @@ typedef struct { int sx, sy, sw, sh; } GfxSpriteDraw; +static void gfx_sprite_process_queue(void); + static pthread_mutex_t g_sprite_mutex = PTHREAD_MUTEX_INITIALIZER; static char g_sprite_base_dir[GFX_SPRITE_PATH_MAX]; static GfxSpriteSlot g_sprite_slots[GFX_SPRITE_MAX_SLOTS]; @@ -195,6 +197,72 @@ int gfx_sprite_slot_height(int slot) return h; } +int gfx_sprite_slots_overlap_aabb(int slot_a, int slot_b) +{ + float ax, ay, aw, ah, bx, by, bw, bh; + float ax2, ay2, bx2, by2; + GfxSpriteSlot *a; + GfxSpriteSlot *b; + + gfx_sprite_process_queue(); + if (slot_a < 0 || slot_a >= GFX_SPRITE_MAX_SLOTS || + slot_b < 0 || slot_b >= GFX_SPRITE_MAX_SLOTS) { + return 0; + } + pthread_mutex_lock(&g_sprite_mutex); + a = &g_sprite_slots[slot_a]; + b = &g_sprite_slots[slot_b]; + if (!a->loaded || !a->visible || !a->draw_active || + !b->loaded || !b->visible || !b->draw_active) { + pthread_mutex_unlock(&g_sprite_mutex); + return 0; + } + { + float swa, sha, swb, shb; + int twa, tha, twb, thb; + twa = a->tex.width; + tha = a->tex.height; + twb = b->tex.width; + thb = b->tex.height; + if (a->draw_sw <= 0 || a->draw_sh <= 0) { + swa = (float)(twa - a->draw_sx); + sha = (float)(tha - a->draw_sy); + } else { + swa = (float)a->draw_sw; + sha = (float)a->draw_sh; + } + if (b->draw_sw <= 0 || b->draw_sh <= 0) { + swb = (float)(twb - b->draw_sx); + shb = (float)(thb - b->draw_sy); + } else { + swb = (float)b->draw_sw; + shb = (float)b->draw_sh; + } + if (swa <= 0 || sha <= 0 || swb <= 0 || shb <= 0) { + pthread_mutex_unlock(&g_sprite_mutex); + return 0; + } + ax = a->draw_x; + ay = a->draw_y; + aw = swa; + ah = sha; + bx = b->draw_x; + by = b->draw_y; + bw = swb; + bh = shb; + } + pthread_mutex_unlock(&g_sprite_mutex); + + ax2 = ax + aw; + ay2 = ay + ah; + bx2 = bx + bw; + by2 = by + bh; + if (ax2 <= bx || bx2 <= ax || ay2 <= by || by2 <= ay) { + return 0; + } + return 1; +} + static int cmp_sprite_draw_z(const void *a, const void *b) { const GfxSpriteDraw *da = (const GfxSpriteDraw *)a; diff --git a/gfx/gfx_software_sprites.c b/gfx/gfx_software_sprites.c index 6dd9a62..e140398 100644 --- a/gfx/gfx_software_sprites.c +++ b/gfx/gfx_software_sprites.c @@ -175,6 +175,54 @@ int gfx_sprite_slot_height(int slot) return g_sprite_slots[slot].h; } +int gfx_sprite_slots_overlap_aabb(int slot_a, int slot_b) +{ + GfxSpriteSlot *a, *b; + float ax, ay, aw, ah, bx, by, bw, bh; + float ax2, ay2, bx2, by2; + + gfx_sprite_process_queue(); + if (slot_a < 0 || slot_a >= GFX_SPRITE_MAX_SLOTS || + slot_b < 0 || slot_b >= GFX_SPRITE_MAX_SLOTS) { + return 0; + } + a = &g_sprite_slots[slot_a]; + b = &g_sprite_slots[slot_b]; + if (!a->loaded || !a->visible || !a->draw_active || + !b->loaded || !b->visible || !b->draw_active) { + return 0; + } + if (a->draw_sw <= 0 || a->draw_sh <= 0) { + aw = (float)(a->w - a->draw_sx); + ah = (float)(a->h - a->draw_sy); + } else { + aw = (float)a->draw_sw; + ah = (float)a->draw_sh; + } + if (b->draw_sw <= 0 || b->draw_sh <= 0) { + bw = (float)(b->w - b->draw_sx); + bh = (float)(b->h - b->draw_sy); + } else { + bw = (float)b->draw_sw; + bh = (float)b->draw_sh; + } + if (aw <= 0 || ah <= 0 || bw <= 0 || bh <= 0) { + return 0; + } + ax = a->draw_x; + ay = a->draw_y; + bx = b->draw_x; + by = b->draw_y; + ax2 = ax + aw; + ay2 = ay + ah; + bx2 = bx + bw; + by2 = by + bh; + if (ax2 <= bx || bx2 <= ax || ay2 <= by || by2 <= ay) { + return 0; + } + return 1; +} + static int cmp_sprite_draw_z(const void *a, const void *b) { const GfxSpriteDraw *da = (const GfxSpriteDraw *)a; diff --git a/to-do.md b/to-do.md index 28d69ab..569137f 100644 --- a/to-do.md +++ b/to-do.md @@ -55,7 +55,7 @@ * **Bitmap graphics & sprites** (incremental; see `docs/bitmap-graphics-plan.md`, `docs/sprite-features-plan.md`) 1. ~**Bitmap mode**~ — `SCREEN 0`/`SCREEN 1` (320×200 hires at `GFX_BITMAP_BASE`), monochrome raylib renderer; `COLOR`/`BACKGROUND` as pen/paper. ~`PSET`/`PRESET`/`LINE`~; POKE still works. - 2. ~**Sprites (minimal)**~ — `LOADSPRITE` / `UNLOADSPRITE` / `DRAWSPRITE` (persistent pose, z-order, optional `sx,sy,sw,sh` crop) / `SPRITEVISIBLE` / `SPRITEW`/`SPRITEH` in basic-gfx; PNG alpha over text/bitmap; paths relative to `.bas` directory. ~Worked example: `examples/gfx_game_shell.bas` (PETSCII map + PNG player/enemy/HUD), `examples/gfx_sprite_hud_demo.bas`, `examples/player.png`, `examples/enemy.png`, `examples/hud_panel.png`.~ Still open: `SPRITECOLLIDE`, tilemap `LOADSPRITE` mode. + 2. ~**Sprites (minimal)**~ — `LOADSPRITE` / `UNLOADSPRITE` / `DRAWSPRITE` (persistent pose, z-order, optional `sx,sy,sw,sh` crop) / `SPRITEVISIBLE` / `SPRITEW`/`SPRITEH` / `SPRITECOLLIDE` in basic-gfx + canvas WASM; PNG alpha over text/bitmap; paths relative to `.bas` directory. ~Worked example: `examples/gfx_game_shell.bas` (PETSCII map + PNG player/enemy/HUD), `examples/gfx_sprite_hud_demo.bas`, `examples/player.png`, `examples/enemy.png`, `examples/hud_panel.png`.~ Still open: tilemap `LOADSPRITE` mode. 3. **Joystick / joypad / controller input** — Poll gamepad via raylib; expose to BASIC as `JOY(port)` or similar read API for D-pad, buttons, axes. Support keyboards-as-joystick and real controllers. 4. **Graphic layers and scrolling** — Layer stack (background, tiles, sprites, text) with independent scroll offsets; `SCROLL x, y` or per-layer scroll; camera/viewport for games. 5. **Tilemap handling** — `LOADSPRITE slot, "path", "tilemap"`; define tile size (e.g. 16×16); render tile grid from sprite sheet. Efficient level/world rendering. @@ -69,7 +69,7 @@ * ~**CI**~ — GitHub Actions WASM job uses **emsdk** (`install latest`), builds both targets, runs Playwright: `tests/wasm_browser_test.py`, `tests/wasm_browser_canvas_test.py`. * ~**Deploy hygiene**~ — `canvas.html` pairs cache-bust query on `basic-canvas.js` and `basic-canvas.wasm`; optional `?debug=1` for console diagnostics (`wasm_canvas_build_stamp`, stack dumps). * ~**Tutorial embedding**~ — `make basic-wasm-modular`; `web/tutorial-embed.js` + `RgcBasicTutorialEmbed.mount()` for **multiple** terminal instances per page. Guide: **`docs/tutorial-embedding.md`**. Example: `web/tutorial-example.html`. CI: `tests/wasm_tutorial_embed_test.py`, `make wasm-tutorial-test`. - * **Still open — richer tutorial UX**: “Live” auto-run on edit, step-through debugging, or synchronized markdown blocks (beyond Run button + static program text). + * **Still open — richer tutorial UX**: step-through debugging or synchronized markdown blocks (beyond Run button + static program text). ~Optional **`runOnEdit`** in `tutorial-embed.js`~ for debounced auto-run after edits. * ~~Subroutines and Functions~~ * **User-defined FUNCTIONS** implemented — `FUNCTION name[(params)]` … `RETURN [expr]` … `END FUNCTION`; call with `name()` or `name(a,b)`; recursion supported. See `docs/user-functions-plan.md`. diff --git a/web/tutorial-embed.js b/web/tutorial-embed.js index f80c39c..2c5302a 100644 --- a/web/tutorial-embed.js +++ b/web/tutorial-embed.js @@ -193,6 +193,8 @@ * @param {boolean} [opts.showEditor=true] * @param {boolean} [opts.showPauseStop=true] * @param {boolean} [opts.showVfsTools=true] - Upload/Download virtual FS (needs vfs-helpers.js beside basic-modular.js) + * @param {boolean} [opts.runOnEdit=false] - After edits, auto-run after a short debounce (handy for tutorials) + * @param {number} [opts.runOnEditMs=600] - Debounce delay for runOnEdit * @returns {Promise<{ run: function, resetOutput: function, destroy: function, getModule: function }>} */ function mount(container, opts) { @@ -207,6 +209,10 @@ var showEditor = opts.showEditor !== false; var showPauseStop = opts.showPauseStop !== false; var showVfsTools = opts.showVfsTools !== false; + var runOnEdit = opts.runOnEdit === true; + var runOnEditMs = + typeof opts.runOnEditMs === 'number' && opts.runOnEditMs >= 0 ? opts.runOnEditMs : 600; + var runOnEditTimer = null; container.classList.add('rgc-tutorial-embed'); container.innerHTML = ''; @@ -421,7 +427,7 @@ } } - runBtn.onclick = function () { + function doRunFromEditor() { clearOutput(); inputRow.style.display = 'none'; inputLine.value = ''; @@ -457,8 +463,26 @@ resumeBtn.disabled = true; stopBtn.disabled = true; }); + } + + runBtn.onclick = function () { + doRunFromEditor(); }; + if (runOnEdit && showEditor) { + ta.addEventListener('input', function () { + if (runOnEditTimer) { + clearTimeout(runOnEditTimer); + } + runOnEditTimer = setTimeout(function () { + runOnEditTimer = null; + if (!destroyed && moduleRef) { + doRunFromEditor(); + } + }, runOnEditMs); + }); + } + pauseBtn.onclick = function () { moduleRef.wasmPaused = 1; pauseBtn.disabled = true; @@ -516,6 +540,10 @@ }, destroy: function () { destroyed = true; + if (runOnEditTimer) { + clearTimeout(runOnEditTimer); + runOnEditTimer = null; + } if (vfsRemove) { try { vfsRemove(); From e416646a33260d160279428412159f810ce2aa64 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 30 Mar 2026 11:01:17 +0000 Subject: [PATCH 2/4] Tutorial: enable runOnEdit on playground embed only - web/tutorial.html: debounced auto-run (550ms) for final playground; short note in copy - docs/tutorial-embedding.md + CHANGELOG: document behavior Co-authored-by: Chris Garrett --- CHANGELOG.md | 2 +- docs/tutorial-embedding.md | 2 +- web/tutorial.html | 7 ++++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3cdb8b..f35910d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### Unreleased -- **Sprites**: **`SPRITECOLLIDE(a, b)`** — returns **1** if two loaded, visible sprites’ axis-aligned bounding boxes overlap (basic-gfx + canvas WASM; **0** otherwise). Terminal **`./basic`** errors if used (requires **basic-gfx** or canvas WASM). **Runtime errors**: optional **`Hint:`** line for **unknown function** (shows name) and **`ensure_num` / `ensure_str`** type mismatches. **`tutorial-embed.js`**: optional **`runOnEdit`** / **`runOnEditMs`** for debounced auto-run after editing. +- **Sprites**: **`SPRITECOLLIDE(a, b)`** — returns **1** if two loaded, visible sprites’ axis-aligned bounding boxes overlap (basic-gfx + canvas WASM; **0** otherwise). Terminal **`./basic`** errors if used (requires **basic-gfx** or canvas WASM). **Runtime errors**: optional **`Hint:`** line for **unknown function** (shows name) and **`ensure_num` / `ensure_str`** type mismatches. **`tutorial-embed.js`**: optional **`runOnEdit`** / **`runOnEditMs`** for debounced auto-run after editing; **`web/tutorial.html`** enables this on the final playground embed only (**550** ms). - **Documentation**: **`docs/basic-to-c-transpiler-plan.md`** — design notes for a future **BASIC → C** backend (**cc65** / **z88dk**), recommended subset, and **explicit exclusions** (host I/O, `EVAL`, graphics, etc.). diff --git a/docs/tutorial-embedding.md b/docs/tutorial-embedding.md index e401679..6ca347d 100644 --- a/docs/tutorial-embedding.md +++ b/docs/tutorial-embedding.md @@ -110,7 +110,7 @@ Each embed gets its own **virtual filesystem** and **Asyncify** state; they do n | `vfsExportPath` | string | `'/out.txt'` | Default path in the download field | | `editorMinHeight` | string | `'120px'` | CSS min-height for textarea | | `outputMinHeight` | string | `'100px'` | CSS min-height for output panel | -| `runOnEdit` | boolean | `false` | If true, re-run the program after a short pause whenever the editor text changes (debounced) | +| `runOnEdit` | boolean | `false` | If true, re-run the program after a short pause whenever the editor text changes (debounced). **`web/tutorial.html`** enables this only on the final “playground” embed. | | `runOnEditMs` | number | `600` | Debounce delay in milliseconds for `runOnEdit` | ## Returned API (`Promise`) diff --git a/web/tutorial.html b/web/tutorial.html index 1e6694c..dadb627 100644 --- a/web/tutorial.html +++ b/web/tutorial.html @@ -202,6 +202,7 @@

13. What to try next

Edit the program below and experiment — for example, add a second PRINT or change the message. + After you stop typing for about half a second, the run updates automatically (you can still use Run anytime).

@@ -333,7 +334,11 @@

13. What to try next

RgcBasicTutorialEmbed.mount(document.getElementById('tut-arrays'), { program: progs.arrays }); RgcBasicTutorialEmbed.mount(document.getElementById('tut-functions'), { program: progs.functions }); RgcBasicTutorialEmbed.mount(document.getElementById('tut-rem'), { program: progs.rem }); - RgcBasicTutorialEmbed.mount(document.getElementById('tut-playground'), { program: progs.playground }); + RgcBasicTutorialEmbed.mount(document.getElementById('tut-playground'), { + program: progs.playground, + runOnEdit: true, + runOnEditMs: 550 + }); })(); From 17a7046d9b4afa592580357ea6801637cfe532ba Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 30 Mar 2026 11:22:11 +0000 Subject: [PATCH 3/4] Runtime hints for GOTO/GOSUB/IF/ON; native Hint stderr; tutorial UX - basic.c: runtime_error_hint prints Hint on native stderr (parity with WASM) - Hints for missing THEN, bad GOTO/GOSUB targets, ON target lines, IF THEN line - tutorial-embed: Ctrl/Cmd+Enter runs; scrollToError scrolls output on error - docs + CHANGELOG Co-authored-by: Chris Garrett --- CHANGELOG.md | 2 ++ basic.c | 57 +++++++++++++++++++++++++++++--------- docs/tutorial-embedding.md | 1 + web/tutorial-embed.js | 18 ++++++++++++ 4 files changed, 65 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f35910d..1c6b785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Unreleased +- **Runtime hints**: **`goto` / `gosub` / `on` / `if … then` line** — clearer **`Hint:`** when a line number or label is missing; **`IF`** without **`THEN`** suggests the required keyword form. **Native** `runtime_error_hint` now prints **`Hint:`** on stderr (was Emscripten-only). **`tutorial-embed.js`**: **Ctrl+Enter** (or **Cmd+Enter**) runs from the editor; optional **`scrollToError`** (default **on**) scrolls the output into view after a failed run. + - **Sprites**: **`SPRITECOLLIDE(a, b)`** — returns **1** if two loaded, visible sprites’ axis-aligned bounding boxes overlap (basic-gfx + canvas WASM; **0** otherwise). Terminal **`./basic`** errors if used (requires **basic-gfx** or canvas WASM). **Runtime errors**: optional **`Hint:`** line for **unknown function** (shows name) and **`ensure_num` / `ensure_str`** type mismatches. **`tutorial-embed.js`**: optional **`runOnEdit`** / **`runOnEditMs`** for debounced auto-run after editing; **`web/tutorial.html`** enables this on the final playground embed only (**550** ms). - **Documentation**: **`docs/basic-to-c-transpiler-plan.md`** — design notes for a future **BASIC → C** backend (**cc65** / **z88dk**), recommended subset, and **explicit exclusions** (host I/O, `EVAL`, graphics, etc.). diff --git a/basic.c b/basic.c index 6eccd1f..d8b0202 100644 --- a/basic.c +++ b/basic.c @@ -2131,6 +2131,9 @@ static void runtime_error_hint(const char *msg, const char *hint) } else { fprintf(stderr, "Error: %s\n", msg); } + if (hint && hint[0]) { + fprintf(stderr, " Hint: %s\n", hint); + } if (line_text && *line_text) { /* Print the full source line for context. */ @@ -6575,7 +6578,11 @@ static void statement_on(char **p) if (idx == current) { int line_index = find_line_index(target); if (line_index < 0) { - runtime_error("Target line not found"); + { + char h[96]; + snprintf(h, sizeof(h), "No line %d in program. Check ON … GOTO/GOSUB list.", target); + runtime_error_hint("Target line not found", h); + } return; } if (is_gosub) { @@ -7457,7 +7464,11 @@ static void statement_goto(char **p) } current_line = find_line_index(line_number); if (current_line < 0) { - runtime_error("Target line not found"); + { + char h[96]; + snprintf(h, sizeof(h), "No line %d in program. Check the line number exists.", line_number); + runtime_error_hint("Target line not found", h); + } return; } statement_pos = NULL; @@ -7472,12 +7483,17 @@ static void statement_goto(char **p) namebuf[len] = '\0'; current_line = find_label_line(namebuf); if (current_line < 0) { - runtime_error("Target label not found"); + { + char h[96]; + snprintf(h, sizeof(h), "Define a line like `%s:` before GOTO.", namebuf); + runtime_error_hint("Target label not found", h); + } return; } statement_pos = NULL; } else { - runtime_error("Expected line number or label in GOTO"); + runtime_error_hint("Expected line number or label in GOTO", + "Use GOTO 100 or GOTO mylabel (label lines look like `mylabel:`)."); } #if defined(__EMSCRIPTEN__) && defined(GFX_VIDEO) wasm_maybe_yield_loop(); @@ -7487,23 +7503,25 @@ static void statement_goto(char **p) static void statement_gosub(char **p) { int target_index = -1; + int target_line_num = -1; + char namebuf[32]; char *return_pos; + int len; if (gosub_top >= MAX_GOSUB) { runtime_error("GOSUB stack overflow"); return; } skip_spaces(p); + namebuf[0] = '\0'; if (isdigit((unsigned char)**p)) { - int target_num; - target_num = atoi(*p); + target_line_num = atoi(*p); while (**p && isdigit((unsigned char)**p)) { (*p)++; } - target_index = find_line_index(target_num); + target_index = find_line_index(target_line_num); } else if (isalpha((unsigned char)**p)) { - char namebuf[32]; - int len = 0; + len = 0; while ((isalpha((unsigned char)**p) || isdigit((unsigned char)**p) || **p == '_') && len < (int)sizeof(namebuf) - 1) { namebuf[len++] = **p; @@ -7512,12 +7530,21 @@ static void statement_gosub(char **p) namebuf[len] = '\0'; target_index = find_label_line(namebuf); } else { - runtime_error("Expected line number or label in GOSUB"); + runtime_error_hint("Expected line number or label in GOSUB", + "Use GOSUB 100 or GOSUB mylabel (label lines look like `mylabel:`)."); return; } if (target_index < 0) { - runtime_error("Target line/label not found"); + if (target_line_num >= 0) { + char h[96]; + snprintf(h, sizeof(h), "No line %d in program. GOSUB needs an existing line.", target_line_num); + runtime_error_hint("Target line not found", h); + } else { + char h[96]; + snprintf(h, sizeof(h), "Define a line like `%s:` before GOSUB.", namebuf); + runtime_error_hint("Target line/label not found", h); + } return; } @@ -7695,7 +7722,7 @@ static void statement_if(char **p) cond_true = eval_condition(p); skip_spaces(p); if (!starts_with_kw(*p, "THEN")) { - runtime_error("Missing THEN"); + runtime_error_hint("Missing THEN", "Write IF condition THEN statement (THEN is required)."); return; } *p += 4; @@ -7726,7 +7753,11 @@ static void statement_if(char **p) } current_line = find_line_index(target); if (current_line < 0) { - runtime_error("Target line not found"); + { + char h[96]; + snprintf(h, sizeof(h), "No line %d in program. IF … THEN line must exist.", target); + runtime_error_hint("Target line not found", h); + } return; } statement_pos = NULL; diff --git a/docs/tutorial-embedding.md b/docs/tutorial-embedding.md index 6ca347d..8feb486 100644 --- a/docs/tutorial-embedding.md +++ b/docs/tutorial-embedding.md @@ -112,6 +112,7 @@ Each embed gets its own **virtual filesystem** and **Asyncify** state; they do n | `outputMinHeight` | string | `'100px'` | CSS min-height for output panel | | `runOnEdit` | boolean | `false` | If true, re-run the program after a short pause whenever the editor text changes (debounced). **`web/tutorial.html`** enables this only on the final “playground” embed. | | `runOnEditMs` | number | `600` | Debounce delay in milliseconds for `runOnEdit` | +| `scrollToError` | boolean | `true` | After a run finishes, scroll the output panel into view if stderr contained an error | ## Returned API (`Promise`) diff --git a/web/tutorial-embed.js b/web/tutorial-embed.js index 2c5302a..7562e1a 100644 --- a/web/tutorial-embed.js +++ b/web/tutorial-embed.js @@ -195,6 +195,7 @@ * @param {boolean} [opts.showVfsTools=true] - Upload/Download virtual FS (needs vfs-helpers.js beside basic-modular.js) * @param {boolean} [opts.runOnEdit=false] - After edits, auto-run after a short debounce (handy for tutorials) * @param {number} [opts.runOnEditMs=600] - Debounce delay for runOnEdit + * @param {boolean} [opts.scrollToError=true] - After a run, scroll the output into view if stderr reported an error * @returns {Promise<{ run: function, resetOutput: function, destroy: function, getModule: function }>} */ function mount(container, opts) { @@ -213,6 +214,7 @@ var runOnEditMs = typeof opts.runOnEditMs === 'number' && opts.runOnEditMs >= 0 ? opts.runOnEditMs : 600; var runOnEditTimer = null; + var scrollToError = opts.scrollToError !== false; container.classList.add('rgc-tutorial-embed'); container.innerHTML = ''; @@ -462,6 +464,13 @@ pauseBtn.disabled = true; resumeBtn.disabled = true; stopBtn.disabled = true; + if (scrollToError && out.dataset.error === '1') { + try { + out.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + } catch (e) { + out.scrollIntoView(true); + } + } }); } @@ -469,6 +478,15 @@ doRunFromEditor(); }; + if (showEditor) { + ta.addEventListener('keydown', function (ev) { + if ((ev.ctrlKey || ev.metaKey) && ev.key === 'Enter') { + ev.preventDefault(); + doRunFromEditor(); + } + }); + } + if (runOnEdit && showEditor) { ta.addEventListener('input', function () { if (runOnEditTimer) { From e4508ca77d5ca9f815026cc69af0f929022161b5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 30 Mar 2026 11:29:02 +0000 Subject: [PATCH 4/4] Runtime hints for DIM/FOR/NEXT and array access; tutorial kbd note - FOR/NEXT/DIM: hints for scalar, TO, STEP, NEXT pairing, DIM syntax and bounds - get_var_reference: hints for reserved names, subscripts, out of range - tutorial.html: Ctrl/Cmd+Enter note; embedding doc keyboard section - CHANGELOG: consolidate runtime-hints bullet Co-authored-by: Chris Garrett --- CHANGELOG.md | 2 +- basic.c | 36 ++++++++++++++++++++++-------------- docs/tutorial-embedding.md | 2 ++ web/tutorial.html | 1 + 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c6b785..5552f91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### Unreleased -- **Runtime hints**: **`goto` / `gosub` / `on` / `if … then` line** — clearer **`Hint:`** when a line number or label is missing; **`IF`** without **`THEN`** suggests the required keyword form. **Native** `runtime_error_hint` now prints **`Hint:`** on stderr (was Emscripten-only). **`tutorial-embed.js`**: **Ctrl+Enter** (or **Cmd+Enter**) runs from the editor; optional **`scrollToError`** (default **on**) scrolls the output into view after a failed run. +- **Runtime hints**: **`goto` / `gosub` / `on` / `if … then`**, **`DIM` / `FOR` / `NEXT`**, and **array subscripts** — short **`Hint:`** lines for common mistakes (missing line/label, **`THEN`**, bounds, parentheses, reserved names). **Native** `runtime_error_hint` prints **`Hint:`** on stderr (parity with WASM). **`tutorial-embed.js`**: **Ctrl+Enter** / **Cmd+Enter** runs; **`scrollToError`** (default **on**). **`web/tutorial.html`** playground mentions keyboard run. - **Sprites**: **`SPRITECOLLIDE(a, b)`** — returns **1** if two loaded, visible sprites’ axis-aligned bounding boxes overlap (basic-gfx + canvas WASM; **0** otherwise). Terminal **`./basic`** errors if used (requires **basic-gfx** or canvas WASM). **Runtime errors**: optional **`Hint:`** line for **unknown function** (shows name) and **`ensure_num` / `ensure_str`** type mismatches. **`tutorial-embed.js`**: optional **`runOnEdit`** / **`runOnEditMs`** for debounced auto-run after editing; **`web/tutorial.html`** enables this on the final playground embed only (**550** ms). diff --git a/basic.c b/basic.c index d8b0202..dbfb579 100644 --- a/basic.c +++ b/basic.c @@ -5392,7 +5392,7 @@ static struct value *get_var_reference(char **p, int *is_array_out, int *is_stri skip_spaces(p); if (!isalpha((unsigned char)**p)) { - runtime_error("Expected variable"); + runtime_error_hint("Expected variable", "Assignment and LET need a variable name (e.g. X or A$)."); return NULL; } read_identifier(p, namebuf, sizeof(namebuf)); @@ -5402,7 +5402,8 @@ static struct value *get_var_reference(char **p, int *is_array_out, int *is_stri name_out[VAR_NAME_MAX - 1] = '\0'; } if (is_reserved_word(namebuf)) { - runtime_error("Reserved word cannot be used as variable"); + runtime_error_hint("Reserved word cannot be used as variable", + "Pick a different name; keywords like IF, FOR, PRINT are not variables."); return NULL; } if (is_string_out) { @@ -5416,14 +5417,15 @@ static struct value *get_var_reference(char **p, int *is_array_out, int *is_stri (*p)++; for (;;) { if (dims >= MAX_DIMS) { - runtime_error("Too many subscripts"); + runtime_error_hint("Too many subscripts", + "At most 3 indices per access (e.g. A(1,2,3))."); return NULL; } idx_val = eval_expr(p); ensure_num(&idx_val); indices[dims] = (int)(idx_val.num + 0.00001); if (indices[dims] < 0) { - runtime_error("Negative array index"); + runtime_error_hint("Negative array index", "Indices must be 0 or positive."); return NULL; } dims++; @@ -5435,7 +5437,7 @@ static struct value *get_var_reference(char **p, int *is_array_out, int *is_stri break; } if (**p != ')') { - runtime_error("Missing ')'"); + runtime_error_hint("Missing ')'", "Close array subscripts: A(1) or M(1,2)."); return NULL; } (*p)++; @@ -5463,11 +5465,12 @@ static struct value *get_var_reference(char **p, int *is_array_out, int *is_stri } } if (dims == 0) { - runtime_error("Array subscript required"); + runtime_error_hint("Array subscript required", "Use A(0) or A(I), not the bare array name."); return NULL; } if (dims > v->dims) { - runtime_error("Too many subscripts"); + runtime_error_hint("Too many subscripts", + "Fewer indices than dimensions in DIM (e.g. DIM A(9) needs one index)."); return NULL; } /* For now we assume dimensions were fixed by DIM; no auto-resize for multi-dim. */ @@ -5476,14 +5479,16 @@ static struct value *get_var_reference(char **p, int *is_array_out, int *is_stri for (d = v->dims - 1; d >= 0; d--) { int idx = (d < dims) ? indices[d] : 0; if (idx < 0 || idx >= v->dim_sizes[d]) { - runtime_error("Subscript out of range"); + runtime_error_hint("Subscript out of range", + "Index must be within DIM bounds (0 .. upper bound)."); return NULL; } flat_index += idx * stride; stride *= v->dim_sizes[d]; } if (flat_index >= v->size) { - runtime_error("Subscript out of range"); + runtime_error_hint("Subscript out of range", + "Index must be within DIM bounds (0 .. upper bound)."); return NULL; } valp = &v->array[flat_index]; @@ -8086,7 +8091,7 @@ static void statement_for(char **p) } if (for_top >= MAX_FOR) { - runtime_error("FOR stack overflow"); + runtime_error_hint("FOR stack overflow", "Too many nested FOR loops; reduce nesting."); return; } for_stack[for_top].end_value = endv.num; @@ -8181,7 +8186,8 @@ static void statement_next(char **p) } #endif if (!vp) { - runtime_error("Loop variable missing"); + runtime_error_hint("Loop variable missing", + "FOR stack inconsistency; avoid GOTO into NEXT without a matching FOR."); return; } vp->num += for_stack[for_top - 1].step; @@ -8226,14 +8232,16 @@ static void statement_dim(char **p) (*p)++; for (;;) { if (dims >= MAX_DIMS) { - runtime_error("Too many dimensions"); + runtime_error_hint("Too many dimensions", + "At most 3 dimensions (e.g. DIM A(9,9,9))."); return; } sizev = eval_expr(p); ensure_num(&sizev); dim_sizes[dims] = (int)sizev.num + 1; if (dim_sizes[dims] <= 0) { - runtime_error("Invalid array size"); + runtime_error_hint("Invalid array size", + "Sizes are upper bounds (0-based); use a non-negative size in DIM."); return; } dims++; @@ -8245,7 +8253,7 @@ static void statement_dim(char **p) break; } if (**p != ')') { - runtime_error("Missing ')'"); + runtime_error_hint("Missing ')'", "Close DIM subscripts: DIM A(10) or DIM M(2,3)."); return; } (*p)++; diff --git a/docs/tutorial-embedding.md b/docs/tutorial-embedding.md index 8feb486..66f6e65 100644 --- a/docs/tutorial-embedding.md +++ b/docs/tutorial-embedding.md @@ -114,6 +114,8 @@ Each embed gets its own **virtual filesystem** and **Asyncify** state; they do n | `runOnEditMs` | number | `600` | Debounce delay in milliseconds for `runOnEdit` | | `scrollToError` | boolean | `true` | After a run finishes, scroll the output panel into view if stderr contained an error | +**Keyboard:** **Ctrl+Enter** / **Cmd+Enter** in the program textarea runs the program (same as **Run**). Documented on **`web/tutorial.html`** for the playground embed. + ## Returned API (`Promise`) `mount` resolves to an object: diff --git a/web/tutorial.html b/web/tutorial.html index dadb627..1860435 100644 --- a/web/tutorial.html +++ b/web/tutorial.html @@ -203,6 +203,7 @@

13. What to try next

Edit the program below and experiment — for example, add a second PRINT or change the message. After you stop typing for about half a second, the run updates automatically (you can still use Run anytime). + With the editor focused, Ctrl+Enter (Windows/Linux) or Cmd+Enter (macOS) runs immediately.