diff --git a/CMakeLists.txt b/CMakeLists.txt index 9476bf4..f4e1334 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,7 @@ set(SOURCES src/program_io.c src/print_using.c src/graphics.c + src/virmem.c src/sound.c src/tui.c platform/hal_posix.c diff --git a/README.md b/README.md index 31ad6db..9cf12ee 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Interactive mode launches the authentic GW-BASIC full-screen editor: ``` $ ./gwbasic -GW-BASIC 2026 0.10.0 +GW-BASIC 2026 0.11.0 (C) Eremey Valetov 2026. MIT License. Based on Microsoft GW-BASIC assembly source. Ok @@ -77,8 +77,9 @@ SPACE$, STRING$, HEX$, OCT$, INSTR, INPUT$ | File management | KILL, NAME, FILES, MKDIR, RMDIR, CHDIR, SHELL | | Date/time | DATE$, TIME$, TIMER | | Screen | LOCATE, COLOR, WIDTH, SCREEN, KEY ON/OFF/LIST | -| Graphics | PSET, PRESET, LINE, CIRCLE, DRAW, PAINT | +| Graphics | PSET, PRESET, LINE, CIRCLE, DRAW, PAINT, GET/PUT (sprites) | | Sound | SOUND, BEEP, PLAY (MML) | +| Memory | DEF SEG, PEEK, POKE | ### Binary and ASCII File Formats @@ -144,6 +145,33 @@ CIRCLE (160,100), 80, 2 PAINT (160,100), 3, 2 ``` +`GET` and `PUT` capture and blit rectangular sprites using CGA-compatible +packed pixel format. `PUT` supports action modes: XOR (default), PSET, +PRESET, AND, OR. + +``` +DIM S%(50) +GET (0,0)-(15,15), S% +PUT (100,50), S%, XOR +``` + +### DEF SEG / PEEK / POKE + +A virtual 8086 address space emulates the memory layout that GW-BASIC programs +expected on a real IBM PC: + +| Segment | Description | +|---------|-------------| +| `0040` | BIOS data area — video mode, cursor position, timer ticks (18.2 Hz) | +| `B800` | CGA text buffer — character/attribute pairs (80×25 = 4000 bytes) | + +``` +DEF SEG = &HB800 +POKE 0, 65 ' write 'A' to top-left screen cell +PRINT PEEK(1) ' read the color attribute +DEF SEG ' reset to default segment +``` + ## Architecture The interpreter follows the original GW-BASIC's internal structure: @@ -174,6 +202,7 @@ Source text → Tokenizer (CRUNCH) → Token stream | File I/O | fileio.c | BIPTRG.ASM | | PRINT USING | print_using.c | BIPRTU.ASM | | Sound | sound.c | — | +| Virtual memory | virmem.c | — | | Platform | hal_posix.c | OEM*.ASM | Key design differences from the original: @@ -191,7 +220,7 @@ input, so they're not part of the automated test suite. ## Tests -58 test programs in `tests/programs/`, with golden-file regression testing +61 test programs in `tests/programs/`, with golden-file regression testing and CI via GitHub Actions: ```bash diff --git a/docs/architecture.md b/docs/architecture.md index 3f3738e..ed69050 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -48,15 +48,16 @@ HAL writes straight to stdout. | Program I/O (SAVE/LOAD) | `program_io.c` | BIMISC.ASM | | INPUT/LINE INPUT | `input.c` | BINTRP.ASM | | Sound engine | `sound.c` | — | +| Virtual memory (PEEK/POKE) | `virmem.c` | — | | Platform abstraction | `hal_posix.c` | OEM*.ASM | ## Source Layout ``` -src/ — core interpreter (20 files) -include/ — headers (12 files) +src/ — core interpreter (21 files) +include/ — headers (13 files) platform/ — HAL backends (1 file) -tests/ — 58 automated test programs, 4 classic interactive programs, compat harness +tests/ — 61 automated test programs, 4 classic interactive programs, compat harness docs/ — Sphinx documentation ``` diff --git a/docs/development.md b/docs/development.md index 065c6fd..dc3ee1f 100644 --- a/docs/development.md +++ b/docs/development.md @@ -14,10 +14,11 @@ | 0.8.0 | `c68167c` | Dynamic TUI screen buffer, `--full` flag, LPRINT/LLIST with `--lpt` | | 0.9.0 | `2a8f98b` | EDIT statement, ON TIMER/ON KEY event trapping, F-key escape parser fixes | | 0.10.0 | | Binary tokenized SAVE/LOAD, INKEY$ extended key sequences, golden-file regression tests, classic BASIC programs | +| 0.11.0 | | DEF SEG / PEEK / POKE virtual memory, GET/PUT graphics sprites, PRINT USING fixes | ## Tests -58 test programs in `tests/programs/`, plus 4 classic interactive programs in +61 test programs in `tests/programs/`, plus 4 classic interactive programs in `tests/classic/` (Hamurabi, Lunar Lander, Gunner, Diamond from David Ahl's *BASIC Computer Games*). @@ -27,9 +28,10 @@ Run the full automated suite: bash tests/run_tests.sh ``` -Each test has a 5-second timeout. 55 tests have `.expected` golden files +Each test has a 5-second timeout. 57 tests have `.expected` golden files for output regression detection. Three timing-dependent tests (datetime, -on_timer, timer_stop) run without golden comparison. +on_timer, timer_stop) and one sound test (play_scale) run without golden +comparison. ### Compatibility Testing @@ -46,6 +48,6 @@ bash tests/run_compat.sh ## CI GitHub Actions runs on every push to `main` and on pull requests. The workflow -builds the project with PulseAudio support and runs all 58 test programs. +builds the project with PulseAudio support and runs all 61 test programs. See [`.github/workflows/ci.yml`](https://github.com/evvaletov/gw-basic-2026/blob/main/.github/workflows/ci.yml). diff --git a/docs/getting-started.md b/docs/getting-started.md index 79b731b..8023faa 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -37,7 +37,7 @@ Running `./gwbasic` with no arguments launches the full-screen editor: ``` $ ./gwbasic -GW-BASIC 2026 0.10.0 +GW-BASIC 2026 0.11.0 (C) Eremey Valetov 2026. MIT License. Based on Microsoft GW-BASIC assembly source. Ok diff --git a/docs/language-reference.md b/docs/language-reference.md index 4073750..188557b 100644 --- a/docs/language-reference.md +++ b/docs/language-reference.md @@ -57,9 +57,10 @@ type suffixes (`%`, `!`, `#`) | File management | `KILL`, `NAME`, `FILES`, `MKDIR`, `RMDIR`, `CHDIR`, `SHELL` | | Date/time | `DATE$`, `TIME$`, `TIMER` | | Screen | `LOCATE`, `COLOR`, `WIDTH`, `SCREEN`, `KEY ON`/`OFF`/`LIST`, `KEY n,"string"` | -| Graphics | `PSET`, `PRESET`, `LINE`, `CIRCLE`, `DRAW`, `PAINT` | +| Graphics | `PSET`, `PRESET`, `LINE`, `CIRCLE`, `DRAW`, `PAINT`, `GET`, `PUT` | | Sound | `SOUND`, `BEEP`, `PLAY` (MML parser, PulseAudio backend) | -| Misc | `POKE`, `KEY`, `TRON`/`TROFF`, `OPTION BASE`, `MID$` assignment | +| Memory | `DEF SEG`, `PEEK`, `POKE` | +| Misc | `KEY`, `TRON`/`TROFF`, `OPTION BASE`, `MID$` assignment | | System | `SYSTEM` | ## Program I/O (SAVE / LOAD) @@ -138,6 +139,25 @@ which works in terminals like xterm, mlterm, foot, and WezTerm. - `POINT (x,y)` — read pixel color - `COLOR fg, bg` — set foreground/background colors +### Sprite Capture and Blit (GET / PUT) + +- `GET (x1,y1)-(x2,y2), array` — capture a screen rectangle into an integer array +- `PUT (x,y), array [, action]` — blit a captured sprite back to the screen + +The `action` parameter controls how the sprite combines with the existing screen: + +| Action | Effect | +|--------|--------| +| `XOR` (default) | Exclusive OR — drawing twice erases the sprite | +| `PSET` | Overwrite screen with sprite pixels | +| `PRESET` | Overwrite screen with inverted sprite pixels | +| `AND` | Bitwise AND of screen and sprite | +| `OR` | Bitwise OR of screen and sprite | + +Sprite data is stored in CGA-compatible packed format: word 0 is the width +in bits, word 1 is the height, and remaining words contain packed pixel data +matching the original GW-BASIC representation. + ### Example ``` @@ -147,6 +167,28 @@ CIRCLE (160,100), 80, 2 PAINT (160,100), 3, 2 ``` +## DEF SEG / PEEK / POKE + +`DEF SEG`, `PEEK`, and `POKE` provide access to a virtual 8086 address space +that emulates the memory layout programs expected on a real IBM PC: + +``` +DEF SEG = &HB800 ' select CGA video buffer segment +POKE 0, 65 ' write 'A' to top-left screen cell +PRINT PEEK(1) ' read the color attribute +DEF SEG ' reset to default segment +``` + +### Emulated Memory Regions + +| Segment | Address Range | Description | +|---------|---------------|-------------| +| `0040` | BIOS data area | Video mode (`0049`), column count (`004A`), cursor position (`0050-0051`), timer ticks (`006C-006F`), screen rows (`0084`) | +| `B800` | CGA text buffer | Character/attribute pairs in text mode (even byte = char, odd byte = attr, 80 columns × 25 rows = 4000 bytes) | + +All other segments read as 0 and writes are silently discarded. The timer +tick counter at `0040:006C` tracks real time at the original 18.2 Hz rate. + ## Sound - `SOUND frequency, duration` — play a tone (frequency in Hz, duration in clock ticks) diff --git a/docs/roadmap.md b/docs/roadmap.md index 05fe56d..9ac09de 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -10,28 +10,20 @@ ## Planned Features -- **DEF SEG / PEEK / POKE emulation** — a virtual address space for the BIOS data - area and CGA screen buffer (B800:0000), so programs that directly twiddle - screen memory or read the keyboard buffer actually work. Not quite cycle- - accurate, but enough to run most "tricks" from the 1980s magazines. - **Hardware I/O simulator** — an optional emulation layer for `OUT`, `INP`, `WAIT`, `MOTOR`, and friends. The idea is to provide a virtual PC peripheral bus so retro programs that talk to the speaker, joystick port, or parallel interface can do *something* useful instead of silently no-oping. Think of it as a tiny museum exhibit for vintage BASIC hardware hacking. - **BSAVE / BLOAD** — binary file save/load for screen buffers and data -- **PRINT USING edge cases** — `**` asterisk fill, `**$` combined, thousands - separator with `,`, and `^^^^` scientific notation corner cases to match - the original output formatting exactly -- **GET/PUT graphics** — sprite capture and blit for graphics mode; the - framebuffer infrastructure is already in place, this is the missing piece - for any BASIC program that does animation - **TUI color support** — map GW-BASIC COLOR attributes to ANSI 16-color output in the TUI screen buffer - **VIEW / WINDOW / PALETTE** — graphics viewport, coordinate mapping, and palette remapping - **MBF format support for binary LOAD** — convert Microsoft Binary Format float constants when loading files saved by the original GWBASIC.EXE +- **Extended PEEK/POKE regions** — keyboard shift flags (0040:0017), CGA + graphics mode framebuffer mapping ## IDE and Notebook Integration @@ -49,7 +41,8 @@ ## Known Limitations -- `PEEK`/`POKE` are stubs (POKE parses and discards, PEEK returns 0) +- Virtual memory emulation covers BIOS data area and CGA text buffer only; + CGA graphics mode PEEK/POKE not yet mapped - Binary files use IEEE 754 floats, not MBF — files saved here are not byte-compatible with original GWBASIC.EXE binary format - String garbage collection not implemented (uses `malloc`/`free` instead) diff --git a/include/graphics.h b/include/graphics.h index 468fe2b..10f12b3 100644 --- a/include/graphics.h +++ b/include/graphics.h @@ -31,4 +31,23 @@ int gfx_get_height(void); #define GFX_BOX 'B' #define GFX_BOXF 'F' +/* GET/PUT sprite support */ +#include + +/* PUT action modes */ +#define GFX_ACTION_XOR 0 +#define GFX_ACTION_PSET 1 +#define GFX_ACTION_PRESET 2 +#define GFX_ACTION_AND 3 +#define GFX_ACTION_OR 4 + +/* Capture screen rectangle into int16 array. Returns required array size. */ +int gfx_sprite_get(int x1, int y1, int x2, int y2, int16_t *buf, int bufsize); + +/* Blit sprite from int16 array to screen. */ +void gfx_sprite_put(int x, int y, const int16_t *buf, int bufsize, int action); + +/* Calculate required array element count for a sprite rectangle. */ +int gfx_sprite_size(int x1, int y1, int x2, int y2); + #endif diff --git a/include/gwbasic.h b/include/gwbasic.h index 1377479..04b5a29 100644 --- a/include/gwbasic.h +++ b/include/gwbasic.h @@ -10,7 +10,7 @@ #include "gw_math.h" #include "strings.h" -#define GW_VERSION "0.10.0" +#define GW_VERSION "0.11.0" #define GW_BANNER "GW-BASIC " GW_VERSION /* Tokenizer */ diff --git a/include/interp.h b/include/interp.h index b31b4c0..a19ce66 100644 --- a/include/interp.h +++ b/include/interp.h @@ -80,6 +80,9 @@ typedef struct { /* Event trapping */ timer_trap_t timer_trap; event_trap_t key_traps[10]; /* KEY(1)-KEY(10) */ + + /* DEF SEG segment (for PEEK/POKE/BSAVE/BLOAD) */ + uint16_t def_seg; } interp_state_t; extern interp_state_t gw; diff --git a/include/virmem.h b/include/virmem.h new file mode 100644 index 0000000..0d9ea1f --- /dev/null +++ b/include/virmem.h @@ -0,0 +1,25 @@ +#ifndef GW_VIRMEM_H +#define GW_VIRMEM_H + +#include + +/* + * Virtual 8086 address space for DEF SEG / PEEK / POKE. + * + * Real GW-BASIC runs in a 1MB address space where programs routinely + * peek and poke at the BIOS data area and CGA video buffer. We emulate + * just enough of that space to make classic tricks work: + * + * 0040:0000 BIOS data area (keyboard flags, timer ticks, cursor pos) + * B800:0000 CGA text video buffer (char/attr pairs, 80x25 = 4000 bytes) + * + * Everything else reads as 0 and writes are silently discarded. + */ + +/* Read a byte from virtual memory at segment:offset */ +uint8_t virmem_peek(uint16_t segment, uint16_t offset); + +/* Write a byte to virtual memory at segment:offset */ +void virmem_poke(uint16_t segment, uint16_t offset, uint8_t value); + +#endif diff --git a/src/eval.c b/src/eval.c index f682bfa..ce88f86 100644 --- a/src/eval.c +++ b/src/eval.c @@ -1,6 +1,7 @@ #include "gwbasic.h" #include "tui.h" #include "graphics.h" +#include "virmem.h" #include #include #include @@ -690,8 +691,16 @@ static gw_value_t eval_function(uint8_t prefix, uint8_t func_tok) return v; } + case FUNC_PEEK: { + gw_expect('('); + uint16_t offset = gw_eval_uint16(); + gw_expect_rparen(); + v.type = VT_INT; + v.ival = virmem_peek(gw.def_seg, offset); + return v; + } + case FUNC_INP: - case FUNC_PEEK: case FUNC_PEN: case FUNC_STICK: case FUNC_STRIG: @@ -1052,10 +1061,13 @@ int16_t gw_eval_int(void) uint16_t gw_eval_uint16(void) { gw_value_t v = gw_eval_num(); + /* Integer values may be negative due to int16 representation of &H8000+ */ + if (v.type == VT_INT) + return (uint16_t)v.ival; double d = gw_to_dbl(&v); - if (d < 0 || d > 65535) + if (d < -32768 || d > 65535) gw_error(ERR_FC); - return (uint16_t)d; + return (uint16_t)(int16_t)d; } void gw_expect_rparen(void) diff --git a/src/graphics.c b/src/graphics.c index 0475ffb..aa15563 100644 --- a/src/graphics.c +++ b/src/graphics.c @@ -461,3 +461,114 @@ void gfx_flush(void) gw_hal->write_raw(buf, pos); free(buf); } + +/* + * GET/PUT sprite support. + * + * Sprites are stored in integer arrays using the CGA packed format: + * Word 0: width in bits (pixel_width * bits_per_pixel) + * Word 1: height in scan lines + * Remaining: packed pixel data, row by row, padded to word boundary + * + * SCREEN 1: 2 bits/pixel (4 pixels/byte) + * SCREEN 2: 1 bit/pixel (8 pixels/byte) + */ + +static int bits_per_pixel(void) +{ + return (screen_mode == 2) ? 1 : 2; +} + +int gfx_sprite_size(int x1, int y1, int x2, int y2) +{ + if (!framebuf) return 0; + int w = abs(x2 - x1) + 1; + int h = abs(y2 - y1) + 1; + int bpp = bits_per_pixel(); + int bytes_per_row = (w * bpp + 7) / 8; + if (bytes_per_row & 1) bytes_per_row++; /* pad to word boundary */ + int data_bytes = bytes_per_row * h; + return 2 + (data_bytes + 1) / 2; /* 2 header words + data words */ +} + +int gfx_sprite_get(int x1, int y1, int x2, int y2, int16_t *buf, int bufsize) +{ + if (!framebuf) return 0; + + /* Normalize coordinates */ + if (x1 > x2) { int t = x1; x1 = x2; x2 = t; } + if (y1 > y2) { int t = y1; y1 = y2; y2 = t; } + + int w = x2 - x1 + 1; + int h = y2 - y1 + 1; + int bpp = bits_per_pixel(); + int width_bits = w * bpp; + int bytes_per_row = (width_bits + 7) / 8; + if (bytes_per_row & 1) bytes_per_row++; /* word-align */ + int data_bytes = bytes_per_row * h; + int required = 2 + (data_bytes + 1) / 2; + + if (bufsize < required) return required; + + buf[0] = (int16_t)width_bits; + buf[1] = (int16_t)h; + + /* Pack pixels into CGA format */ + uint8_t *out = (uint8_t *)&buf[2]; + memset(out, 0, data_bytes); + + for (int row = 0; row < h; row++) { + for (int col = 0; col < w; col++) { + int px = get_pixel(x1 + col, y1 + row); + int bit_offset = col * bpp; + int byte_idx = bit_offset / 8; + int bit_pos = 8 - bpp - (bit_offset % 8); /* MSB first */ + out[row * bytes_per_row + byte_idx] |= + (px & ((1 << bpp) - 1)) << bit_pos; + } + } + + return required; +} + +void gfx_sprite_put(int x, int y, const int16_t *buf, int bufsize, int action) +{ + if (!framebuf || bufsize < 2) return; + + int bpp = bits_per_pixel(); + int width_bits = (uint16_t)buf[0]; + int h = (uint16_t)buf[1]; + int w = width_bits / bpp; + if (w <= 0 || h <= 0) return; + + int bytes_per_row = (width_bits + 7) / 8; + if (bytes_per_row & 1) bytes_per_row++; + + const uint8_t *src = (const uint8_t *)&buf[2]; + int mask = (1 << bpp) - 1; + + for (int row = 0; row < h; row++) { + int sy = y + row; + if (sy < 0 || sy >= fb_height) continue; + for (int col = 0; col < w; col++) { + int sx = x + col; + if (sx < 0 || sx >= fb_width) continue; + + int bit_offset = col * bpp; + int byte_idx = bit_offset / 8; + int bit_pos = 8 - bpp - (bit_offset % 8); + int sprite_px = (src[row * bytes_per_row + byte_idx] >> bit_pos) & mask; + + int old_px = get_pixel(sx, sy); + int new_px; + switch (action) { + case GFX_ACTION_PSET: new_px = sprite_px; break; + case GFX_ACTION_PRESET: new_px = (~sprite_px) & mask; break; + case GFX_ACTION_AND: new_px = old_px & sprite_px; break; + case GFX_ACTION_OR: new_px = old_px | sprite_px; break; + default: new_px = old_px ^ sprite_px; break; /* XOR */ + } + set_pixel(sx, sy, new_px); + } + } +} diff --git a/src/interp.c b/src/interp.c index 9d3c6c7..40964d2 100644 --- a/src/interp.c +++ b/src/interp.c @@ -1,6 +1,7 @@ #include "gwbasic.h" #include "tui.h" #include "graphics.h" +#include "virmem.h" #include "sound.h" #include #include @@ -722,6 +723,113 @@ void gw_stmt_chain(void) } } +/* Graphics GET (x1,y1)-(x2,y2), array */ +static void stmt_gfx_get(void) +{ + gw_expect('('); + int x1 = gw_eval_int(); + gw_skip_spaces(); + gw_expect(','); + int y1 = gw_eval_int(); + gw_expect_rparen(); + gw_skip_spaces(); + gw_expect(TOK_MINUS); + gw_expect('('); + int x2 = gw_eval_int(); + gw_skip_spaces(); + gw_expect(','); + int y2 = gw_eval_int(); + gw_expect_rparen(); + gw_skip_spaces(); + gw_expect(','); + + char name[2]; + gw_valtype_t type = gw_parse_varname(name); + if (type != VT_INT) + gw_error(ERR_TM); + + int required = gfx_sprite_size(x1, y1, x2, y2); + if (required <= 0) gw_error(ERR_FC); + + /* Find or auto-dim the array; skip its subscript so parser is happy */ + gw_skip_spaces(); + array_entry_t *a = NULL; + for (int i = 0; i < gw.array_count; i++) { + if (gw.arrays[i].name[0] == name[0] && gw.arrays[i].name[1] == name[1] + && gw.arrays[i].type == VT_INT) { + a = &gw.arrays[i]; + break; + } + } + if (!a) gw_error(ERR_FC); + if (a->total_elements < required) gw_error(ERR_FC); + + /* Build a flat int16 buffer from the array's ival fields */ + int16_t *buf = malloc(a->total_elements * sizeof(int16_t)); + if (!buf) gw_error(ERR_OM); + + gfx_sprite_get(x1, y1, x2, y2, buf, a->total_elements); + + /* Copy back to array */ + for (int i = 0; i < a->total_elements && i < required; i++) + a->data[i].ival = buf[i]; + + free(buf); +} + +/* Graphics PUT (x,y), array [, action] */ +static void stmt_gfx_put(void) +{ + gw_expect('('); + int x = gw_eval_int(); + gw_skip_spaces(); + gw_expect(','); + int y = gw_eval_int(); + gw_expect_rparen(); + gw_skip_spaces(); + gw_expect(','); + + char name[2]; + gw_valtype_t type = gw_parse_varname(name); + if (type != VT_INT) + gw_error(ERR_TM); + + /* Find the array */ + array_entry_t *a = NULL; + for (int i = 0; i < gw.array_count; i++) { + if (gw.arrays[i].name[0] == name[0] && gw.arrays[i].name[1] == name[1] + && gw.arrays[i].type == VT_INT) { + a = &gw.arrays[i]; + break; + } + } + if (!a) gw_error(ERR_FC); + + /* Parse optional action verb */ + int action = GFX_ACTION_XOR; + gw_skip_spaces(); + if (gw_chrgot() == ',') { + gw_chrget(); + gw_skip_spaces(); + uint8_t tok = gw_chrgot(); + if (tok == TOK_PSET) { action = GFX_ACTION_PSET; gw_chrget(); } + else if (tok == TOK_PRESET) { action = GFX_ACTION_PRESET; gw_chrget(); } + else if (tok == TOK_AND) { action = GFX_ACTION_AND; gw_chrget(); } + else if (tok == TOK_OR) { action = GFX_ACTION_OR; gw_chrget(); } + else if (tok == TOK_XOR) { action = GFX_ACTION_XOR; gw_chrget(); } + else gw_error(ERR_SN); + } + + /* Build int16 buffer from array */ + int16_t *buf = malloc(a->total_elements * sizeof(int16_t)); + if (!buf) gw_error(ERR_OM); + for (int i = 0; i < a->total_elements; i++) + buf[i] = a->data[i].ival; + + gfx_sprite_put(x, y, buf, a->total_elements, action); + free(buf); +} + /* ================================================================ * Statement Dispatcher * ================================================================ */ @@ -841,12 +949,22 @@ void gw_exec_stmt(void) } if (xstmt == XSTMT_PUT) { gw_chrget(); - gw_stmt_put(); + gw_skip_spaces(); + if (gfx_active() && gw_chrgot() == '(') { + stmt_gfx_put(); + } else { + gw_stmt_put(); + } return; } if (xstmt == XSTMT_GET) { gw_chrget(); - gw_stmt_get(); + gw_skip_spaces(); + if (gfx_active() && gw_chrgot() == '(') { + stmt_gfx_get(); + } else { + gw_stmt_get(); + } return; } if (xstmt == XSTMT_KILL) { @@ -2181,6 +2299,21 @@ void gw_exec_stmt(void) stmt_def_fn(); return; } + /* DEF SEG [= segment] */ + uint8_t ch = gw_chrgot(); + if ((ch == 'S' || ch == 's') && + (gw.text_ptr[1] == 'E' || gw.text_ptr[1] == 'e') && + (gw.text_ptr[2] == 'G' || gw.text_ptr[2] == 'g')) { + gw.text_ptr += 3; + gw_skip_spaces(); + if (gw_chrgot() == TOK_EQ) { + gw_chrget(); + gw.def_seg = gw_eval_uint16(); + } else { + gw.def_seg = 0; /* DEF SEG with no argument resets to default */ + } + return; + } gw_error(ERR_SN); } @@ -2241,13 +2374,15 @@ void gw_exec_stmt(void) return; } - /* POKE - ignore for now */ + /* POKE address, value */ if (tok == TOK_POKE) { gw_chrget(); - gw_eval_int(); /* address */ + uint16_t addr = gw_eval_uint16(); gw_skip_spaces(); gw_expect(','); - gw_eval_int(); /* value */ + int val = gw_eval_int(); + if (val < 0 || val > 255) gw_error(ERR_FC); + virmem_poke(gw.def_seg, addr, (uint8_t)val); return; } diff --git a/src/print_using.c b/src/print_using.c index f1bc6a9..505aada 100644 --- a/src/print_using.c +++ b/src/print_using.c @@ -160,8 +160,9 @@ static void format_number(FILE *fp, const char *fmt, int fmtlen, double val) exp_val = (int)floor(log10(absval)); double mant = absval / pow(10, exp_val); snprintf(mantissa, sizeof(mantissa), "%.*f", dec, mant); - /* Check for rounding overflow (e.g., 9.95 -> 10.0 with 1 dec) */ - if (mantissa[0] != '0' && (mantissa[0] - '0') >= 10) { + /* Check for rounding/precision overflow (e.g., 9.95 -> 10.0, + or log10 imprecision giving mant >= 10) */ + if (mant >= 9.9995 || (mantissa[0] == '1' && mantissa[1] == '0')) { exp_val++; mant = absval / pow(10, exp_val); snprintf(mantissa, sizeof(mantissa), "%.*f", dec, mant); @@ -227,11 +228,13 @@ static void format_number(FILE *fp, const char *fmt, int fmtlen, double val) /* Calculate total field width for integer part */ int int_field = total_digits; int total_field = int_field + (has_decimal ? 1 + dec : 0); + /* Commas in the actual formatted number expand the field */ + int actual_commas = (int)strlen(int_with_commas) - (int)strlen(intpart); /* Pad the number to fill the field */ char result[80]; int numlen = strlen(numstr); - int padding = total_field - numlen; + int padding = total_field + actual_commas - numlen; if (padding < 0) padding = 0; int ri = 0; @@ -247,7 +250,7 @@ static void format_number(FILE *fp, const char *fmt, int fmtlen, double val) char fillch = asterisk_fill ? '*' : ' '; bool dollar_placed = false; - if (numlen > total_field) { + if (numlen > total_field + actual_commas) { /* Overflow: print % followed by the number */ pu_putch(fp, '%'); if (!sign_placed && !trailing_plus && !trailing_minus) { @@ -263,9 +266,6 @@ static void format_number(FILE *fp, const char *fmt, int fmtlen, double val) for (int j = 0; j < padding; j++) { if (dollar && !dollar_placed && j == padding - 1) { - if (!sign_placed && negative && !trailing_plus && !trailing_minus) { - result[ri++] = fillch == '*' ? '*' : ' '; - } result[ri++] = '$'; dollar_placed = true; } else { diff --git a/src/virmem.c b/src/virmem.c new file mode 100644 index 0000000..53b4bf7 --- /dev/null +++ b/src/virmem.c @@ -0,0 +1,132 @@ +#include "virmem.h" +#include "tui.h" +#include "graphics.h" +#include + +/* + * BIOS data area emulation (segment 0x0040). + * + * Real addresses we care about: + * 0040:0017 Keyboard shift flags byte 1 + * 0040:0018 Keyboard shift flags byte 2 + * 0040:0049 Current video mode + * 0040:004A Number of screen columns (word) + * 0040:0050 Cursor position page 0 (col, row) + * 0040:006C Timer tick count (dword, ~18.2 Hz) + * 0040:0084 Rows on screen minus 1 + */ +static uint8_t bios_peek(uint16_t offset) +{ + switch (offset) { + case 0x17: + return 0; /* no shift keys pressed */ + case 0x18: + return 0; + + case 0x49: + /* Current video mode: 3 = 80x25 color text, 1 = 320x200, 2 = 640x200 */ + return gfx_active() ? (uint8_t)gfx_get_mode() : 3; + + case 0x4A: + return tui.active ? (uint8_t)tui.cols : 80; + case 0x4B: + return 0; /* high byte of column count */ + + case 0x50: + return tui.active ? (uint8_t)tui.cursor_col : 0; + case 0x51: + return tui.active ? (uint8_t)tui.cursor_row : 0; + + case 0x6C: case 0x6D: case 0x6E: case 0x6F: { + /* Timer ticks since midnight (~18.2065 Hz) */ + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + struct tm tm; + localtime_r(&ts.tv_sec, &tm); + long secs = tm.tm_hour * 3600L + tm.tm_min * 60L + tm.tm_sec; + uint32_t ticks = (uint32_t)(secs * 18.2065 + ts.tv_nsec / 54925400.0); + return (ticks >> ((offset - 0x6C) * 8)) & 0xFF; + } + + case 0x84: + return tui.active ? (uint8_t)(tui.rows - 1) : 24; + + default: + return 0; + } +} + +static void bios_poke(uint16_t offset, uint8_t value) +{ + (void)offset; + (void)value; + /* BIOS data area writes are silently ignored */ +} + +/* + * CGA text video buffer emulation (segment 0xB800). + * + * Layout: 80x25 = 4000 bytes, alternating character and attribute bytes. + * offset = (row * 80 + col) * 2 → character + * offset = (row * 80 + col) * 2 + 1 → attribute (fg | bg<<4) + */ +static uint8_t cga_text_peek(uint16_t offset) +{ + if (!tui.active || !tui.screen) return 0; + + int cell = offset / 2; + int cols = tui.cols; + int row = cell / cols; + int col = cell % cols; + + if (row >= tui.rows) return 0; + + if (offset & 1) + return TUI_CELL(row, col).attr; + else + return TUI_CELL(row, col).ch; +} + +static void cga_text_poke(uint16_t offset, uint8_t value) +{ + if (!tui.active || !tui.screen) return; + + int cell = offset / 2; + int cols = tui.cols; + int row = cell / cols; + int col = cell % cols; + + if (row >= tui.rows) return; + + if (offset & 1) + TUI_CELL(row, col).attr = value; + else + TUI_CELL(row, col).ch = value; +} + +uint8_t virmem_peek(uint16_t segment, uint16_t offset) +{ + if (segment == 0x0040) + return bios_peek(offset); + + if (segment == 0xB800 && !gfx_active()) + return cga_text_peek(offset); + + /* Unhandled regions return 0 */ + return 0; +} + +void virmem_poke(uint16_t segment, uint16_t offset, uint8_t value) +{ + if (segment == 0x0040) { + bios_poke(offset, value); + return; + } + + if (segment == 0xB800 && !gfx_active()) { + cga_text_poke(offset, value); + return; + } + + /* Unhandled regions: silently discard */ +} diff --git a/tests/expected/get_put.expected b/tests/expected/get_put.expected new file mode 100644 index 0000000..37c971e --- /dev/null +++ b/tests/expected/get_put.expected @@ -0,0 +1,7 @@ +Pq#0;2;0;0;0#1;2;0;0;66#0!320~-#0!10~!11N!299~$#1!10?!11o!299?-#0!10~!11?!299~$#1!10?!11~!299?-#0!10~!11w!299~$#1!10?!11F!299?-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320B-\WIDTH BITS= 22 +HEIGHT= 11 +Pq#0;2;0;0;0#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320B-\XOR PUT OK +XOR ERASE OK +PSET PUT OK +Pq#0;2;0;0;0#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320~-#0!320B-\OR PUT OK +DONE diff --git a/tests/expected/peek_poke.expected b/tests/expected/peek_poke.expected new file mode 100644 index 0000000..d1ada2f --- /dev/null +++ b/tests/expected/peek_poke.expected @@ -0,0 +1,9 @@ +PEEK(0)= 0 +VIDEO MODE= 3 +COLUMNS= 80 +TIMER TICKS OK +B800 PEEK= 0 +RESET PEEK= 0 +SEG 0 PEEK= 0 +SEG FFFF PEEK= 0 +DONE diff --git a/tests/expected/print_using_edge.expected b/tests/expected/print_using_edge.expected new file mode 100644 index 0000000..464fc8d --- /dev/null +++ b/tests/expected/print_using_edge.expected @@ -0,0 +1,23 @@ +****1.50 +-***1.50 +12345.67 +***$1.50 +-**$1.50 + $1.50 + -$1.50 + 1,234.56 +123,456.78 + $1,234.56 + 1.23E+03 + 1.00E-04 +-1.00E-04 + 0.00E+00 +123.45 +123.45- +123.45+ +123.45- ++123.45 +-123.45 +%123.45 + 0.00 +DONE diff --git a/tests/programs/get_put.bas b/tests/programs/get_put.bas new file mode 100644 index 0000000..66c3f15 --- /dev/null +++ b/tests/programs/get_put.bas @@ -0,0 +1,27 @@ +10 REM Test GET/PUT graphics sprites +20 SCREEN 1 +30 DIM SP%(50) +40 REM Draw a small box +50 LINE (10,10)-(20,20), 1, BF +60 REM Capture it +70 GET (10,10)-(20,20), SP% +80 REM Check header (width in bits, height) +90 PRINT "WIDTH BITS="; SP%(0) +100 PRINT "HEIGHT="; SP%(1) +110 REM Clear screen and PUT it elsewhere with XOR +120 CLS +130 PUT (50,50), SP%, XOR +140 REM Check a pixel was placed +150 IF POINT(55,55) > 0 THEN PRINT "XOR PUT OK" ELSE PRINT "XOR PUT FAIL" +160 REM PUT again with XOR to erase +170 PUT (50,50), SP%, XOR +180 IF POINT(55,55) = 0 THEN PRINT "XOR ERASE OK" ELSE PRINT "XOR ERASE FAIL" +190 REM Test PSET mode +200 PUT (50,50), SP%, PSET +210 IF POINT(55,55) > 0 THEN PRINT "PSET PUT OK" ELSE PRINT "PSET PUT FAIL" +220 REM Test OR mode +230 CLS +240 PUT (50,50), SP%, OR +250 IF POINT(55,55) > 0 THEN PRINT "OR PUT OK" ELSE PRINT "OR PUT FAIL" +260 SCREEN 0 +270 PRINT "DONE" diff --git a/tests/programs/peek_poke.bas b/tests/programs/peek_poke.bas new file mode 100644 index 0000000..bfedb77 --- /dev/null +++ b/tests/programs/peek_poke.bas @@ -0,0 +1,27 @@ +10 REM Test DEF SEG / PEEK / POKE +20 REM --- Default segment: PEEK returns 0 --- +30 PRINT "PEEK(0)="; PEEK(0) +40 REM --- BIOS data area: video mode --- +50 DEF SEG = &H40 +60 PRINT "VIDEO MODE="; PEEK(&H49) +70 REM --- BIOS data area: columns --- +80 PRINT "COLUMNS="; PEEK(&H4A) +90 REM --- BIOS data area: timer ticks (low byte, just check it runs) --- +100 T! = PEEK(&H6C) +110 IF T! >= 0 THEN PRINT "TIMER TICKS OK" ELSE PRINT "TIMER TICKS FAIL" +120 REM --- CGA text buffer: POKE a character (no TUI in piped mode) --- +130 DEF SEG = &HB800 +140 POKE 0, 65 +150 REM In piped mode TUI is not active, so PEEK returns 0 +160 P = PEEK(0) +170 PRINT "B800 PEEK="; P +180 REM --- DEF SEG reset --- +190 DEF SEG +200 PRINT "RESET PEEK="; PEEK(0) +210 REM --- Boundary values --- +220 DEF SEG = 0 +230 PRINT "SEG 0 PEEK="; PEEK(0) +240 DEF SEG = 65535 +250 PRINT "SEG FFFF PEEK="; PEEK(0) +260 REM --- Done --- +270 PRINT "DONE" diff --git a/tests/programs/print_using_edge.bas b/tests/programs/print_using_edge.bas new file mode 100644 index 0000000..c925167 --- /dev/null +++ b/tests/programs/print_using_edge.bas @@ -0,0 +1,32 @@ +10 REM PRINT USING edge cases +20 REM --- Asterisk fill --- +30 PRINT USING "**###.##"; 1.5 +40 PRINT USING "**###.##"; -1.5 +50 PRINT USING "**###.##"; 12345.67 +60 REM --- Asterisk fill + dollar --- +70 PRINT USING "**$##.##"; 1.5 +80 PRINT USING "**$##.##"; -1.5 +90 REM --- Dollar sign --- +100 PRINT USING "$$###.##"; 1.5 +110 PRINT USING "$$###.##"; -1.5 +120 REM --- Thousands separator --- +130 PRINT USING "###,###.##"; 1234.56 +140 PRINT USING "###,###.##"; 123456.78 +150 PRINT USING "$$#,###.##"; 1234.56 +160 REM --- Scientific notation --- +170 PRINT USING "#.##^^^^"; 1234.5 +180 PRINT USING "#.##^^^^"; 0.0001 +190 PRINT USING "#.##^^^^"; -0.0001 +200 PRINT USING "#.##^^^^"; 0 +210 REM --- Trailing signs --- +220 PRINT USING "###.##-"; 123.45 +230 PRINT USING "###.##-"; -123.45 +240 PRINT USING "###.##+"; 123.45 +250 PRINT USING "###.##+"; -123.45 +260 PRINT USING "+###.##"; 123.45 +270 PRINT USING "+###.##"; -123.45 +280 REM --- Overflow --- +290 PRINT USING "##.##"; 123.45 +300 REM --- Zero --- +310 PRINT USING "###.##"; 0 +320 PRINT "DONE"