Skip to content

lang support Updates - #13

Merged
jgarzik merged 11 commits into
masterfrom
updates
Aug 17, 2026
Merged

lang support Updates#13
jgarzik merged 11 commits into
masterfrom
updates

Conversation

@jgarzik

@jgarzik jgarzik commented Aug 17, 2026

Copy link
Copy Markdown
Owner

No description provided.

jgarzik and others added 6 commits August 16, 2026 23:49
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>
@jgarzik
jgarzik requested a lite review from Copilot August 17, 2026 00:15
@jgarzik jgarzik self-assigned this Aug 17, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-ins POS, 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.

Comment thread src/sema.rs Outdated
Comment thread src/codegen.rs
jgarzik and others added 5 commits August 17, 2026 00:44
`.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>
@jgarzik
jgarzik merged commit a9108de into master Aug 17, 2026
4 checks passed
@jgarzik
jgarzik deleted the updates branch August 17, 2026 03:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants