Conversation
RND was useless for the thing it exists for. The generator's state was the constant 0x12345678DEADBEEF with no way to change it, and _rt_rnd's own comment admitted it ignored its argument, so every run of every program produced the same numbers -- three runs of a three-dice program printed 939913 each time. Every game shuffled the same deck and generated the same maze on every play, and no statement existed to say otherwise. RANDOMIZE takes an expression, so `RANDOMIZE TIMER` needs no special case -- TIMER is an ordinary builtin, and it has microsecond resolution, so two runs a moment apart get different seeds. Bare RANDOMIZE takes the clock; GW-BASIC asks the operator, and a compiled program has nobody to ask. A named seed replays exactly, which is what makes a program debuggable. RND's argument now means what GW-BASIC says: negative reseeds, zero returns the previous number again, positive advances. `X = RND(-1)` at the top of a listing is a real idiom and did nothing at all before. Two details. The seed goes through a splitmix64 avalanche rather than into the state directly, because BASIC seeds are small integers and xorshift64 started from a small state produces visibly poor first values; zero is replaced, since it is xorshift's fixed point and would return 0 forever. And a bare `RND` used to leave whatever happened to be in xmm0 as the argument -- harmless only while the argument was ignored, decisive now -- so it passes an explicit 1.0. LANGREF documented RND(0) as "same as RND" and RND(-1) as "reseed with system time". Both were wrong descriptions of code that did neither; corrected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`DEFINT A-Z` is the first line of a great many listings. Without it every unsuffixed name is a Double, which changes the arithmetic and what PRINT shows, so a listing that opens this way did not compile at all. The implementation deliberately avoids the obvious approach. Nineteen places answer "what type is this name" -- `DataType::from_suffix` in thirteen of them and `is_string_var` in six more -- and teaching each to consult a table is how two oracles come to disagree. That is exactly the bug fixed earlier this session, where sema decided string-ness from the `$` suffix alone and so rejected `LEN(S)` on a `DIM S AS STRING * 4`; `DEFSTR` would have recreated it precisely. So sema rewrites the names instead: with `DEFINT A-Z` in force, `X` becomes `X%` before anything looks at it, and all nineteen oracles then answer correctly without being touched. It reuses the AST-rewrite machinery already there for FnCall-to-ArrayAccess, and `for_each_expr_mut` is wildcard-free, so a statement kind cannot be silently skipped. Merging is the correct reading rather than a side effect: in GW-BASIC `DEFINT A` makes `A` and `A%` the same variable, and rewriting the first to the second is what makes them share storage here. Builtins and procedure names are excluded -- `DEFSTR A-Z` must not turn `LEN` into `LEN$`, and a procedure's name has to read the same at its definition and at every call. Two divergences, both documented: the default applies to the whole program rather than from the statement onwards, which is where these are always written; and assignment to an integer still truncates rather than rounds, which LANGREF already recorded and which the new tests state explicitly rather than assuming GW-BASIC's rounding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Text-mode listings position and colour constantly, and none of it was available. All three are ANSI escape sequences, which is the model CLS has used on both platforms since it was written -- so this adds helpers beside it rather than a console subsystem. Colours are GW-BASIC's 0-15 mapped onto ANSI's two ranges, 30-37 and 90-97 for the foreground and 40-47 and 100-107 for the background, since 8-15 is the bright half. GW-BASIC's third COLOR argument sets the border, which a terminal has no equivalent for, and is refused rather than quietly dropped. Two latent bugs fixed on the way in. CLS never reset the column tracker. After clearing, the cursor is at column 1 but the tracker still believed wherever it had been, so a following TAB emitted a newline to reach a column it had already passed -- reproduced with a 40-column line, a CLS, and TAB(5). LOCATE sets the tracker for the same reason, which is also what makes POS agree with it. The Windows runtime wrote ANSI escapes with no SetConsoleMode call, so CLS had always assumed VT processing was already enabled; where it is not, it printed "^[[2J^[[H" and cleared nothing. _rt_platform_init now turns it on, treating failure as "not a console" -- output redirected to a file, where escapes are just bytes, as with any terminal program. POS needed a Long return type as well as a helper: without it the value came back in eax while PRINT read xmm0, printing 1.09e-315. That is the same register-versus-type mismatch as the bitwise operators, and worth naming because the type list is easy to forget. examples/guess.bas is a listing in the period style -- DEFINT A-Z, RANDOMIZE TIMER, CLS, COLOR, LOCATE, GOTO by line number -- so the batch is exercised end to end through the real assembler and linker. It binary-searches its own secret number, so it needs no input. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
EQV and IMP were the pointed ones: neither was a keyword, so `PRINT 1 EQV 1` printed 101 -- the items 1, an undefined variable named EQV, and 1 -- where GW-BASIC gives -1. A wrong answer with no diagnostic. Both are bitwise and are built from instructions already present: EQV is NOT (a XOR b), IMP is (NOT a) OR b. They sit below OR and XOR with IMP below EQV, which needed the precedence levels renumbered to make room underneath. ERASE is recognised only before a name, like CALL, rather than reserved. That is not fastidiousness: LANGREF's own `ON Choice GOSUB Draw, Erase` example uses the word as a label, and reserving it broke that example -- the doc tests caught it, since every fenced block in LANGREF is compiled. It frees the storage and nulls the descriptor, so the array reads as undimensioned again and a later DIM allocates afresh; sema permits the redeclaration for any array the program erases somewhere. DATE$ and TIME$ go through strftime in GW-BASIC's shapes, MM-DD-YYYY and HH:MM:SS. Assigning to them -- which in GW-BASIC sets the system clock -- is still refused, now because they are functions rather than because they are unimplemented, and the test says so. FRE answers a plausible constant rather than pretending to run out: a compiled program has no BASIC string heap, and listings use it as `IF FRE(0) < n THEN`, which then passes. BEEP writes BEL. SYSTEM ends the program, which is what END already compiles to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
NONGOALS.md was generated rather than authored and committed by accident, but deleting the file alone would leave the judgment behind: the same "never" is hard-coded in sema's UNSUPPORTED table, which told users that sound "is not supported" and that graphics, the line printer and soft function keys were settled matters. Only some of them are. Split the table's messages in two. Graphics, the 8086's memory map and I/O ports, and the interpreter's own commands stay refused, because they describe a machine and a way of working a compiled 64-bit program does not have -- and a POKE that appeared to work would be the worst outcome available. Everything else -- SOUND, PLAY, LPRINT, LPOS, KEY -- now says "not implemented yet", which is what it always meant. NAME was documented in LANGREF as unimplemented but was absent from the table, so it fell through to "unknown subroutine": the generic message the table exists to replace, and indistinguishable from a misspelling. Add it, and pin the whole distinction with tests, since a message is exactly the kind of thing that rots unnoticed. NAME "a" AS "b" still stops at the AS with a parse error rather than the table's message -- the refusal is reached only for names whose GW-BASIC syntax this parser happens to accept. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The other half of 4dab9ef, which committed the file deletion alone: `git add` aborted on a stale pathspec and took the rest of the change with it. Its message describes this diff. Split UNSUPPORTED's messages in two. Graphics, the 8086's memory map and I/O ports, and the interpreter's own commands keep a firm refusal -- they describe a machine a compiled 64-bit program does not have, and a POKE that appeared to work would be the worst outcome available. SOUND, PLAY, LPRINT, LPOS and KEY now say "not implemented yet", which is what they always meant. Add NAME, documented in LANGREF as unimplemented but absent from the table, so it fell through to "unknown subroutine" -- the generic message the table exists to replace, and indistinguishable from a misspelling. Two tests pin the distinction, since a diagnostic is exactly the kind of thing that rots unnoticed. NAME "a" AS "b" still stops at the AS with a parse error rather than the table's message: the refusal is reached only for names whose GW-BASIC syntax this parser happens to accept. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR expands xbasic64’s GW-BASIC/QuickBASIC compatibility by implementing several previously-refused language features (default typing via DEF*, additional logical operators, console control, time/date, RNG reseeding, and array ERASE), along with extensive integration tests and updated documentation.
Changes:
- Add language support for
DEFINT/DEFLNG/DEFSNG/DEFDBL/DEFSTR,EQV,IMP,BEEP,SYSTEM,ERASE,LOCATE,COLOR,RANDOMIZE, plus built-insPOS,FRE,DATE$,TIME$. - Update semantic analysis, code generation, and both runtimes (SysV + Win64) to implement these features (including Windows VT processing for ANSI escapes).
- Add/adjust integration tests and update LANGREF/docs and examples to cover the new behavior and revised “out of scope” messaging.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/types/mod.rs | Adds integration tests for DEF* default typing behavior and precedence rules vs suffixes. |
| tests/print/mod.rs | Adds integration tests for LOCATE, COLOR, POS(0), and CLS column tracking behavior. |
| tests/math/mod.rs | Adds integration tests for RANDOMIZE, RND argument semantics, DATE$, TIME$, and FRE. |
| tests/errors/mod.rs | Updates “unsupported/unimplemented” assertions and adds coverage ensuring documented unimplemented names are in the refusal table. |
| tests/control/mod.rs | Adds integration tests for SYSTEM, BEEP, and ERASE allowing a second DIM. |
| tests/arithmetic/mod.rs | Adds integration tests for EQV/IMP correctness and precedence. |
| src/sema.rs | Implements DEF-type rewriting, tracks ERASE to allow subsequent DIM, and extends operator naming. |
| src/runtime/win64-native/print.s | Enables Windows console VT processing so ANSI escapes used by CLS/LOCATE/COLOR are interpreted. |
| src/runtime/win64-native/math.s | Implements RNG reseeding/RND(0) behavior, plus BEEP, DATE$, TIME$, POS, LOCATE, COLOR, and CLS column reset on Win64. |
| src/runtime/win64-native/data_defs.s | Adds _console_mode storage for GetConsoleMode/SetConsoleMode. |
| src/runtime/sysv/math.s | Implements RNG reseeding/RND(0) behavior, plus BEEP, DATE$, TIME$, POS, LOCATE, COLOR, and CLS column reset on SysV. |
| src/runtime/sysv/data_defs.s | Adds _rng_last and new format strings for cursor/color and date/time formatting. |
| src/parser.rs | Parses new statements (DEF*, BEEP, SYSTEM, ERASE, LOCATE, COLOR, RANDOMIZE) and adds EQV/IMP precedence + AST variants. |
| src/lexer.rs | Adds tokens for new keywords/operators and introduces DataTypeWord for DEF*. |
| src/codegen.rs | Generates code for new statements/built-ins and implements EQV/IMP bitwise semantics. |
| README.md | Removes the link to the removed Non-Goals document. |
| NONGOALS.md | Deletes the Non-Goals document (content moved/condensed into LANGREF). |
| LANGREF.md | Documents new features and updates the “not implemented yet” vs “out of scope” categorization. |
| examples/guess.bas | Adds a period-style example program using new features (DEFINT, RANDOMIZE, CLS, COLOR, LOCATE). |
| CLAUDE.md | Removes the reference to NONGOALS.md while keeping the “refused names” guidance. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
`.equ _cls_seq_len, . - _cls_seq` measures from the label to wherever the directive sits, and LOCATE, COLOR, DATE$ and TIME$ each added a format string between the two. The length went from 7 to 75, so CLS on Windows wrote its escape sequence followed by "\033[%d;%dH", "%m-%d-%Y", "%H:%M:%S", "\033[%d;%dm" and 32 bytes of scratch buffer. The file's own comment warns against hand-counting a length; this is the other way to get one wrong. Moving the directive back where it belongs is the fix. The test that demanded `. - label` now also demands that it sit directly after the data, since the adjacency is the whole reason the idiom is safe. guess.bas is the only example that calls CLS, which is why Windows CI had never run this code before. Verified by running the compiler's actual Win64 output on Linux: forcing abi.rs and runtime.rs to the Windows tree produces the same assembly CI compiles, and ms_abi stand-ins for the Win32 and CRT calls let it run. CLS now emits 7 bytes, and all fourteen examples complete. That harness does not reproduce the CI failure -- guess.bas runs to ExitProcess(0) under it -- so the examples step now reports the exit code itself. 1 is a diagnosed abort; 0xC0000005 and its neighbours are not, and each names a different defect. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The exit code the last change asked for came back -1073741819, which is 0xC0000005: guess.bas dies of an access violation, not of anything the compiler diagnosed. (The step's own hex conversion threw on the negative value, which is fixed here -- -band on two int32s stays int32.) An example is a whole program, so that only narrows the fault to a program using six statements no earlier example touches. The new step runs each of them alone and reports every exit code before failing, so one run names the statement. These probes are also the only coverage CLS, LOCATE and COLOR have anywhere: the test suite runs on Linux, and those are the helpers whose two implementations differ most. The hunt found one real ABI violation, though not that one. `rdi` is callee-saved on Win64 and scratch on System V, and `_rt_print_using_str` used it as a copy pointer -- wrong in a way no Linux run can show, and surviving only because the code that calls it happens to keep nothing there. A test now checks both trees against their own callee-saved lists; it catches the rdi clobber when reintroduced, and `_rt_random_prepare` is listed as the one helper with a private convention its own comment states. Not the crash: a harness that runs the compiler's actual Win64 output on Linux -- both platform switches forced, ms_abi stand-ins for the Win32 and CRT calls, and every stub asserting rsp was 16-byte aligned at the call -- now runs all fourteen examples clean, guess.bas included. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rase Both reported by Copilot, both real, and both reproduced before being fixed. DEFINT A-Z renames every unsuffixed name the range covers, so DIM A(3) declares A%. ERASE carries its names outside an expression, and the arm that renames such names had a wildcard, so ERASE went on naming A. Nothing matched: the erase quietly did nothing and the second DIM was then rejected as redeclaring an array the program had just asked to be rid of. The wildcard is the actual defect, so that match is now exhaustive, like for_each_expr_mut and gen_stmt. A statement kind that carries a name and is missing from it keeps the name the program wrote while every other mention is renamed, which is a silent wrong answer rather than a compile error. Now it is a compile error. Separately, codegen skips an ERASE name it cannot resolve to an array on the grounds that sema has already complained -- and sema had no Erase arm at all, so `ERASE TOTLA` for `ERASE TOTAL`, or ERASE of a scalar, compiled clean and erased nothing. Sema now looks the name up the way every array use is looked up, the enclosing procedure first and then the module, and says so when it finds nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The probe step reported nothing and took no time, which says the first probe crashed and the step then died formatting its exit code. Two bugs, and the formatting one is why the table nobody got would have named the statement: in PowerShell 0xFFFFFFFF is Int32 -1, so the -band was a no-op, and [uint32] threw on the very value the cast existed to display. `-f` with X8 renders a negative Int32 as two's-complement hex by itself. The first probe is CLS, and CLS is the one console statement whose exit code no test checks. That is not a coincidence. compile_and_run_raw returns Ok whatever the program did, so a crash arrives as truncated output -- and test_cls_resets_the_column asserts that a newline does *not* appear after the escape, which a program that died writing the escape satisfies perfectly. It has been green on Windows throughout. RunOutput::assert_ran_to_completion fixes the shape: it fails on any nonzero exit and prints the code in hex, since Windows says what went wrong in it. Every test that reads stdout for what a console statement wrote now calls it, and a new test runs all sixteen console programs and asks only whether they survived -- which a crash cannot fake. Verified by pointing one entry at `PRINT 1/0`, which fails it. The probes now split CLS by what its program links: alone it pulls in almost nothing, while the same statement after a PRINT pulls in the whole file half of the runtime. If one crashes and the other does not, the fault is in what the linker kept rather than in the instructions, which is the question a harness running the real Win64 output on Linux cannot answer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The probe table named it: CLS is the only statement that crashes, with or without a preceding PRINT, so it was never about what the linker kept. `mov r8, _cls_seq_len`. error.s has said what is wrong with that line since it was written -- "GAS cannot yet know the symbol is absolute and assembles a memory load from that address instead of an immediate" -- it just said it about forward references, and measured its own messages at run time to avoid them. `_cls_seq_len` is `.equ len, . - label`, and where an assembler does not fold that to a constant, `mov r8, _cls_seq_len` loads eight bytes from address 7. That is the access violation, on every CLS, in every program. Why nothing here could see it: main.rs assembles with GNU `as` on Linux and `clang -c` on Windows. Two assemblers, not just two runtimes -- and GNU `as` folds the expression, so the Linux build runs the immediate the source appears to say. Every check in this repo, and the harness that ran the real Win64 output under ms_abi stubs, went through GNU `as` and agreed with each other about a line the Windows build never assembled that way. CLAUDE.md now says so. The two lengths written that way, `_cls_seq_len` and `_redo_msg_len`, are now bracketed by `_end` labels and subtracted at run time, which is what error.s concluded and costs two instructions. The old test demanded `. - label` as the alternative to hand-counting; it now forbids naming a label in a length constant at all, and a second test keeps an `_end` label adjacent to its data so the 75-byte CLS cannot come back. Both fail on the shapes they replace. `_redo_msg_len` is the same defect on the "?Redo from start" path, which no test or example reaches -- it would have crashed the first Windows user who typed a letter at an INPUT prompt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.