7 Commits
23 ... 16

Author SHA1 Message Date
699ac9ab93 Add native RND and ASC functions
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 40s
akbasic CI Build / sanitizers (push) Failing after 45s
akbasic CI Build / coverage (push) Failing after 48s
akbasic CI Build / akgl_build (push) Failing after 49s
akbasic CI Build / mutation_test (push) Failing after 45s
Implement bounded random integers with lazy clock seeding and add ASC as the inverse of CHR. Cover dispatch, validation, deterministic LCG output, UTF-8 round trips, function reference, and the breakout tutorial.

Closes #16.

Co-authored-by: andrew <andrew@aklabs.net>
2026-08-05 06:43:17 -04:00
fac84acdaa Merge pull request 'Port onto libakstdlib 2b79aca and convert the eight bool predicates (libakstdlib#26)' (#31) from libakstdlib-26 into main
All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m39s
akbasic CI Build / sanitizers (push) Successful in 5m1s
akbasic CI Build / coverage (push) Successful in 4m47s
akbasic CI Build / akgl_build (push) Successful in 8m7s
akbasic CI Build / mutation_test (push) Successful in 29m11s
Reviewed-on: #31
Reviewed-by: andrew <andrew@aklabs.net>
2026-08-03 21:25:07 -04:00
0d6a817b7c Bound CI build parallelism so five concurrent jobs stop starving the runner
All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m47s
akbasic CI Build / coverage (push) Successful in 4m4s
akbasic CI Build / sanitizers (push) Successful in 5m1s
akbasic CI Build / akgl_build (push) Successful in 9m29s
akbasic CI Build / mutation_test (push) Successful in 22m11s
Run #31's sanitizers job failed, and not on anything in src/. The ASan build
reached 100% and the runner's Docker daemon went away before ctest emitted a
single line: "Cannot connect to the Docker daemon at unix:///var/run/docker.sock".

The tree under test is identical to run #30's -- 9e5496f touched only this
file -- and run #30's sanitizers job passed 112/112 in 83 s. What changed is
that #30 was still losing akgl_build and coverage early, so #31 was the first
push on which all five jobs did real work at the same time. All five say
runs-on: ubuntu-latest, so all five share one runner, and every build step
asked for a bare --parallel, i.e. nproc compilers each.

The timestamps are unambiguous. Through 20:36:16 the sanitizers job links an
executable in well under a second. akgl_build starts at 20:36:02 and begins
compiling SDL, SDL_image, SDL_mixer, SDL_ttf, libccd and clay; mutation_test
starts at 20:36:41. From 20:36:16 onward a single `Linking C executable` step
takes one to two minutes -- a sixtyfold regression with no source change --
and at 20:49:55 the daemon is gone. ASan links are memory-hungry and the SDL
tree is large; the runner ran out.

The only lever on that is how many compiler and linker processes exist at
once, so bound each build to 2. Worst case is now 8 rather than 5 x nproc.
mutation_test drives its own builds through the harness and is left alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 21:23:32 -04:00
78b7b55103 Clone libakgl's other two submodules, and pin upload-artifact to v3
Two more gaps that only became visible once the errno fix let the jobs
run far enough to hit them.

akgl_build cloned six of libakgl's submodules and needs eight. libakgl
does not add_subdirectory deps/libccd or deps/clay -- their own
CMakeLists are unusable as subprojects -- it compiles them into itself,
so nothing declares them and configuration dies at
deps/libakgl/CMakeLists.txt:282 with "File
deps/libccd/src/ccd/config.h.cmake.in does not exist". deps/tg stays out:
nothing references it.

The X11 packages from the previous commit did their job -- SDL now
configures the lot: "X11 libraries: xcursor xdbe xfixes xinput2 xrandr
xscrnsaver xshape xsync xtest".

coverage passes its gate now (112/112, lines 94.1% against a 90 floor)
and failed on the upload instead. actions/upload-artifact@v4 bundles
@actions/artifact v2, whose isGhes() treats any GITHUB_SERVER_URL that is
not github.com as GitHub Enterprise Server and refuses outright. @v3 uses
the older artifact API, which Gitea 1.25.3 does implement. The step had
never had a file to upload before -- gcovr was never reached, so it
warned "No files were found" and passed -- which is why this surfaced
only now. Same class of accommodation as annotate_only on the junit
reporter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 21:23:32 -04:00
c8d90c2743 Install the build dependencies CI has been missing since run #2
Every job in ci.yaml has failed on every run since #2, on main and on
every branch, for two reasons that have nothing to do with any of the
code under test.

moreutils, for errno(1). deps/libakerror/scripts/generrno.sh shells out
to it to generate its errno name table and to stamp
AKERR_LAST_ERRNO_VALUE into the generated akerror.h. The runner does not
have it, and its absence does not fail the build -- which is what made
this expensive to find. The script prints "errno: command not found",
emits an empty akerr_init_errno() so no errno ever gets a name, and
substitutes an empty AKERR_LAST_ERRNO_VALUE, so every code in
libakerror's reserved band collapses from (134 + n) to ( + n), i.e. to n.
AKERR_OUTOFBOUNDS is (AKERR_LAST_ERRNO_VALUE + 2) and therefore becomes
literally 2, so ERR(2) answers "Out Of Bounds Error" where ENOENT's "No
such file or directory" belongs. That is the docs_examples failure at
docs/15-error-codes.md:105, and it took cmake_build, sanitizers and
coverage down with it; mutation_test then aborted on "baseline is not
green" because the same suite is its baseline.

Reproduced locally by shimming errno(1) out of PATH: identical failure,
same test #47, same two lines of diff. With errno present the suite is
112/112.

The nine X11 dev packages, for akgl_build, which has never once been
green. SDL_X11 and its sub-options default ON on Linux, and CheckX11()
calls SDL_missing_dependency() -- a hard CMake error, not a downgrade --
for any whose header is absent. The runner has libx11-dev but none of
the extension packages, so configuration stopped at the first of them:
"Couldn't find dependency package for XCURSOR". The list is SDL's own,
from deps/SDL/docs/README-linux.md, reduced to the X11 entries; the
audio, Wayland, KMSDRM and Vulkan backends all degrade to "not found"
and skip themselves rather than erroring, so they cost nothing to leave
out. Turning the missing options OFF would also configure and is the
wrong answer -- the point of that job is that an AKGL build is a real
SDL program. The tests still run headless under SDL_VIDEODRIVER=dummy.

release.yaml's full_mutation job builds the same tree and gets moreutils
for the same reason.

No source file changes. The port itself was never the problem.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 21:23:32 -04:00
b434be1901 Port onto libakstdlib 2b79aca and convert the eight bool predicates
akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2%
bypassed, against 86.4% on the same tree before this. The submodule bump
669b2b3 -> 2b79aca needed no source change of its own: the release is
drop-in for what akbasic already used.

Seven of the eight sites the earlier port left on raw libc change their own
signature rather than swallowing an error, per andrew's ruling on
libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end,
peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's
scroll/newline/putchar_at/echo_line/edit_key chain all return an
akerr_ErrorContext * and hand the answer back through an out parameter.
is_waiting_for and is_waiting_for_any are a public header change; every
call site that used one as a term in a condition hoists it into a
statement first.

verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the
comparator's signature, so there is no out parameter to report through --
which is what libakstdlib#38 concluded. It carries a comment saying so and
saying why the bypass is safe there.

Six snprintf sites stay raw because they want truncation as an answer
rather than an error, and aksl_snprintf cannot express that until
libakstdlib#34 hands the required length back. Each of the six says so at
the site. Two of them, in host.c, are a latent defect rather than a
decision: a host type name over 31 characters truncates silently and two
sharing a prefix then collide, where structtype.c refuses the same case.

DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the
PASS in it returned past CLEANUP, so a scan error left the file open.
Hoisting the loop into its own helper to convert fgets fixes it.

Refs libakstdlib#26, libakstdlib#38

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 21:23:32 -04:00
330d731cfe Merge pull request 'Make GRAPHIC select the text plane (#23)' (#29) from 23 into main
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m42s
akbasic CI Build / coverage (push) Failing after 3m41s
akbasic CI Build / sanitizers (push) Failing after 4m41s
akbasic CI Build / mutation_test (push) Failing after 3m28s
akbasic CI Build / akgl_build (push) Failing after 5m26s
Reviewed-on: #29
2026-08-03 12:04:12 -04:00
49 changed files with 1443 additions and 647 deletions

View File

@@ -26,14 +26,42 @@ jobs:
# skipped for the same reason: our CMakeLists declares akerror::akerror # skipped for the same reason: our CMakeLists declares akerror::akerror
# first and libakstdlib guards on if(NOT TARGET ...). # first and libakstdlib guards on if(NOT TARGET ...).
submodules: true submodules: true
# moreutils is load-bearing, not incidental: it supplies errno(1), which
# deps/libakerror/scripts/generrno.sh shells out to at build time to
# generate its errno name table and to stamp AKERR_LAST_ERRNO_VALUE into
# the generated akerror.h.
#
# Its absence does not fail the build, which is what made this expensive to
# find. The script prints "errno: command not found" twice, emits an empty
# akerr_init_errno() so no errno ever gets a name, and substitutes an empty
# AKERR_LAST_ERRNO_VALUE -- so every code in libakerror's reserved band
# collapses from (134 + n) to ( + n), i.e. to n. AKERR_OUTOFBOUNDS is
# (AKERR_LAST_ERRNO_VALUE + 2), so it becomes literally 2, and ERR(2)
# answers "Out Of Bounds Error" where ENOENT's "No such file or directory"
# belongs. That is the docs_examples failure at docs/15-error-codes.md:105
# that this job carried on every run since #2.
- name: dependencies - name: dependencies
run: | run: |
sudo apt-get update -y sudo apt-get update -y
sudo apt-get install -y cmake gcc sudo apt-get install -y cmake gcc moreutils
# --parallel 2 rather than a bare --parallel, and that bound is measured
# rather than cautious. All five jobs in this file say runs-on:
# ubuntu-latest and land on the same runner, so a bare --parallel asks for
# nproc compilers *per job* and the runner is handed five times that at
# once. Run #30 never showed it because akgl_build and coverage were still
# dying early; run #31 was the first push on which all five did real work
# simultaneously, and the sanitizers job went from a 55-second link phase
# to a 14-minute one -- individual `Linking C executable` steps taking two
# minutes each, starting at the exact second akgl_build began its SDL tree
# -- before the runner's Docker daemon fell over outright and took the job
# with it. That is memory exhaustion, and the only lever on it is the
# number of concurrent compiler and linker processes. Applied at all four
# build sites for the same reason; mutation_test drives its own builds
# through the harness and is not bounded here.
- name: build - name: build
run: | run: |
cmake -S . -B build cmake -S . -B build
cmake --build build --parallel cmake --build build --parallel 2
# The suite is 78 cases: 41 golden files byte-compared against the Go # The suite is 78 cases: 41 golden files byte-compared against the Go
# reference's own corpus (checked in at tests/reference/, see its README), # reference's own corpus (checked in at tests/reference/, see its README),
# 9 local golden cases for verbs the reference never implemented, 25 unit # 9 local golden cases for verbs the reference never implemented, 25 unit
@@ -74,7 +102,7 @@ jobs:
- name: dependencies - name: dependencies
run: | run: |
sudo apt-get update -y sudo apt-get update -y
sudo apt-get install -y cmake gcc sudo apt-get install -y cmake gcc moreutils
# The whole suite under ASan and UBSan, golden files included. This is the # The whole suite under ASan and UBSan, golden files included. This is the
# gate libakstdlib's TODO.md section 1 calls its highest-value missing item # gate libakstdlib's TODO.md section 1 calls its highest-value missing item
# -- worth having here from the start, because this library is all fixed # -- worth having here from the start, because this library is all fixed
@@ -84,7 +112,7 @@ jobs:
cmake -S . -B build-asan \ cmake -S . -B build-asan \
-DAKBASIC_SANITIZE=ON \ -DAKBASIC_SANITIZE=ON \
-DCMAKE_BUILD_TYPE=Debug -DCMAKE_BUILD_TYPE=Debug
cmake --build build-asan --parallel cmake --build build-asan --parallel 2
ctest --test-dir build-asan --output-on-failure ctest --test-dir build-asan --output-on-failure
- run: echo "🍏 This job's status is ${{ job.status }}." - run: echo "🍏 This job's status is ${{ job.status }}."
@@ -98,7 +126,7 @@ jobs:
- name: dependencies - name: dependencies
run: | run: |
sudo apt-get update -y sudo apt-get update -y
sudo apt-get install -y cmake gcc gcovr sudo apt-get install -y cmake gcc gcovr moreutils
# The gate is a ratchet, not a target: src/ sits at 94.6% of lines and # The gate is a ratchet, not a target: src/ sits at 94.6% of lines and
# 98.6% of functions, so 90 fails on a real regression (a test deleted, or # 98.6% of functions, so 90 fails on a real regression (a test deleted, or
# new untested code added) without tripping over rounding. # new untested code added) without tripping over rounding.
@@ -124,7 +152,7 @@ jobs:
cmake -S . -B build-coverage \ cmake -S . -B build-coverage \
-DAKBASIC_COVERAGE=ON \ -DAKBASIC_COVERAGE=ON \
-DCMAKE_BUILD_TYPE=Debug -DCMAKE_BUILD_TYPE=Debug
cmake --build build-coverage --parallel cmake --build build-coverage --parallel 2
ctest --test-dir build-coverage --output-on-failure ctest --test-dir build-coverage --output-on-failure
mkdir -p build-coverage/coverage mkdir -p build-coverage/coverage
gcovr --root . --filter 'src/.*' \ gcovr --root . --filter 'src/.*' \
@@ -134,9 +162,21 @@ jobs:
--fail-under-line 90 --fail-under-line 90
# Publish even when the threshold gate fails, so the uncovered lines are # Publish even when the threshold gate fails, so the uncovered lines are
# visible -- each one is a missing test. # visible -- each one is a missing test.
#
# @v3, not @v4, and that is Gitea rather than preference. @v4 bundles
# @actions/artifact v2, whose isGhes() treats any GITHUB_SERVER_URL that is
# not github.com as GitHub Enterprise Server and refuses outright:
# "GHESNotSupportedError: @actions/artifact v2.0.0+, upload-artifact@v4+
# and download-artifact@v4+ are not currently supported on GHES". @v3 uses
# the older artifact API, which this forge does implement.
#
# This never showed before because the step had nothing to upload: gcovr
# was never reached, so the step warned "No files were found" and passed.
# Fixing the suite is what first gave it a real file to refuse. Same class
# of accommodation as the annotate_only flag on the junit reporter above.
- name: upload coverage reports - name: upload coverage reports
if: always() if: always()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: code-coverage name: code-coverage
path: build-coverage/coverage/ path: build-coverage/coverage/
@@ -161,22 +201,56 @@ jobs:
# embedded it takes a find_package path instead, so our CMakeLists declares # embedded it takes a find_package path instead, so our CMakeLists declares
# those targets first and needs the submodules present. Six of them, none # those targets first and needs the submodules present. Six of them, none
# recursive. Filed upstream as libakgl API-gap item 5. # recursive. Filed upstream as libakgl API-gap item 5.
#
# Eight, not six: deps/libccd and deps/clay were missing and are not
# optional. libakgl does not add_subdirectory either of them -- their own
# CMakeLists are unusable as subprojects -- it compiles them into itself,
# so nothing declares them and configuration dies late with "File
# deps/libccd/src/ccd/config.h.cmake.in does not exist" at
# deps/libakgl/CMakeLists.txt:282. clay is the same shape one step later:
# deps/clay/clay.h is on the include path and installed.
#
# deps/tg is a real submodule of libakgl and is deliberately not here --
# nothing in its CMakeLists references it. libakerror and libakstdlib are
# skipped for the reason the checkout note above gives.
- name: libakgl dependencies - name: libakgl dependencies
run: | run: |
git -C deps/libakgl submodule update --init \ git -C deps/libakgl submodule update --init \
deps/SDL deps/SDL_image deps/SDL_mixer deps/SDL_ttf \ deps/SDL deps/SDL_image deps/SDL_mixer deps/SDL_ttf \
deps/jansson deps/semver deps/jansson deps/semver deps/libccd deps/clay
# libfreetype-dev and libharfbuzz-dev are load-bearing, not incidental. # libfreetype-dev and libharfbuzz-dev are load-bearing, not incidental.
# SDL_ttf prefers the system copies -- it reports "Using system freetype # SDL_ttf prefers the system copies -- it reports "Using system freetype
# library" and links libfreetype.so.6 -- and without them it would reach # library" and links libfreetype.so.6 -- and without them it would reach
# for deps/SDL_ttf/external/freetype, which the checkout above # for deps/SDL_ttf/external/freetype, which the checkout above
# deliberately does not clone. Installing two dev packages is much cheaper # deliberately does not clone. Installing two dev packages is much cheaper
# than cloning freetype and harfbuzz. # than cloning freetype and harfbuzz.
#
# The X11 dev packages are the other half, and they are what this job has
# been dying on since run #2 -- it has never once been green. SDL_X11 and
# its nine sub-options default ON on Linux, and CheckX11() in
# deps/SDL/cmake/sdlchecks.cmake calls SDL_missing_dependency() -- a hard
# CMake error, not a downgrade -- for any one of them whose header is
# absent. The runner image carries libx11-dev (X11/XKBlib.h resolved) but
# none of the extension packages, so configuration stopped at the first of
# the nine: "Couldn't find dependency package for XCURSOR".
#
# Turning the missing ones OFF would also configure, and is the wrong
# answer: the point of this job is that an AKGL build is a real SDL
# program, and a real SDL program on Linux builds the X11 backend. The
# tests still run headless under SDL_VIDEODRIVER=dummy -- see the env block
# below -- so this buys a faithful build, not a display.
#
# The list is SDL's own, from deps/SDL/docs/README-linux.md, reduced to the
# X11 entries: the audio, Wayland, KMSDRM and Vulkan backends all degrade
# to "not found" and skip themselves rather than erroring, so they cost
# nothing to leave out. Nine packages, all in noble.
- name: dependencies - name: dependencies
run: | run: |
sudo apt-get update -y sudo apt-get update -y
sudo apt-get install -y cmake gcc g++ pkg-config \ sudo apt-get install -y cmake gcc g++ pkg-config moreutils \
libfreetype-dev libharfbuzz-dev libfreetype-dev libharfbuzz-dev \
libx11-dev libxext-dev libxcursor-dev libxi-dev libxfixes-dev \
libxrandr-dev libxss-dev libxtst-dev libxkbcommon-dev
# The akgl-backed half: the text sink and the graphics, audio and input # The akgl-backed half: the text sink and the graphics, audio and input
# backends, the standalone SDL frontend, and the two suites that drive # backends, the standalone SDL frontend, and the two suites that drive
# them against a real software renderer and read the pixels back. # them against a real software renderer and read the pixels back.
@@ -194,7 +268,7 @@ jobs:
- name: build with libakgl - name: build with libakgl
run: | run: |
cmake -S . -B build-akgl -DAKBASIC_WITH_AKGL=ON cmake -S . -B build-akgl -DAKBASIC_WITH_AKGL=ON
cmake --build build-akgl --parallel cmake --build build-akgl --parallel 2
# Dummy video and audio drivers, set per test by CMakeLists rather than in # Dummy video and audio drivers, set per test by CMakeLists rather than in
# this job's environment, because an AKGL build of `basic` is now an SDL # this job's environment, because an AKGL build of `basic` is now an SDL
# program and the golden cases run *it*: forty-one real windows is not what # program and the golden cases run *it*: forty-one real windows is not what
@@ -256,7 +330,7 @@ jobs:
- name: dependencies - name: dependencies
run: | run: |
sudo apt-get update -y sudo apt-get update -y
sudo apt-get install -y cmake gcc python3 sudo apt-get install -y cmake gcc python3 moreutils
# Verify the tests actually catch bugs: break the library many ways and # Verify the tests actually catch bugs: break the library many ways and
# confirm the suite fails. This matters more here than in an ordinary C # confirm the suite fails. This matters more here than in an ordinary C
# library, because the akerror control-flow macros expand at their call # library, because the akerror control-flow macros expand at their call

View File

@@ -70,10 +70,14 @@ jobs:
# corpus is part of what kills mutants and it is checked in now, so # corpus is part of what kills mutants and it is checked in now, so
# that is libakerror and libakstdlib and nothing else. # that is libakerror and libakstdlib and nothing else.
submodules: true submodules: true
# moreutils for errno(1), which deps/libakerror's scripts/generrno.sh needs
# to generate its errno table. Without it the generated table is empty and
# every AKERR_* code collapses onto a low integer -- see the long note on
# ci.yaml's cmake_build job for what that breaks and how it presents.
- name: dependencies - name: dependencies
run: | run: |
sudo apt-get update -y sudo apt-get update -y
sudo apt-get install -y cmake gcc python3 sudo apt-get install -y cmake gcc python3 moreutils
# The whole akbasic-owned src/ tree. ci.yaml runs a two-file subset on every # The whole akbasic-owned src/ tree. ci.yaml runs a two-file subset on every
# push; this is the one that actually covers the interpreter. # push; this is the one that actually covers the interpreter.
# #

View File

@@ -9,6 +9,7 @@ so a call with the wrong number is a syntax error rather than a surprise.
| Function | Args | Form | What it gives | | Function | Args | Form | What it gives |
|---|---|---|---| |---|---|---|---|
| `ABS` | 1 | `ABS(n)` | The absolute value of an integer or float. | | `ABS` | 1 | `ABS(n)` | The absolute value of an integer or float. |
| `ASC` | 1 | `ASC(A$)` | The Unicode code point of a string's first character. |
| `ATN` | 1 | `ATN(n)` | Arctangent, in radians. | | `ATN` | 1 | `ATN(n)` | Arctangent, in radians. |
| `BUMP` | 1 | `BUMP(1)` | Which sprites have collided, as a bitmask. **Reading clears it.** | | `BUMP` | 1 | `BUMP(1)` | Which sprites have collided, as a bitmask. **Reading clears it.** |
| `CHR` | 1 | `CHR(n)` | The character for a Unicode code point, as a string. | | `CHR` | 1 | `CHR(n)` | The character for a Unicode code point, as a string. |
@@ -29,6 +30,7 @@ so a call with the wrong number is a syntax error rather than a surprise.
| `RGR` | 1 | `RGR(f)` | The `GRAPHIC` mode (0), the drawing surface's width (1) or height (2) in pixels, or a character cell's width (3) or height (4). | | `RGR` | 1 | `RGR(f)` | The `GRAPHIC` mode (0), the drawing surface's width (1) or height (2) in pixels, or a character cell's width (3) or height (4). |
| `RIGHT` | 2 | `RIGHT(A$, n)` | The rightmost `n` characters. Clamped. | | `RIGHT` | 2 | `RIGHT(A$, n)` | The rightmost `n` characters. Clamped. |
| `RMENU` | 2 | `RMENU(n, f)` | A menu's state: field 0 the highlighted entry, field 1 whether it has been confirmed. **Reading field 1 clears it.** | | `RMENU` | 2 | `RMENU(n, f)` | A menu's state: field 0 the highlighted entry, field 1 whether it has been confirmed. **Reading field 1 clears it.** |
| `RND` | 1 | `RND(n)` | A random integer from 0 up to but not including `n`. |
| `RWINDOW` | 1 | `RWINDOW(f)` | The current text window's rows (0) or columns (1). Field 2 is a C128 screen mode and is refused. | | `RWINDOW` | 1 | `RWINDOW(f)` | The current text window's rows (0) or columns (1). Field 2 is a C128 screen mode and is refused. |
| `RSPCOLOR` | 1 | `RSPCOLOR(n)` | One of `SPRCOLOR`'s two shared registers, 1 or 2. | | `RSPCOLOR` | 1 | `RSPCOLOR(n)` | One of `SPRCOLOR`'s two shared registers, 1 or 2. |
| `RSPHIT` | 2 | `RSPHIT(n, f)` | One of `SPRHIT`'s settings for sprite `n`, in `SPRHIT`'s own argument order: 0 the kind, 1 to 4 the two corners. | | `RSPHIT` | 2 | `RSPHIT(n, f)` | One of `SPRHIT`'s settings for sprite `n`, in `SPRHIT`'s own argument order: 0 the kind, 1 to 4 the two corners. |

View File

@@ -1005,20 +1005,48 @@ IF NUDGE# = 1 THEN GOSUB UNSTICK
LABEL UNSTICK LABEL UNSTICK
NUDGE# = 0 NUDGE# = 0
STALL# = 0 STALL# = 0
RMAX# = 4 BVX# = (RND(4) * 3) - 6
GOSUB RANDOM
BVX# = (RND# * 3) - 6
IF BVX# = 0 THEN BVX# = 3 IF BVX# = 0 THEN BVX# = 3
RETURN RETURN
``` ```
### You have to write your own random numbers ### Random numbers are built in
**There is no `RND` in this dialect**, and no `INT`, `SQR`, `ASC` or `TIMER` either. A There is no `INT`, `SQR` or `TIMER` in this dialect, but
linear congruential generator is nine tokens and does the job. Put the number of possible `RND(n)` returns an integer from zero through `n - 1`. It seeds itself
answers in `RMAX#` and read the result from `RND#`: from the host clock the first time it is called, so a program only needs the bound:
```basic ```basic
I# = 0
FOR I# = 1 TO 5
PRINT "ROLL " + (RND(6) + 1)
NEXT I#
END
```
Use `RND` for the serve, too, so the ball does not always leave in the same direction:
```basic norun
LABEL SERVE
PX# = (SCW# - PW#) / 2
HELD# = 1
BX# = PX# + ((PW# / 2) - 4)
BY# = PY# - 10
BVX# = BSPD#
IF RND(2) = 0 THEN BVX# = 0 - BSPD#
BVY# = 0 - BSPD#
PDEC# = 0
GOSUB SHOWSPR
RETURN
```
<details>
<summary>Historical aside: the LCG this chapter used to teach</summary>
Before `RND` existed, this nine-token linear congruential generator was copied into
every program. It remains a useful from-scratch PRNG example:
```basic norun
SEED# = 12345 SEED# = 12345
RMAX# = 6 RMAX# = 6
RND# = 0 RND# = 0
@@ -1035,43 +1063,11 @@ RND# = MOD((SEED# / 65536), RMAX#)
RETURN RETURN
``` ```
```output The multiplication stays inside a 64-bit integer for any seed below 2147483648. The
ROLL 1 answer is taken from the middle bits because the low bits of a power-of-two modulus
ROLL 5 barely change from one call to the next. This used to be required; it is now built in.
ROLL 2
ROLL 1
ROLL 2
```
The multiplication stays inside a 64-bit integer for any seed below 2147483648, which is </details>
why the modulus is that number. The answer is taken from the middle bits — `SEED# / 65536`
— because the low bits of a power-of-two modulus barely change from one call to the next.
Integer division truncating for free is the `INT` you do not have.
Seed it from the clock at startup. `TI#` is the host's uptime in sixtieths of a second,
which is different every time the game is run:
```basic norun
SEED# = TI#
```
Use `RANDOM` for the serve, too, so the ball does not always leave in the same direction:
```basic norun
LABEL SERVE
PX# = (SCW# - PW#) / 2
HELD# = 1
BX# = PX# + ((PW# / 2) - 4)
BY# = PY# - 10
RMAX# = 2
GOSUB RANDOM
BVX# = BSPD#
IF RND# = 0 THEN BVX# = 0 - BSPD#
BVY# = 0 - BSPD#
PDEC# = 0
GOSUB SHOWSPR
RETURN
```
`HELD#` is the flag Step 6's loop tests: while it is 1 the ball sits on the paddle, and `HELD#` is the flag Step 6's loop tests: while it is 1 the ball sits on the paddle, and
`HOLDBAL` keeps it there: `HOLDBAL` keeps it there:
@@ -1422,9 +1418,7 @@ PX# = PX# + D#
RETURN RETURN
LABEL DEMOAIM LABEL DEMOAIM
RMAX# = 81 DOFF# = RND(81) - 40
GOSUB RANDOM
DOFF# = RND# - 40
RETURN RETURN
``` ```
@@ -1501,7 +1495,7 @@ This is the shape of the whole file:
LABEL SETUP the geometry from Step 2 LABEL SETUP the geometry from Step 2
the declaration block from Step 3 the declaration block from Step 3
the brick faces from Step 5 the brick faces from Step 5
SEED# = TI# RND(n) seeds itself from the host clock
the ceiling from Step 9 the ceiling from Step 9
GOSUB MKSPR Step 4 GOSUB MKSPR Step 4
GOSUB SNDPROBE Step 14 GOSUB SNDPROBE Step 14
@@ -1576,10 +1570,7 @@ BB# = 0
RX# = 0 RX# = 0
N# = 0 N# = 0
MROW# = 0 MROW# = 0
RMAX# = 2
RND# = 0
SND# = 0 SND# = 0
SEED# = 0
P$ = "" P$ = ""
H$ = "" H$ = ""
S$ = "" S$ = ""

View File

@@ -156,18 +156,32 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_new_leaf(akbasic_Environm
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_wait_for_command(akbasic_Environment *obj, const char *command); akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_wait_for_command(akbasic_Environment *obj, const char *command);
/** /**
* @brief True when this scope or any enclosing one is skipping forward. * @brief Whether this scope or any enclosing one is skipping forward.
*
* The answer leaves through @p dest rather than the return value. Deciding it
* reads a recorded verb name, that read can fail, and a `bool` return has
* nowhere to report the failure -- so the signature changes rather than the
* error being swallowed. libakstdlib #38 is the ruling; `probe` in `symtab.c`
* and `loop_continues` in `runtime_structure.c` are the same shape.
*
* @param obj Scope to inspect; NULL is not waiting. * @param obj Scope to inspect; NULL is not waiting.
* @return `true` when execution is currently suppressed. * @param[out] dest `true` when execution is currently suppressed. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When dest is NULL.
*/ */
bool akbasic_environment_is_waiting_for_any(akbasic_Environment *obj); akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_is_waiting_for_any(akbasic_Environment *obj, bool *dest);
/** /**
* @brief True when this scope or an enclosing one is waiting for a given verb. * @brief Whether this scope or an enclosing one is waiting for a given verb.
*
* Reports through @p dest for the same reason its sibling above does.
*
* @param obj Scope to inspect; NULL is not waiting. * @param obj Scope to inspect; NULL is not waiting.
* @param command Verb to test for. * @param command Verb to test for; NULL is not waiting.
* @return `true` when that verb is what execution is waiting on. * @param[out] dest `true` when that verb is what execution is waiting on. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When dest is NULL.
*/ */
bool akbasic_environment_is_waiting_for(akbasic_Environment *obj, const char *command); akerr_ErrorContext AKERR_NOIGNORE *akbasic_environment_is_waiting_for(akbasic_Environment *obj, const char *command, bool *dest);
/** /**
* @brief Clear a pending wait, searching the parent chain for it. * @brief Clear a pending wait, searching the parent chain for it.
* *

View File

@@ -248,6 +248,11 @@ typedef struct akbasic_Runtime
*/ */
int64_t timems; int64_t timems;
/* RND's lazy seed state. The flag distinguishes an unseeded run from a
* legitimate LCG state of zero. */
int64_t rndseed;
bool rndseeded;
/* /*
* Set by a branch that has decided the remaining statements on its line * Set by a branch that has decided the remaining statements on its line
* belong to the arm it did not take, and cleared at the top of every line. * belong to the arm it did not take, and cleared at the top of every line.

View File

@@ -8,7 +8,6 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <strings.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h> #include <akstdlib.h>
@@ -112,6 +111,7 @@ static akerr_ErrorContext *scan_line(akbasic_DataState *obj, const char *code, i
const char *cursor = code; const char *cursor = code;
bool statementstart = true; bool statementstart = true;
bool instring = false; bool instring = false;
int cmp = 0;
while ( *cursor != '\0' ) { while ( *cursor != '\0' ) {
if ( instring ) { if ( instring ) {
@@ -141,12 +141,14 @@ static akerr_ErrorContext *scan_line(akbasic_DataState *obj, const char *code, i
} }
continue; continue;
} }
if ( statementstart && strncasecmp(cursor, "DATA", 4) == 0 if ( statementstart ) {
&& !isalnum((unsigned char)cursor[4]) ) { PASS(errctx, aksl_strncasecmp(cursor, "DATA", 4, &cmp));
cursor += 4; if ( cmp == 0 && !isalnum((unsigned char)cursor[4]) ) {
PASS(errctx, collect_items(obj, &cursor, lineno)); cursor += 4;
statementstart = false; PASS(errctx, collect_items(obj, &cursor, lineno));
continue; statementstart = false;
continue;
}
} }
statementstart = false; statementstart = false;
cursor += 1; cursor += 1;
@@ -184,7 +186,7 @@ akerr_ErrorContext *akbasic_data_next(akbasic_Runtime *obj, akbasic_Type type, a
PASS(errctx, akbasic_value_zero(dest)); PASS(errctx, akbasic_value_zero(dest));
if ( type == AKBASIC_TYPE_STRING ) { if ( type == AKBASIC_TYPE_STRING ) {
dest->valuetype = AKBASIC_TYPE_STRING; dest->valuetype = AKBASIC_TYPE_STRING;
snprintf(dest->stringval, sizeof(dest->stringval), "%s", item->text); PASS(errctx, aksl_strcpy(dest->stringval, sizeof(dest->stringval), item->text));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }

View File

@@ -7,6 +7,7 @@
#include <string.h> #include <string.h>