9 Commits

Author SHA1 Message Date
eb93bb7da0 Fix 80-column fixtures and tutorial expectations
All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m29s
akbasic CI Build / sanitizers (push) Successful in 4m42s
akbasic CI Build / coverage (push) Successful in 3m56s
akbasic CI Build / akgl_build (push) Successful in 8m34s
akbasic CI Build / mutation_test (push) Successful in 18m50s
2026-08-04 16:36:27 -04:00
1e514f679b Rework the megademo to fit the 80-column source line limit
Some checks failed
akbasic CI Build / sanitizers (push) Failing after 9m13s
akbasic CI Build / cmake_build (push) Failing after 14m42s
akbasic CI Build / mutation_test (push) Failing after 5m29s
akbasic CI Build / akgl_build (push) Failing after 9m27s
akbasic CI Build / coverage (push) Failing after 15m27s
AKBASIC_MAX_LINE_LENGTH's cut from 256 to 80 left seventeen lines of
examples/megademo unloadable: the sixteen IM$() picture strings (up to
252 characters) and TUNEA/TUNEB's four-bar PLAY strings (174 and 175).
The real ceiling is 78 characters, not 80 -- stdio_readline() refuses a
read that fills the 80-byte buffer without a newline, so content plus
its terminator must fit in 79.

The picture: vaporwave.py's PAYLOAD drops from 240 to 64, so every
emitted IM$(NN) = "..." line fits under the ceiling. chop() no longer
slices blind; it walks the stream a record at a time -- two characters
for a run, three for an R row record -- and never cuts inside one,
because the decoder reads a record's tail with MID on the string it is
walking and a record straddling two IM$ entries decodes as garbage.
The old blind slice at 240 only happened to be safe. verify() now
simulates the CHOPPED strings with the cursor threaded across the
boundaries exactly the way DRAWSTREAM executes them, so a bad cut is
an assertion failure instead of a corrupted screen, and emit_block()
asserts every emitted line fits. The picture is 56 strings where it
was 16; the decoder needed no changes at all, since it already carries
X#/Y# from one IM$ entry to the next.

The music: TUNEA and TUNEB each become four PLAY statements, one bar
apiece. play.c keeps voice, envelope, level and duration state on the
runtime across statements and every PLAY appends to the same queue, so
four bars queue exactly as one long string did. Each bar restates the
V1T3U9S prefix so a bar dropped by QFULL cannot leave the next batch
playing on the drum kit's envelope.

Everything still clears the shrunken pools with room to spare: 1625
source lines of 2048, ~704 array slots of 2048, identifiers within the
24-character symtab key. Verified end to end against this branch's
build: the demo loads, the offscreen host renders every scene, and the
scene-5 still is pixel-identical to vaporwave.py's own preview. The
test suite fails the same seventeen cases with and without this
commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACffnV6F7sxQuG3Y8a1L3s
2026-08-04 11:13:31 -04:00
17af2d406c Cut akbasic_Runtime's static footprint from 10.75 MiB to 2.40 MiB
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m29s
akbasic CI Build / coverage (push) Failing after 3m40s
akbasic CI Build / sanitizers (push) Failing after 4m37s
akbasic CI Build / mutation_test (push) Failing after 3m35s
akbasic CI Build / akgl_build (push) Failing after 7m20s
Nothing in this interpreter mallocs; every pool is a fixed array sized by an
AKBASIC_MAX_* constant, so sizeof(akbasic_Runtime) is a compile-time number
and most of it was headroom nobody was using. Measured concurrent-use
high-water marks off examples/breakout and examples/megademo -- the two most
demanding programs this interpreter runs -- against each pool's ceiling:

  AKBASIC_MAX_ENVIRONMENTS   32 -> 12    (measured peak concurrency: 6-7)
  AKBASIC_MAX_FUNCTIONS      64 -> 8     (measured: 0, neither program uses DEF FN)
  AKBASIC_MAX_ARRAY_VALUES 4096 -> 2048  (measured peak: 1618 slots)
  AKBASIC_MAX_SOURCE_LINES 9999 -> 2048  (measured: ~1270-1496 non-blank lines)
  AKBASIC_SYMTAB_MAX_SLOTS  256 -> 172   (no caller ever requests more than 128)
  AKBASIC_SYMTAB_MAX_KEY     64 -> 24    (longest identifier measured: 11 chars)
  AKBASIC_MAX_LINE_LENGTH   256 -> 80    (Commodore BASIC's own line limit)

AKBASIC_MAX_VARIABLES (128) is untouched on purpose: breakout alone reaches
121 of 128 concurrent named variables, so it has the least slack of any pool
measured and is not a shrink candidate.

akbasic_Variable.name shrinks from AKBASIC_MAX_STRING_LENGTH (256) to
AKBASIC_SYMTAB_MAX_KEY: every variable name is registered with
akbasic_symtab_set() right after this field is populated
(akbasic_environment_create(), src/environment.c), and that call already
refuses anything AKBASIC_SYMTAB_MAX_KEY characters or longer. The wider field
was headroom nothing could ever put a byte into.

Two defects surfaced while testing the line-length drop against the golden
corpus, both fixed here because the 80-byte ceiling makes them routine rather
than theoretical:

- sourcepath (runtime.h) was borrowing AKBASIC_MAX_LINE_LENGTH by accident.
  It holds a directory, not a line of BASIC, and this checkout's own test
  paths are 81+ characters deep -- every golden test failed to load until
  this split into its own AKBASIC_MAX_SOURCE_PATH_LENGTH, backed by PATH_MAX
  the way libakerror already sizes its own path buffers.

- src/sink_stdio.c's stdio_readline() called aksl_fgets() but never checked
  its own documented contract: a full buffer with no trailing newline means
  the line was longer than the buffer, and the unread remainder is still in
  the stream. Unchecked, the next readline() picks that remainder up as its
  own statement -- a real line silently becomes two wrong ones instead of a
  clean AKBASIC_ERR_BOUNDS refusal. At 256 bytes this was theoretical; at 80
  it is not, so it now refuses loudly.

tests/value_pool.c's test_pool_is_untouched_by_scopes() was pinned to the old
4x1024=4096 pool math (four max-size arrays proving nothing leaked); rewritten
to 2x1024=2048 for the same proof against the new AKBASIC_MAX_ARRAY_VALUES.

Known consequence, tracked in andrew/akbasic#32 rather than worked around
here: two files in the protected tests/reference/ corpus
(language/functions/mod.bas, language/flowcontrol/nestedforloopwaitingfor
command.bas) have 82-character lines and cannot be shortened -- MAINTENANCE.md
and CMakeLists.txt:585 are explicit that tests/reference/ is never edited to
suit this interpreter. Twelve tests/language/ cases and one docs/18 line are
in the same position but are this project's own content. Shipping 80 anyway,
with the fallout tracked rather than hidden, was an explicit call on this PR
rather than something decided here.

Verified: cmake --build build-akgl && ctest --test-dir build-akgl, 97/112 (15
known failures, all AKBASIC_MAX_LINE_LENGTH-related, filed as #32).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-03 21:44:16 -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
68 changed files with 1566 additions and 703 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

@@ -685,9 +685,9 @@ seconds asks for fifty frames a second.
### Why `GOTO` rather than `DO ... LOOP` ### Why `GOTO` rather than `DO ... LOOP`
A `DO ... LOOP` around the frame would read better, and it is not usable here: **a `GOTO` A `DO ... LOOP` around the frame would read better, and it is not usable here: **a `GOTO`
that jumps out of a `FOR` or a `DO` does not release the loop's scope.** There are 32 that jumps out of a `FOR` or a `DO` does not release the loop's scope.** There are 12
scopes, so a game that leaves its main loop once per lost life stops on the scopes, so a game that leaves its main loop once per lost life stops on the
thirty-second one: twelfth one:
```basic ```basic
N# = 0 N# = 0
@@ -700,7 +700,7 @@ PRINT "SURVIVED " + N#
``` ```
```output ```output
? 3 : PARSE ERROR Environment pool exhausted at line 3 (32 in use) ? 3 : PARSE ERROR Environment pool exhausted at line 3 (12 in use)
``` ```

View File

@@ -1423,7 +1423,8 @@ IF STATE# = 2 THEN GOSUB UNSTICK
RETURN RETURN
LABEL PRESSPAUSE LABEL PRESSPAUSE
IF STATE# = 2 THEN STATE# = 6 : GMTYP# = 0 : BAN$ = "PAUSED" : GOSUB SETBANNER : RETURN IF STATE# = 2 THEN STATE# = 6 : GMTYP# = 0 : BAN$ = "PAUSED"
IF STATE# = 6 THEN GOSUB SETBANNER : RETURN
IF STATE# = 6 THEN STATE# = 2 : BAN$ = "" : GOSUB SETBANNER IF STATE# = 6 THEN STATE# = 2 : BAN$ = "" : GOSUB SETBANNER
RETURN RETURN
``` ```

View File

@@ -56,7 +56,10 @@ This is a tour of the interpreter's edges, on purpose:
counterpoint: a genuine 24x21 `SPRSAV` type-in sprite, 63 bytes, which is counterpoint: a genuine 24x21 `SPRSAV` type-in sprite, 63 bytes, which is
everything `DATA` has room to say. The paint is the reveal, and the exit is a everything `DATA` has room to say. The paint is the reveal, and the exit is a
stride-63 column dissolve — 63 is coprime to 160, so the walk hits every stride-63 column dissolve — 63 is coprime to 160, so the walk hits every
column once and looks random while carrying no state. column once and looks random while carrying no state. The strings come in
64-character chunks because a source line is 80 columns, like the machines
this pretends to be — the decoder carries its cursor from one `IM$` entry to
the next, and the generator refuses to cut inside a run.
- **And then the picture is video.** Six delta frames loop the floor grid toward - **And then the picture is video.** Six delta frames loop the floor grid toward
you and crawl the sun's slice pattern — full motion video, 1.3 KB total, about you and crawl the sun's slice pattern — full motion video, 1.3 KB total, about
220 bytes a frame. A delta re-encodes only the rows that changed: an `R` 220 bytes a frame. A delta re-encodes only the rows that changed: an `R`
@@ -82,6 +85,10 @@ This is a tour of the interpreter's edges, on purpose:
batch a bar of noise drums. Noise is real: `ENVELOPE`'s sixth argument is a batch a bar of noise drums. Noise is real: `ENVELOPE`'s sixth argument is a
waveform, 0 to 3, which Chapter 7 forgot to mention. Voice 1 carries the tune so waveform, 0 to 3, which Chapter 7 forgot to mention. Voice 1 carries the tune so
the collision blips (voice 2) and scene sweeps (voice 3) never steal its channel. the collision blips (voice 2) and scene sweeps (voice 3) never steal its channel.
A batch is four `PLAY` statements, one bar each — the 80-column line limit will
not hold four bars in one string, and it does not need to: the parser's voice,
envelope and duration state persists across statements and every `PLAY` appends
to the same queue, so four bars queue exactly as one line would.
It also probes for its hardware like a proper boot loader, with one `TRAP` per It also probes for its hardware like a proper boot loader, with one `TRAP` per
device: no graphics refuses by name and exits, no audio mutes the soundtrack, no device: no graphics refuses by name and exits, no audio mutes the soundtrack, no

View File

@@ -461,38 +461,78 @@ BS% = W# / 160
IF BS% < 1 THEN BS% = 1 IF BS% < 1 THEN BS% = 1
REM ---- PICTURE-BEGIN (generated by vaporwave.py; do not REM ---- PICTURE-BEGIN (generated by vaporwave.py; do not
REM ---- hand-edit -- rerun the script to change the picture) REM ---- hand-edit -- rerun the script to change the picture)
DIM IM$(16) DIM IM$(56)
DIM VA#(6) DIM VA#(6)
DIM VB#(6) DIM VB#(6)
NS# = 9 NS# = 32
VA#(0) = 9 VA#(0) = 32
VB#(0) = 9 VB#(0) = 35
VA#(1) = 10 VA#(1) = 36
VB#(1) = 10 VB#(1) = 39
VA#(2) = 11 VA#(2) = 40
VB#(2) = 12 VB#(2) = 43
VA#(3) = 13 VA#(3) = 44
VB#(3) = 13 VB#(3) = 47
VA#(4) = 14 VA#(4) = 48
VB#(4) = 14 VB#(4) = 51
VA#(5) = 15 VA#(5) = 52
VB#(5) = 15 VB#(5) = 55
IM$(0) = "G9G9G9G9GPGHEHG9GTEHG9GTEHGPGPEHG9GTEHG9GTEHGHGXEHGTBAG8EHG9GTEHG5EHGPEHG5EHGPEHG5EHGAPAG3EHGPEHG5EHGPEHGXGHEHG5EHGPEHG5EHGFBAGIEHGPGPEHGPEHGHEHGPEHGPEHGHEHGPEHGHEHGPEHGPEHGHEHGBPAGMEHGPEHGHEHGPEHGHEHGPEHGPEHGHEHGPEHGPEHGHEHGPEHGHEPGHEHGPEH" IM$(0) = "G9G9G9G9GPGHEHG9GTEHG9GTEHGPGPEHG9GTEHG9GTEHGHGXEHGTBAG8EHG9GTEH"
IM$(1) = "GHEPGHEHGPEHGHEPGHEHGHEGBAEHGHEHGPEHGHEPGHEHGPEHGHEHGPEHGHEPGHEHGPEHGHEPGHEHGPECPAEDGHEPGHEHGHEPGHEPGHEHGHEPGHEPGHEHGHEPGHEHGHEPGHEPGHECBAEDGHEPGHEPGHEHGHEPGHEHGHEPGHEPGHEHGHEPGHEPEPGHEPGHE5GHEPGHE5GHEHEXGHEPGHEEPAEZGHEPGHE5GHE5GHEPGHE5GHEP" IM$(1) = "G5EHGPEHG5EHGPEHG5EHGAPAG3EHGPEHG5EHGPEHGXGHEHG5EHGPEHG5EHGFBAGI"
IM$(2) = "GHE5GHE9ETGHE9ETGHEXEHGHE9ETGHHAE9ESGHEPEPGBBAGEE9EMHOE9ETGHEHE9E7HUE9E6E9E4H0E9E3E9E3H2E9EHBAETEHKHE9ELH6E9ECKHEPEPKHE9EBH9HAE9EIKHEHEXKHE2H9HCE9EPKHE5KHEPKDH9HEKCEPKHE5KHE5KHEKH9HGEBKHEPKHEXEHKHE5KHEBH9HIEIKHEPKHEPEPKHEPKHEHKAH9HKKHEHKHEP" IM$(2) = "EHGPGPEHGPEHGHEHGPEHGPEHGHEHGPEHGHEHGPEHGPEHGHEHGBPAGMEHGPEHGHEH"
IM$(3) = "KHEHKHEPKHEPKHH9HMEGKHEHKHEPKHEHKHEPKHEPH9HMEOKHEHKHEPKHEHKPEHKHEGH9HOKFEPKHEHKPEHKHEHKPEHKGH9HOEFKHEPKHEHKHEPKHEHKPEFH9HQKEEHKHEPKHEHKHEPKHEHKMH9HSKLEHKHEPKHEHKPEHKHEHKEH9HSEDKPEHKPEHKHEHKPEHKHEEH9HSKDEHKPEHKPKPEHKPEHKDH9HUECKHEHKPEHKHKXEH" IM$(3) = "GPEHGHEHGPEHGPEHGHEHGPEHGPEHGHEHGPEHGHEPGHEHGPEHGHEPGHEHGPEHGHEP"
IM$(4) = "KPEDH9HUKCEHK5EHK5EHKLH9HUKKEHK5EHK5EHKCH9HWEBKPEHKXKHEHK8I9IWKZEHKPKPEHK0I9IWK7EHKHKXEHKSI9IWK9KFEHK9KOI9IWK9KNK9KOI9IWK9KNK9KOI9IWK9KNKPIHKZI9IYK6IHKHKXIHKSI9IWK9KFIHK5IHKKI9IWK9KNIHK5IHKCI9IYKPIHKXKHIHK5IHKPIHK5IHKPIHKPKPIHK0I9IWKJIHKPIH" IM$(4) = "GHEHGHEGBAEHGHEHGPEHGHEPGHEHGPEHGHEHGPEHGHEPGHEHGPEHGHEPGHEHGPEC"
IM$(5) = "KHIHKPIHKPI9IZKBIHKHIHKPIHKHIHKPIHKKI9IWKJIHKHIHKPIHKHIHKPIHKDI9IXKPIHKHIHKHKHIHKHIPKHI9IYKCIHKPIHKHIHKPIHKHIPKHIHKPIHKHIPKHIHKPIHKHIHKPIHKHI9I9IHKHIHKPIHKHIPKHIHKHIEC9CSKDIPKHIPKHIHKHIPKHIHKEC9CSIDKHIPKHIPIPKHIPKHIFC9CQKEIHKHIPKHIHIXKHIPKG" IM$(5) = "PAEDGHEPGHEHGHEPGHEPGHEHGHEPGHEPGHEHGHEPGHEHGHEPGHEPGHECBAEDGHEP"
IM$(6) = "C9COIFKHI5KHI5KHIPKHI5KHIPKHI5KHI5KHIPKHI5KHIPKHIXIHKHI9IDC9CMI4KHIPIPKHI6C9CKI9IDKHIHIXKHIZC9CII9IMKHI9IWC9CGI9IVI9I9I9I9IPI9I9I9I9IPQ9Q9Q9Q9QPE9E9E9E9EPE9E9E9E9EPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9" IM$(6) = "GHEPGHEHGHEPGHEHGHEPGHEPGHEHGHEPGHEPEPGHEPGHE5GHEPGHE5GHEHEXGHEP"
IM$(7) = "Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9" IM$(7) = "GHEEPAEZGHEPGHE5GHE5GHEPGHE5GHEPGHE5GHE9ETGHE9ETGHEXEHGHE9ETGHHA"
IM$(8) = "Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QP" IM$(8) = "E9ESGHEPEPGBBAGEE9EMHOE9ETGHEHE9E7HUE9E6E9E4H0E9E3E9E3H2E9EHBAET"
IM$(9) = "RBRQ9QOKMQHK5RBSQ9QTI9IRRBXQ9QTKPQHKHQPKERBYQ9QPI9IHRB3Q9QSKAI5KHIJRB5Q9QTC9CMRB9Q9QWI9IGRCBQ9QYC9CCRCIA9A9A9A9APRCJE9E9E9E9EPRCNA9A9A9A9APRCOE9E9E9E9EPRCUA9A9A9A9APRCVE9E9E9E9EPRC5A9A9A9A9APRC7E9E9E9E9EP" IM$(9) = "EHKHE9ELH6E9ECKHEPEPKHE9EBH9HAE9EIKHEHEXKHE2H9HCE9EPKHE5KHEPKDH9"
IM$(10) = "RBQQ9QOK9KIQHKFRBRQ9QOI9IQRBWQ9QPKLQHKHQHKPRBXQ9QTI9QLIERB2Q9QRIBKHIPKHIPKCRB4Q9QSC9CORB8Q9QVI3KHIGRCAQ9QXC9CERCJA9A9A9A9APRCKE9E9E9E9EPRCOA9A9A9A9APRCPE9E9E9E9EPRCVA9A9A9A9APRCXE9E9E9E9EPRC7A9A9A9A9APRDAE9E9E9E9EP" IM$(10) = "HEKCEPKHE5KHE5KHEKH9HGEBKHEPKHEXEHKHE5KHEBH9HIEIKHEPKHEPEPKHEPKH"
IM$(11) = "RBPQ9QOK9KAQHKNRBQQ9QOI9IWRBVQ9QOKEQHKHQHKPQHKFRBWQ9QPI9IPRB1Q9QQKCIPKHIPKHIDRB3Q9QSC9CORB7Q9QUIWKHIPRB9Q9QWC9CGRCEA9A9A9A9APRCFE9E9E9E9EPRCGA9A9A9A9APRCHE9E9E9E9EPRCPA9A9A9A9APRCQE9E9E9E9EPRCXA9A9A9A9APRCZE9E9E9E9EPRDAA9A9A9A9APRDCE9E9E9E9" IM$(11) = "EHKAH9HKKHEHKHEPKHEHKHEPKHEPKHH9HMEGKHEHKHEPKHEHKHEPKHEPH9HMEOKH"
IM$(12) = "EP" IM$(12) = "EHKHEPKHEHKPEHKHEGH9HOKFEPKHEHKPEHKHEHKPEHKGH9HOEFKHEPKHEHKHEPKH"
IM$(13) = "RBOQ9QNK3QHKWRBPQ9QOI9IWRBUQ9QTKHQHKPQHKNRBVQ9QOI9IWRB0Q9QQIKKHIPKHIHKDRB1Q9QQC9CSRB2Q9QRC9CQRB6Q9QTIPKHIYRB8Q9QVC9CIRCKA9A9A9A9APRCLE9E9E9E9EPRCQA9A9A9A9APRCRE9E9E9E9EPRCZA9A9A9A9APRC1E9E9E9E9EPRDCA9A9A9A9APRDFE9E9E9E9EP" IM$(13) = "EHKPEFH9HQKEEHKHEPKHEHKHEPKHEHKMH9HSKLEHKHEPKHEHKPEHKHEHKEH9HSED"
IM$(14) = "RBOQ9QNI9IYRBTQ9QOKEQHKPQHKVRBUQ9QTI9IRRBZQ9QTKHQHKPQHKHRB0Q9QQC9CSRB5Q9QTIHKHI5KARB7Q9QUC9CKRCBQ9QYI9ICRCHA9A9A9A9APRCIE9E9E9E9EPRCLA9A9A9A9APRCME9E9E9E9EPRCRA9A9A9A9APRCTE9E9E9E9EPRC1A9A9A9A9APRC3E9E9E9E9EPRDFA9A9A9A9APRDIE9E9E9E9EP" IM$(14) = "KPEHKPEHKHEHKPEHKHEEH9HSKDEHKPEHKPKPEHKPEHKDH9HUECKHEHKPEHKHKXEH"
IM$(15) = "RBSQ9QTKPQHK3RBTQ9QOI9IWRBYQ9QPKDQHKPQHKHRBZQ9QTI9ILRB4Q9QSIAKHI5KHIBRB6Q9QTC9CMRCAQ9QXI9IERCEE9E9E9E9EPRCFA9A9A9A9APRCGE9E9E9E9EPRCMA9A9A9A9APRCNE9E9E9E9EPRCTA9A9A9A9APRCUE9E9E9E9EPRC3A9A9A9A9APRC5E9E9E9E9EPRDIA9A9A9A9AP" IM$(15) = "KPEDH9HUKCEHK5EHK5EHKLH9HUKKEHK5EHK5EHKCH9HWEBKPEHKXKHEHK8I9IWKZ"
IM$(16) = "EHKPKPEHK0I9IWK7EHKHKXEHKSI9IWK9KFEHK9KOI9IWK9KNK9KOI9IWK9KNK9KO"
IM$(17) = "I9IWK9KNKPIHKZI9IYK6IHKHKXIHKSI9IWK9KFIHK5IHKKI9IWK9KNIHK5IHKCI9"
IM$(18) = "IYKPIHKXKHIHK5IHKPIHK5IHKPIHKPKPIHK0I9IWKJIHKPIHKHIHKPIHKPI9IZKB"
IM$(19) = "IHKHIHKPIHKHIHKPIHKKI9IWKJIHKHIHKPIHKHIHKPIHKDI9IXKPIHKHIHKHKHIH"
IM$(20) = "KHIPKHI9IYKCIHKPIHKHIHKPIHKHIPKHIHKPIHKHIPKHIHKPIHKHIHKPIHKHI9I9"
IM$(21) = "IHKHIHKPIHKHIPKHIHKHIEC9CSKDIPKHIPKHIHKHIPKHIHKEC9CSIDKHIPKHIPIP"
IM$(22) = "KHIPKHIFC9CQKEIHKHIPKHIHIXKHIPKGC9COIFKHI5KHI5KHIPKHI5KHIPKHI5KH"
IM$(23) = "I5KHIPKHI5KHIPKHIXIHKHI9IDC9CMI4KHIPIPKHI6C9CKI9IDKHIHIXKHIZC9CI"
IM$(24) = "I9IMKHI9IWC9CGI9IVI9I9I9I9IPI9I9I9I9IPQ9Q9Q9Q9QPE9E9E9E9EPE9E9E9"
IM$(25) = "E9EPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QP"
IM$(26) = "Q9Q9Q9Q9QPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9"
IM$(27) = "Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9"
IM$(28) = "QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9"
IM$(29) = "Q9Q9Q9QPE9E9E9E9EPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9"
IM$(30) = "Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QP"
IM$(31) = "Q9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QPQ9Q9Q9Q9QP"
IM$(32) = "RBRQ9QOKMQHK5RBSQ9QTI9IRRBXQ9QTKPQHKHQPKERBYQ9QPI9IHRB3Q9QSKAI5"
IM$(33) = "KHIJRB5Q9QTC9CMRB9Q9QWI9IGRCBQ9QYC9CCRCIA9A9A9A9APRCJE9E9E9E9EP"
IM$(34) = "RCNA9A9A9A9APRCOE9E9E9E9EPRCUA9A9A9A9APRCVE9E9E9E9EPRC5A9A9A9A9"
IM$(35) = "APRC7E9E9E9E9EP"
IM$(36) = "RBQQ9QOK9KIQHKFRBRQ9QOI9IQRBWQ9QPKLQHKHQHKPRBXQ9QTI9QLIERB2Q9QR"
IM$(37) = "IBKHIPKHIPKCRB4Q9QSC9CORB8Q9QVI3KHIGRCAQ9QXC9CERCJA9A9A9A9APRCK"
IM$(38) = "E9E9E9E9EPRCOA9A9A9A9APRCPE9E9E9E9EPRCVA9A9A9A9APRCXE9E9E9E9EP"
IM$(39) = "RC7A9A9A9A9APRDAE9E9E9E9EP"
IM$(40) = "RBPQ9QOK9KAQHKNRBQQ9QOI9IWRBVQ9QOKEQHKHQHKPQHKFRBWQ9QPI9IPRB1Q9"
IM$(41) = "QQKCIPKHIPKHIDRB3Q9QSC9CORB7Q9QUIWKHIPRB9Q9QWC9CGRCEA9A9A9A9AP"
IM$(42) = "RCFE9E9E9E9EPRCGA9A9A9A9APRCHE9E9E9E9EPRCPA9A9A9A9APRCQE9E9E9E9"
IM$(43) = "EPRCXA9A9A9A9APRCZE9E9E9E9EPRDAA9A9A9A9APRDCE9E9E9E9EP"
IM$(44) = "RBOQ9QNK3QHKWRBPQ9QOI9IWRBUQ9QTKHQHKPQHKNRBVQ9QOI9IWRB0Q9QQIKKH"
IM$(45) = "IPKHIHKDRB1Q9QQC9CSRB2Q9QRC9CQRB6Q9QTIPKHIYRB8Q9QVC9CIRCKA9A9A9"
IM$(46) = "A9APRCLE9E9E9E9EPRCQA9A9A9A9APRCRE9E9E9E9EPRCZA9A9A9A9APRC1E9E9"
IM$(47) = "E9E9EPRDCA9A9A9A9APRDFE9E9E9E9EP"
IM$(48) = "RBOQ9QNI9IYRBTQ9QOKEQHKPQHKVRBUQ9QTI9IRRBZQ9QTKHQHKPQHKHRB0Q9QQ"
IM$(49) = "C9CSRB5Q9QTIHKHI5KARB7Q9QUC9CKRCBQ9QYI9ICRCHA9A9A9A9APRCIE9E9E9"
IM$(50) = "E9EPRCLA9A9A9A9APRCME9E9E9E9EPRCRA9A9A9A9APRCTE9E9E9E9EPRC1A9A9"
IM$(51) = "A9A9APRC3E9E9E9E9EPRDFA9A9A9A9APRDIE9E9E9E9EP"
IM$(52) = "RBSQ9QTKPQHK3RBTQ9QOI9IWRBYQ9QPKDQHKPQHKHRBZQ9QTI9ILRB4Q9QSIAKH"
IM$(53) = "I5KHIBRB6Q9QTC9CMRCAQ9QXI9IERCEE9E9E9E9EPRCFA9A9A9A9APRCGE9E9E9"
IM$(54) = "E9EPRCMA9A9A9A9APRCNE9E9E9E9EPRCTA9A9A9A9APRCUE9E9E9E9EPRC3A9A9"
IM$(55) = "A9A9APRC5E9E9E9E9EPRDIA9A9A9A9AP"
REM ---- PICTURE-END REM ---- PICTURE-END
SS# = 0 SS# = 0
SE# = NS# - 1 SE# = NS# - 1
@@ -1317,13 +1357,25 @@ TB# = TB# + 1
IF TB# > 2 THEN TB# = 0 IF TB# > 2 THEN TB# = 0
RETURN RETURN
REM A batch is four PLAY statements, one bar each: the parser's V, T,
REM U and duration state persists across statements and every PLAY
REM appends to the same queue, so four bars queue exactly as one long
REM string would -- which the 80-column line limit no longer allows.
REM Each bar restates the prefix anyway, so a bar dropped by QFULL
REM never leaves the next one playing with drum-kit state.
LABEL TUNEA LABEL TUNEA
PLAY "V1T3U9S O1AO2AO3AO2AO3AO4CEAO1AO2AO4CEAO5CO4AE O1FO2FO3FO2FO3FAO4CFO1FO2FO3AO4CFAO5CO4A O1CO2CO3CO2CO3CEGO4CO1CO2CO3EGO4CEGO5C O1GO2GO3GO2GO3GBO4DGO1GO2GO3BO4DGBO5DO4B" PLAY "V1T3U9S O1AO2AO3AO2AO3AO4CEAO1AO2AO4CEAO5CO4AE"
PLAY "V1T3U9S O1FO2FO3FO2FO3FAO4CFO1FO2FO3AO4CFAO5CO4A"
PLAY "V1T3U9S O1CO2CO3CO2CO3CEGO4CO1CO2CO3EGO4CEGO5C"
PLAY "V1T3U9S O1GO2GO3GO2GO3GBO4DGO1GO2GO3BO4DGBO5DO4B"
MT# = TI# + 270 MT# = TI# + 270
RETURN RETURN
LABEL TUNEB LABEL TUNEB
PLAY "V1T3U9S O1AO2AO3AO2AO3AO4CEAO1AO2AO4CEAO5CEO4A O1FO2FO3FO2FO3FAO4CFO1FO2FO3AO4CFAO5CO4A O1DO2DO3DO2DO3DFAO4DO1DO2DO3FAO4DFAO5D O1EO2EO3EO2EO3E#GBO4EO1EO2EO3#GBO4E#GBO5E" PLAY "V1T3U9S O1AO2AO3AO2AO3AO4CEAO1AO2AO4CEAO5CEO4A"
PLAY "V1T3U9S O1FO2FO3FO2FO3FAO4CFO1FO2FO3AO4CFAO5CO4A"
PLAY "V1T3U9S O1DO2DO3DO2DO3DFAO4DO1DO2DO3FAO4DFAO5D"
PLAY "V1T3U9S O1EO2EO3EO2EO3E#GBO4EO1EO2EO3#GBO4E#GBO5E"
MT# = TI# + 270 MT# = TI# + 270
RETURN RETURN

View File

@@ -58,7 +58,14 @@ SKIP = "Q"
ROWREC = "R" ROWREC = "R"
LENCH = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" LENCH = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
MAXRUN = len(LENCH) MAXRUN = len(LENCH)
PAYLOAD = 240
# The interpreter reads source through an 80-byte line buffer and refuses
# any line that fills it (AKBASIC_MAX_LINE_LENGTH, sink_stdio.c), so a
# stored line is at most 78 characters plus its newline. 'IM$(NN) = "' and
# the closing quote spend 12 of those; 64 keeps the emitted lines under
# the ceiling with margin to spare while the index stays two digits.
PAYLOAD = 64
MAXLINE = 78
SUN_CX, SUN_CY, SUN_R = 80, 50, 30 SUN_CX, SUN_CY, SUN_R = 80, 50, 30
HORIZON = 74 HORIZON = 74
@@ -206,13 +213,32 @@ def encode_delta(prev, cur):
def chop(blob): def chop(blob):
return [blob[i:i + PAYLOAD] for i in range(0, len(blob), PAYLOAD)] """Split a stream into strings of at most PAYLOAD characters, cutting
only between records. The decoder reads a record's tail characters
with MID on the string it is walking, so a run (two characters) or a
row record (three) that straddled two IM$ entries would decode as
garbage; DRAWSTREAM only carries the cursor, never a partial record."""
out, cur = [], ""
p = 0
while p < len(blob):
n = 3 if blob[p] == ROWREC else 2
if len(cur) + n > PAYLOAD:
out.append(cur)
cur = ""
cur += blob[p:p + n]
p += n
if cur:
out.append(cur)
return out
def simulate(raster, blob): def simulate(raster, blob, x=0, y=0):
"""Apply one encoded stream to a raster exactly the way the BASIC """Apply one encoded stream to a raster exactly the way the BASIC
decoder does, skips-draw-nothing and all.""" decoder does, skips-draw-nothing and all. The cursor comes in and
x = y = p = 0 goes back out because DRAWSTREAM carries it from one IM$ entry to
the next -- decoding the chopped strings one at a time with the
cursor threaded through is exactly what the demo will execute."""
p = 0
while p < len(blob): while p < len(blob):
c = blob[p] c = blob[p]
if c == ROWREC: if c == ROWREC:
@@ -230,20 +256,31 @@ def simulate(raster, blob):
x = 0 x = 0
y += 1 y += 1
p += 2 p += 2
return raster, x, y
def simulate_lines(raster, lines):
"""One stream as its chopped strings, cursor carried across the
boundaries the way DRAWSTREAM carries X# and Y#."""
x = y = 0
for line in lines:
raster, x, y = simulate(raster, line, x, y)
return raster return raster
def verify(frames, base_blob, delta_blobs): def verify(frames, base_lines, delta_line_groups):
"""The base must reproduce frame 0 exactly, and each delta must """The base must reproduce frame 0 exactly, and each delta must
carry the raster exactly to the next frame. A skip leaves the cell carry the raster exactly to the next frame. A skip leaves the cell
the encoder promised was already right, so equality is total and the encoder promised was already right, so equality is total and
any difference at all is an encoder bug.""" any difference at all is an encoder bug. This decodes the CHOPPED
strings, not the blobs, so a chop that split a record would fail
here instead of corrupting the screen."""
raster = [[1] * W for _ in range(H)] raster = [[1] * W for _ in range(H)]
raster = simulate(raster, base_blob) raster = simulate_lines(raster, base_lines)
assert raster == frames[0], "base stream does not reproduce frame 0" assert raster == frames[0], "base stream does not reproduce frame 0"
for i, blob in enumerate(delta_blobs): for i, lines in enumerate(delta_line_groups):
want = frames[(i + 1) % PHASES] want = frames[(i + 1) % PHASES]
raster = simulate(raster, blob) raster = simulate_lines(raster, lines)
assert raster == want, "delta %d does not reproduce its frame" % i assert raster == want, "delta %d does not reproduce its frame" % i
@@ -261,6 +298,9 @@ def emit_block(base_lines, delta_ranges, all_lines):
for i, s in enumerate(all_lines): for i, s in enumerate(all_lines):
out.append('IM$(%d) = "%s"' % (i, s)) out.append('IM$(%d) = "%s"' % (i, s))
out.append("REM ---- PICTURE-END") out.append("REM ---- PICTURE-END")
for line in out:
assert len(line) <= MAXLINE, "emitted line over %d chars: %r" % (
MAXLINE, line)
return out return out
@@ -303,15 +343,15 @@ def main():
base_lines = chop(base_blob) base_lines = chop(base_blob)
all_lines = list(base_lines) all_lines = list(base_lines)
delta_ranges = [] delta_ranges = []
delta_blobs = [] delta_line_groups = []
for i in range(PHASES): for i in range(PHASES):
blob = encode_delta(frames[i], frames[(i + 1) % PHASES]) blob = encode_delta(frames[i], frames[(i + 1) % PHASES])
delta_blobs.append(blob)
lines = chop(blob) lines = chop(blob)
delta_line_groups.append(lines)
delta_ranges.append((len(all_lines), len(all_lines) + len(lines) - 1)) delta_ranges.append((len(all_lines), len(all_lines) + len(lines) - 1))
all_lines.extend(lines) all_lines.extend(lines)
verify(frames, base_blob, delta_blobs) verify(frames, base_lines, delta_line_groups)
dbytes = sum(len(b) for b in delta_blobs) dbytes = sum(len(l) for g in delta_line_groups for l in g)
print("base %d bytes in %d strings; video %d bytes in %d strings; " print("base %d bytes in %d strings; video %d bytes in %d strings; "
"%d strings total" % "%d strings total" %
(sum(len(s) for s in base_lines), len(base_lines), dbytes, (sum(len(s) for s in base_lines), len(base_lines), dbytes,

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

@@ -211,8 +211,12 @@ typedef struct akbasic_Runtime
* not exist relative to the working directory, which is what makes a `.bas` * not exist relative to the working directory, which is what makes a `.bas`
* beside its art work from anywhere. Group F's disk verbs will want the * beside its art work from anywhere. Group F's disk verbs will want the
* same, which is why it is on the runtime rather than in the sprite state. * same, which is why it is on the runtime rather than in the sprite state.
*
* Sized to AKBASIC_MAX_SOURCE_PATH_LENGTH, not AKBASIC_MAX_LINE_LENGTH: a
* directory is a filesystem path, not a line of BASIC, and the two do not
* belong to the same budget.
*/ */
char sourcepath[AKBASIC_MAX_LINE_LENGTH]; char sourcepath[AKBASIC_MAX_SOURCE_PATH_LENGTH];
/* /*
* The armed interrupts, and the environment the one currently running was * The armed interrupts, and the environment the one currently running was
@@ -365,7 +369,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_set_ui(akbasic_Runtime *obj,
* @param path Path to the program file, or NULL for none. * @param path Path to the program file, or NULL for none.
* @return `NULL` on success, otherwise an error context owned by the caller. * @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL. * @throws AKERR_NULLPOINTER When `obj` is NULL.
* @throws AKBASIC_ERR_BOUNDS When the path is longer than AKBASIC_MAX_LINE_LENGTH. * @throws AKBASIC_ERR_BOUNDS When the path is longer than AKBASIC_MAX_SOURCE_PATH_LENGTH.
*/ */
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_set_source_path(akbasic_Runtime *obj, const char *path); akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_set_source_path(akbasic_Runtime *obj, const char *path);

View File

@@ -23,9 +23,17 @@
* serves variables, functions and labels. Capacity is a member; the table is * serves variables, functions and labels. Capacity is a member; the table is
* kept below a 75% load factor by construction because `capacity` counts slots * kept below a 75% load factor by construction because `capacity` counts slots
* and the caller's logical maximum is smaller. * and the caller's logical maximum is smaller.
*
* 172 rather than the old 256: no caller passes akbasic_symtab_init() anything
* larger than AKBASIC_MAX_VARIABLES (128), and 172 keeps that under the 75%
* load factor the comment above promises (128 / 0.75 = 170.7).
*
* AKBASIC_SYMTAB_MAX_KEY 24 rather than 64: the longest identifier across
* examples/breakout and examples/megademo -- variables, DIMmed arrays, labels
* and DEF FN names alike -- is 11 characters (TITLESCREEN, CLEARPOWERS).
*/ */
#define AKBASIC_SYMTAB_MAX_SLOTS 256 #define AKBASIC_SYMTAB_MAX_SLOTS 172
#define AKBASIC_SYMTAB_MAX_KEY 64 #define AKBASIC_SYMTAB_MAX_KEY 24
typedef struct typedef struct
{ {

View File

@@ -6,6 +6,7 @@
#ifndef _AKBASIC_TYPES_H_ #ifndef _AKBASIC_TYPES_H_
#define _AKBASIC_TYPES_H_ #define _AKBASIC_TYPES_H_
#include <limits.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
@@ -27,15 +28,39 @@
#define AKBASIC_MAX_VALUES 64 #define AKBASIC_MAX_VALUES 64
#define AKBASIC_MAX_VARIABLES 128 #define AKBASIC_MAX_VARIABLES 128
/* Whole-runtime pools */ /*
#define AKBASIC_MAX_SOURCE_LINES 9999 * Whole-runtime pools.
#define AKBASIC_MAX_LINE_LENGTH 256 *
* AKBASIC_MAX_SOURCE_LINES, AKBASIC_MAX_LINE_LENGTH, AKBASIC_MAX_ARRAY_VALUES,
* AKBASIC_MAX_ENVIRONMENTS and AKBASIC_MAX_FUNCTIONS were cut from their
* original values against measurements taken off examples/breakout and
* examples/megademo, the two most demanding programs this interpreter runs --
* see the memory-footprint discussion this commit's PR body links. Each is
* sized at roughly 1.5-2x the peak the reference corpus actually reaches, not
* at the peak itself.
*
* AKBASIC_MAX_LINE_LENGTH in particular follows Commodore BASIC's own 80-column
* line limit rather than a measurement, which is why sink_stdio.c now refuses a
* line that fills the buffer with no terminator instead of silently truncating
* it: at 256 bytes that failure mode was theoretical, and at 80 it is not.
*
* AKBASIC_MAX_SOURCE_PATH_LENGTH is its own constant rather than a reuse of
* AKBASIC_MAX_LINE_LENGTH, which is where it used to come from. `sourcepath`
* (runtime.h) holds a directory, not a line of BASIC, and the two ideas do not
* scale together: shrinking the line limit to 80 broke every golden test in
* this checkout, because this repository's own working directory is deeper
* than that. PATH_MAX is the actual bound a filesystem path is subject to, so
* it is the one this borrows.
*/
#define AKBASIC_MAX_SOURCE_LINES 2048
#define AKBASIC_MAX_LINE_LENGTH 80 /* Commodore BASIC's own line limit */
#define AKBASIC_MAX_SOURCE_PATH_LENGTH PATH_MAX
#define AKBASIC_MAX_ARRAY_DEPTH 64 /* dimensions per array */ #define AKBASIC_MAX_ARRAY_DEPTH 64 /* dimensions per array */
#define AKBASIC_MAX_ARRAY_ELEMENTS 1024 /* elements in one array */ #define AKBASIC_MAX_ARRAY_ELEMENTS 1024 /* elements in one array */
#define AKBASIC_MAX_ARRAY_VALUES 4096 /* array elements across all variables */ #define AKBASIC_MAX_ARRAY_VALUES 2048 /* array elements across all variables */
#define AKBASIC_MAX_STRING_LENGTH 256 /* see TODO.md 1.2 */ #define AKBASIC_MAX_STRING_LENGTH 256 /* see TODO.md 1.2 */
#define AKBASIC_MAX_ENVIRONMENTS 32 /* new: Go allocated these unbounded */ #define AKBASIC_MAX_ENVIRONMENTS 12 /* new: Go allocated these unbounded */
#define AKBASIC_MAX_FUNCTIONS 64 /* new: Go used an unbounded map */ #define AKBASIC_MAX_FUNCTIONS 8 /* new: Go used an unbounded map */
#define AKBASIC_MAX_LABELS 64 /* new: Go used an unbounded map */ #define AKBASIC_MAX_LABELS 64 /* new: Go used an unbounded map */
/* /*
* Leaves a DO/LOOP condition may use. Its own small pool rather than a second * Leaves a DO/LOOP condition may use. Its own small pool rather than a second

View File

@@ -13,12 +13,22 @@
#include <akerror.h> #include <akerror.h>
#include <akbasic/symtab.h>
#include <akbasic/types.h> #include <akbasic/types.h>
#include <akbasic/value.h> #include <akbasic/value.h>
typedef struct typedef struct
{ {
char name[AKBASIC_MAX_STRING_LENGTH]; /*
* Sized to AKBASIC_SYMTAB_MAX_KEY, not AKBASIC_MAX_STRING_LENGTH: this name
* only ever gets here by surviving akbasic_symtab_set() first
* (akbasic_environment_create() calls it right after this field is
* populated), and that call refuses anything AKBASIC_SYMTAB_MAX_KEY
* characters or longer with AKBASIC_ERR_BOUNDS. A variable whose name did
* not fit could never exist, so the wider buffer was 232 bytes of headroom
* nothing could ever put a byte into.
*/
char name[AKBASIC_SYMTAB_MAX_KEY];
akbasic_Type valuetype; akbasic_Type valuetype;
akbasic_Value *values; /** The pool, or `inlinevalue` for a scalar */ akbasic_Value *values; /** The pool, or `inlinevalue` for a scalar */
int valuecount; int valuecount;

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>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h>
#include <akbasic/environment.h> #include <akbasic/environment.h>
#include <akbasic/error.h> #include <akbasic/error.h>
@@ -39,7 +40,7 @@ akerr_ErrorContext *akbasic_environment_init(akbasic_Environment *obj, akbasic_R
obj->readIdentifierIdx = 0; obj->readIdentifierIdx = 0;
obj->waitingForCommand[0] = '\0'; obj->waitingForCommand[0] = '\0';
obj->errorToken = NULL; obj->errorToken = NULL;
memset(obj->readIdentifierLeaves, 0, sizeof(obj->readIdentifierLeaves)); PASS(errctx, aksl_memset(obj->readIdentifierLeaves, 0, sizeof(obj->readIdentifierLeaves)));
obj->doLeafPool.next = 0; obj->doLeafPool.next = 0;
obj->doLeafPool.capacity = AKBASIC_MAX_CONDITION_LEAVES; obj->doLeafPool.capacity = AKBASIC_MAX_CONDITION_LEAVES;
obj->doLeafPool.leaves = obj->doLeafStorage; obj->doLeafPool.leaves = obj->doLeafStorage;
@@ -123,6 +124,7 @@ akerr_ErrorContext *akbasic_environment_new_leaf(akbasic_Environment *obj, akbas
akerr_ErrorContext *akbasic_environment_wait_for_command(akbasic_Environment *obj, const char *command) akerr_ErrorContext *akbasic_environment_wait_for_command(akbasic_Environment *obj, const char *command)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
size_t length = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL && command != NULL), AKERR_NULLPOINTER, FAIL_ZERO_RETURN(errctx, (obj != NULL && command != NULL), AKERR_NULLPOINTER,
"NULL argument in wait_for_command"); "NULL argument in wait_for_command");
@@ -135,38 +137,56 @@ akerr_ErrorContext *akbasic_environment_wait_for_command(akbasic_Environment *ob
FAIL_NONZERO_RETURN(errctx, (obj->waitingForCommand[0] != '\0'), AKBASIC_ERR_STATE, FAIL_NONZERO_RETURN(errctx, (obj->waitingForCommand[0] != '\0'), AKBASIC_ERR_STATE,
"Can't wait on multiple commands in the same environment : %s", "Can't wait on multiple commands in the same environment : %s",
obj->waitingForCommand); obj->waitingForCommand);
FAIL_ZERO_RETURN(errctx, (strlen(command) < sizeof(obj->waitingForCommand)), PASS(errctx, aksl_strlen(command, &length));
FAIL_ZERO_RETURN(errctx, (length < sizeof(obj->waitingForCommand)),
AKBASIC_ERR_BOUNDS, "Command name '%s' is too long to wait on", command); AKBASIC_ERR_BOUNDS, "Command name '%s' is too long to wait on", command);
strncpy(obj->waitingForCommand, command, sizeof(obj->waitingForCommand) - 1); PASS(errctx, aksl_strcpy(obj->waitingForCommand, sizeof(obj->waitingForCommand), command));
obj->waitingForCommand[sizeof(obj->waitingForCommand) - 1] = '\0';
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
bool akbasic_environment_is_waiting_for_any(akbasic_Environment *obj) akerr_ErrorContext *akbasic_environment_is_waiting_for_any(akbasic_Environment *obj, bool *dest)
{ {
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER,
"NULL destination in is_waiting_for_any");
*dest = false;
if ( obj == NULL ) { if ( obj == NULL ) {
return false; SUCCEED_RETURN(errctx);
} }
if ( obj->waitingForCommand[0] != '\0' ) { if ( obj->waitingForCommand[0] != '\0' ) {
return true; *dest = true;
SUCCEED_RETURN(errctx);
} }
return akbasic_environment_is_waiting_for_any(obj->parent); /* The recursive tail becomes a PASS; dest carries the answer back up. */
PASS(errctx, akbasic_environment_is_waiting_for_any(obj->parent, dest));
SUCCEED_RETURN(errctx);
} }
bool akbasic_environment_is_waiting_for(akbasic_Environment *obj, const char *command) akerr_ErrorContext *akbasic_environment_is_waiting_for(akbasic_Environment *obj, const char *command, bool *dest)
{ {
PREPARE_ERROR(errctx);
int cmp = 0;
FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER,
"NULL destination in is_waiting_for");
*dest = false;
if ( obj == NULL || command == NULL ) { if ( obj == NULL || command == NULL ) {
return false; SUCCEED_RETURN(errctx);
} }
if ( strcmp(obj->waitingForCommand, command) == 0 ) { PASS(errctx, aksl_strcmp(obj->waitingForCommand, command, &cmp));
return true; if ( cmp == 0 ) {
*dest = true;
SUCCEED_RETURN(errctx);
} }
return akbasic_environment_is_waiting_for(obj->parent, command); PASS(errctx, akbasic_environment_is_waiting_for(obj->parent, command, dest));
SUCCEED_RETURN(errctx);
} }
akerr_ErrorContext *akbasic_environment_stop_waiting(akbasic_Environment *obj, const char *command) akerr_ErrorContext *akbasic_environment_stop_waiting(akbasic_Environment *obj, const char *command)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
int cmp = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL && command != NULL), AKERR_NULLPOINTER, FAIL_ZERO_RETURN(errctx, (obj != NULL && command != NULL), AKERR_NULLPOINTER,
"NULL argument in stop_waiting"); "NULL argument in stop_waiting");
@@ -179,7 +199,8 @@ akerr_ErrorContext *akbasic_environment_stop_waiting(akbasic_Environment *obj, c
* a miss is silently tolerated, exactly as today. * a miss is silently tolerated, exactly as today.
*/ */
while ( obj != NULL ) { while ( obj != NULL ) {
if ( strcmp(obj->waitingForCommand, command) == 0 ) { PASS(errctx, aksl_strcmp(obj->waitingForCommand, command, &cmp));
if ( cmp == 0 ) {
obj->waitingForCommand[0] = '\0'; obj->waitingForCommand[0] = '\0';
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -197,7 +218,7 @@ akerr_ErrorContext *akbasic_environment_get_function(akbasic_Environment *obj, c
FAIL_ZERO_RETURN(errctx, (obj != NULL && fname != NULL && dest != NULL), AKERR_NULLPOINTER, FAIL_ZERO_RETURN(errctx, (obj != NULL && fname != NULL && dest != NULL), AKERR_NULLPOINTER,
"NULL argument in get_function"); "NULL argument in get_function");
len = strlen(fname); PASS(errctx, aksl_strlen(fname, &len));
FAIL_ZERO_RETURN(errctx, (len < sizeof(upper)), AKERR_KEY, "Function '%s' is not defined", fname); FAIL_ZERO_RETURN(errctx, (len < sizeof(upper)), AKERR_KEY, "Function '%s' is not defined", fname);
for ( i = 0; i < len; i++ ) { for ( i = 0; i < len; i++ ) {
char c = fname[i]; char c = fname[i];
@@ -291,6 +312,7 @@ akerr_ErrorContext *akbasic_environment_create(akbasic_Environment *obj, const c
akbasic_Variable *variable = NULL; akbasic_Variable *variable = NULL;
int64_t sizes[1] = { 1 }; int64_t sizes[1] = { 1 };
void *slot = NULL; void *slot = NULL;
size_t namelen = 0;
akerr_ErrorContext *found = NULL; akerr_ErrorContext *found = NULL;
FAIL_ZERO_RETURN(errctx, (obj != NULL && varname != NULL && dest != NULL), AKERR_NULLPOINTER, FAIL_ZERO_RETURN(errctx, (obj != NULL && varname != NULL && dest != NULL), AKERR_NULLPOINTER,
@@ -312,10 +334,10 @@ akerr_ErrorContext *akbasic_environment_create(akbasic_Environment *obj, const c
IGNORE(akerr_release_error(found)); IGNORE(akerr_release_error(found));
PASS(errctx, akbasic_runtime_new_variable(obj->runtime, &variable)); PASS(errctx, akbasic_runtime_new_variable(obj->runtime, &variable));
FAIL_ZERO_RETURN(errctx, (strlen(varname) < sizeof(variable->name)), AKBASIC_ERR_BOUNDS, PASS(errctx, aksl_strlen(varname, &namelen));
FAIL_ZERO_RETURN(errctx, (namelen < sizeof(variable->name)), AKBASIC_ERR_BOUNDS,
"Variable name '%s' is too long", varname); "Variable name '%s' is too long", varname);
strncpy(variable->name, varname, sizeof(variable->name) - 1); PASS(errctx, aksl_strcpy(variable->name, sizeof(variable->name), varname));
variable->name[sizeof(variable->name) - 1] = '\0';
variable->valuetype = AKBASIC_TYPE_UNDEFINED; variable->valuetype = AKBASIC_TYPE_UNDEFINED;
variable->mutable_ = true; variable->mutable_ = true;
PASS(errctx, akbasic_variable_init(variable, &obj->runtime->valuepool, sizes, 1)); PASS(errctx, akbasic_variable_init(variable, &obj->runtime->valuepool, sizes, 1));

View File

@@ -12,6 +12,7 @@
#include <string.h> #include <string.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h>
#include <akbasic/error.h> #include <akbasic/error.h>
#include <akbasic/format.h> #include <akbasic/format.h>
@@ -36,7 +37,7 @@ akerr_ErrorContext *akbasic_format_state_init(akbasic_FormatState *obj)
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL format state in init"); FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL format state in init");
memcpy(obj->chars, PUDEF_DEFAULTS, sizeof(obj->chars)); PASS(errctx, aksl_memcpy(obj->chars, PUDEF_DEFAULTS, sizeof(obj->chars)));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -161,11 +162,20 @@ static void measure_numeric(const char *field, size_t length, int *before, int *
} }
} }
/** @brief Fill @p dest with @p width copies of `*`, the overflow marker. */ /**
static void overflow(char *dest, size_t width) * @brief Fill @p dest with @p width copies of `*`, the overflow marker.
*
* Returns a context rather than `void` because the fill can fail and there was
* nowhere to say so. Both call sites are already inside `render_numeric`, which
* returns one. See libakstdlib #38.
*/
static akerr_ErrorContext *overflow(char *dest, size_t width)
{ {
memset(dest, '*', width); PREPARE_ERROR(errctx);
PASS(errctx, aksl_memset(dest, '*', width));
dest[width] = '\0'; dest[width] = '\0';
SUCCEED_RETURN(errctx);
} }
/** /**
@@ -191,15 +201,21 @@ static akerr_ErrorContext *render_numeric(akbasic_FormatState *obj, const char *
bool leadsign = false; bool leadsign = false;
bool trailsign = false; bool trailsign = false;
bool negative = (number < 0.0); bool negative = (number < 0.0);
int written = 0;
measure_numeric(field, length, &before, &after, &commas, &dollar, &leadsign, &trailsign); measure_numeric(field, length, &before, &after, &commas, &dollar, &leadsign, &trailsign);
FAIL_ZERO_RETURN(errctx, (length + 1 <= len), AKBASIC_ERR_BOUNDS, FAIL_ZERO_RETURN(errctx, (length + 1 <= len), AKBASIC_ERR_BOUNDS,
"PRINT USING field of %zu characters does not fit", length); "PRINT USING field of %zu characters does not fit", length);
snprintf(digits, sizeof(digits), "%.*f", after, (negative ? -number : number)); PASS(errctx, aksl_snprintf(&written, digits, sizeof(digits), "%.*f",
after, (negative ? -number : number)));
point = strchr(digits, '.'); PASS(errctx, aksl_strchr(digits, '.', &point));
intlen = (point != NULL ? (size_t)(point - digits) : strlen(digits)); if ( point != NULL ) {
intlen = (size_t)(point - digits);
} else {
PASS(errctx, aksl_strlen(digits, &intlen));
}
/* Group the integer part, if the field asked for separators. */ /* Group the integer part, if the field asked for separators. */
used = 0; used = 0;
@@ -213,7 +229,7 @@ static akerr_ErrorContext *render_numeric(akbasic_FormatState *obj, const char *
used += 1; used += 1;
} }
} else { } else {
memcpy(grouped, digits, intlen); PASS(errctx, aksl_memcpy(grouped, digits, intlen));
used = intlen; used = intlen;
} }
grouped[used] = '\0'; grouped[used] = '\0';
@@ -226,7 +242,7 @@ static akerr_ErrorContext *render_numeric(akbasic_FormatState *obj, const char *
* printing wider than asked, which would misalign every later column. * printing wider than asked, which would misalign every later column.
*/ */
if ( (int)intlen > before ) { if ( (int)intlen > before ) {
overflow(dest, length); PASS(errctx, overflow(dest, length));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -252,13 +268,13 @@ static akerr_ErrorContext *render_numeric(akbasic_FormatState *obj, const char *
used += 1; used += 1;
} }
} }
memcpy(dest + used, grouped, (size_t)group); PASS(errctx, aksl_memcpy(dest + used, grouped, (size_t)group));
used += (size_t)group; used += (size_t)group;
if ( after > 0 ) { if ( after > 0 ) {
dest[used] = obj->chars[AKBASIC_PUDEF_POINT]; dest[used] = obj->chars[AKBASIC_PUDEF_POINT];
used += 1; used += 1;
memcpy(dest + used, (point != NULL ? point + 1 : ""), (size_t)after); PASS(errctx, aksl_memcpy(dest + used, (point != NULL ? point + 1 : ""), (size_t)after));
used += (size_t)after; used += (size_t)after;
} }
if ( trailsign ) { if ( trailsign ) {
@@ -271,7 +287,7 @@ static akerr_ErrorContext *render_numeric(akbasic_FormatState *obj, const char *
* because a printed -5 that reads as 5 is worse than a row of stars. * because a printed -5 that reads as 5 is worse than a row of stars.
*/ */
if ( negative && !leadsign && !trailsign ) { if ( negative && !leadsign && !trailsign ) {
overflow(dest, length); PASS(errctx, overflow(dest, length));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
dest[used] = '\0'; dest[used] = '\0';
@@ -283,26 +299,27 @@ static akerr_ErrorContext *render_string(const char *field, size_t length, const
char *dest, size_t len) char *dest, size_t len)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
size_t textlen = strlen(text); size_t textlen = 0;
size_t pad = 0; size_t pad = 0;
PASS(errctx, aksl_strlen(text, &textlen));
FAIL_ZERO_RETURN(errctx, (length + 1 <= len), AKBASIC_ERR_BOUNDS, FAIL_ZERO_RETURN(errctx, (length + 1 <= len), AKBASIC_ERR_BOUNDS,
"PRINT USING field of %zu characters does not fit", length); "PRINT USING field of %zu characters does not fit", length);
if ( textlen > length ) { if ( textlen > length ) {
/* Truncated, not starred: BASIC 7.0 cuts a string to its field. */ /* Truncated, not starred: BASIC 7.0 cuts a string to its field. */
memcpy(dest, text, length); PASS(errctx, aksl_memcpy(dest, text, length));
dest[length] = '\0'; dest[length] = '\0';
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
memset(dest, ' ', length); PASS(errctx, aksl_memset(dest, ' ', length));
dest[length] = '\0'; dest[length] = '\0';
if ( field[0] == '=' ) { if ( field[0] == '=' ) {
pad = (length - textlen) / 2; pad = (length - textlen) / 2;
} else { } else {
pad = length - textlen; pad = length - textlen;
} }
memcpy(dest + pad, text, textlen); PASS(errctx, aksl_memcpy(dest + pad, text, textlen));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -313,6 +330,9 @@ akerr_ErrorContext *akbasic_format_using(akbasic_FormatState *obj, const char *f
size_t start = 0; size_t start = 0;
size_t length = 0; size_t length = 0;
size_t used = 0; size_t used = 0;
size_t renderedlen = 0;
size_t formatlen = 0;
int written = 0;
bool numeric = false; bool numeric = false;
FAIL_ZERO_RETURN(errctx, (obj != NULL && format != NULL && value != NULL && dest != NULL), FAIL_ZERO_RETURN(errctx, (obj != NULL && format != NULL && value != NULL && dest != NULL),
@@ -343,12 +363,15 @@ akerr_ErrorContext *akbasic_format_using(akbasic_FormatState *obj, const char *f
} }
/* Literal text before the field, the field, then literal text after it. */ /* Literal text before the field, the field, then literal text after it. */
used = strlen(rendered) + strlen(format) - length; PASS(errctx, aksl_strlen(rendered, &renderedlen));
PASS(errctx, aksl_strlen(format, &formatlen));
used = renderedlen + formatlen - length;
FAIL_ZERO_RETURN(errctx, (used + 1 <= len), AKBASIC_ERR_BOUNDS, FAIL_ZERO_RETURN(errctx, (used + 1 <= len), AKBASIC_ERR_BOUNDS,
"PRINT USING result of %zu characters does not fit", used); "PRINT USING result of %zu characters does not fit", used);
memcpy(dest, format, start); PASS(errctx, aksl_memcpy(dest, format, start));
memcpy(dest + start, rendered, strlen(rendered)); PASS(errctx, aksl_memcpy(dest + start, rendered, renderedlen));
snprintf(dest + start + strlen(rendered), len - start - strlen(rendered), /* The check above already proved the tail fits, so this cannot truncate. */
"%s", format + start + length); PASS(errctx, aksl_snprintf(&written, dest + start + renderedlen,
len - start - renderedlen, "%s", format + start + length));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }

View File

@@ -23,9 +23,9 @@
*/ */
#include <stddef.h> #include <stddef.h>
#include <string.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h>
#include <akgl/controller.h> #include <akgl/controller.h>
#include <akgl/error.h> #include <akgl/error.h>
@@ -55,7 +55,7 @@ akerr_ErrorContext *akbasic_frontend_akgl_init(akbasic_AkglFrontend *obj, const
FAIL_ZERO_RETURN(errctx, (w > 0 && h > 0 && fontsize > 0), AKBASIC_ERR_VALUE, FAIL_ZERO_RETURN(errctx, (w > 0 && h > 0 && fontsize > 0), AKBASIC_ERR_VALUE,
"A %dx%d window at %d points is not a window", w, h, fontsize); "A %dx%d window at %d points is not a window", w, h, fontsize);
memset(obj, 0, sizeof(*obj)); PASS(errctx, aksl_memset(obj, 0, sizeof(*obj)));
obj->width = w; obj->width = w;
obj->height = h; obj->height = h;

View File

@@ -17,14 +17,19 @@
static akerr_ErrorContext *copy_bounded(char *dest, const char *src, const char *what) static akerr_ErrorContext *copy_bounded(char *dest, const char *src, const char *what)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
size_t srclen = 0;
FAIL_ZERO_RETURN(errctx, (src != NULL), AKERR_NULLPOINTER, "NULL %s", what); FAIL_ZERO_RETURN(errctx, (src != NULL), AKERR_NULLPOINTER, "NULL %s", what);
FAIL_ZERO_RETURN(errctx, (strlen(src) < AKBASIC_MAX_STRING_LENGTH), PASS(errctx, aksl_strlen(src, &srclen));
FAIL_ZERO_RETURN(errctx, (srclen < AKBASIC_MAX_STRING_LENGTH),
AKBASIC_ERR_VALUE, AKBASIC_ERR_VALUE,
"%s of %zu characters exceeds the %d character limit", "%s of %zu characters exceeds the %d character limit",
what, strlen(src), AKBASIC_MAX_STRING_LENGTH - 1); what, srclen, AKBASIC_MAX_STRING_LENGTH - 1);
strncpy(dest, src, AKBASIC_MAX_STRING_LENGTH - 1); /* aksl_strcpy always terminates and refuses rather than truncates, which is
dest[AKBASIC_MAX_STRING_LENGTH - 1] = '\0'; the behaviour this site already wanted. The length check above stays
because it names the limit in the message. Every destination is one of the
leaf's AKBASIC_MAX_STRING_LENGTH buffers, which is what sizes the copy. */
PASS(errctx, aksl_strcpy(dest, AKBASIC_MAX_STRING_LENGTH, src));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -90,8 +95,8 @@ static akerr_ErrorContext *clone_into(akbasic_ASTLeaf *self, akbasic_LeafPool *p
copy->parent = self->parent; copy->parent = self->parent;
copy->literal_int = self->literal_int; copy->literal_int = self->literal_int;
copy->literal_float = self->literal_float; copy->literal_float = self->literal_float;
memcpy(copy->literal_string, self->literal_string, sizeof(copy->literal_string)); PASS(errctx, aksl_memcpy(copy->literal_string, self->literal_string, sizeof(copy->literal_string)));
memcpy(copy->identifier, self->identifier, sizeof(copy->identifier)); PASS(errctx, aksl_memcpy(copy->identifier, self->identifier, sizeof(copy->identifier)));
copy->operator_ = self->operator_; copy->operator_ = self->operator_;
PASS(errctx, clone_into(self->left, pool, &copy->left)); PASS(errctx, clone_into(self->left, pool, &copy->left));
@@ -289,7 +294,9 @@ akerr_ErrorContext *akbasic_leaf_new_literal_int(akbasic_ASTLeaf *obj, const cha
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
long long value = 0; long long value = 0;
size_t lexlen = 0;
int base = 10; int base = 10;
int cmp = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL leaf in new_literal_int"); FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL leaf in new_literal_int");
FAIL_ZERO_RETURN(errctx, (lexeme != NULL), AKERR_NULLPOINTER, "NULL lexeme in new_literal_int"); FAIL_ZERO_RETURN(errctx, (lexeme != NULL), AKERR_NULLPOINTER, "NULL lexeme in new_literal_int");
@@ -302,7 +309,9 @@ akerr_ErrorContext *akbasic_leaf_new_literal_int(akbasic_ASTLeaf *obj, const cha
* Commodore BASIC has no octal literals, and a leading zero in a listing is * Commodore BASIC has no octal literals, and a leading zero in a listing is
* padding, not a radix. * padding, not a radix.
*/ */
if ( strlen(lexeme) > 2 && strncmp(lexeme, "0x", 2) == 0 ) { PASS(errctx, aksl_strlen(lexeme, &lexlen));
PASS(errctx, aksl_strncmp(lexeme, "0x", 2, &cmp));
if ( lexlen > 2 && cmp == 0 ) {
base = 16; base = 16;
} }
PASS(errctx, akbasic_leaf_init(obj, AKBASIC_LEAF_LITERAL_INT)); PASS(errctx, akbasic_leaf_init(obj, AKBASIC_LEAF_LITERAL_INT));
@@ -388,6 +397,7 @@ akerr_ErrorContext *akbasic_leaf_to_string(akbasic_ASTLeaf *self, char *dest, si
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
char sub1[AKBASIC_MAX_STRING_LENGTH]; char sub1[AKBASIC_MAX_STRING_LENGTH];
char sub2[AKBASIC_MAX_STRING_LENGTH]; char sub2[AKBASIC_MAX_STRING_LENGTH];
int written = 0;
FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER, "NULL leaf in to_string"); FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER, "NULL leaf in to_string");
FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER, "NULL destination in to_string"); FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER, "NULL destination in to_string");
@@ -395,40 +405,42 @@ akerr_ErrorContext *akbasic_leaf_to_string(akbasic_ASTLeaf *self, char *dest, si
switch ( self->leaftype ) { switch ( self->leaftype ) {
case AKBASIC_LEAF_LITERAL_INT: case AKBASIC_LEAF_LITERAL_INT:
snprintf(dest, len, "%" PRId64, self->literal_int); PASS(errctx, aksl_snprintf(&written, dest, len, "%" PRId64, self->literal_int));
break; break;
case AKBASIC_LEAF_LITERAL_FLOAT: case AKBASIC_LEAF_LITERAL_FLOAT:
snprintf(dest, len, "%f", self->literal_float); PASS(errctx, aksl_snprintf(&written, dest, len, "%f", self->literal_float));
break; break;
case AKBASIC_LEAF_LITERAL_STRING: case AKBASIC_LEAF_LITERAL_STRING:
snprintf(dest, len, "%s", self->literal_string); PASS(errctx, aksl_snprintf(&written, dest, len, "%s", self->literal_string));
break; break;
case AKBASIC_LEAF_IDENTIFIER_INT: case AKBASIC_LEAF_IDENTIFIER_INT:
case AKBASIC_LEAF_IDENTIFIER_FLOAT: case AKBASIC_LEAF_IDENTIFIER_FLOAT:
case AKBASIC_LEAF_IDENTIFIER_STRING: case AKBASIC_LEAF_IDENTIFIER_STRING:
case AKBASIC_LEAF_IDENTIFIER: case AKBASIC_LEAF_IDENTIFIER:
snprintf(dest, len, "%s", self->identifier); PASS(errctx, aksl_snprintf(&written, dest, len, "%s", self->identifier));
break; break;
case AKBASIC_LEAF_IDENTIFIER_STRUCT: case AKBASIC_LEAF_IDENTIFIER_STRUCT:
snprintf(dest, len, "%s", self->identifier); PASS(errctx, aksl_snprintf(&written, dest, len, "%s", self->identifier));
break; break;
case AKBASIC_LEAF_FIELD: case AKBASIC_LEAF_FIELD:
PASS(errctx, akbasic_leaf_to_string(self->left, sub1, sizeof(sub1))); PASS(errctx, akbasic_leaf_to_string(self->left, sub1, sizeof(sub1)));
snprintf(dest, len, "%s%s%s", sub1, PASS(errctx, aksl_snprintf(&written, dest, len, "%s%s%s", sub1,
(self->operator_ == AKBASIC_TOK_ARROW ? "->" : "."), self->identifier); (self->operator_ == AKBASIC_TOK_ARROW ? "->" : "."), self->identifier));
break; break;
case AKBASIC_LEAF_UNARY: case AKBASIC_LEAF_UNARY:
PASS(errctx, akbasic_leaf_to_string(self->left, sub1, sizeof(sub1))); PASS(errctx, akbasic_leaf_to_string(self->left, sub1, sizeof(sub1)));
snprintf(dest, len, "(%s %s)", operator_to_str(self->operator_), sub1); PASS(errctx, aksl_snprintf(&written, dest, len, "(%s %s)",
operator_to_str(self->operator_), sub1));
break; break;
case AKBASIC_LEAF_BINARY: case AKBASIC_LEAF_BINARY:
PASS(errctx, akbasic_leaf_to_string(self->left, sub1, sizeof(sub1))); PASS(errctx, akbasic_leaf_to_string(self->left, sub1, sizeof(sub1)));
PASS(errctx, akbasic_leaf_to_string(self->right, sub2, sizeof(sub2))); PASS(errctx, akbasic_leaf_to_string(self->right, sub2, sizeof(sub2)));
snprintf(dest, len, "(%s %s %s)", operator_to_str(self->operator_), sub1, sub2); PASS(errctx, aksl_snprintf(&written, dest, len, "(%s %s %s)",
operator_to_str(self->operator_), sub1, sub2));
break; break;
case AKBASIC_LEAF_GROUPING: case AKBASIC_LEAF_GROUPING:
PASS(errctx, akbasic_leaf_to_string(self->expr, sub1, sizeof(sub1))); PASS(errctx, akbasic_leaf_to_string(self->expr, sub1, sizeof(sub1)));
snprintf(dest, len, "(group %s)", sub1); PASS(errctx, aksl_snprintf(&written, dest, len, "(group %s)", sub1));
break; break;
case AKBASIC_LEAF_COMMAND: case AKBASIC_LEAF_COMMAND:
case AKBASIC_LEAF_COMMAND_IMMEDIATE: case AKBASIC_LEAF_COMMAND_IMMEDIATE:
@@ -437,10 +449,10 @@ akerr_ErrorContext *akbasic_leaf_to_string(akbasic_ASTLeaf *self, char *dest, si
* The reference falls through to Go's %+v struct dump here, which has no * The reference falls through to Go's %+v struct dump here, which has no
* useful C equivalent. Print something a test can assert on instead. * useful C equivalent. Print something a test can assert on instead.
*/ */
snprintf(dest, len, "(%s)", self->identifier); PASS(errctx, aksl_snprintf(&written, dest, len, "(%s)", self->identifier));
break; break;
default: default:
snprintf(dest, len, "(leaf %d)", (int)self->leaftype); PASS(errctx, aksl_snprintf(&written, dest, len, "(leaf %d)", (int)self->leaftype));
break; break;
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);

View File

@@ -12,9 +12,8 @@
* reaching for a global, which is exactly what goal 3 needs. * reaching for a global, which is exactly what goal 3 needs.
*/ */
#include <string.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h>
#include <akgl/draw.h> #include <akgl/draw.h>
#include <akgl/error.h> #include <akgl/error.h>
@@ -246,7 +245,7 @@ akerr_ErrorContext *akbasic_graphics_init_akgl(akbasic_GraphicsBackend *obj, akb
"NULL renderer in graphics_init_akgl: the host creates it, not this"); "NULL renderer in graphics_init_akgl: the host creates it, not this");
PASS(errctx, akgl_error_init()); PASS(errctx, akgl_error_init());
memset(state, 0, sizeof(*state)); PASS(errctx, aksl_memset(state, 0, sizeof(*state)));
state->renderer = renderer; state->renderer = renderer;
obj->self = state; obj->self = state;

View File

@@ -19,9 +19,9 @@
#include <inttypes.h> #include <inttypes.h>
#include <stdio.h> #include <stdio.h>
#include <string.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h>
#include <akbasic/error.h> #include <akbasic/error.h>
#include <akbasic/host.h> #include <akbasic/host.h>
@@ -89,8 +89,14 @@ akerr_ErrorContext *akbasic_host_read_field(akbasic_Runtime *obj, akbasic_Struct
* left it full: a BASIC string is inline and fixed, so there is nothing * left it full: a BASIC string is inline and fixed, so there is nothing
* to alias and nothing that can outlive the read. * to alias and nothing that can outlive the read.
*/ */
/*
* Raw snprintf: the host's bytes need not be terminated, and `%.*s`
* bounded by hostwidth is what reads at most that many of them. A field
* wider than the destination truncates, which is the documented contract
* for reading a host string into a fixed BASIC one. libakstdlib #34.
*/
snprintf(text, sizeof(text), "%.*s", (int)field->hostwidth, at); snprintf(text, sizeof(text), "%.*s", (int)field->hostwidth, at);
snprintf(dest->stringval, sizeof(dest->stringval), "%s", text); PASS(errctx, aksl_strcpy(dest->stringval, sizeof(dest->stringval), text));
break; break;
default: default:
FAIL_RETURN(errctx, AKBASIC_ERR_TYPE, FAIL_RETURN(errctx, AKBASIC_ERR_TYPE,
@@ -115,6 +121,7 @@ akerr_ErrorContext *akbasic_host_write_field(akbasic_Runtime *obj, akbasic_Struc
char *at = (char *)hostbase + field->hostoffset; char *at = (char *)hostbase + field->hostoffset;
int64_t n = 0; int64_t n = 0;
double d = 0.0; double d = 0.0;
size_t len = 0;
(void)obj; (void)obj;
FAIL_ZERO_RETURN(errctx, (hostbase != NULL && src != NULL), AKERR_NULLPOINTER, FAIL_ZERO_RETURN(errctx, (hostbase != NULL && src != NULL), AKERR_NULLPOINTER,
@@ -166,12 +173,13 @@ akerr_ErrorContext *akbasic_host_write_field(akbasic_Runtime *obj, akbasic_Struc
* eight of them. BASIC strings have no width, so this is the one place * eight of them. BASIC strings have no width, so this is the one place
* the two models genuinely disagree and the disagreement is reported. * the two models genuinely disagree and the disagreement is reported.
*/ */
FAIL_ZERO_RETURN(errctx, (strlen(src->stringval) < field->hostwidth), PASS(errctx, aksl_strlen(src->stringval, &len));
FAIL_ZERO_RETURN(errctx, (len < field->hostwidth),
AKBASIC_ERR_VALUE, AKBASIC_ERR_VALUE,
"A string of %zu characters does not fit in %s, which holds %zu", "A string of %zu characters does not fit in %s, which holds %zu",
strlen(src->stringval), field->name, field->hostwidth - 1); len, field->name, field->hostwidth - 1);
memset(at, 0, field->hostwidth); PASS(errctx, aksl_memset(at, 0, field->hostwidth));
memcpy(at, src->stringval, strlen(src->stringval)); PASS(errctx, aksl_memcpy(at, src->stringval, len));
break; break;
default: default:
FAIL_RETURN(errctx, AKBASIC_ERR_TYPE, FAIL_RETURN(errctx, AKBASIC_ERR_TYPE,
@@ -226,7 +234,16 @@ akerr_ErrorContext *akbasic_host_register_type(akbasic_Runtime *obj, const akbas
type->name, type->fieldcount, AKBASIC_MAX_STRUCT_FIELDS); type->name, type->fieldcount, AKBASIC_MAX_STRUCT_FIELDS);
dest = &obj->structtypes.types[obj->structtypes.count]; dest = &obj->structtypes.types[obj->structtypes.count];
memset(dest, 0, sizeof(*dest)); PASS(errctx, aksl_memset(dest, 0, sizeof(*dest)));
/*
* Raw snprintf, and a latent defect rather than a settled decision: a host
* type name over 31 characters truncates silently here, and two that share a
* 31-character prefix then collide in akbasic_structtype_find. scan_names()
* in structtype.c already refuses the same case for a script-declared type
* with an explicit limit message, so the two paths disagree. Converting this
* to aksl_strcpy is the fix and it is a behaviour change on a public
* registration call, so it wants its own issue rather than this port.
*/
snprintf(dest->name, sizeof(dest->name), "%s", type->name); snprintf(dest->name, sizeof(dest->name), "%s", type->name);
dest->used = true; dest->used = true;
dest->ishost = true; dest->ishost = true;
@@ -241,7 +258,7 @@ akerr_ErrorContext *akbasic_host_register_type(akbasic_Runtime *obj, const akbas
FAIL_ZERO_RETURN(errctx, (src->name != NULL), AKERR_NULLPOINTER, FAIL_ZERO_RETURN(errctx, (src->name != NULL), AKERR_NULLPOINTER,
"%s field %d has no name", type->name, i); "%s field %d has no name", type->name, i);
len = strlen(src->name); PASS(errctx, aksl_strlen(src->name, &len));
/* /*
* The suffix has to agree with the C type. A host that writes "HP%" over * The suffix has to agree with the C type. A host that writes "HP%" over
* an int32_t has said two different things about one field, and the * an int32_t has said two different things about one field, and the
@@ -253,6 +270,7 @@ akerr_ErrorContext *akbasic_host_register_type(akbasic_Runtime *obj, const akbas
"%s.%s must end in '%c' for the C type it describes", "%s.%s must end in '%c' for the C type it describes",
type->name, src->name, suffix_for(src->kind)); type->name, src->name, suffix_for(src->kind));
/* Same silent truncation as the type name above, and the same fix. */
snprintf(field->name, sizeof(field->name), "%s", src->name); snprintf(field->name, sizeof(field->name), "%s", src->name);
field->hostkind = src->kind; field->hostkind = src->kind;
field->hostoffset = src->offset; field->hostoffset = src->offset;
@@ -296,7 +314,7 @@ akerr_ErrorContext *akbasic_host_bind(akbasic_Runtime *obj, const char *name,
FAIL_ZERO_RETURN(errctx, (obj != NULL && name != NULL && typename_ != NULL && instance != NULL), FAIL_ZERO_RETURN(errctx, (obj != NULL && name != NULL && typename_ != NULL && instance != NULL),
AKERR_NULLPOINTER, "NULL argument in host bind"); AKERR_NULLPOINTER, "NULL argument in host bind");
len = strlen(name); PASS(errctx, aksl_strlen(name, &len));
FAIL_ZERO_RETURN(errctx, (len >= 2 && name[len - 1] == '@'), AKBASIC_ERR_VALUE, FAIL_ZERO_RETURN(errctx, (len >= 2 && name[len - 1] == '@'), AKBASIC_ERR_VALUE,
"A structure variable's name ends in '@', so \"%s\" cannot be bound", name); "A structure variable's name ends in '@', so \"%s\" cannot be bound", name);
PASS(errctx, akbasic_structtype_find(&obj->structtypes, typename_, &typeindex)); PASS(errctx, akbasic_structtype_find(&obj->structtypes, typename_, &typeindex));

View File

@@ -157,6 +157,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *run_akgl(FILE *program, const char *pa
const char *fontpath = getenv("AKBASIC_FONT"); const char *fontpath = getenv("AKBASIC_FONT");
FILE *input = program; FILE *input = program;
int mode = AKBASIC_MODE_RUNSTREAM; int mode = AKBASIC_MODE_RUNSTREAM;
int fd = 0;
if ( fontpath == NULL ) { if ( fontpath == NULL ) {
fontpath = AKBASIC_FONT_PATH; fontpath = AKBASIC_FONT_PATH;
@@ -169,7 +170,8 @@ static akerr_ErrorContext AKERR_NOIGNORE *run_akgl(FILE *program, const char *pa
* in an AKGL build, and the golden corpus can be driven through this * in an AKGL build, and the golden corpus can be driven through this
* binary as well as through the stdio one. * binary as well as through the stdio one.
*/ */
input = (isatty(fileno(stdin)) ? NULL : stdin); PASS(errctx, aksl_fileno(stdin, &fd));
input = (isatty(fd) ? NULL : stdin);
mode = AKBASIC_MODE_REPL; mode = AKBASIC_MODE_REPL;
} }

View File

@@ -10,9 +10,9 @@
*/ */
#include <inttypes.h> #include <inttypes.h>
#include <string.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h>
#include <akbasic/error.h> #include <akbasic/error.h>
#include <akbasic/parser.h> #include <akbasic/parser.h>
@@ -206,8 +206,7 @@ akerr_ErrorContext *akbasic_parser_command(akbasic_Parser *obj, akbasic_ASTLeaf
PASS(errctx, akbasic_parser_previous(obj, &operator_)); PASS(errctx, akbasic_parser_previous(obj, &operator_));
optype = operator_->tokentype; optype = operator_->tokentype;
strncpy(opname, operator_->lexeme, sizeof(opname) - 1); PASS(errctx, aksl_strcpy(opname, sizeof(opname), operator_->lexeme));
opname[sizeof(opname) - 1] = '\0';
/* Does this verb need its own parse path? */ /* Does this verb need its own parse path? */
PASS(errctx, akbasic_verb_lookup(opname, &verb)); PASS(errctx, akbasic_verb_lookup(opname, &verb));
@@ -596,8 +595,7 @@ static akerr_ErrorContext *function_call(akbasic_Parser *obj, akbasic_ASTLeaf **
} }
PASS(errctx, akbasic_parser_previous(obj, &operator_)); PASS(errctx, akbasic_parser_previous(obj, &operator_));
strncpy(fname, operator_->lexeme, sizeof(fname) - 1); PASS(errctx, aksl_strcpy(fname, sizeof(fname), operator_->lexeme));
fname[sizeof(fname) - 1] = '\0';
PASS(errctx, akbasic_verb_lookup(fname, &verb)); PASS(errctx, akbasic_verb_lookup(fname, &verb));
if ( verb != NULL && verb->tokentype == AKBASIC_TOK_FUNCTION ) { if ( verb != NULL && verb->tokentype == AKBASIC_TOK_FUNCTION ) {

View File

@@ -10,10 +10,9 @@
*/ */
#include <inttypes.h> #include <inttypes.h>
#include <string.h>
#include <strings.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h>
#include <akbasic/error.h> #include <akbasic/error.h>
#include <akbasic/parser.h> #include <akbasic/parser.h>
@@ -78,20 +77,23 @@ akerr_ErrorContext *akbasic_parse_graphic(akbasic_Parser *parser, akbasic_ASTLea
akbasic_ASTLeaf *arglist = NULL; akbasic_ASTLeaf *arglist = NULL;
akbasic_ASTLeaf *expr = NULL; akbasic_ASTLeaf *expr = NULL;
akbasic_Token *peeked = NULL; akbasic_Token *peeked = NULL;
int cmp = 0;
peeked = akbasic_parser_peek(parser); peeked = akbasic_parser_peek(parser);
if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND && if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND ) {
strcmp(peeked->lexeme, "CLR") == 0 ) { PASS(errctx, aksl_strcmp(peeked->lexeme, "CLR", &cmp));
(void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND); if ( cmp == 0 ) {
PASS(errctx, akbasic_parser_new_leaf(parser, &arglist)); (void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND);
arglist->leaftype = AKBASIC_LEAF_ARGUMENTLIST; PASS(errctx, akbasic_parser_new_leaf(parser, &arglist));
arglist->operator_ = AKBASIC_TOK_FUNCTION_ARGUMENT; arglist->leaftype = AKBASIC_LEAF_ARGUMENTLIST;
PASS(errctx, akbasic_parser_new_leaf(parser, &arglist->right)); arglist->operator_ = AKBASIC_TOK_FUNCTION_ARGUMENT;
PASS(errctx, akbasic_leaf_new_literal_int(arglist->right, "5")); PASS(errctx, akbasic_parser_new_leaf(parser, &arglist->right));
PASS(errctx, akbasic_parser_new_leaf(parser, &expr)); PASS(errctx, akbasic_leaf_new_literal_int(arglist->right, "5"));
PASS(errctx, akbasic_leaf_new_command(expr, "GRAPHIC", arglist)); PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
*dest = expr; PASS(errctx, akbasic_leaf_new_command(expr, "GRAPHIC", arglist));
SUCCEED_RETURN(errctx); *dest = expr;
SUCCEED_RETURN(errctx);
}
} }
PASS(errctx, akbasic_parse_arglist(parser, dest)); PASS(errctx, akbasic_parse_arglist(parser, dest));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
@@ -105,6 +107,7 @@ akerr_ErrorContext *akbasic_parse_draw(akbasic_Parser *parser, akbasic_ASTLeaf *
akbasic_ASTLeaf *tail = NULL; akbasic_ASTLeaf *tail = NULL;
akbasic_Token *peeked = NULL; akbasic_Token *peeked = NULL;
akbasic_Token *operator_ = NULL; akbasic_Token *operator_ = NULL;
int cmp = 0;
/* /*
* DRAW source, x1,y1 TO x2,y2 TO x3,y3 -- a polyline, with TO between pairs * DRAW source, x1,y1 TO x2,y2 TO x3,y3 -- a polyline, with TO between pairs
@@ -125,9 +128,11 @@ akerr_ErrorContext *akbasic_parse_draw(akbasic_Parser *parser, akbasic_ASTLeaf *
for ( ;; ) { for ( ;; ) {
peeked = akbasic_parser_peek(parser); peeked = akbasic_parser_peek(parser);
if ( peeked == NULL || if ( peeked == NULL || peeked->tokentype != AKBASIC_TOK_COMMAND ) {
peeked->tokentype != AKBASIC_TOK_COMMAND || break;
strcmp(peeked->lexeme, "TO") != 0 ) { }
PASS(errctx, aksl_strcmp(peeked->lexeme, "TO", &cmp));
if ( cmp != 0 ) {
break; break;
} }
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND), FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND),
@@ -259,6 +264,7 @@ static akerr_ErrorContext *parse_loop_condition(akbasic_Parser *parser, akbasic_
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
akbasic_Token *peeked = NULL; akbasic_Token *peeked = NULL;
int cmp = 0;
*condition = NULL; *condition = NULL;
*kind = AKBASIC_LOOPCOND_NONE; *kind = AKBASIC_LOOPCOND_NONE;
@@ -267,12 +273,15 @@ static akerr_ErrorContext *parse_loop_condition(akbasic_Parser *parser, akbasic_
if ( peeked == NULL || peeked->tokentype != AKBASIC_TOK_COMMAND ) { if ( peeked == NULL || peeked->tokentype != AKBASIC_TOK_COMMAND ) {
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
if ( strcmp(peeked->lexeme, "WHILE") == 0 ) { PASS(errctx, aksl_strcmp(peeked->lexeme, "WHILE", &cmp));
if ( cmp == 0 ) {
*kind = AKBASIC_LOOPCOND_WHILE; *kind = AKBASIC_LOOPCOND_WHILE;
} else if ( strcmp(peeked->lexeme, "UNTIL") == 0 ) {
*kind = AKBASIC_LOOPCOND_UNTIL;
} else { } else {
SUCCEED_RETURN(errctx); PASS(errctx, aksl_strcmp(peeked->lexeme, "UNTIL", &cmp));
if ( cmp != 0 ) {
SUCCEED_RETURN(errctx);
}
*kind = AKBASIC_LOOPCOND_UNTIL;
} }
(void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND); (void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND);
@@ -372,6 +381,7 @@ akerr_ErrorContext *akbasic_parse_on(akbasic_Parser *parser, akbasic_ASTLeaf **d
akbasic_ASTLeaf *expr = NULL; akbasic_ASTLeaf *expr = NULL;
akbasic_ASTLeaf *tail = NULL; akbasic_ASTLeaf *tail = NULL;
akbasic_Token *operator_ = NULL; akbasic_Token *operator_ = NULL;
int cmp = 0;
bool gosub = false; bool gosub = false;
PASS(errctx, akbasic_parser_new_leaf(parser, &arglist)); PASS(errctx, akbasic_parser_new_leaf(parser, &arglist));
@@ -389,10 +399,12 @@ akerr_ErrorContext *akbasic_parse_on(akbasic_Parser *parser, akbasic_ASTLeaf **d
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND), FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND),
AKBASIC_ERR_SYNTAX, "Expected GOTO or GOSUB after ON (expression)"); AKBASIC_ERR_SYNTAX, "Expected GOTO or GOSUB after ON (expression)");
PASS(errctx, akbasic_parser_previous(parser, &operator_)); PASS(errctx, akbasic_parser_previous(parser, &operator_));
if ( strcmp(operator_->lexeme, "GOSUB") == 0 ) { PASS(errctx, aksl_strcmp(operator_->lexeme, "GOSUB", &cmp));
if ( cmp == 0 ) {
gosub = true; gosub = true;
} else { } else {
FAIL_NONZERO_RETURN(errctx, strcmp(operator_->lexeme, "GOTO"), AKBASIC_ERR_SYNTAX, PASS(errctx, aksl_strcmp(operator_->lexeme, "GOTO", &cmp));
FAIL_NONZERO_RETURN(errctx, cmp, AKBASIC_ERR_SYNTAX,
"Expected GOTO or GOSUB after ON (expression)"); "Expected GOTO or GOSUB after ON (expression)");
} }
marker->literal_int = (gosub ? 1 : 0); marker->literal_int = (gosub ? 1 : 0);
@@ -429,14 +441,20 @@ akerr_ErrorContext *akbasic_parse_resume(akbasic_Parser *parser, akbasic_ASTLeaf
akbasic_ASTLeaf *expr = NULL; akbasic_ASTLeaf *expr = NULL;
akbasic_ASTLeaf *target = NULL; akbasic_ASTLeaf *target = NULL;
akbasic_Token *peeked = NULL; akbasic_Token *peeked = NULL;
int cmp = 0;
peeked = akbasic_parser_peek(parser); peeked = akbasic_parser_peek(parser);
if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND && if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND ) {
strcmp(peeked->lexeme, "NEXT") == 0 ) { PASS(errctx, aksl_strcmp(peeked->lexeme, "NEXT", &cmp));
(void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND); if ( cmp == 0 ) {
PASS(errctx, akbasic_parser_new_leaf(parser, &target)); (void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND);
PASS(errctx, akbasic_leaf_new_command(target, "NEXT", NULL)); PASS(errctx, akbasic_parser_new_leaf(parser, &target));
} else if ( peeked != NULL && peeked->tokentype != AKBASIC_TOK_COLON ) { PASS(errctx, akbasic_leaf_new_command(target, "NEXT", NULL));
}
}
/* The other two forms, taken only when the word was not NEXT -- which is
what a target already built says. */
if ( target == NULL && peeked != NULL && peeked->tokentype != AKBASIC_TOK_COLON ) {
PASS(errctx, akbasic_parser_expression(parser, &target)); PASS(errctx, akbasic_parser_expression(parser, &target));
} }
@@ -464,6 +482,8 @@ akerr_ErrorContext *akbasic_parse_print(akbasic_Parser *parser, akbasic_ASTLeaf
akbasic_ASTLeaf *arglist = NULL; akbasic_ASTLeaf *arglist = NULL;
akbasic_ASTLeaf *right = NULL; akbasic_ASTLeaf *right = NULL;
akbasic_Token *peeked = NULL; akbasic_Token *peeked = NULL;
int cmp = 0;
bool matched = false;
peeked = akbasic_parser_peek(parser); peeked = akbasic_parser_peek(parser);
@@ -499,8 +519,11 @@ akerr_ErrorContext *akbasic_parse_print(akbasic_Parser *parser, akbasic_ASTLeaf
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
if ( peeked == NULL || peeked->tokentype != AKBASIC_TOK_COMMAND || if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND ) {
strcmp(peeked->lexeme, "USING") != 0 ) { PASS(errctx, aksl_strcmp(peeked->lexeme, "USING", &cmp));
matched = (cmp == 0);
}
if ( !matched ) {
/* The ordinary PRINT: one expression, or none at all. */ /* The ordinary PRINT: one expression, or none at all. */
if ( peeked != NULL && peeked->tokentype != AKBASIC_TOK_COLON ) { if ( peeked != NULL && peeked->tokentype != AKBASIC_TOK_COLON ) {
PASS(errctx, akbasic_parser_expression(parser, &right)); PASS(errctx, akbasic_parser_expression(parser, &right));
@@ -627,12 +650,14 @@ akerr_ErrorContext *akbasic_parse_point(akbasic_Parser *parser, akbasic_ASTLeaf
akbasic_ASTLeaf *arglist = NULL; akbasic_ASTLeaf *arglist = NULL;
akbasic_ASTLeaf *command = NULL; akbasic_ASTLeaf *command = NULL;
akbasic_Token *word = NULL; akbasic_Token *word = NULL;
int cmp = 0;
PASS(errctx, akbasic_parser_expression(parser, &pointer)); PASS(errctx, akbasic_parser_expression(parser, &pointer));
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER), FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER),
AKBASIC_ERR_SYNTAX, "Expected POINT <pointer> AT <structure>"); AKBASIC_ERR_SYNTAX, "Expected POINT <pointer> AT <structure>");
PASS(errctx, akbasic_parser_previous(parser, &word)); PASS(errctx, akbasic_parser_previous(parser, &word));
FAIL_ZERO_RETURN(errctx, (strcasecmp(word->lexeme, "AT") == 0), AKBASIC_ERR_SYNTAX, PASS(errctx, aksl_strcasecmp(word->lexeme, "AT", &cmp));
FAIL_ZERO_RETURN(errctx, (cmp == 0), AKBASIC_ERR_SYNTAX,
"Expected AT after POINT <pointer>, not %s", word->lexeme); "Expected AT after POINT <pointer>, not %s", word->lexeme);
PASS(errctx, akbasic_parser_expression(parser, &target)); PASS(errctx, akbasic_parser_expression(parser, &target));
@@ -668,6 +693,7 @@ akerr_ErrorContext *akbasic_parse_dim(akbasic_Parser *parser, akbasic_ASTLeaf **
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
akbasic_ASTLeaf *command = NULL; akbasic_ASTLeaf *command = NULL;
akbasic_Token *word = NULL; akbasic_Token *word = NULL;
int cmp = 0;
PASS(errctx, parse_verb_with_identifier(parser, "DIM", dest)); PASS(errctx, parse_verb_with_identifier(parser, "DIM", dest));
command = *dest; command = *dest;
@@ -676,13 +702,15 @@ akerr_ErrorContext *akbasic_parse_dim(akbasic_Parser *parser, akbasic_ASTLeaf **
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
PASS(errctx, akbasic_parser_previous(parser, &word)); PASS(errctx, akbasic_parser_previous(parser, &word));
FAIL_ZERO_RETURN(errctx, (strcasecmp(word->lexeme, "AS") == 0), AKBASIC_ERR_SYNTAX, PASS(errctx, aksl_strcasecmp(word->lexeme, "AS", &cmp));
FAIL_ZERO_RETURN(errctx, (cmp == 0), AKBASIC_ERR_SYNTAX,
"Expected AS after DIM %s, not %s", command->right->identifier, word->lexeme); "Expected AS after DIM %s, not %s", command->right->identifier, word->lexeme);
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER), FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER),
AKBASIC_ERR_SYNTAX, "Expected a type name after AS"); AKBASIC_ERR_SYNTAX, "Expected a type name after AS");
PASS(errctx, akbasic_parser_previous(parser, &word)); PASS(errctx, akbasic_parser_previous(parser, &word));
if ( strcasecmp(word->lexeme, "PTR") == 0 ) { PASS(errctx, aksl_strcasecmp(word->lexeme, "PTR", &cmp));
if ( cmp == 0 ) {
command->literal_int = 1; command->literal_int = 1;
/* /*
* `TO` is already a verb -- FOR ... TO owns it -- so it arrives as a * `TO` is already a verb -- FOR ... TO owns it -- so it arrives as a
@@ -694,13 +722,14 @@ akerr_ErrorContext *akbasic_parse_dim(akbasic_Parser *parser, akbasic_ASTLeaf **
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND), FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND),
AKBASIC_ERR_SYNTAX, "Expected TO after PTR"); AKBASIC_ERR_SYNTAX, "Expected TO after PTR");
PASS(errctx, akbasic_parser_previous(parser, &word)); PASS(errctx, akbasic_parser_previous(parser, &word));
FAIL_ZERO_RETURN(errctx, (strcasecmp(word->lexeme, "TO") == 0), AKBASIC_ERR_SYNTAX, PASS(errctx, aksl_strcasecmp(word->lexeme, "TO", &cmp));
FAIL_ZERO_RETURN(errctx, (cmp == 0), AKBASIC_ERR_SYNTAX,
"Expected PTR TO TYPENAME, not PTR %s", word->lexeme); "Expected PTR TO TYPENAME, not PTR %s", word->lexeme);
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER), FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER),
AKBASIC_ERR_SYNTAX, "Expected a type name after PTR TO"); AKBASIC_ERR_SYNTAX, "Expected a type name after PTR TO");
PASS(errctx, akbasic_parser_previous(parser, &word)); PASS(errctx, akbasic_parser_previous(parser, &word));
} }
snprintf(command->literal_string, sizeof(command->literal_string), "%s", word->lexeme); PASS(errctx, aksl_strcpy(command->literal_string, sizeof(command->literal_string), word->lexeme));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -738,6 +767,7 @@ static akerr_ErrorContext *parse_def_parameters(akbasic_Parser *parser, akbasic_
akbasic_ASTLeaf *param = NULL; akbasic_ASTLeaf *param = NULL;
akbasic_ASTLeaf *tail = NULL; akbasic_ASTLeaf *tail = NULL;
akbasic_Token *word = NULL; akbasic_Token *word = NULL;
int cmp = 0;
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_LEFT_PAREN), FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_LEFT_PAREN),
AKBASIC_ERR_SYNTAX, "Expected an argument list after DEF <name>"); AKBASIC_ERR_SYNTAX, "Expected an argument list after DEF <name>");
@@ -762,25 +792,28 @@ static akerr_ErrorContext *parse_def_parameters(akbasic_Parser *parser, akbasic_
"%s must name its type: %s AS TYPENAME, or %s AS PTR TO TYPENAME", "%s must name its type: %s AS TYPENAME, or %s AS PTR TO TYPENAME",
param->identifier, param->identifier, param->identifier); param->identifier, param->identifier, param->identifier);
PASS(errctx, akbasic_parser_previous(parser, &word)); PASS(errctx, akbasic_parser_previous(parser, &word));
FAIL_ZERO_RETURN(errctx, (strcasecmp(word->lexeme, "AS") == 0), AKBASIC_ERR_SYNTAX, PASS(errctx, aksl_strcasecmp(word->lexeme, "AS", &cmp));
FAIL_ZERO_RETURN(errctx, (cmp == 0), AKBASIC_ERR_SYNTAX,
"Expected AS after %s, not %s", param->identifier, word->lexeme); "Expected AS after %s, not %s", param->identifier, word->lexeme);
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER), FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER),
AKBASIC_ERR_SYNTAX, "Expected a type name after AS"); AKBASIC_ERR_SYNTAX, "Expected a type name after AS");
PASS(errctx, akbasic_parser_previous(parser, &word)); PASS(errctx, akbasic_parser_previous(parser, &word));
if ( strcasecmp(word->lexeme, "PTR") == 0 ) { PASS(errctx, aksl_strcasecmp(word->lexeme, "PTR", &cmp));
if ( cmp == 0 ) {
param->literal_int = 1; param->literal_int = 1;
/* `TO` is already a verb -- FOR ... TO owns it -- so it arrives /* `TO` is already a verb -- FOR ... TO owns it -- so it arrives
as a command token rather than an identifier. */ as a command token rather than an identifier. */
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND), FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND),
AKBASIC_ERR_SYNTAX, "Expected TO after PTR"); AKBASIC_ERR_SYNTAX, "Expected TO after PTR");
PASS(errctx, akbasic_parser_previous(parser, &word)); PASS(errctx, akbasic_parser_previous(parser, &word));
FAIL_ZERO_RETURN(errctx, (strcasecmp(word->lexeme, "TO") == 0), AKBASIC_ERR_SYNTAX, PASS(errctx, aksl_strcasecmp(word->lexeme, "TO", &cmp));
FAIL_ZERO_RETURN(errctx, (cmp == 0), AKBASIC_ERR_SYNTAX,
"Expected PTR TO TYPENAME, not PTR %s", word->lexeme); "Expected PTR TO TYPENAME, not PTR %s", word->lexeme);
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER), FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_IDENTIFIER),
AKBASIC_ERR_SYNTAX, "Expected a type name after PTR TO"); AKBASIC_ERR_SYNTAX, "Expected a type name after PTR TO");
PASS(errctx, akbasic_parser_previous(parser, &word)); PASS(errctx, akbasic_parser_previous(parser, &word));
} }
snprintf(param->literal_string, sizeof(param->literal_string), "%s", word->lexeme); PASS(errctx, aksl_strcpy(param->literal_string, sizeof(param->literal_string), word->lexeme));
} }
if ( tail == NULL ) { if ( tail == NULL ) {
@@ -806,6 +839,7 @@ akerr_ErrorContext *akbasic_parse_def(akbasic_Parser *parser, akbasic_ASTLeaf **
akbasic_ASTLeaf *expression = NULL; akbasic_ASTLeaf *expression = NULL;
akbasic_ASTLeaf *command = NULL; akbasic_ASTLeaf *command = NULL;
akbasic_FunctionDef *fndef = NULL; akbasic_FunctionDef *fndef = NULL;
size_t namelen = 0;
size_t i = 0; size_t i = 0;
PASS(errctx, akbasic_parser_primary(parser, &identifier)); PASS(errctx, akbasic_parser_primary(parser, &identifier));
@@ -817,13 +851,14 @@ akerr_ErrorContext *akbasic_parse_def(akbasic_Parser *parser, akbasic_ASTLeaf **
PASS(errctx, akbasic_runtime_new_function(runtime, &fndef)); PASS(errctx, akbasic_runtime_new_function(runtime, &fndef));
/* Uppercase the name: verbs and function names are case-insensitive. */ /* Uppercase the name: verbs and function names are case-insensitive. */
FAIL_ZERO_RETURN(errctx, (strlen(identifier->identifier) < sizeof(fndef->name)), PASS(errctx, aksl_strlen(identifier->identifier, &namelen));
FAIL_ZERO_RETURN(errctx, (namelen < sizeof(fndef->name)),
AKBASIC_ERR_BOUNDS, "Function name '%s' is too long", identifier->identifier); AKBASIC_ERR_BOUNDS, "Function name '%s' is too long", identifier->identifier);
for ( i = 0; i < strlen(identifier->identifier); i++ ) { for ( i = 0; i < namelen; i++ ) {
char c = identifier->identifier[i]; char c = identifier->identifier[i];
fndef->name[i] = (char)((c >= 'a' && c <= 'z') ? (c - 'a' + 'A') : c); fndef->name[i] = (char)((c >= 'a' && c <= 'z') ? (c - 'a' + 'A') : c);
} }
fndef->name[strlen(identifier->identifier)] = '\0'; fndef->name[namelen] = '\0';
if ( akbasic_parser_match1(parser, AKBASIC_TOK_ASSIGNMENT) ) { if ( akbasic_parser_match1(parser, AKBASIC_TOK_ASSIGNMENT) ) {
PASS(errctx, akbasic_parser_expression(parser, &expression)); PASS(errctx, akbasic_parser_expression(parser, &expression));
@@ -866,13 +901,15 @@ akerr_ErrorContext *akbasic_parse_for(akbasic_Parser *parser, akbasic_ASTLeaf **
akbasic_Environment *parent = runtime->environment; akbasic_Environment *parent = runtime->environment;
akbasic_Environment *newenv = NULL; akbasic_Environment *newenv = NULL;
int64_t firstline = 0; int64_t firstline = 0;
int cmp = 0;
PASS(errctx, akbasic_parser_assignment(parser, &assignment)); PASS(errctx, akbasic_parser_assignment(parser, &assignment));
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND), FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND),
AKBASIC_ERR_SYNTAX, AKBASIC_ERR_SYNTAX,
"Expected FOR (assignment) TO (expression) [STEP (expression)]"); "Expected FOR (assignment) TO (expression) [STEP (expression)]");
PASS(errctx, akbasic_parser_previous(parser, &operator_)); PASS(errctx, akbasic_parser_previous(parser, &operator_));
FAIL_NONZERO_RETURN(errctx, strcmp(operator_->lexeme, "TO"), AKBASIC_ERR_SYNTAX, PASS(errctx, aksl_strcmp(operator_->lexeme, "TO", &cmp));
FAIL_NONZERO_RETURN(errctx, cmp, AKBASIC_ERR_SYNTAX,
"Expected FOR (assignment) TO (expression) [STEP (expression)]"); "Expected FOR (assignment) TO (expression) [STEP (expression)]");
FAIL_ZERO_RETURN(errctx, FAIL_ZERO_RETURN(errctx,
(assignment != NULL && akbasic_leaf_is_identifier(assignment->left)), (assignment != NULL && akbasic_leaf_is_identifier(assignment->left)),
@@ -894,7 +931,8 @@ akerr_ErrorContext *akbasic_parse_for(akbasic_Parser *parser, akbasic_ASTLeaf **
if ( akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND) ) { if ( akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND) ) {
PASS(errctx, akbasic_parser_previous(parser, &operator_)); PASS(errctx, akbasic_parser_previous(parser, &operator_));
FAIL_NONZERO_RETURN(errctx, strcmp(operator_->lexeme, "STEP"), AKBASIC_ERR_SYNTAX, PASS(errctx, aksl_strcmp(operator_->lexeme, "STEP", &cmp));
FAIL_NONZERO_RETURN(errctx, cmp, AKBASIC_ERR_SYNTAX,
"Expected FOR (assignment) TO (expression) [STEP (expression)]"); "Expected FOR (assignment) TO (expression) [STEP (expression)]");
PASS(errctx, akbasic_parser_expression(parser, &newenv->forStepLeaf)); PASS(errctx, akbasic_parser_expression(parser, &newenv->forStepLeaf));
} else { } else {
@@ -907,7 +945,8 @@ akerr_ErrorContext *akbasic_parse_for(akbasic_Parser *parser, akbasic_ASTLeaf **
} }
/* A NEXT already being awaited means this is an inner loop over the same variable. */ /* A NEXT already being awaited means this is an inner loop over the same variable. */
if ( strcmp(parent->waitingForCommand, "NEXT") == 0 ) { PASS(errctx, aksl_strcmp(parent->waitingForCommand, "NEXT", &cmp));
if ( cmp == 0 ) {
newenv->forNextVariable = parent->forNextVariable; newenv->forNextVariable = parent->forNextVariable;
} }
newenv->loopFirstLine = firstline; newenv->loopFirstLine = firstline;
@@ -1038,6 +1077,7 @@ akerr_ErrorContext *akbasic_parse_if(akbasic_Parser *parser, akbasic_ASTLeaf **d
akbasic_ASTLeaf *else_command = NULL; akbasic_ASTLeaf *else_command = NULL;
akbasic_ASTLeaf *branch = NULL; akbasic_ASTLeaf *branch = NULL;
akbasic_Token *operator_ = NULL; akbasic_Token *operator_ = NULL;
int cmp = 0;
/* /*
* Everything from here to the THEN is a condition, so a lone `=` in it is an * Everything from here to the THEN is a condition, so a lone `=` in it is an
@@ -1057,14 +1097,16 @@ akerr_ErrorContext *akbasic_parse_if(akbasic_Parser *parser, akbasic_ASTLeaf **d
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND), FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND),
AKBASIC_ERR_SYNTAX, "Incomplete IF statement"); AKBASIC_ERR_SYNTAX, "Incomplete IF statement");
PASS(errctx, akbasic_parser_previous(parser, &operator_)); PASS(errctx, akbasic_parser_previous(parser, &operator_));
FAIL_NONZERO_RETURN(errctx, strcmp(operator_->lexeme, "THEN"), AKBASIC_ERR_SYNTAX, PASS(errctx, aksl_strcmp(operator_->lexeme, "THEN", &cmp));
FAIL_NONZERO_RETURN(errctx, cmp, AKBASIC_ERR_SYNTAX,
"Expected IF ... THEN"); "Expected IF ... THEN");
PASS(errctx, akbasic_parser_command(parser, &then_command)); PASS(errctx, akbasic_parser_command(parser, &then_command));
if ( akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND) ) { if ( akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND) ) {
PASS(errctx, akbasic_parser_previous(parser, &operator_)); PASS(errctx, akbasic_parser_previous(parser, &operator_));
FAIL_NONZERO_RETURN(errctx, strcmp(operator_->lexeme, "ELSE"), AKBASIC_ERR_SYNTAX, PASS(errctx, aksl_strcmp(operator_->lexeme, "ELSE", &cmp));
FAIL_NONZERO_RETURN(errctx, cmp, AKBASIC_ERR_SYNTAX,
"Expected IF ... THEN ... ELSE ..."); "Expected IF ... THEN ... ELSE ...");
PASS(errctx, akbasic_parser_command(parser, &else_command)); PASS(errctx, akbasic_parser_command(parser, &else_command));
} }

View File

@@ -15,9 +15,9 @@
*/ */
#include <ctype.h> #include <ctype.h>
#include <string.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h>
#include <akbasic/audio.h> #include <akbasic/audio.h>
#include <akbasic/error.h> #include <akbasic/error.h>
@@ -96,7 +96,7 @@ akerr_ErrorContext *akbasic_play_parse(akbasic_Runtime *obj, const char *notes)
FAIL_ZERO_RETURN(errctx, (obj != NULL && notes != NULL), AKERR_NULLPOINTER, FAIL_ZERO_RETURN(errctx, (obj != NULL && notes != NULL), AKERR_NULLPOINTER,
"NULL argument in play_parse"); "NULL argument in play_parse");
audio = &obj->audio_state; audio = &obj->audio_state;
len = strlen(notes); PASS(errctx, aksl_strlen(notes, &len));
/* /*
* A loop, so no CATCH and no _BREAK macros in here -- they expand to a C * A loop, so no CATCH and no _BREAK macros in here -- they expand to a C

View File

@@ -27,10 +27,9 @@
#include <inttypes.h> #include <inttypes.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h>
#include <akbasic/error.h> #include <akbasic/error.h>
#include <akbasic/runtime.h> #include <akbasic/runtime.h>
@@ -103,7 +102,7 @@ static akerr_ErrorContext *rewrite_targets(akbasic_TargetWalk *walk, const char
for ( ;; ) { for ( ;; ) {
char replacement[32]; char replacement[32];
int64_t line = 0; int64_t line = 0;
int written = 0; size_t written = 0;
while ( *p == ' ' || *p == '\t' ) { while ( *p == ' ' || *p == '\t' ) {
FAIL_ZERO_RETURN(errctx, (out < limit), AKBASIC_ERR_BOUNDS, FAIL_ZERO_RETURN(errctx, (out < limit), AKBASIC_ERR_BOUNDS,
@@ -126,9 +125,16 @@ static akerr_ErrorContext *rewrite_targets(akbasic_TargetWalk *walk, const char
p += 1; p += 1;
} }
PASS(errctx, walk->visit(walk, line, replacement, sizeof(replacement))); PASS(errctx, walk->visit(walk, line, replacement, sizeof(replacement)));
written = snprintf(out, (size_t)(limit - out), "%s", replacement); /*
* The fit is decided before the copy rather than read back out of a
* return value afterwards: aksl_strcpy refuses rather than truncates,
* so the bounds check has to come first to keep the diagnosis this
* function already gives.
*/
PASS(errctx, aksl_strlen(replacement, &written));
FAIL_ZERO_RETURN(errctx, (written > 0 && out + written < limit), AKBASIC_ERR_BOUNDS, FAIL_ZERO_RETURN(errctx, (written > 0 && out + written < limit), AKBASIC_ERR_BOUNDS,
"RENUMBER: a rewritten line does not fit"); "RENUMBER: a rewritten line does not fit");
PASS(errctx, aksl_strcpy(out, (size_t)(limit - out), replacement));
out += written; out += written;
/* /*
@@ -176,6 +182,7 @@ static akerr_ErrorContext *rewrite_line(akbasic_TargetWalk *walk, const char *co
const char *limit = dest + len - 1; const char *limit = dest + len - 1;
bool statementstart = true; bool statementstart = true;
bool instring = false; bool instring = false;
int cmp = 0;
int i = 0; int i = 0;
while ( *p != '\0' ) { while ( *p != '\0' ) {
@@ -227,12 +234,12 @@ static akerr_ErrorContext *rewrite_line(akbasic_TargetWalk *walk, const char *co
* name in the language to reach it. * name in the language to reach it.
*/ */
for ( i = 0; i < BRANCH_VERB_COUNT; i++ ) { for ( i = 0; i < BRANCH_VERB_COUNT; i++ ) {
verblen = strlen(BRANCH_VERBS[i]); PASS(errctx, aksl_strlen(BRANCH_VERBS[i], &verblen));
if ( p > code && isalnum((unsigned char)p[-1]) ) { if ( p > code && isalnum((unsigned char)p[-1]) ) {
break; break;
} }
if ( strncasecmp(p, BRANCH_VERBS[i], verblen) == 0 && PASS(errctx, aksl_strncasecmp(p, BRANCH_VERBS[i], verblen, &cmp));
!isalnum((unsigned char)p[verblen]) ) { if ( cmp == 0 && !isalnum((unsigned char)p[verblen]) ) {
matched = true; matched = true;
break; break;
} }
@@ -247,7 +254,7 @@ static akerr_ErrorContext *rewrite_line(akbasic_TargetWalk *walk, const char *co
if ( matched ) { if ( matched ) {
FAIL_ZERO_RETURN(errctx, (out + verblen < (size_t)(limit - dest) + dest), FAIL_ZERO_RETURN(errctx, (out + verblen < (size_t)(limit - dest) + dest),
AKBASIC_ERR_BOUNDS, "RENUMBER: a rewritten line does not fit"); AKBASIC_ERR_BOUNDS, "RENUMBER: a rewritten line does not fit");
memcpy(out, p, verblen); PASS(errctx, aksl_memcpy(out, p, verblen));
out += verblen; out += verblen;
p += verblen; p += verblen;
PASS(errctx, rewrite_targets(walk, &p, &out, limit)); PASS(errctx, rewrite_targets(walk, &p, &out, limit));
@@ -260,9 +267,10 @@ static akerr_ErrorContext *rewrite_line(akbasic_TargetWalk *walk, const char *co
* cannot be pointed at it directly. Copy the type and the comma, then * cannot be pointed at it directly. Copy the type and the comma, then
* rewrite what follows. * rewrite what follows.
*/ */
if ( strncasecmp(p, "COLLISION", 9) == 0 && !isalnum((unsigned char)p[9]) PASS(errctx, aksl_strncasecmp(p, "COLLISION", 9, &cmp));
if ( cmp == 0 && !isalnum((unsigned char)p[9])
&& !(p > code && isalnum((unsigned char)p[-1])) ) { && !(p > code && isalnum((unsigned char)p[-1])) ) {
memcpy(out, p, 9); PASS(errctx, aksl_memcpy(out, p, 9));
out += 9; out += 9;
p += 9; p += 9;
while ( *p != '\0' && *p != ',' && *p != ':' ) { while ( *p != '\0' && *p != ',' && *p != ':' ) {
@@ -299,8 +307,9 @@ static akerr_ErrorContext *visit_renumber(akbasic_TargetWalk *walk, int64_t targ
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
const int64_t *map = (const int64_t *)walk->self; const int64_t *map = (const int64_t *)walk->self;
int written = 0;
snprintf(dest, len, "%" PRId64, mapped(map, target)); PASS(errctx, aksl_snprintf(&written, dest, len, "%" PRId64, mapped(map, target)));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -347,7 +356,7 @@ akerr_ErrorContext *akbasic_renumber(akbasic_Runtime *obj, int64_t newstart, int
next += increment; next += increment;
} }
memset(rewritten, 0, sizeof(rewritten)); PASS(errctx, aksl_memset(rewritten, 0, sizeof(rewritten)));
for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) { for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) {
int64_t target = 0; int64_t target = 0;
@@ -371,7 +380,7 @@ akerr_ErrorContext *akbasic_renumber(akbasic_Runtime *obj, int64_t newstart, int
rewritten[target].numbered = true; rewritten[target].numbered = true;
} }
memcpy(obj->source, rewritten, sizeof(obj->source)); PASS(errctx, aksl_memcpy(obj->source, rewritten, sizeof(obj->source)));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -402,8 +411,9 @@ static akerr_ErrorContext *visit_check(akbasic_TargetWalk *walk, int64_t target,
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
CheckState *state = (CheckState *)walk->self; CheckState *state = (CheckState *)walk->self;
int written = 0;
snprintf(dest, len, "%" PRId64, target); PASS(errctx, aksl_snprintf(&written, dest, len, "%" PRId64, target));
if ( target >= 0 && target < AKBASIC_MAX_SOURCE_LINES ) { if ( target >= 0 && target < AKBASIC_MAX_SOURCE_LINES ) {
FAIL_NONZERO_RETURN(errctx, FAIL_NONZERO_RETURN(errctx,
(state->runtime->source[target].code[0] != '\0' (state->runtime->source[target].code[0] != '\0'

View File

@@ -6,10 +6,9 @@
#include <ctype.h> #include <ctype.h>
#include <inttypes.h> #include <inttypes.h>
#include <stdio.h> #include <stdio.h>
#include <string.h>
#include <strings.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h>
#include <akbasic/error.h> #include <akbasic/error.h>
#include <akbasic/parser.h> #include <akbasic/parser.h>
@@ -28,7 +27,7 @@ akerr_ErrorContext *akbasic_runtime_new_variable(akbasic_Runtime *obj, akbasic_V
"NULL argument in new_variable"); "NULL argument in new_variable");
for ( i = 0; i < AKBASIC_MAX_VARIABLES; i++ ) { for ( i = 0; i < AKBASIC_MAX_VARIABLES; i++ ) {
if ( !obj->variables[i].used ) { if ( !obj->variables[i].used ) {
memset(&obj->variables[i], 0, sizeof(obj->variables[i])); PASS(errctx, aksl_memset(&obj->variables[i], 0, sizeof(obj->variables[i])));
obj->variables[i].used = true; obj->variables[i].used = true;
/* /*
* Not zero: zero is a valid structure type index, so a memset alone * Not zero: zero is a valid structure type index, so a memset alone
@@ -102,7 +101,7 @@ akerr_ErrorContext *akbasic_runtime_new_function(akbasic_Runtime *obj, akbasic_F
"NULL argument in new_function"); "NULL argument in new_function");
for ( i = 0; i < AKBASIC_MAX_FUNCTIONS; i++ ) { for ( i = 0; i < AKBASIC_MAX_FUNCTIONS; i++ ) {
if ( !obj->functions[i].used ) { if ( !obj->functions[i].used ) {
memset(&obj->functions[i], 0, sizeof(obj->functions[i])); PASS(errctx, aksl_memset(&obj->functions[i], 0, sizeof(obj->functions[i])));
obj->functions[i].used = true; obj->functions[i].used = true;
obj->functions[i].leafpool.next = 0; obj->functions[i].leafpool.next = 0;
obj->functions[i].leafpool.capacity = AKBASIC_MAX_LEAVES * 2; obj->functions[i].leafpool.capacity = AKBASIC_MAX_LEAVES * 2;
@@ -215,7 +214,7 @@ akerr_ErrorContext *akbasic_runtime_init(akbasic_Runtime *obj, akbasic_TextSink
*/ */
PASS(errctx, akbasic_error_register()); PASS(errctx, akbasic_error_register());
memset(obj, 0, sizeof(*obj)); PASS(errctx, aksl_memset(obj, 0, sizeof(*obj)));
obj->sink = sink; obj->sink = sink;
obj->environment = NULL; obj->environment = NULL;
obj->autoLineNumber = 0; obj->autoLineNumber = 0;
@@ -279,7 +278,7 @@ akerr_ErrorContext *akbasic_runtime_set_ui(akbasic_Runtime *obj, akbasic_UiBacke
akerr_ErrorContext *akbasic_runtime_set_source_path(akbasic_Runtime *obj, const char *path) akerr_ErrorContext *akbasic_runtime_set_source_path(akbasic_Runtime *obj, const char *path)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
const char *slash = NULL; char *slash = NULL;
size_t length = 0; size_t length = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in set_source_path"); FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in set_source_path");
@@ -292,18 +291,18 @@ akerr_ErrorContext *akbasic_runtime_set_source_path(akbasic_Runtime *obj, const
* would do it but it is allowed to modify its argument and two of the three * would do it but it is allowed to modify its argument and two of the three
* libcs this has to build on disagree about which one they implement. * libcs this has to build on disagree about which one they implement.
*/ */
slash = strrchr(path, '/'); PASS(errctx, aksl_strrchr(path, '/', &slash));
length = (slash == NULL ? 0 : (size_t)(slash - path)); length = (slash == NULL ? 0 : (size_t)(slash - path));
if ( length == 0 ) { if ( length == 0 ) {
/* Either no directory at all, or the root. */ /* Either no directory at all, or the root. */
strncpy(obj->sourcepath, (slash == NULL ? "." : "/"), sizeof(obj->sourcepath) - 1); PASS(errctx, aksl_strcpy(obj->sourcepath, sizeof(obj->sourcepath),
obj->sourcepath[sizeof(obj->sourcepath) - 1] = '\0'; (slash == NULL ? "." : "/")));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
FAIL_ZERO_RETURN(errctx, (length < sizeof(obj->sourcepath)), AKBASIC_ERR_BOUNDS, FAIL_ZERO_RETURN(errctx, (length < sizeof(obj->sourcepath)), AKBASIC_ERR_BOUNDS,
"Program path of %zu characters exceeds the %d character limit", "Program path of %zu characters exceeds the %d character limit",
length, AKBASIC_MAX_LINE_LENGTH - 1); length, AKBASIC_MAX_SOURCE_PATH_LENGTH - 1);
memcpy(obj->sourcepath, path, length); PASS(errctx, aksl_memcpy(obj->sourcepath, path, length));
obj->sourcepath[length] = '\0'; obj->sourcepath[length] = '\0';
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -393,6 +392,16 @@ akerr_ErrorContext *akbasic_runtime_error(akbasic_Runtime *obj, akbasic_ErrorCla
* The format, the trailing \n inside the string, and the second newline * The format, the trailing \n inside the string, and the second newline
* writeln adds are all part of the acceptance contract -- * writeln adds are all part of the acceptance contract --
* tests/language/array_outofbounds.txt ends in 0a 0a. See TODO.md 1.8. * tests/language/array_outofbounds.txt ends in 0a 0a. See TODO.md 1.8.
*
* **Raw snprintf on purpose, and this is the site where it matters most.**
* `line` is 512 bytes; `message` arrives from an akerr_ErrorContext, whose
* own buffer is AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH -- 12384. An ordinary
* long diagnostic therefore truncates, and truncating a report is correct
* here: this is the one function that tells the user *what went wrong*, and
* aksl_snprintf would turn a long message into a second, different failure
* that replaces the first. A report may be shortened; it may not be lost.
* libakstdlib #34 is the issue tracking the contract that would let a caller
* ask for the length instead.
*/ */
snprintf(line, sizeof(line), "? %" PRId64 " : %s %s\n", snprintf(line, sizeof(line), "? %" PRId64 " : %s %s\n",
obj->environment->lineno, errclass_to_string(errclass), message); obj->environment->lineno, errclass_to_string(errclass), message);
@@ -469,7 +478,15 @@ akerr_ErrorContext *akbasic_runtime_set_mode(akbasic_Runtime *obj, int mode)
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE_DEFAULT(errctx) { } HANDLE_DEFAULT(errctx) {
char message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH]; char message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH];
snprintf(message, sizeof(message), "%s", errctx->message); /*
* Ignored rather than propagated: `errctx` is the error being
* handled, so a PASS here would overwrite it with the copy's own
* failure and the next line would read a released context. Same
* reason the two calls below are ignored. aksl_strcpy empties the
* destination before it copies, so a refusal leaves an empty
* message rather than an uninitialised one.
*/
IGNORE(aksl_strcpy(message, sizeof(message), errctx->message));
obj->lasterrorstatus = errctx->status; obj->lasterrorstatus = errctx->status;
IGNORE(akbasic_runtime_error(obj, AKBASIC_ERRCLASS_PARSE, message)); IGNORE(akbasic_runtime_error(obj, AKBASIC_ERRCLASS_PARSE, message));
IGNORE(akbasic_runtime_set_mode(obj, obj->run_finished_mode)); IGNORE(akbasic_runtime_set_mode(obj, obj->run_finished_mode));
@@ -492,7 +509,14 @@ static akerr_ErrorContext *report_and_reraise(akbasic_Runtime *obj, akerr_ErrorC
char message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH]; char message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH];
int status = cause->status; int status = cause->status;
snprintf(message, sizeof(message), "%s", cause->message); /*
* Ignored rather than passed: this function's whole contract is that the
* program's own error is the one that leaves, so a failure to copy the
* message must not become the error that gets raised. aksl_strcpy empties
* the destination first, so a refusal reports an empty message rather than
* an uninitialised one.
*/
IGNORE(aksl_strcpy(message, sizeof(message), cause->message));
/* What ER# reports, if a TRAP is armed. Recorded before the context goes. */ /* What ER# reports, if a TRAP is armed. Recorded before the context goes. */
obj->lasterrorstatus = status; obj->lasterrorstatus = status;
cause->handled = true; cause->handled = true;
@@ -675,6 +699,7 @@ akerr_ErrorContext *akbasic_runtime_evaluate(akbasic_Runtime *obj, akbasic_ASTLe
akbasic_Value *rval = NULL; akbasic_Value *rval = NULL;
akbasic_Value *scratch = NULL; akbasic_Value *scratch = NULL;
const akbasic_Verb *verb = NULL; const akbasic_Verb *verb = NULL;
int cmp = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
"NULL argument in evaluate"); "NULL argument in evaluate");
@@ -724,9 +749,11 @@ akerr_ErrorContext *akbasic_runtime_evaluate(akbasic_Runtime *obj, akbasic_ASTLe
* reaches the end of this line. Arming the wait is what makes a * reaches the end of this line. Arming the wait is what makes a
* multi-line IF possible at all; BEND clears it. * multi-line IF possible at all; BEND clears it.
*/ */
if ( notaken != NULL && notaken->leaftype == AKBASIC_LEAF_COMMAND && if ( notaken != NULL && notaken->leaftype == AKBASIC_LEAF_COMMAND ) {
strcmp(notaken->identifier, "BEGIN") == 0 ) { PASS(errctx, aksl_strcmp(notaken->identifier, "BEGIN", &cmp));
PASS(errctx, akbasic_environment_wait_for_command(obj->environment, "BEND")); if ( cmp == 0 ) {
PASS(errctx, akbasic_environment_wait_for_command(obj->environment, "BEND"));
}
} }
if ( taken ) { if ( taken ) {
PASS(errctx, akbasic_runtime_evaluate(obj, expr->left, dest)); PASS(errctx, akbasic_runtime_evaluate(obj, expr->left, dest));
@@ -765,7 +792,7 @@ akerr_ErrorContext *akbasic_runtime_evaluate(akbasic_Runtime *obj, akbasic_ASTLe
case AKBASIC_LEAF_LITERAL_STRING: case AKBASIC_LEAF_LITERAL_STRING:
lval->valuetype = AKBASIC_TYPE_STRING; lval->valuetype = AKBASIC_TYPE_STRING;
memcpy(lval->stringval, expr->literal_string, sizeof(lval->stringval)); PASS(errctx, aksl_memcpy(lval->stringval, expr->literal_string, sizeof(lval->stringval)));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
case AKBASIC_LEAF_UNARY: case AKBASIC_LEAF_UNARY:
@@ -816,6 +843,10 @@ akerr_ErrorContext *akbasic_runtime_evaluate(akbasic_Runtime *obj, akbasic_ASTLe
akerr_ErrorContext *akbasic_runtime_interpret(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value **dest) akerr_ErrorContext *akbasic_runtime_interpret(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
int cmp = 0;
bool waiting = false;
bool matchesverb = false;
bool waitingbend = false;
FAIL_ZERO_RETURN(errctx, (obj != NULL && expr != NULL && dest != NULL), AKERR_NULLPOINTER, FAIL_ZERO_RETURN(errctx, (obj != NULL && expr != NULL && dest != NULL), AKERR_NULLPOINTER,
"NULL argument in interpret"); "NULL argument in interpret");
@@ -824,9 +855,21 @@ akerr_ErrorContext *akbasic_runtime_interpret(akbasic_Runtime *obj, akbasic_ASTL
* verb. This is what keeps a zero-iteration FOR body from executing, given * verb. This is what keeps a zero-iteration FOR body from executing, given
* that the loop condition is evaluated at the bottom of the structure. * that the loop condition is evaluated at the bottom of the structure.
*/ */
if ( akbasic_environment_is_waiting_for_any(obj->environment) ) { PASS(errctx, akbasic_environment_is_waiting_for_any(obj->environment, &waiting));
if ( expr->leaftype != AKBASIC_LEAF_COMMAND || if ( waiting ) {
!akbasic_environment_is_waiting_for(obj->environment, expr->identifier) ) { /*
* Hoisted out of the condition it used to be a term in: the test reads a
* recorded verb name, that read can fail, and the old `bool` return had
* nowhere to report it. `matchesverb` stays false unless the leaf really
* is a command whose name is what the scope is waiting for, which is what
* the `||` short-circuit used to say. See libakstdlib #38.
*/
matchesverb = false;
if ( expr->leaftype == AKBASIC_LEAF_COMMAND ) {
PASS(errctx, akbasic_environment_is_waiting_for(obj->environment,
expr->identifier, &matchesverb));
}
if ( !matchesverb ) {
/* /*
* **A skipped loop has already pushed its scope, and this is where it * **A skipped loop has already pushed its scope, and this is where it
* comes back.** * comes back.**
@@ -860,11 +903,18 @@ akerr_ErrorContext *akbasic_runtime_interpret(akbasic_Runtime *obj, akbasic_ASTL
*/ */
if ( expr->leaftype == AKBASIC_LEAF_COMMAND && if ( expr->leaftype == AKBASIC_LEAF_COMMAND &&
obj->environment->loopFirstLine != 0 && obj->environment->loopFirstLine != 0 &&
obj->environment->waitingForCommand[0] == '\0' && obj->environment->waitingForCommand[0] == '\0' ) {
akbasic_environment_is_waiting_for(obj->environment, "BEND") && PASS(errctx, akbasic_environment_is_waiting_for(obj->environment, "BEND",
(strcmp(expr->identifier, "FOR") == 0 || &waitingbend));
strcmp(expr->identifier, "DO") == 0) ) { if ( waitingbend ) {
PASS(errctx, akbasic_runtime_prev_environment(obj)); PASS(errctx, aksl_strcmp(expr->identifier, "FOR", &cmp));
if ( cmp != 0 ) {
PASS(errctx, aksl_strcmp(expr->identifier, "DO", &cmp));
}
if ( cmp == 0 ) {
PASS(errctx, akbasic_runtime_prev_environment(obj));
}
}
} }
*dest = &obj->staticTrueValue; *dest = &obj->staticTrueValue;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
@@ -1125,15 +1175,18 @@ int64_t akbasic_runtime_find_previous_lineno(akbasic_Runtime *obj)
akerr_ErrorContext *akbasic_runtime_store_line(akbasic_Runtime *obj, int64_t lineno, const char *code, bool numbered) akerr_ErrorContext *akbasic_runtime_store_line(akbasic_Runtime *obj, int64_t lineno, const char *code, bool numbered)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
size_t length = 0;
FAIL_ZERO_RETURN(errctx, (lineno >= 0 && lineno < AKBASIC_MAX_SOURCE_LINES), FAIL_ZERO_RETURN(errctx, (lineno >= 0 && lineno < AKBASIC_MAX_SOURCE_LINES),
AKBASIC_ERR_BOUNDS, AKBASIC_ERR_BOUNDS,
"Line number %" PRId64 " is outside 0..%d", "Line number %" PRId64 " is outside 0..%d",
lineno, AKBASIC_MAX_SOURCE_LINES - 1); lineno, AKBASIC_MAX_SOURCE_LINES - 1);
FAIL_ZERO_RETURN(errctx, (strlen(code) < AKBASIC_MAX_LINE_LENGTH), AKBASIC_ERR_BOUNDS, PASS(errctx, aksl_strlen(code, &length));
FAIL_ZERO_RETURN(errctx, (length < AKBASIC_MAX_LINE_LENGTH), AKBASIC_ERR_BOUNDS,
"Source line exceeds the %d character limit", AKBASIC_MAX_LINE_LENGTH - 1); "Source line exceeds the %d character limit", AKBASIC_MAX_LINE_LENGTH - 1);
strncpy(obj->source[lineno].code, code, AKBASIC_MAX_LINE_LENGTH - 1); /* aksl_strcpy always terminates and refuses rather than truncates; the
obj->source[lineno].code[AKBASIC_MAX_LINE_LENGTH - 1] = '\0'; length check above is what makes the refusal unreachable. */
PASS(errctx, aksl_strcpy(obj->source[lineno].code, sizeof(obj->source[lineno].code), code));
obj->source[lineno].lineno = lineno; obj->source[lineno].lineno = lineno;
obj->source[lineno].numbered = numbered; obj->source[lineno].numbered = numbered;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
@@ -1242,11 +1295,12 @@ akerr_ErrorContext *akbasic_runtime_process_line_repl(akbasic_Runtime *obj)
akbasic_ASTLeaf *leaf = NULL; akbasic_ASTLeaf *leaf = NULL;
akbasic_Value *value = NULL; akbasic_Value *value = NULL;
akbasic_Parser parser; akbasic_Parser parser;
int written = 0;
bool eof = false; bool eof = false;
if ( obj->autoLineNumber > 0 ) { if ( obj->autoLineNumber > 0 ) {
snprintf(prompt, sizeof(prompt), "%" PRId64 " ", PASS(errctx, aksl_snprintf(&written, prompt, sizeof(prompt), "%" PRId64 " ",
obj->environment->lineno + obj->autoLineNumber); obj->environment->lineno + obj->autoLineNumber));
PASS(errctx, akbasic_runtime_write(obj, prompt)); PASS(errctx, akbasic_runtime_write(obj, prompt));
} }
@@ -1272,7 +1326,8 @@ akerr_ErrorContext *akbasic_runtime_process_line_repl(akbasic_Runtime *obj)
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE_DEFAULT(errctx) { } HANDLE_DEFAULT(errctx) {
char message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH]; char message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH];
snprintf(message, sizeof(message), "%s", errctx->message); /* Ignored, not passed: `errctx` is the error being handled here. */
IGNORE(aksl_strcpy(message, sizeof(message), errctx->message));
obj->lasterrorstatus = errctx->status; obj->lasterrorstatus = errctx->status;
IGNORE(akbasic_runtime_error(obj, AKBASIC_ERRCLASS_PARSE, message)); IGNORE(akbasic_runtime_error(obj, AKBASIC_ERRCLASS_PARSE, message));
} FINISH(errctx, false); } FINISH(errctx, false);
@@ -1337,14 +1392,14 @@ akerr_ErrorContext *akbasic_runtime_process_line_run(akbasic_Runtime *obj)
akbasic_ASTLeaf *leaf = NULL; akbasic_ASTLeaf *leaf = NULL;
akbasic_Value *value = NULL; akbasic_Value *value = NULL;
akbasic_Parser parser; akbasic_Parser parser;
int written = 0;
if ( obj->environment->nextline >= AKBASIC_MAX_SOURCE_LINES ) { if ( obj->environment->nextline >= AKBASIC_MAX_SOURCE_LINES ) {
PASS(errctx, akbasic_runtime_set_mode(obj, obj->run_finished_mode)); PASS(errctx, akbasic_runtime_set_mode(obj, obj->run_finished_mode));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
strncpy(line, obj->source[obj->environment->nextline].code, sizeof(line) - 1); PASS(errctx, aksl_strcpy(line, sizeof(line), obj->source[obj->environment->nextline].code));
line[sizeof(line) - 1] = '\0';
obj->environment->lineno = obj->environment->nextline; obj->environment->lineno = obj->environment->nextline;
obj->environment->nextline += 1; obj->environment->nextline += 1;
if ( line[0] == '\0' ) { if ( line[0] == '\0' ) {
@@ -1358,7 +1413,8 @@ akerr_ErrorContext *akbasic_runtime_process_line_run(akbasic_Runtime *obj)
*/ */
if ( obj->trace ) { if ( obj->trace ) {
char tracemark[32]; char tracemark[32];
snprintf(tracemark, sizeof(tracemark), "[%" PRId64 "]", obj->environment->lineno); PASS(errctx, aksl_snprintf(&written, tracemark, sizeof(tracemark), "[%" PRId64 "]",
obj->environment->lineno));
PASS(errctx, akbasic_runtime_write(obj, tracemark)); PASS(errctx, akbasic_runtime_write(obj, tracemark));
} }
@@ -1373,7 +1429,8 @@ akerr_ErrorContext *akbasic_runtime_process_line_run(akbasic_Runtime *obj)
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE_DEFAULT(errctx) { } HANDLE_DEFAULT(errctx) {
char message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH]; char message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH];
snprintf(message, sizeof(message), "%s", errctx->message); /* Ignored, not passed: `errctx` is the error being handled here. */
IGNORE(aksl_strcpy(message, sizeof(message), errctx->message));
/* /*
* What ER# reports, recorded before the context goes -- the same line * What ER# reports, recorded before the context goes -- the same line
* report_and_reraise() carries for a runtime error, and it was missing * report_and_reraise() carries for a runtime error, and it was missing
@@ -1442,6 +1499,7 @@ static akerr_ErrorContext *scan_line_labels(akbasic_Environment *root, const cha
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
const char *cursor = code; const char *cursor = code;
int cmp = 0;
bool statementstart = true; bool statementstart = true;
bool instring = false; bool instring = false;
@@ -1479,31 +1537,33 @@ static akerr_ErrorContext *scan_line_labels(akbasic_Environment *root, const cha
} }
continue; continue;
} }
if ( statementstart && strncasecmp(cursor, "LABEL", 5) == 0 if ( statementstart ) {
&& !isalnum((unsigned char)cursor[5]) ) { PASS(errctx, aksl_strncasecmp(cursor, "LABEL", 5, &cmp));
char name[AKBASIC_SYMTAB_MAX_KEY]; if ( cmp == 0 && !isalnum((unsigned char)cursor[5]) ) {
size_t used = 0; char name[AKBASIC_SYMTAB_MAX_KEY];
size_t used = 0;
cursor += 5; cursor += 5;
while ( isspace((unsigned char)*cursor) ) { while ( isspace((unsigned char)*cursor) ) {
cursor += 1; cursor += 1;
}
/*
* Copied as written. Verbs are case-insensitive in this dialect and
* identifiers are not, so folding the name here would file a label
* under a spelling `LABEL` itself never uses.
*/
while ( isalnum((unsigned char)*cursor) && used < sizeof(name) - 1 ) {
name[used] = *cursor;
used += 1;
cursor += 1;
}
name[used] = '\0';
if ( used > 0 ) {
PASS(errctx, akbasic_symtab_set(&root->labels, name, NULL, lineno));
}
statementstart = false;
continue;
} }
/*
* Copied as written. Verbs are case-insensitive in this dialect and
* identifiers are not, so folding the name here would file a label
* under a spelling `LABEL` itself never uses.
*/
while ( isalnum((unsigned char)*cursor) && used < sizeof(name) - 1 ) {
name[used] = *cursor;
used += 1;
cursor += 1;
}
name[used] = '\0';
if ( used > 0 ) {
PASS(errctx, akbasic_symtab_set(&root->labels, name, NULL, lineno));
}
statementstart = false;
continue;
} }
statementstart = false; statementstart = false;
cursor += 1; cursor += 1;
@@ -1536,6 +1596,7 @@ akerr_ErrorContext *akbasic_runtime_arm_interrupt(akbasic_Runtime *obj, akbasic_
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
akbasic_Interrupt *slot = NULL; akbasic_Interrupt *slot = NULL;
size_t length = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in arm_interrupt"); FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in arm_interrupt");
FAIL_ZERO_RETURN(errctx, (source >= 0 && source < AKBASIC_MAX_INTERRUPTS), FAIL_ZERO_RETURN(errctx, (source >= 0 && source < AKBASIC_MAX_INTERRUPTS),
@@ -1550,11 +1611,13 @@ akerr_ErrorContext *akbasic_runtime_arm_interrupt(akbasic_Runtime *obj, akbasic_
slot->line = line; slot->line = line;
slot->label[0] = '\0'; slot->label[0] = '\0';
if ( label != NULL && label[0] != '\0' ) { if ( label != NULL && label[0] != '\0' ) {
FAIL_ZERO_RETURN(errctx, (strlen(label) < sizeof(slot->label)), AKBASIC_ERR_BOUNDS, PASS(errctx, aksl_strlen(label, &length));
FAIL_ZERO_RETURN(errctx, (length < sizeof(slot->label)), AKBASIC_ERR_BOUNDS,
"Handler label \"%s\" exceeds the %zu character limit", "Handler label \"%s\" exceeds the %zu character limit",
label, sizeof(slot->label) - 1); label, sizeof(slot->label) - 1);
strncpy(slot->label, label, sizeof(slot->label) - 1); /* aksl_strcpy always terminates and refuses rather than truncates; the
slot->label[sizeof(slot->label) - 1] = '\0'; length check above is what makes the refusal unreachable. */
PASS(errctx, aksl_strcpy(slot->label, sizeof(slot->label), label));
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -1567,7 +1630,7 @@ akerr_ErrorContext *akbasic_runtime_disarm_interrupt(akbasic_Runtime *obj, akbas
FAIL_ZERO_RETURN(errctx, (source >= 0 && source < AKBASIC_MAX_INTERRUPTS), FAIL_ZERO_RETURN(errctx, (source >= 0 && source < AKBASIC_MAX_INTERRUPTS),
AKBASIC_ERR_BOUNDS, "Interrupt source %d is outside 0..%d", AKBASIC_ERR_BOUNDS, "Interrupt source %d is outside 0..%d",
(int)source, AKBASIC_MAX_INTERRUPTS - 1); (int)source, AKBASIC_MAX_INTERRUPTS - 1);
memset(&obj->interrupts[source], 0, sizeof(obj->interrupts[source])); PASS(errctx, aksl_memset(&obj->interrupts[source], 0, sizeof(obj->interrupts[source])));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -1682,6 +1745,7 @@ akerr_ErrorContext *akbasic_runtime_load(akbasic_Runtime *obj, const char *sourc
char scanned[AKBASIC_MAX_LINE_LENGTH]; char scanned[AKBASIC_MAX_LINE_LENGTH];
const char *cursor = NULL; const char *cursor = NULL;
const char *eol = NULL; const char *eol = NULL;
char *match = NULL;
size_t length = 0; size_t length = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in load"); FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in load");
@@ -1696,9 +1760,11 @@ akerr_ErrorContext *akbasic_runtime_load(akbasic_Runtime *obj, const char *sourc
obj->environment->lineno = 0; obj->environment->lineno = 0;
for ( cursor = source; *cursor != '\0'; cursor = (*eol == '\0' ? eol : eol + 1) ) { for ( cursor = source; *cursor != '\0'; cursor = (*eol == '\0' ? eol : eol + 1) ) {
eol = strchr(cursor, '\n'); PASS(errctx, aksl_strchr(cursor, '\n', &match));
eol = match;
if ( eol == NULL ) { if ( eol == NULL ) {
eol = cursor + strlen(cursor); PASS(errctx, aksl_strlen(cursor, &length));
eol = cursor + length;
} }
length = (size_t)(eol - cursor); length = (size_t)(eol - cursor);
if ( length > 0 && cursor[length - 1] == '\r' ) { if ( length > 0 && cursor[length - 1] == '\r' ) {
@@ -1707,7 +1773,7 @@ akerr_ErrorContext *akbasic_runtime_load(akbasic_Runtime *obj, const char *sourc
FAIL_ZERO_RETURN(errctx, (length < sizeof(line)), AKBASIC_ERR_BOUNDS, FAIL_ZERO_RETURN(errctx, (length < sizeof(line)), AKBASIC_ERR_BOUNDS,
"Source line of %zu characters exceeds the %d character limit", "Source line of %zu characters exceeds the %d character limit",
length, AKBASIC_MAX_LINE_LENGTH - 1); length, AKBASIC_MAX_LINE_LENGTH - 1);
memcpy(line, cursor, length); PASS(errctx, aksl_memcpy(line, cursor, length));
line[length] = '\0'; line[length] = '\0';
if ( line[0] == '\0' ) { if ( line[0] == '\0' ) {
continue; continue;
@@ -1815,7 +1881,8 @@ akerr_ErrorContext *akbasic_runtime_step(akbasic_Runtime *obj)
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE_DEFAULT(errctx) { } HANDLE_DEFAULT(errctx) {
char message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH]; char message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH];
snprintf(message, sizeof(message), "%s", errctx->message); /* Ignored, not passed: `errctx` is the error being handled here. */
IGNORE(aksl_strcpy(message, sizeof(message), errctx->message));
IGNORE(akbasic_runtime_error(obj, AKBASIC_ERRCLASS_RUNTIME, message)); IGNORE(akbasic_runtime_error(obj, AKBASIC_ERRCLASS_RUNTIME, message));
} FINISH(errctx, false); } FINISH(errctx, false);
if ( obj->errclass != AKBASIC_ERRCLASS_NONE ) { if ( obj->errclass != AKBASIC_ERRCLASS_NONE ) {

View File

@@ -9,7 +9,6 @@
#include <inttypes.h> #include <inttypes.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h> #include <akstdlib.h>
@@ -96,7 +95,7 @@ akerr_ErrorContext *akbasic_cmd_print(akbasic_Runtime *obj, akbasic_ASTLeaf *exp
*/ */
if ( (*dest)->valuetype == AKBASIC_TYPE_STRUCT || (*dest)->valuetype == AKBASIC_TYPE_POINTER ) { if ( (*dest)->valuetype == AKBASIC_TYPE_STRUCT || (*dest)->valuetype == AKBASIC_TYPE_POINTER ) {
if ( (*dest)->structbase == NULL ) { if ( (*dest)->structbase == NULL ) {
snprintf(rendered, sizeof(rendered), "NOTHING"); PASS(errctx, aksl_strcpy(rendered, sizeof(rendered), "NOTHING"));
} else { } else {
PASS(errctx, akbasic_struct_to_string(obj, (*dest)->structtype, (*dest)->structbase, PASS(errctx, akbasic_struct_to_string(obj, (*dest)->structtype, (*dest)->structbase,
0, rendered, sizeof(rendered))); 0, rendered, sizeof(rendered)));
@@ -149,13 +148,15 @@ akerr_ErrorContext *akbasic_cmd_return(akbasic_Runtime *obj, akbasic_ASTLeaf *ex
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
akbasic_Value *result = NULL; akbasic_Value *result = NULL;
bool waiting = false;
(void)lval; (void)rval; (void)lval; (void)rval;
/* /*
* A RETURN reached while skipping forward to one is the end of a DEF body, * A RETURN reached while skipping forward to one is the end of a DEF body,
* not a subroutine return. Stop waiting and carry on. * not a subroutine return. Stop waiting and carry on.
*/ */
if ( akbasic_environment_is_waiting_for(obj->environment, "RETURN") ) { PASS(errctx, akbasic_environment_is_waiting_for(obj->environment, "RETURN", &waiting));
if ( waiting ) {
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "RETURN")); PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "RETURN"));
SUCCEED_TRUE(obj, dest); SUCCEED_TRUE(obj, dest);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
@@ -445,6 +446,7 @@ akerr_ErrorContext *akbasic_cmd_input(akbasic_Runtime *obj, akbasic_ASTLeaf *exp
char rendered[AKBASIC_MAX_STRING_LENGTH]; char rendered[AKBASIC_MAX_STRING_LENGTH];
char buffer[AKBASIC_MAX_LINE_LENGTH]; char buffer[AKBASIC_MAX_LINE_LENGTH];
long long converted = 0; long long converted = 0;
size_t len = 0;
bool eof = false; bool eof = false;
(void)lval; (void)rval; (void)lval; (void)rval;
@@ -479,10 +481,11 @@ akerr_ErrorContext *akbasic_cmd_input(akbasic_Runtime *obj, akbasic_ASTLeaf *exp
break; break;
default: default:
entered->valuetype = AKBASIC_TYPE_STRING; entered->valuetype = AKBASIC_TYPE_STRING;
FAIL_ZERO_RETURN(errctx, (strlen(buffer) < AKBASIC_MAX_STRING_LENGTH), AKBASIC_ERR_VALUE, PASS(errctx, aksl_strlen(buffer, &len));
FAIL_ZERO_RETURN(errctx, (len < AKBASIC_MAX_STRING_LENGTH), AKBASIC_ERR_VALUE,
"Input line exceeds the %d character limit", AKBASIC_MAX_STRING_LENGTH - 1); "Input line exceeds the %d character limit", AKBASIC_MAX_STRING_LENGTH - 1);
strncpy(entered->stringval, buffer, AKBASIC_MAX_STRING_LENGTH - 1); /* aksl_strcpy always terminates, so the explicit terminator is gone. */
entered->stringval[AKBASIC_MAX_STRING_LENGTH - 1] = '\0'; PASS(errctx, aksl_strcpy(entered->stringval, sizeof(entered->stringval), buffer));
break; break;
} }
PASS(errctx, akbasic_environment_assign(obj->environment, identifier, entered, &unused)); PASS(errctx, akbasic_environment_assign(obj->environment, identifier, entered, &unused));
@@ -539,6 +542,7 @@ akerr_ErrorContext *akbasic_cmd_list(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
int64_t startidx = 0; int64_t startidx = 0;
int64_t endidx = 0; int64_t endidx = 0;
int64_t i = 0; int64_t i = 0;
int written = 0;
(void)lval; (void)rval; (void)lval; (void)rval;
PASS(errctx, parse_line_range(obj, expr, &startidx, &endidx)); PASS(errctx, parse_line_range(obj, expr, &startidx, &endidx));
@@ -552,7 +556,8 @@ akerr_ErrorContext *akbasic_cmd_list(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
if ( obj->source[i].code[0] == '\0' ) { if ( obj->source[i].code[0] == '\0' ) {
continue; continue;
} }
snprintf(line, sizeof(line), "%" PRId64 " %s", i, obj->source[i].code); PASS(errctx, aksl_snprintf(&written, line, sizeof(line), "%" PRId64 " %s",
i, obj->source[i].code));
PASS(errctx, akbasic_runtime_println(obj, line)); PASS(errctx, akbasic_runtime_println(obj, line));
} }
SUCCEED_TRUE(obj, dest); SUCCEED_TRUE(obj, dest);
@@ -588,6 +593,7 @@ static akerr_ErrorContext *filename_argument(akbasic_Runtime *obj, akbasic_ASTLe
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
akbasic_Value *value = NULL; akbasic_Value *value = NULL;
size_t namelen = 0;
FAIL_ZERO_RETURN(errctx, (expr != NULL && expr->right != NULL), AKBASIC_ERR_SYNTAX, FAIL_ZERO_RETURN(errctx, (expr != NULL && expr->right != NULL), AKBASIC_ERR_SYNTAX,
"Expected a filename"); "Expected a filename");
@@ -596,10 +602,107 @@ static akerr_ErrorContext *filename_argument(akbasic_Runtime *obj, akbasic_ASTLe
"Expected a filename string"); "Expected a filename string");
FAIL_ZERO_RETURN(errctx, (value->stringval[0] != '\0'), AKBASIC_ERR_VALUE, FAIL_ZERO_RETURN(errctx, (value->stringval[0] != '\0'), AKBASIC_ERR_VALUE,
"Filename must not be empty"); "Filename must not be empty");
FAIL_ZERO_RETURN(errctx, (strlen(value->stringval) < len), AKBASIC_ERR_BOUNDS, PASS(errctx, aksl_strlen(value->stringval, &namelen));
FAIL_ZERO_RETURN(errctx, (namelen < len), AKBASIC_ERR_BOUNDS,
"Filename exceeds the %zu character limit", len - 1); "Filename exceeds the %zu character limit", len - 1);
strncpy(dest, value->stringval, len - 1); /* aksl_strcpy always terminates, so the explicit terminator is gone. */
dest[len - 1] = '\0'; PASS(errctx, aksl_strcpy(dest, len, value->stringval));
SUCCEED_RETURN(errctx);
}
/**
* @brief Read one line, reporting end of stream rather than raising it.
*
* aksl_fgets raises AKERR_EOF where fgets(3) returned NULL, so the end of an
* ordinary file arrives as an error and has to be handled. It is handled here,
* in a function whose ATTEMPT block contains no loop at all, because a CATCH
* inside a loop expands to a break that leaves the loop rather than the
* ATTEMPT. The caller reads @p eof instead.
*/
static akerr_ErrorContext *dload_read_line(FILE *fp, char *buffer, size_t len, bool *eof)
{
PREPARE_ERROR(errctx);
size_t used = 0;
*eof = false;
ATTEMPT {
CATCH(errctx, aksl_fgets(buffer, len, fp, &used));
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_EOF) {
*eof = true;
used = 0;
} FINISH(errctx, true);
while ( used > 0 && (buffer[used - 1] == '\n' || buffer[used - 1] == '\r') ) {
buffer[used - 1] = '\0';
used -= 1;
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Read a whole program in, one line at a time.
*
* Hoisted out of akbasic_cmd_dload()'s ATTEMPT block because a loop inside one
* can use neither CATCH -- which breaks the loop -- nor PASS, which returns
* past CLEANUP and leaves the file open. Out here PASS is correct, and the
* caller CATCHes this one call.
*/
static akerr_ErrorContext *dload_read_program(akbasic_Runtime *obj, FILE *fp,
char *buffer, size_t buflen,
char *scanned, size_t scanlen)
{
PREPARE_ERROR(errctx);
bool eof = false;
for ( ;; ) {
PASS(errctx, dload_read_line(fp, buffer, buflen, &eof));
if ( eof ) {
break;
}
if ( buffer[0] == '\0' ) {
continue;
}
PASS(errctx, akbasic_scanner_scan(obj, buffer, scanned, scanlen));
PASS(errctx, akbasic_runtime_file_line(obj, scanned));
}
SUCCEED_RETURN(errctx);
}
/**
* @brief Write every stored line out, numbered.
*
* Hoisted out of akbasic_cmd_dsave()'s ATTEMPT block for the same reason
* dload_read_program() is.
*/
static akerr_ErrorContext *dsave_write_program(akbasic_Runtime *obj, FILE *fp, char *line, size_t len)
{
PREPARE_ERROR(errctx);
size_t written = 0;
int64_t i = 0;
int count = 0;
for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) {
if ( obj->source[i].code[0] == '\0' ) {
continue;
}
/*
* aksl_snprintf's count is the length it wrote, and it treats
* truncation as an error rather than reporting a length it did not
* write -- so it is what goes to fwrite, and the strlen that used to
* recompute it is gone.
*/
PASS(errctx, aksl_snprintf(&count, line, len, "%" PRId64 " %s\n", i, obj->source[i].code));
/*
* The written count is required by libakstdlib 0.2.0 and discarded
* here on purpose: a short write is no longer something a caller has
* to notice for itself, because that release made it an AKERR_IO
* rather than a silent success. Before it, a DSAVE onto a full disk
* reported nothing.
*/
PASS(errctx, aksl_fwrite(line, 1, (size_t)count, fp, &written));
}
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -610,7 +713,6 @@ akerr_ErrorContext *akbasic_cmd_dload(akbasic_Runtime *obj, akbasic_ASTLeaf *exp
char buffer[AKBASIC_MAX_LINE_LENGTH]; char buffer[AKBASIC_MAX_LINE_LENGTH];
char scanned[AKBASIC_MAX_LINE_LENGTH]; char scanned[AKBASIC_MAX_LINE_LENGTH];
FILE *fp = NULL; FILE *fp = NULL;
size_t used = 0;
int64_t i = 0; int64_t i = 0;
(void)lval; (void)rval; (void)lval; (void)rval;
@@ -632,23 +734,14 @@ akerr_ErrorContext *akbasic_cmd_dload(akbasic_Runtime *obj, akbasic_ASTLeaf *exp
ATTEMPT { ATTEMPT {
CATCH(errctx, aksl_fopen(filename, "r", &fp)); CATCH(errctx, aksl_fopen(filename, "r", &fp));
while ( fgets(buffer, sizeof(buffer), fp) != NULL ) { /*
used = strlen(buffer); * The read is one CATCH of one call, because the loop it used to be
while ( used > 0 && (buffer[used - 1] == '\n' || buffer[used - 1] == '\r') ) { * lives in dload_read_program() now: inside this block a loop can use
buffer[used - 1] = '\0'; * neither CATCH nor PASS without either escaping the loop or returning
used -= 1; * past the fclose below.
} */
if ( buffer[0] == '\0' ) { CATCH(errctx, dload_read_program(obj, fp, buffer, sizeof(buffer),
continue; scanned, sizeof(scanned)));
}
/*
* PASS inside the loop, never CATCH: CATCH expands to a break, which
* would leave this loop rather than the ATTEMPT and let the rest of
* the block run with an error pending.
*/
PASS(errctx, akbasic_scanner_scan(obj, buffer, scanned, sizeof(scanned)));
PASS(errctx, akbasic_runtime_file_line(obj, scanned));
}
} CLEANUP { } CLEANUP {
if ( fp != NULL ) { if ( fp != NULL ) {
IGNORE(aksl_fclose(fp)); IGNORE(aksl_fclose(fp));
@@ -676,30 +769,13 @@ akerr_ErrorContext *akbasic_cmd_dsave(akbasic_Runtime *obj, akbasic_ASTLeaf *exp
char filename[AKBASIC_MAX_STRING_LENGTH]; char filename[AKBASIC_MAX_STRING_LENGTH];
char line[AKBASIC_MAX_LINE_LENGTH * 2]; char line[AKBASIC_MAX_LINE_LENGTH * 2];
FILE *fp = NULL; FILE *fp = NULL;
int64_t i = 0;
int count = 0;
size_t written = 0;
(void)lval; (void)rval; (void)lval; (void)rval;
PASS(errctx, filename_argument(obj, expr, filename, sizeof(filename))); PASS(errctx, filename_argument(obj, expr, filename, sizeof(filename)));
ATTEMPT { ATTEMPT {
CATCH(errctx, aksl_fopen(filename, "w", &fp)); CATCH(errctx, aksl_fopen(filename, "w", &fp));
for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) { CATCH(errctx, dsave_write_program(obj, fp, line, sizeof(line)));
if ( obj->source[i].code[0] == '\0' ) {
continue;
}
snprintf(line, sizeof(line), "%" PRId64 " %s\n", i, obj->source[i].code);
/*
* The written count is required by libakstdlib 0.2.0 and discarded
* here on purpose: a short write is no longer something a caller has
* to notice for itself, because that release made it an AKERR_IO
* rather than a silent success. Before it, a DSAVE onto a full disk
* reported nothing.
*/
PASS(errctx, aksl_fwrite(line, 1, strlen(line), fp, &written));
count += 1;
}
} CLEANUP { } CLEANUP {
if ( fp != NULL ) { if ( fp != NULL ) {
IGNORE(aksl_fclose(fp)); IGNORE(aksl_fclose(fp));
@@ -807,6 +883,7 @@ akerr_ErrorContext *akbasic_cmd_next(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
akbasic_Value *updated = NULL; akbasic_Value *updated = NULL;
akbasic_Value scratch; akbasic_Value scratch;
int64_t zerosubscript[1] = { 0 }; int64_t zerosubscript[1] = { 0 };
int cmp = 0;
bool met = false; bool met = false;
(void)lval; (void)rval; (void)lval; (void)rval;
@@ -842,7 +919,9 @@ akerr_ErrorContext *akbasic_cmd_next(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
* A NEXT for someone else's loop variable: this environment is done, hand * A NEXT for someone else's loop variable: this environment is done, hand
* the line back to the parent and pop. That is how nested loops unwind. * the line back to the parent and pop. That is how nested loops unwind.
*/ */
if ( strcmp(expr->right->identifier, obj->environment->forNextVariable->name) != 0 ) { PASS(errctx, aksl_strcmp(expr->right->identifier,
obj->environment->forNextVariable->name, &cmp));
if ( cmp != 0 ) {
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT, FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
"NEXT in an orphaned environment"); "NEXT in an orphaned environment");
obj->environment->parent->nextline = obj->environment->nextline; obj->environment->parent->nextline = obj->environment->nextline;
@@ -951,7 +1030,8 @@ akerr_ErrorContext *akbasic_cmd_read(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
PASS(errctx, akbasic_leaf_init(&literal, AKBASIC_LEAF_LITERAL_INT)); PASS(errctx, akbasic_leaf_init(&literal, AKBASIC_LEAF_LITERAL_INT));
if ( value.valuetype == AKBASIC_TYPE_STRING ) { if ( value.valuetype == AKBASIC_TYPE_STRING ) {
literal.leaftype = AKBASIC_LEAF_LITERAL_STRING; literal.leaftype = AKBASIC_LEAF_LITERAL_STRING;
snprintf(literal.literal_string, sizeof(literal.literal_string), "%s", value.stringval); PASS(errctx, aksl_strcpy(literal.literal_string, sizeof(literal.literal_string),
value.stringval));
} else if ( value.valuetype == AKBASIC_TYPE_FLOAT ) { } else if ( value.valuetype == AKBASIC_TYPE_FLOAT ) {
literal.leaftype = AKBASIC_LEAF_LITERAL_FLOAT; literal.leaftype = AKBASIC_LEAF_LITERAL_FLOAT;
literal.literal_float = value.floatval; literal.literal_float = value.floatval;

View File

@@ -15,6 +15,7 @@
#include <string.h> #include <string.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h>
#include <akbasic/args.h> #include <akbasic/args.h>
#include <akbasic/error.h> #include <akbasic/error.h>
@@ -114,6 +115,7 @@ akerr_ErrorContext *akbasic_cmd_key(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
/* Room for the longest macro plus `KEY n, ""` around it. */ /* Room for the longest macro plus `KEY n, ""` around it. */
char line[AKBASIC_MAX_STRING_LENGTH + 32]; char line[AKBASIC_MAX_STRING_LENGTH + 32];
int64_t number = 0; int64_t number = 0;
int written = 0;
int i = 0; int i = 0;
(void)lval; (void)rval; (void)lval; (void)rval;
@@ -124,8 +126,8 @@ akerr_ErrorContext *akbasic_cmd_key(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
if ( arg == NULL ) { if ( arg == NULL ) {
/* Bare KEY lists the definitions, which is what a C128 does. */ /* Bare KEY lists the definitions, which is what a C128 does. */
for ( i = 0; i < AKBASIC_MAX_FUNCTION_KEYS; i++ ) { for ( i = 0; i < AKBASIC_MAX_FUNCTION_KEYS; i++ ) {
snprintf(line, sizeof(line), "KEY %d, \"%s\"", PASS(errctx, aksl_snprintf(&written, line, sizeof(line), "KEY %d, \"%s\"",
i + 1, obj->console_state.keys[i]); i + 1, obj->console_state.keys[i]));
PASS(errctx, akbasic_runtime_println(obj, line)); PASS(errctx, akbasic_runtime_println(obj, line));
} }
SUCCEED_TRUE(obj, dest); SUCCEED_TRUE(obj, dest);
@@ -146,8 +148,8 @@ akerr_ErrorContext *akbasic_cmd_key(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value)); PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
FAIL_NONZERO_RETURN(errctx, (value->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE, FAIL_NONZERO_RETURN(errctx, (value->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
"KEY expected a string"); "KEY expected a string");
snprintf(obj->console_state.keys[number - 1], PASS(errctx, aksl_snprintf(&written, obj->console_state.keys[number - 1],
sizeof(obj->console_state.keys[0]), "%s", value->stringval); sizeof(obj->console_state.keys[0]), "%s", value->stringval));
SUCCEED_TRUE(obj, dest); SUCCEED_TRUE(obj, dest);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -253,7 +255,7 @@ akerr_ErrorContext *akbasic_console_state_init(akbasic_ConsoleState *obj)
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL console state in init"); FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL console state in init");
memset(obj, 0, sizeof(*obj)); PASS(errctx, aksl_memset(obj, 0, sizeof(*obj)));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -302,6 +304,7 @@ akerr_ErrorContext *akbasic_console_update_clock(akbasic_Runtime *obj)
int64_t jiffies = 0; int64_t jiffies = 0;
int64_t seconds = 0; int64_t seconds = 0;
char text[16]; char text[16];
int written = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in update_clock"); FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in update_clock");
if ( obj->environment == NULL ) { if ( obj->environment == NULL ) {
@@ -324,8 +327,9 @@ akerr_ErrorContext *akbasic_console_update_clock(akbasic_Runtime *obj)
PASS(errctx, akbasic_variable_set_integer(variable, jiffies, zerosubscript, 1)); PASS(errctx, akbasic_variable_set_integer(variable, jiffies, zerosubscript, 1));
seconds = obj->timems / 1000; seconds = obj->timems / 1000;
snprintf(text, sizeof(text), "%02" PRId64 "%02" PRId64 "%02" PRId64, PASS(errctx, aksl_snprintf(&written, text, sizeof(text),
(seconds / 3600) % 24, (seconds / 60) % 60, seconds % 60); "%02" PRId64 "%02" PRId64 "%02" PRId64,
(seconds / 3600) % 24, (seconds / 60) % 60, seconds % 60));
PASS(errctx, akbasic_runtime_global(obj, "TI$", &variable)); PASS(errctx, akbasic_runtime_global(obj, "TI$", &variable));
PASS(errctx, akbasic_variable_set_string(variable, text, zerosubscript, 1)); PASS(errctx, akbasic_variable_set_string(variable, text, zerosubscript, 1));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);

View File

@@ -22,7 +22,6 @@
#include <inttypes.h> #include <inttypes.h>
#include <stdio.h> #include <stdio.h>
#include <string.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h> #include <akstdlib.h>
@@ -44,7 +43,7 @@ akerr_ErrorContext *akbasic_disk_state_init(akbasic_DiskState *obj)
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL disk state in init"); FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL disk state in init");
memset(obj, 0, sizeof(*obj)); PASS(errctx, aksl_memset(obj, 0, sizeof(*obj)));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -103,7 +102,12 @@ static akerr_ErrorContext *string_arg(akbasic_Runtime *obj, akbasic_ASTLeaf *arg
PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value)); PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
FAIL_NONZERO_RETURN(errctx, (value->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE, FAIL_NONZERO_RETURN(errctx, (value->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
"%s expected a file name", verb); "%s expected a file name", verb);
snprintf(dest, len, "%s", value->stringval); /*
* A plain copy, so aksl_strcpy rather than aksl_snprintf: it refuses rather
* than truncates, and every caller here hands in a buffer the size of a
* string value, so a name that did not fit could only ever be a bug.
*/
PASS(errctx, aksl_strcpy(dest, len, value->stringval));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -157,7 +161,7 @@ static akerr_ErrorContext *open_file(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
} }
PASS(errctx, aksl_fopen(name, mode, &channel->fp)); PASS(errctx, aksl_fopen(name, mode, &channel->fp));
snprintf(channel->name, sizeof(channel->name), "%s", name); PASS(errctx, aksl_strcpy(channel->name, sizeof(channel->name), name));
channel->writing = writing; channel->writing = writing;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -265,13 +269,24 @@ akerr_ErrorContext *akbasic_cmd_record(akbasic_Runtime *obj, akbasic_ASTLeaf *ex
akerr_ErrorContext *akbasic_cmd_scratch(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest) akerr_ErrorContext *akbasic_cmd_scratch(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
akerr_ErrorContext *failure = NULL;
char name[AKBASIC_MAX_STRING_LENGTH]; char name[AKBASIC_MAX_STRING_LENGTH];
(void)lval; (void)rval; (void)lval; (void)rval;
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in SCRATCH"); FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in SCRATCH");
PASS(errctx, string_arg(obj, akbasic_leaf_first_argument(expr), "SCRATCH", name, sizeof(name))); PASS(errctx, string_arg(obj, akbasic_leaf_first_argument(expr), "SCRATCH", name, sizeof(name)));
FAIL_ZERO_RETURN(errctx, (remove(name) == 0), AKERR_IO, /*
"SCRATCH could not delete \"%s\"", name); * Caught and restated rather than passed. aksl_remove raises the bare errno,
* and "No such file or directory" on its own does not say which file or
* which verb wanted it -- so the wrapper's error is retired here and the
* message the program sees keeps naming both.
*/
failure = aksl_remove(name);
if ( failure != NULL ) {
failure->handled = true;
IGNORE(akerr_release_error(failure));
FAIL_RETURN(errctx, AKERR_IO, "SCRATCH could not delete \"%s\"", name);
}
SUCCEED_TRUE(obj, dest); SUCCEED_TRUE(obj, dest);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -279,6 +294,7 @@ akerr_ErrorContext *akbasic_cmd_scratch(akbasic_Runtime *obj, akbasic_ASTLeaf *e
akerr_ErrorContext *akbasic_cmd_rename(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest) akerr_ErrorContext *akbasic_cmd_rename(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
akerr_ErrorContext *failure = NULL;
akbasic_ASTLeaf *arg = NULL; akbasic_ASTLeaf *arg = NULL;
char from[AKBASIC_MAX_STRING_LENGTH]; char from[AKBASIC_MAX_STRING_LENGTH];
char to[AKBASIC_MAX_STRING_LENGTH]; char to[AKBASIC_MAX_STRING_LENGTH];
@@ -288,8 +304,13 @@ akerr_ErrorContext *akbasic_cmd_rename(akbasic_Runtime *obj, akbasic_ASTLeaf *ex
arg = akbasic_leaf_first_argument(expr); arg = akbasic_leaf_first_argument(expr);
PASS(errctx, string_arg(obj, arg, "RENAME", from, sizeof(from))); PASS(errctx, string_arg(obj, arg, "RENAME", from, sizeof(from)));
PASS(errctx, string_arg(obj, (arg != NULL ? arg->next : NULL), "RENAME", to, sizeof(to))); PASS(errctx, string_arg(obj, (arg != NULL ? arg->next : NULL), "RENAME", to, sizeof(to)));
FAIL_ZERO_RETURN(errctx, (rename(from, to) == 0), AKERR_IO, /* Caught and restated, for the reason SCRATCH gives: the message names both files. */
"RENAME could not rename \"%s\" to \"%s\"", from, to); failure = aksl_rename(from, to);
if ( failure != NULL ) {
failure->handled = true;
IGNORE(akerr_release_error(failure));
FAIL_RETURN(errctx, AKERR_IO, "RENAME could not rename \"%s\" to \"%s\"", from, to);
}
SUCCEED_TRUE(obj, dest); SUCCEED_TRUE(obj, dest);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -486,6 +507,7 @@ akerr_ErrorContext *akbasic_disk_write(akbasic_Runtime *obj, int64_t number, con
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
akbasic_Channel *channel = NULL; akbasic_Channel *channel = NULL;
size_t length = 0;
size_t put = 0; size_t put = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL && text != NULL), AKERR_NULLPOINTER, FAIL_ZERO_RETURN(errctx, (obj != NULL && text != NULL), AKERR_NULLPOINTER,
@@ -493,7 +515,8 @@ akerr_ErrorContext *akbasic_disk_write(akbasic_Runtime *obj, int64_t number, con
PASS(errctx, open_channel(obj, number, &channel)); PASS(errctx, open_channel(obj, number, &channel));
FAIL_ZERO_RETURN(errctx, channel->writing, AKBASIC_ERR_STATE, FAIL_ZERO_RETURN(errctx, channel->writing, AKBASIC_ERR_STATE,
"Channel %" PRId64 " was opened for reading", number); "Channel %" PRId64 " was opened for reading", number);
PASS(errctx, aksl_fwrite(text, 1, strlen(text), channel->fp, &put)); PASS(errctx, aksl_strlen(text, &length));
PASS(errctx, aksl_fwrite(text, 1, length, channel->fp, &put));
PASS(errctx, aksl_fwrite("\n", 1, 1, channel->fp, &put)); PASS(errctx, aksl_fwrite("\n", 1, 1, channel->fp, &put));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -525,7 +548,7 @@ akerr_ErrorContext *akbasic_disk_readline(akbasic_Runtime *obj, int64_t number,
*eof = true; *eof = true;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
newline = strchr(dest, '\n'); PASS(errctx, aksl_strchr(dest, '\n', &newline));
if ( newline != NULL ) { if ( newline != NULL ) {
*newline = '\0'; *newline = '\0';
} }
@@ -582,7 +605,7 @@ akerr_ErrorContext *akbasic_cmd_input_channel(akbasic_Runtime *obj, akbasic_ASTL
* variable reading an empty line gets zero, the same as a C128. * variable reading an empty line gets zero, the same as a C128.
*/ */
PASS(errctx, akbasic_leaf_init(&literal, AKBASIC_LEAF_LITERAL_STRING)); PASS(errctx, akbasic_leaf_init(&literal, AKBASIC_LEAF_LITERAL_STRING));
snprintf(literal.literal_string, sizeof(literal.literal_string), "%s", text); PASS(errctx, aksl_strcpy(literal.literal_string, sizeof(literal.literal_string), text));
if ( akbasic_leaf_identifier_type(identifier) == AKBASIC_TYPE_INTEGER ) { if ( akbasic_leaf_identifier_type(identifier) == AKBASIC_TYPE_INTEGER ) {
long long converted = 0; long long converted = 0;
@@ -610,13 +633,69 @@ akerr_ErrorContext *akbasic_cmd_input_channel(akbasic_Runtime *obj, akbasic_ASTL
/* ---------------------------------------------------------------- VERIFY -- */ /* ---------------------------------------------------------------- VERIFY -- */
/**
* @brief Count the lines of @p fp that differ from the program in memory.
*
* Its own function so that the comparison can be reached with a single CATCH.
* Written inline it would be a loop inside VERIFY's ATTEMPT, where neither form
* is available: CATCH expands to a break that would escape only the loop, and
* PASS returns past the CLEANUP that closes the file. One call is neither, so
* the wrappers below can PASS in the ordinary way.
*/
static akerr_ErrorContext *verify_lines(akbasic_Runtime *obj, FILE *fp, int64_t *dest)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *got = NULL;
char line[AKBASIC_MAX_LINE_LENGTH];
const char *filetext = NULL;
char *newline = NULL;
int64_t lineno = 0;
size_t length = 0;
int cmp = 0;
*dest = 0;
for ( lineno = 0; lineno < AKBASIC_MAX_SOURCE_LINES; lineno++ ) {
if ( obj->source[lineno].code[0] == '\0' ) {
continue;
}
got = aksl_fgets(line, sizeof(line), fp, &length);
if ( got != NULL ) {
got->handled = true;
IGNORE(akerr_release_error(got));
*dest += 1;
break;
}
PASS(errctx, aksl_strchr(line, '\n', &newline));
if ( newline != NULL ) {
*newline = '\0';
}
/*
* The stored line has had its number stripped, so the file's copy is
* compared from past its own number.
*/
filetext = line;
while ( *filetext == ' ' ) {
filetext += 1;
}
while ( *filetext >= '0' && *filetext <= '9' ) {
filetext += 1;
}
while ( *filetext == ' ' ) {
filetext += 1;
}
PASS(errctx, aksl_strcmp(filetext, obj->source[lineno].code, &cmp));
if ( cmp != 0 ) {
*dest += 1;
}
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_cmd_dverify(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest) akerr_ErrorContext *akbasic_cmd_dverify(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FILE *fp = NULL; FILE *fp = NULL;
char name[AKBASIC_MAX_STRING_LENGTH]; char name[AKBASIC_MAX_STRING_LENGTH];
char line[AKBASIC_MAX_LINE_LENGTH];
int64_t lineno = 0;
int64_t mismatch = 0; int64_t mismatch = 0;
(void)lval; (void)rval; (void)lval; (void)rval;
@@ -632,47 +711,7 @@ akerr_ErrorContext *akbasic_cmd_dverify(akbasic_Runtime *obj, akbasic_ASTLeaf *e
*/ */
PASS(errctx, aksl_fopen(name, "r", &fp)); PASS(errctx, aksl_fopen(name, "r", &fp));
ATTEMPT { ATTEMPT {
for ( lineno = 0; lineno < AKBASIC_MAX_SOURCE_LINES; lineno++ ) { CATCH(errctx, verify_lines(obj, fp, &mismatch));
size_t length = 0;
akerr_ErrorContext *got = NULL;
char *newline = NULL;
if ( obj->source[lineno].code[0] == '\0' ) {
continue;
}
got = aksl_fgets(line, sizeof(line), fp, &length);
if ( got != NULL ) {
got->handled = true;
IGNORE(akerr_release_error(got));
mismatch += 1;
break;
}
newline = strchr(line, '\n');
if ( newline != NULL ) {
*newline = '\0';
}
/*
* The stored line has had its number stripped, so the file's copy is
* compared from past its own number. PASS rather than CATCH: this is
* a loop.
*/
{
const char *filetext = line;
while ( *filetext == ' ' ) {
filetext += 1;
}
while ( *filetext >= '0' && *filetext <= '9' ) {
filetext += 1;
}
while ( *filetext == ' ' ) {
filetext += 1;
}
if ( strcmp(filetext, obj->source[lineno].code) != 0 ) {
mismatch += 1;
}
}
}
} CLEANUP { } CLEANUP {
IGNORE(aksl_fclose(fp)); IGNORE(aksl_fclose(fp));
} PROCESS(errctx) { } PROCESS(errctx) {

View File

@@ -14,7 +14,6 @@
#include <inttypes.h> #include <inttypes.h>
#include <math.h> #include <math.h>
#include <stdio.h> #include <stdio.h>
#include <string.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h> #include <akstdlib.h>
@@ -189,13 +188,19 @@ akerr_ErrorContext *akbasic_fn_hex(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
akbasic_Value *arg = NULL; akbasic_Value *arg = NULL;
akbasic_Value *out = NULL; akbasic_Value *out = NULL;
int written = 0;
(void)lval; (void)rval; (void)lval; (void)rval;
PASS(errctx, first_arg(obj, expr, "HEX", NULL, &arg, &out)); PASS(errctx, first_arg(obj, expr, "HEX", NULL, &arg, &out));
FAIL_NONZERO_RETURN(errctx, (arg->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE, FAIL_NONZERO_RETURN(errctx, (arg->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
"HEX expected an integer"); "HEX expected an integer");
out->valuetype = AKBASIC_TYPE_STRING; out->valuetype = AKBASIC_TYPE_STRING;
snprintf(out->stringval, sizeof(out->stringval), "%" PRIx64, arg->intval); /*
* aksl_snprintf refuses to truncate, which costs nothing here: sixteen hex
* digits is the longest an int64_t can be and a string value holds far more.
*/
PASS(errctx, aksl_snprintf(&written, out->stringval, sizeof(out->stringval),
"%" PRIx64, arg->intval));
*dest = out; *dest = out;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -270,6 +275,7 @@ akerr_ErrorContext *akbasic_fn_len(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
akbasic_Value *strval = NULL; akbasic_Value *strval = NULL;
akbasic_Value *out = NULL; akbasic_Value *out = NULL;
akbasic_Variable *variable = NULL; akbasic_Variable *variable = NULL;
size_t length = 0;
(void)lval; (void)rval; (void)lval; (void)rval;
/* /*
@@ -291,7 +297,8 @@ akerr_ErrorContext *akbasic_fn_len(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
if ( argleaf->leaftype == AKBASIC_LEAF_LITERAL_STRING || if ( argleaf->leaftype == AKBASIC_LEAF_LITERAL_STRING ||
argleaf->leaftype == AKBASIC_LEAF_IDENTIFIER_STRING ) { argleaf->leaftype == AKBASIC_LEAF_IDENTIFIER_STRING ) {
PASS(errctx, akbasic_runtime_evaluate(obj, argleaf, &strval)); PASS(errctx, akbasic_runtime_evaluate(obj, argleaf, &strval));
out->intval = (int64_t)strlen(strval->stringval); PASS(errctx, aksl_strlen(strval->stringval, &length));
out->intval = (int64_t)length;
} else { } else {
PASS(errctx, akbasic_environment_get(obj->environment, argleaf->identifier, &variable)); PASS(errctx, akbasic_environment_get(obj->environment, argleaf->identifier, &variable));
FAIL_ZERO_RETURN(errctx, (variable != NULL), AKBASIC_ERR_UNDEFINED, FAIL_ZERO_RETURN(errctx, (variable != NULL), AKBASIC_ERR_UNDEFINED,
@@ -338,7 +345,8 @@ akerr_ErrorContext *akbasic_fn_instr(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
PASS(errctx, akbasic_environment_new_value(obj->environment, &out)); PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
PASS(errctx, akbasic_value_zero(out)); PASS(errctx, akbasic_value_zero(out));
out->valuetype = AKBASIC_TYPE_INTEGER; out->valuetype = AKBASIC_TYPE_INTEGER;
hit = strstr(haystack->stringval, needle->stringval); /* Not finding it is a successful NULL, which is exactly the -1 answer below. */
PASS(errctx, aksl_strstr(haystack->stringval, needle->stringval, &hit));
out->intval = (hit == NULL ? -1 : (int64_t)(hit - haystack->stringval)); out->intval = (hit == NULL ? -1 : (int64_t)(hit - haystack->stringval));
*dest = out; *dest = out;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
@@ -360,7 +368,7 @@ akerr_ErrorContext *akbasic_fn_left(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
"LEFT expected a string"); "LEFT expected a string");
FAIL_NONZERO_RETURN(errctx, (count->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE, FAIL_NONZERO_RETURN(errctx, (count->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
"LEFT expected an integer count"); "LEFT expected an integer count");
sourcelen = strlen(source->stringval); PASS(errctx, aksl_strlen(source->stringval, &sourcelen));
take = count->intval; take = count->intval;
if ( take < 0 ) { if ( take < 0 ) {
take = 0; take = 0;
@@ -371,7 +379,7 @@ akerr_ErrorContext *akbasic_fn_left(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
PASS(errctx, akbasic_environment_new_value(obj->environment, &out)); PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
PASS(errctx, akbasic_value_zero(out)); PASS(errctx, akbasic_value_zero(out));
out->valuetype = AKBASIC_TYPE_STRING; out->valuetype = AKBASIC_TYPE_STRING;
memcpy(out->stringval, source->stringval, (size_t)take); PASS(errctx, aksl_memcpy(out->stringval, source->stringval, (size_t)take));
out->stringval[take] = '\0'; out->stringval[take] = '\0';
*dest = out; *dest = out;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
@@ -393,7 +401,7 @@ akerr_ErrorContext *akbasic_fn_right(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
"RIGHT expected a string"); "RIGHT expected a string");
FAIL_NONZERO_RETURN(errctx, (count->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE, FAIL_NONZERO_RETURN(errctx, (count->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
"RIGHT expected an integer count"); "RIGHT expected an integer count");
sourcelen = strlen(source->stringval); PASS(errctx, aksl_strlen(source->stringval, &sourcelen));
take = count->intval; take = count->intval;
if ( take < 0 ) { if ( take < 0 ) {
take = 0; take = 0;
@@ -404,7 +412,8 @@ akerr_ErrorContext *akbasic_fn_right(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
PASS(errctx, akbasic_environment_new_value(obj->environment, &out)); PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
PASS(errctx, akbasic_value_zero(out)); PASS(errctx, akbasic_value_zero(out));
out->valuetype = AKBASIC_TYPE_STRING; out->valuetype = AKBASIC_TYPE_STRING;
memcpy(out->stringval, source->stringval + (sourcelen - (size_t)take), (size_t)take); PASS(errctx, aksl_memcpy(out->stringval, source->stringval + (sourcelen - (size_t)take),
(size_t)take));
out->stringval[take] = '\0'; out->stringval[take] = '\0';
*dest = out; *dest = out;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
@@ -432,7 +441,7 @@ akerr_ErrorContext *akbasic_fn_mid(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
lengthval->valuetype == AKBASIC_TYPE_INTEGER), lengthval->valuetype == AKBASIC_TYPE_INTEGER),
AKBASIC_ERR_TYPE, "MID expected integer start and length"); AKBASIC_ERR_TYPE, "MID expected integer start and length");
sourcelen = strlen(source->stringval); PASS(errctx, aksl_strlen(source->stringval, &sourcelen));
start = startval->intval; start = startval->intval;
length = lengthval->intval; length = lengthval->intval;
FAIL_ZERO_RETURN(errctx, (start >= 0 && (size_t)start <= sourcelen), AKBASIC_ERR_BOUNDS, FAIL_ZERO_RETURN(errctx, (start >= 0 && (size_t)start <= sourcelen), AKBASIC_ERR_BOUNDS,
@@ -446,7 +455,7 @@ akerr_ErrorContext *akbasic_fn_mid(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
PASS(errctx, akbasic_environment_new_value(obj->environment, &out)); PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
PASS(errctx, akbasic_value_zero(out)); PASS(errctx, akbasic_value_zero(out));
out->valuetype = AKBASIC_TYPE_STRING; out->valuetype = AKBASIC_TYPE_STRING;
memcpy(out->stringval, source->stringval + start, (size_t)length); PASS(errctx, aksl_memcpy(out->stringval, source->stringval + start, (size_t)length));
out->stringval[length] = '\0'; out->stringval[length] = '\0';
*dest = out; *dest = out;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);

View File

@@ -709,6 +709,7 @@ akerr_ErrorContext *akbasic_cmd_sshape(akbasic_Runtime *obj, akbasic_ASTLeaf *ex
char encoded[AKBASIC_MAX_STRING_LENGTH]; char encoded[AKBASIC_MAX_STRING_LENGTH];
int64_t subscripts[AKBASIC_MAX_ARRAY_DEPTH]; int64_t subscripts[AKBASIC_MAX_ARRAY_DEPTH];
int subscriptcount = 0; int subscriptcount = 0;
int written = 0;
(void)lval; (void)rval; (void)lval; (void)rval;
PASS(errctx, require_graphics(obj, "SSHAPE")); PASS(errctx, require_graphics(obj, "SSHAPE"));
@@ -742,7 +743,7 @@ akerr_ErrorContext *akbasic_cmd_sshape(akbasic_Runtime *obj, akbasic_ASTLeaf *ex
* BASIC ever does with it; what it can no longer do is save it to disk or * BASIC ever does with it; what it can no longer do is save it to disk or
* take its LEN and get a size. TODO.md section 5. * take its LEN and get a size. TODO.md section 5.
*/ */
snprintf(encoded, sizeof(encoded), "%s%d", SHAPE_PREFIX, handle); PASS(errctx, aksl_snprintf(&written, encoded, sizeof(encoded), "%s%d", SHAPE_PREFIX, handle));
PASS(errctx, akbasic_variable_set_string(variable, encoded, subscripts, subscriptcount)); PASS(errctx, akbasic_variable_set_string(variable, encoded, subscripts, subscriptcount));
SUCCEED_TRUE(obj, dest); SUCCEED_TRUE(obj, dest);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
@@ -762,7 +763,9 @@ akerr_ErrorContext *akbasic_cmd_gshape(akbasic_Runtime *obj, akbasic_ASTLeaf *ex
double x = 0.0; double x = 0.0;
double y = 0.0; double y = 0.0;
int64_t subscripts[AKBASIC_MAX_ARRAY_DEPTH]; int64_t subscripts[AKBASIC_MAX_ARRAY_DEPTH];
size_t prefixlen = 0;
int subscriptcount = 0; int subscriptcount = 0;
int cmp = 0;
(void)lval; (void)rval; (void)lval; (void)rval;
PASS(errctx, require_graphics(obj, "GSHAPE")); PASS(errctx, require_graphics(obj, "GSHAPE"));
@@ -782,11 +785,11 @@ akerr_ErrorContext *akbasic_cmd_gshape(akbasic_Runtime *obj, akbasic_ASTLeaf *ex
y = (count >= 2) ? coords[1] : obj->gfx.y; y = (count >= 2) ? coords[1] : obj->gfx.y;
PASS(errctx, akbasic_variable_get_subscript(variable, subscripts, subscriptcount, &stored)); PASS(errctx, akbasic_variable_get_subscript(variable, subscripts, subscriptcount, &stored));
FAIL_ZERO_RETURN(errctx, PASS(errctx, aksl_strlen(SHAPE_PREFIX, &prefixlen));
(strncmp(stored->stringval, SHAPE_PREFIX, strlen(SHAPE_PREFIX)) == 0), PASS(errctx, aksl_strncmp(stored->stringval, SHAPE_PREFIX, prefixlen, &cmp));
AKBASIC_ERR_VALUE, FAIL_ZERO_RETURN(errctx, (cmp == 0), AKBASIC_ERR_VALUE,
"GSHAPE was given a string that did not come from SSHAPE"); "GSHAPE was given a string that did not come from SSHAPE");
PASS(errctx, aksl_atoll(stored->stringval + strlen(SHAPE_PREFIX), &converted)); PASS(errctx, aksl_atoll(stored->stringval + prefixlen, &converted));
handle = (int64_t)converted; handle = (int64_t)converted;
scale_point(&obj->gfx, &x, &y); scale_point(&obj->gfx, &x, &y);

View File

@@ -13,10 +13,9 @@
*/ */
#include <inttypes.h> #include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h>
#include <akbasic/args.h> #include <akbasic/args.h>
#include <akbasic/error.h> #include <akbasic/error.h>
@@ -322,13 +321,13 @@ akerr_ErrorContext *akbasic_cmd_swap(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
* exactly this reason. The names stay put: they are what the symbol table * exactly this reason. The names stay put: they are what the symbol table
* points at. * points at.
*/ */
memcpy(namea, a->name, sizeof(namea)); PASS(errctx, aksl_memcpy(namea, a->name, sizeof(namea)));
memcpy(nameb, b->name, sizeof(nameb)); PASS(errctx, aksl_memcpy(nameb, b->name, sizeof(nameb)));
memcpy(&swap, a, sizeof(swap)); PASS(errctx, aksl_memcpy(&swap, a, sizeof(swap)));
memcpy(a, b, sizeof(*a)); PASS(errctx, aksl_memcpy(a, b, sizeof(*a)));
memcpy(b, &swap, sizeof(*b)); PASS(errctx, aksl_memcpy(b, &swap, sizeof(*b)));
memcpy(a->name, namea, sizeof(a->name)); PASS(errctx, aksl_memcpy(a->name, namea, sizeof(a->name)));
memcpy(b->name, nameb, sizeof(b->name)); PASS(errctx, aksl_memcpy(b->name, nameb, sizeof(b->name)));
/* /*
* A scalar's storage is *inside* the record, so the pointer that came over * A scalar's storage is *inside* the record, so the pointer that came over
* with it names the other variable's inline slot -- which now holds this * with it names the other variable's inline slot -- which now holds this
@@ -352,6 +351,7 @@ akerr_ErrorContext *akbasic_cmd_help(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
char line[AKBASIC_MAX_LINE_LENGTH * 2]; char line[AKBASIC_MAX_LINE_LENGTH * 2];
int written = 0;
(void)expr; (void)lval; (void)rval; (void)expr; (void)lval; (void)rval;
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in HELP"); FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in HELP");
@@ -369,8 +369,14 @@ akerr_ErrorContext *akbasic_cmd_help(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
SUCCEED_TRUE(obj, dest); SUCCEED_TRUE(obj, dest);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
snprintf(line, sizeof(line), "%" PRId64 " %s", /*
obj->errorline, obj->source[obj->errorline].code); * aksl_snprintf treats truncation as an error, and this destination is twice
* AKBASIC_MAX_LINE_LENGTH against a stored line that is at most one of them
* plus a line number, so the fit is a property of the buffer rather than
* something to check for.
*/
PASS(errctx, aksl_snprintf(&written, line, sizeof(line), "%" PRId64 " %s",
obj->errorline, obj->source[obj->errorline].code));
PASS(errctx, akbasic_runtime_println(obj, line)); PASS(errctx, akbasic_runtime_println(obj, line));
SUCCEED_TRUE(obj, dest); SUCCEED_TRUE(obj, dest);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);

View File

@@ -14,9 +14,8 @@
* as sitting on the keyboard. See TODO.md section 5. * as sitting on the keyboard. See TODO.md section 5.
*/ */
#include <string.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h>
#include <akbasic/error.h> #include <akbasic/error.h>
#include <akbasic/input.h> #include <akbasic/input.h>
@@ -152,9 +151,8 @@ akerr_ErrorContext *akbasic_cmd_getkey(akbasic_Runtime *obj, akbasic_ASTLeaf *ex
} }
obj->input_state.waiting = true; obj->input_state.waiting = true;
obj->input_state.numeric = numeric; obj->input_state.numeric = numeric;
strncpy(obj->input_state.variable, arg->identifier, PASS(errctx, aksl_strcpy(obj->input_state.variable,
sizeof(obj->input_state.variable) - 1); sizeof(obj->input_state.variable), arg->identifier));
obj->input_state.variable[sizeof(obj->input_state.variable) - 1] = '\0';
SUCCEED_TRUE(obj, dest); SUCCEED_TRUE(obj, dest);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }

View File

@@ -14,9 +14,9 @@
#include <inttypes.h> #include <inttypes.h>
#include <stdio.h> #include <stdio.h>
#include <string.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h>
#include <akbasic/args.h> #include <akbasic/args.h>
#include <akbasic/error.h> #include <akbasic/error.h>
@@ -69,7 +69,8 @@ static akerr_ErrorContext *copy_bytes(akbasic_Runtime *obj, akbasic_ASTLeaf *exp
* POKE and PEEK. A BASIC integer here is a real address, so a wrong one is a * POKE and PEEK. A BASIC integer here is a real address, so a wrong one is a
* segmentation fault rather than an error message. See TODO.md section 5. * segmentation fault rather than an error message. See TODO.md section 5.
*/ */
memmove((void *)(uintptr_t)args[2], (const void *)(uintptr_t)args[1], (size_t)length); PASS(errctx, aksl_memmove((void *)(uintptr_t)args[2], (const void *)(uintptr_t)args[1],
(size_t)length));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }

View File

@@ -31,6 +31,7 @@
#include <string.h> #include <string.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h>
#include <akbasic/args.h> #include <akbasic/args.h>
#include <akbasic/error.h> #include <akbasic/error.h>
@@ -403,6 +404,9 @@ static akerr_ErrorContext *define_from_leaf(akbasic_Runtime *obj, akbasic_ASTLea
akbasic_Color fg; akbasic_Color fg;
akbasic_Color bg; akbasic_Color bg;
uint8_t pattern[AKBASIC_SPRITE_PATTERN_BYTES]; uint8_t pattern[AKBASIC_SPRITE_PATTERN_BYTES];
size_t prefixlen = 0;
int handle = 0;
int cmp = 0;
if ( arg->leaftype == AKBASIC_LEAF_IDENTIFIER_INT && if ( arg->leaftype == AKBASIC_LEAF_IDENTIFIER_INT &&
akbasic_leaf_first_subscript(arg) == NULL ) { akbasic_leaf_first_subscript(arg) == NULL ) {
@@ -428,11 +432,17 @@ static akerr_ErrorContext *define_from_leaf(akbasic_Runtime *obj, akbasic_ASTLea
FAIL_ZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE, FAIL_ZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
"SPRSAV expected a sprite number, an image path, a saved shape or an integer array"); "SPRSAV expected a sprite number, an image path, a saved shape or an integer array");
if ( strncmp(value->stringval, SHAPE_PREFIX, strlen(SHAPE_PREFIX)) == 0 ) { PASS(errctx, aksl_strlen(SHAPE_PREFIX, &prefixlen));
int handle = atoi(value->stringval + strlen(SHAPE_PREFIX)); PASS(errctx, aksl_strncmp(value->stringval, SHAPE_PREFIX, prefixlen, &cmp));
if ( cmp == 0 ) {
FAIL_ZERO_RETURN(errctx, (obj->sprites->define_shape != NULL), AKBASIC_ERR_DEVICE, FAIL_ZERO_RETURN(errctx, (obj->sprites->define_shape != NULL), AKBASIC_ERR_DEVICE,
"This sprite device cannot take a saved shape"); "This sprite device cannot take a saved shape");
/*
* aksl_atoi refuses a slot number that is not one, where atoi(3) answered
* a silent 0 -- so `SPRSAV "SHAPE:", 1` is now told what is wrong with it
* rather than being handed shape 0.
*/
PASS(errctx, aksl_atoi(value->stringval + prefixlen, &handle));
PASS(errctx, obj->sprites->define_shape(obj->sprites, index + 1, handle)); PASS(errctx, obj->sprites->define_shape(obj->sprites, index + 1, handle));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -779,8 +789,9 @@ akerr_ErrorContext *akbasic_cmd_solid(akbasic_Runtime *obj, akbasic_ASTLeaf *exp
if ( !obj->sprite_state.solids[i].active ) { if ( !obj->sprite_state.solids[i].active ) {
continue; continue;
} }
memset(&obj->sprite_state.solids[i], 0, sizeof(obj->sprite_state.solids[i]));
/* PASS rather than CATCH: this is a loop. */ /* PASS rather than CATCH: this is a loop. */
PASS(errctx, aksl_memset(&obj->sprite_state.solids[i], 0,
sizeof(obj->sprite_state.solids[i])));
PASS(errctx, obj->sprites->solid(obj->sprites, i + 1, false, 0.0, 0.0, 0.0, 0.0)); PASS(errctx, obj->sprites->solid(obj->sprites, i + 1, false, 0.0, 0.0, 0.0, 0.0));
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
@@ -792,7 +803,7 @@ akerr_ErrorContext *akbasic_cmd_solid(akbasic_Runtime *obj, akbasic_ASTLeaf *exp
solid = &obj->sprite_state.solids[id - 1]; solid = &obj->sprite_state.solids[id - 1];
if ( count == 1 ) { if ( count == 1 ) {
memset(solid, 0, sizeof(*solid)); PASS(errctx, aksl_memset(solid, 0, sizeof(*solid)));
PASS(errctx, obj->sprites->solid(obj->sprites, id, false, 0.0, 0.0, 0.0, 0.0)); PASS(errctx, obj->sprites->solid(obj->sprites, id, false, 0.0, 0.0, 0.0, 0.0));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }

View File

@@ -28,6 +28,7 @@
#include <string.h> #include <string.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h>
#include <akbasic/error.h> #include <akbasic/error.h>
#include <akbasic/runtime.h> #include <akbasic/runtime.h>
@@ -194,7 +195,11 @@ akerr_ErrorContext *akbasic_struct_to_string(akbasic_Runtime *obj, int typeindex
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
char rendered[AKBASIC_MAX_STRING_LENGTH]; char rendered[AKBASIC_MAX_STRING_LENGTH];
akbasic_StructType *type = NULL; akbasic_StructType *type = NULL;
const char *separator = NULL;
size_t used = 0; size_t used = 0;
size_t seplen = 0;
size_t namelen = 0;
size_t renderedlen = 0;
int written = 0; int written = 0;
int i = 0; int i = 0;
@@ -204,6 +209,20 @@ akerr_ErrorContext *akbasic_struct_to_string(akbasic_Runtime *obj, int typeindex
AKBASIC_ERR_BOUNDS, "Structure type index %d is out of range", typeindex); AKBASIC_ERR_BOUNDS, "Structure type index %d is out of range", typeindex);
type = &obj->structtypes.types[typeindex]; type = &obj->structtypes.types[typeindex];
/*
* These two stay on raw snprintf on purpose, and are two of the six such
* sites left in src/. Both want truncation as an *answer*: `dest` and `len`
* come from the caller, this renders a value for display, and a name too long
* for the buffer should print short rather than fail the whole PRINT. The
* second one reads the return value to detect it -- on overflow `used` lands
* past `len`, which is exactly what makes the field loop below and the
* closing ")" both skip.
*
* aksl_snprintf treats truncation as AKERR_OUTOFBOUNDS and its `count` is 0
* on that path, so it can express neither the tolerance nor the detection.
* libakstdlib #34 is the issue that would give `count` the required length
* back; until it lands there is nothing to convert these to.
*/
if ( depth >= AKBASIC_MAX_STRUCT_DEPTH ) { if ( depth >= AKBASIC_MAX_STRUCT_DEPTH ) {
snprintf(dest, len, "%s(...)", type->name); snprintf(dest, len, "%s(...)", type->name);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
@@ -222,7 +241,7 @@ akerr_ErrorContext *akbasic_struct_to_string(akbasic_Runtime *obj, int typeindex
break; break;
case AKBASIC_FIELD_POINTER: case AKBASIC_FIELD_POINTER:
if ( slot->structbase == NULL ) { if ( slot->structbase == NULL ) {
snprintf(rendered, sizeof(rendered), "NOTHING"); PASS(errctx, aksl_snprintf(&written, rendered, sizeof(rendered), "NOTHING"));
} else { } else {
PASS(errctx, akbasic_struct_to_string(obj, slot->structtype, slot->structbase, PASS(errctx, akbasic_struct_to_string(obj, slot->structtype, slot->structbase,
depth + 1, rendered, sizeof(rendered))); depth + 1, rendered, sizeof(rendered)));
@@ -232,15 +251,24 @@ akerr_ErrorContext *akbasic_struct_to_string(akbasic_Runtime *obj, int typeindex
PASS(errctx, akbasic_value_to_string(slot, rendered, sizeof(rendered))); PASS(errctx, akbasic_value_to_string(slot, rendered, sizeof(rendered)));
break; break;
} }
written = snprintf(dest + used, len - used, "%s%s=%s", separator = (i == 0 ? "" : ", ");
(i == 0 ? "" : ", "), field->name, rendered); PASS(errctx, aksl_strlen(separator, &seplen));
if ( written < 0 || (size_t)written >= len - used ) { PASS(errctx, aksl_strlen(field->name, &namelen));
PASS(errctx, aksl_strlen(rendered, &renderedlen));
/*
* A render that does not fit ends the list rather than failing it, so the
* fit is decided here -- aksl_snprintf treats truncation as an error, and
* the one byte counted beyond the three lengths is the `=`.
*/
if ( used + seplen + namelen + 1 + renderedlen >= len ) {
break; break;
} }
PASS(errctx, aksl_snprintf(&written, dest + used, len - used, "%s%s=%s",
separator, field->name, rendered));
used += (size_t)written; used += (size_t)written;
} }
if ( used + 1 < len ) { if ( used + 1 < len ) {
snprintf(dest + used, len - used, ")"); PASS(errctx, aksl_snprintf(&written, dest + used, len - used, ")"));
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }

View File

@@ -173,6 +173,7 @@ akerr_ErrorContext *akbasic_cmd_begin(akbasic_Runtime *obj, akbasic_ASTLeaf *exp
akerr_ErrorContext *akbasic_cmd_bend(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest) akerr_ErrorContext *akbasic_cmd_bend(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
bool waiting = false;
(void)expr; (void)lval; (void)rval; (void)expr; (void)lval; (void)rval;
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
@@ -182,7 +183,8 @@ akerr_ErrorContext *akbasic_cmd_bend(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
* to do, or it is the BEND a skipped block was skipping to, in which case * to do, or it is the BEND a skipped block was skipping to, in which case
* stopping the skip is the whole job. * stopping the skip is the whole job.
*/ */
if ( akbasic_environment_is_waiting_for(obj->environment, "BEND") ) { PASS(errctx, akbasic_environment_is_waiting_for(obj->environment, "BEND", &waiting));
if ( waiting ) {
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "BEND")); PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "BEND"));
} }
SUCCEED_TRUE(obj, dest); SUCCEED_TRUE(obj, dest);

View File

@@ -16,10 +16,9 @@
*/ */
#include <inttypes.h> #include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h>
#include <akbasic/args.h> #include <akbasic/args.h>
#include <akbasic/error.h> #include <akbasic/error.h>
@@ -107,6 +106,8 @@ akerr_ErrorContext *akbasic_cmd_resume(akbasic_Runtime *obj, akbasic_ASTLeaf *ex
akbasic_Variable *variable = NULL; akbasic_Variable *variable = NULL;
int64_t zerosubscript[1] = { 0 }; int64_t zerosubscript[1] = { 0 };
int64_t resumeline = 0; int64_t resumeline = 0;
int cmp = 0;
bool isnext = false;
(void)lval; (void)rval; (void)lval; (void)rval;
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
@@ -124,6 +125,10 @@ akerr_ErrorContext *akbasic_cmd_resume(akbasic_Runtime *obj, akbasic_ASTLeaf *ex
resumeline = value->intval; resumeline = value->intval;
target = (expr != NULL ? expr->right : NULL); target = (expr != NULL ? expr->right : NULL);
if ( target != NULL && target->leaftype == AKBASIC_LEAF_COMMAND ) {
PASS(errctx, aksl_strcmp(target->identifier, "NEXT", &cmp));
isnext = (cmp == 0);
}
if ( target == NULL ) { if ( target == NULL ) {
/* /*
* Bare RESUME retries the line that failed. That is a loop unless the * Bare RESUME retries the line that failed. That is a loop unless the
@@ -131,8 +136,7 @@ akerr_ErrorContext *akbasic_cmd_resume(akbasic_Runtime *obj, akbasic_ASTLeaf *ex
* is exactly what the verb is for. * is exactly what the verb is for.
*/ */
obj->environment->parent->nextline = resumeline; obj->environment->parent->nextline = resumeline;
} else if ( target->leaftype == AKBASIC_LEAF_COMMAND && } else if ( isnext ) {
strcmp(target->identifier, "NEXT") == 0 ) {
/* RESUME NEXT carries on at the line after the one that failed. */ /* RESUME NEXT carries on at the line after the one that failed. */
obj->environment->parent->nextline = resumeline + 1; obj->environment->parent->nextline = resumeline + 1;
} else { } else {
@@ -182,8 +186,8 @@ akerr_ErrorContext *akbasic_fn_err(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
* that ever decides otherwise. * that ever decides otherwise.
*/ */
name = akerr_name_for_status((int)status, NULL); name = akerr_name_for_status((int)status, NULL);
snprintf(out->stringval, sizeof(out->stringval), "%s", PASS(errctx, aksl_strcpy(out->stringval, sizeof(out->stringval),
(name != NULL ? name : "Unknown Error")); (name != NULL ? name : "Unknown Error")));
*dest = out; *dest = out;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }

View File

@@ -19,9 +19,9 @@
*/ */
#include <inttypes.h> #include <inttypes.h>
#include <string.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h>
#include <akbasic/args.h> #include <akbasic/args.h>
#include <akbasic/error.h> #include <akbasic/error.h>
@@ -153,8 +153,7 @@ static akerr_ErrorContext *nth_string(akbasic_Runtime *obj, akbasic_ASTLeaf *exp
PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value)); PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
FAIL_ZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE, FAIL_ZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
"%s expected a string in argument %d", verb, n + 1); "%s expected a string in argument %d", verb, n + 1);
strncpy(dest, value->stringval, len - 1); PASS(errctx, aksl_strcpy(dest, len, value->stringval));
dest[len - 1] = '\0';
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -259,8 +258,7 @@ static akerr_ErrorContext *target_variable(akbasic_Runtime *obj, akbasic_ASTLeaf
PASS(errctx, akbasic_environment_get(obj->environment, arg->identifier, dest)); PASS(errctx, akbasic_environment_get(obj->environment, arg->identifier, dest));
FAIL_ZERO_RETURN(errctx, (*dest != NULL), AKBASIC_ERR_UNDEFINED, FAIL_ZERO_RETURN(errctx, (*dest != NULL), AKBASIC_ERR_UNDEFINED,
"GETMENU could not reach the variable %s", arg->identifier); "GETMENU could not reach the variable %s", arg->identifier);
strncpy(name, arg->identifier, len - 1); PASS(errctx, aksl_strcpy(name, len, arg->identifier));
name[len - 1] = '\0';
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -309,8 +307,7 @@ akerr_ErrorContext *akbasic_cmd_getmenu(akbasic_Runtime *obj, akbasic_ASTLeaf *e
*/ */
obj->ui_state.waiting = true; obj->ui_state.waiting = true;
obj->ui_state.waitmenu = slot; obj->ui_state.waitmenu = slot;
strncpy(obj->ui_state.variable, name, sizeof(obj->ui_state.variable) - 1); PASS(errctx, aksl_strcpy(obj->ui_state.variable, sizeof(obj->ui_state.variable), name));
obj->ui_state.variable[sizeof(obj->ui_state.variable) - 1] = '\0';
SUCCEED_TRUE(obj, dest); SUCCEED_TRUE(obj, dest);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -487,7 +484,7 @@ akerr_ErrorContext *akbasic_ui_state_init(akbasic_UiState *obj)
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
"NULL argument in ui_state_init"); "NULL argument in ui_state_init");
memset(obj, 0, sizeof(*obj)); PASS(errctx, aksl_memset(obj, 0, sizeof(*obj)));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }

View File

@@ -26,27 +26,67 @@ akerr_ErrorContext *akbasic_scanner_zero(akbasic_Runtime *obj)
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
static bool is_at_end(akbasic_Runtime *obj) /**
* @brief Is the cursor past the end of the line?
*
* The answer leaves through @p dest rather than the return value because
* measuring the line can fail, and a `bool` has nowhere to put that. Same shape
* as symtab.c's `probe`, and for the same reason. See libakstdlib #38.
*/
static akerr_ErrorContext *is_at_end(akbasic_Runtime *obj, bool *dest)
{ {
return (obj->current >= (int)strlen(obj->line)); PREPARE_ERROR(errctx);
size_t linelen = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
"NULL argument in is_at_end");
PASS(errctx, aksl_strlen(obj->line, &linelen));
*dest = (obj->current >= (int)linelen);
SUCCEED_RETURN(errctx);
} }
static bool peek(akbasic_Runtime *obj, char *dest) /**
* @brief The character under the cursor.
* @param[out] dest The character. Untouched when there is none.
* @param[out] got Whether there was one. The old `bool` return.
*/
static akerr_ErrorContext *peek(akbasic_Runtime *obj, char *dest, bool *got)
{ {
if ( is_at_end(obj) ) { PREPARE_ERROR(errctx);
return false; bool atend = false;
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL && got != NULL), AKERR_NULLPOINTER,
"NULL argument in peek");
PASS(errctx, is_at_end(obj, &atend));
if ( atend ) {
*got = false;
SUCCEED_RETURN(errctx);
} }
*dest = obj->line[obj->current]; *dest = obj->line[obj->current];
return true; *got = true;
SUCCEED_RETURN(errctx);
} }
static bool peek_next(akbasic_Runtime *obj, char *dest) /**
* @brief The character one past the cursor.
* @param[out] dest The character. Untouched when there is none.
* @param[out] got Whether there was one. The old `bool` return.
*/
static akerr_ErrorContext *peek_next(akbasic_Runtime *obj, char *dest, bool *got)
{ {
if ( (obj->current + 1) >= (int)strlen(obj->line) ) { PREPARE_ERROR(errctx);
return false; size_t linelen = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL && got != NULL), AKERR_NULLPOINTER,
"NULL argument in peek_next");
PASS(errctx, aksl_strlen(obj->line, &linelen));
if ( (obj->current + 1) >= (int)linelen ) {
*got = false;
SUCCEED_RETURN(errctx);
} }
*dest = obj->line[obj->current + 1]; *dest = obj->line[obj->current + 1];
return true; *got = true;
SUCCEED_RETURN(errctx);
} }
/* /*
@@ -58,9 +98,12 @@ static bool peek_next(akbasic_Runtime *obj, char *dest)
static akerr_ErrorContext *get_lexeme(akbasic_Runtime *obj, char *dest, size_t len) static akerr_ErrorContext *get_lexeme(akbasic_Runtime *obj, char *dest, size_t len)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
int linelen = (int)strlen(obj->line); size_t measured = 0;
int linelen = 0;
int span = 0; int span = 0;
PASS(errctx, aksl_strlen(obj->line, &measured));
linelen = (int)measured;
if ( obj->current == linelen ) { if ( obj->current == linelen ) {
span = linelen - obj->start; span = linelen - obj->start;
} else if ( obj->start == obj->current ) { } else if ( obj->start == obj->current ) {
@@ -74,7 +117,7 @@ static akerr_ErrorContext *get_lexeme(akbasic_Runtime *obj, char *dest, size_t l
} }
FAIL_ZERO_RETURN(errctx, (span >= 0 && (size_t)span < len), AKBASIC_ERR_BOUNDS, FAIL_ZERO_RETURN(errctx, (span >= 0 && (size_t)span < len), AKBASIC_ERR_BOUNDS,
"Lexeme of %d characters exceeds the %zu character limit", span, len - 1); "Lexeme of %d characters exceeds the %zu character limit", span, len - 1);
memcpy(dest, obj->line + obj->start, (size_t)span); PASS(errctx, aksl_memcpy(dest, obj->line + obj->start, (size_t)span));
dest[span] = '\0'; dest[span] = '\0';
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -83,26 +126,39 @@ static akerr_ErrorContext *add_token(akbasic_Runtime *obj, akbasic_TokenType tok
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
akbasic_Environment *env = obj->environment; akbasic_Environment *env = obj->environment;
size_t lexemelen = 0;
FAIL_ZERO_RETURN(errctx, (env->nexttoken < AKBASIC_MAX_TOKENS), AKBASIC_ERR_BOUNDS, FAIL_ZERO_RETURN(errctx, (env->nexttoken < AKBASIC_MAX_TOKENS), AKBASIC_ERR_BOUNDS,
"Line %" PRId64 " has more than %d tokens", "Line %" PRId64 " has more than %d tokens",
env->lineno, AKBASIC_MAX_TOKENS); env->lineno, AKBASIC_MAX_TOKENS);
FAIL_ZERO_RETURN(errctx, (strlen(lexeme) < AKBASIC_MAX_LINE_LENGTH), AKBASIC_ERR_BOUNDS, PASS(errctx, aksl_strlen(lexeme, &lexemelen));
FAIL_ZERO_RETURN(errctx, (lexemelen < AKBASIC_MAX_LINE_LENGTH), AKBASIC_ERR_BOUNDS,
"Token lexeme exceeds the %d character limit", AKBASIC_MAX_LINE_LENGTH - 1); "Token lexeme exceeds the %d character limit", AKBASIC_MAX_LINE_LENGTH - 1);
env->tokens[env->nexttoken].tokentype = token; env->tokens[env->nexttoken].tokentype = token;
env->tokens[env->nexttoken].lineno = env->lineno; env->tokens[env->nexttoken].lineno = env->lineno;
strncpy(env->tokens[env->nexttoken].lexeme, lexeme, AKBASIC_MAX_LINE_LENGTH - 1); PASS(errctx, aksl_strcpy(env->tokens[env->nexttoken].lexeme,
env->tokens[env->nexttoken].lexeme[AKBASIC_MAX_LINE_LENGTH - 1] = '\0'; sizeof(env->tokens[env->nexttoken].lexeme), lexeme));
env->nexttoken += 1; env->nexttoken += 1;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
/* Consume one more character when it matches, choosing between two token types. */ /**
static bool match_next_char(akbasic_Runtime *obj, char cm, akbasic_TokenType truetype, akbasic_TokenType falsetype) * @brief Consume one more character when it matches, choosing between two token types.
* @param[out] matched Whether the character was consumed. The old `bool` return.
*
* On the chain below `peek`, so it reports the same way. See libakstdlib #38.
*/
static akerr_ErrorContext *match_next_char(akbasic_Runtime *obj, char cm, akbasic_TokenType truetype,
akbasic_TokenType falsetype, bool *matched)
{ {
PREPARE_ERROR(errctx);
char nc = '\0'; char nc = '\0';
bool got = false;
if ( !peek(obj, &nc) ) { FAIL_ZERO_RETURN(errctx, (obj != NULL && matched != NULL), AKERR_NULLPOINTER,
"NULL argument in match_next_char");
PASS(errctx, peek(obj, &nc, &got));
if ( !got ) {
/* /*
* Nothing left to peek at, so the operator is whatever it is on its * Nothing left to peek at, so the operator is whatever it is on its
* own. The reference returns here *without* setting a type * own. The reference returns here *without* setting a type
@@ -112,24 +168,34 @@ static bool match_next_char(akbasic_Runtime *obj, char cm, akbasic_TokenType tru
* TODO.md section 6 item 14. * TODO.md section 6 item 14.
*/ */
obj->tokentype = falsetype; obj->tokentype = falsetype;
return false; *matched = false;
SUCCEED_RETURN(errctx);
} }
if ( nc == cm ) { if ( nc == cm ) {
obj->current += 1; obj->current += 1;
obj->tokentype = truetype; obj->tokentype = truetype;
return true; *matched = true;
SUCCEED_RETURN(errctx);
} }
obj->tokentype = falsetype; obj->tokentype = falsetype;
return false; *matched = false;
SUCCEED_RETURN(errctx);
} }
static akerr_ErrorContext *match_string(akbasic_Runtime *obj) static akerr_ErrorContext *match_string(akbasic_Runtime *obj)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
char c = '\0'; char c = '\0';
bool atend = false;
bool got = false;
while ( !is_at_end(obj) ) { for ( ;; ) {
if ( !peek(obj, &c) ) { PASS(errctx, is_at_end(obj, &atend));
if ( atend ) {
break;
}
PASS(errctx, peek(obj, &c, &got));
if ( !got ) {
PASS(errctx, akbasic_runtime_error(obj, AKBASIC_ERRCLASS_PARSE, PASS(errctx, akbasic_runtime_error(obj, AKBASIC_ERRCLASS_PARSE,
"UNTERMINATED STRING LITERAL\n")); "UNTERMINATED STRING LITERAL\n"));
obj->hasError = true; obj->hasError = true;
@@ -154,12 +220,19 @@ static akerr_ErrorContext *match_number(akbasic_Runtime *obj)
int64_t lineno = 0; int64_t lineno = 0;
long long converted = 0; long long converted = 0;
bool hex = false; bool hex = false;
bool atend = false;
bool got = false;
obj->tokentype = AKBASIC_TOK_LITERAL_INT; obj->tokentype = AKBASIC_TOK_LITERAL_INT;
while ( !is_at_end(obj) ) { for ( ;; ) {
(void)peek(obj, &c); PASS(errctx, is_at_end(obj, &atend));
if ( atend ) {
break;
}
PASS(errctx, peek(obj, &c, &got));
if ( c == '.' ) { if ( c == '.' ) {
if ( !peek_next(obj, &nc) || !isdigit((unsigned char)nc) ) { PASS(errctx, peek_next(obj, &nc, &got));
if ( !got || !isdigit((unsigned char)nc) ) {
PASS(errctx, akbasic_runtime_error(obj, AKBASIC_ERRCLASS_PARSE, PASS(errctx, akbasic_runtime_error(obj, AKBASIC_ERRCLASS_PARSE,
"INVALID FLOATING POINT LITERAL\n")); "INVALID FLOATING POINT LITERAL\n"));
obj->hasError = true; obj->hasError = true;
@@ -200,13 +273,17 @@ static akerr_ErrorContext *match_number(akbasic_Runtime *obj)
} CLEANUP { } CLEANUP {
} PROCESS(errctx) { } PROCESS(errctx) {
} HANDLE_DEFAULT(errctx) { } HANDLE_DEFAULT(errctx) {
char message[AKBASIC_MAX_LINE_LENGTH + 32]; char message[AKBASIC_MAX_LINE_LENGTH + 32] = "INTEGER CONVERSION";
snprintf(message, sizeof(message), "INTEGER CONVERSION ON '%s'", lexeme); int written = 0;
/* /*
* Reporting can itself fail if the sink is broken. Nothing useful * Reporting can itself fail if the sink is broken, and so can
* remains to be done about that here, so record the flag and let the * formatting the message. Neither leaves anything useful to do here --
* next operation surface it. * this block is already handling an error -- so both are ignored, the
* message keeps the initialiser above if the format fails, and the flag
* lets the next operation surface it.
*/ */
IGNORE(aksl_snprintf(&written, message, sizeof(message),
"INTEGER CONVERSION ON '%s'", lexeme));
IGNORE(akbasic_runtime_error(obj, AKBASIC_ERRCLASS_PARSE, message)); IGNORE(akbasic_runtime_error(obj, AKBASIC_ERRCLASS_PARSE, message));
obj->hasError = true; obj->hasError = true;
} FINISH(errctx, false); } FINISH(errctx, false);
@@ -230,10 +307,16 @@ static akerr_ErrorContext *match_identifier(akbasic_Runtime *obj)
bool userfunction = false; bool userfunction = false;
char c = '\0'; char c = '\0';
size_t used = 0; size_t used = 0;
bool atend = false;
bool got = false;
obj->tokentype = AKBASIC_TOK_IDENTIFIER; obj->tokentype = AKBASIC_TOK_IDENTIFIER;
while ( !is_at_end(obj) ) { for ( ;; ) {
(void)peek(obj, &c); PASS(errctx, is_at_end(obj, &atend));
if ( atend ) {
break;
}
PASS(errctx, peek(obj, &c, &got));
if ( isdigit((unsigned char)c) || isalpha((unsigned char)c) ) { if ( isdigit((unsigned char)c) || isalpha((unsigned char)c) ) {
obj->current += 1; obj->current += 1;
continue; continue;
@@ -270,9 +353,8 @@ static akerr_ErrorContext *match_identifier(akbasic_Runtime *obj)
* in variable name" branch below is dead code, and `PRINT$ = 1` is quietly * in variable name" branch below is dead code, and `PRINT$ = 1` is quietly
* accepted as an ordinary string variable. TODO.md section 6 item 16. * accepted as an ordinary string variable. TODO.md section 6 item 16.
*/ */
strncpy(basename, lexeme, sizeof(basename) - 1); PASS(errctx, aksl_strcpy(basename, sizeof(basename), lexeme));
basename[sizeof(basename) - 1] = '\0'; PASS(errctx, aksl_strlen(basename, &used));
used = strlen(basename);
if ( obj->tokentype != AKBASIC_TOK_IDENTIFIER && used > 0 ) { if ( obj->tokentype != AKBASIC_TOK_IDENTIFIER && used > 0 ) {
basename[used - 1] = '\0'; basename[used - 1] = '\0';
} }
@@ -311,15 +393,18 @@ akerr_ErrorContext *akbasic_scanner_scan(akbasic_Runtime *obj, const char *line,
char lexeme[AKBASIC_MAX_LINE_LENGTH]; char lexeme[AKBASIC_MAX_LINE_LENGTH];
char c = '\0'; char c = '\0';
bool done = false; bool done = false;
bool atend = false;
bool matched = false;
size_t linelen = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in scan"); FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in scan");
FAIL_ZERO_RETURN(errctx, (line != NULL), AKERR_NULLPOINTER, "NULL line in scan"); FAIL_ZERO_RETURN(errctx, (line != NULL), AKERR_NULLPOINTER, "NULL line in scan");
FAIL_ZERO_RETURN(errctx, (strlen(line) < AKBASIC_MAX_LINE_LENGTH), AKBASIC_ERR_BOUNDS, PASS(errctx, aksl_strlen(line, &linelen));
FAIL_ZERO_RETURN(errctx, (linelen < AKBASIC_MAX_LINE_LENGTH), AKBASIC_ERR_BOUNDS,
"Source line of %zu characters exceeds the %d character limit", "Source line of %zu characters exceeds the %d character limit",
strlen(line), AKBASIC_MAX_LINE_LENGTH - 1); linelen, AKBASIC_MAX_LINE_LENGTH - 1);
strncpy(obj->line, line, AKBASIC_MAX_LINE_LENGTH - 1); PASS(errctx, aksl_strcpy(obj->line, sizeof(obj->line), line));
obj->line[AKBASIC_MAX_LINE_LENGTH - 1] = '\0';
PASS(errctx, akbasic_environment_zero_parser(obj->environment)); PASS(errctx, akbasic_environment_zero_parser(obj->environment));
obj->current = 0; obj->current = 0;
obj->start = 0; obj->start = 0;
@@ -333,7 +418,11 @@ akerr_ErrorContext *akbasic_scanner_scan(akbasic_Runtime *obj, const char *line,
*/ */
obj->hadlinenumber = false; obj->hadlinenumber = false;
while ( !is_at_end(obj) && !done ) { for ( ;; ) {
PASS(errctx, is_at_end(obj, &atend));
if ( atend || done ) {
break;
}
c = obj->line[obj->current]; c = obj->line[obj->current];
obj->current += 1; obj->current += 1;
@@ -350,7 +439,7 @@ akerr_ErrorContext *akbasic_scanner_scan(akbasic_Runtime *obj, const char *line,
* no spelling this makes ambiguous. * no spelling this makes ambiguous.
*/ */
case '-': case '-':
(void)match_next_char(obj, '>', AKBASIC_TOK_ARROW, AKBASIC_TOK_MINUS); PASS(errctx, match_next_char(obj, '>', AKBASIC_TOK_ARROW, AKBASIC_TOK_MINUS, &matched));
break; break;
case '/': obj->tokentype = AKBASIC_TOK_LEFT_SLASH; break; case '/': obj->tokentype = AKBASIC_TOK_LEFT_SLASH; break;
case '*': obj->tokentype = AKBASIC_TOK_STAR; break; case '*': obj->tokentype = AKBASIC_TOK_STAR; break;
@@ -373,15 +462,19 @@ akerr_ErrorContext *akbasic_scanner_scan(akbasic_Runtime *obj, const char *line,
case '[': obj->tokentype = AKBASIC_TOK_LEFT_SQUAREBRACKET; break; case '[': obj->tokentype = AKBASIC_TOK_LEFT_SQUAREBRACKET; break;
case ']': obj->tokentype = AKBASIC_TOK_RIGHT_SQUAREBRACKET; break; case ']': obj->tokentype = AKBASIC_TOK_RIGHT_SQUAREBRACKET; break;
case '=': case '=':
(void)match_next_char(obj, '=', AKBASIC_TOK_EQUAL, AKBASIC_TOK_ASSIGNMENT); PASS(errctx, match_next_char(obj, '=', AKBASIC_TOK_EQUAL, AKBASIC_TOK_ASSIGNMENT, &matched));
break; break;
case '<': case '<':
if ( !match_next_char(obj, '=', AKBASIC_TOK_LESS_THAN_EQUAL, AKBASIC_TOK_LESS_THAN) ) { PASS(errctx, match_next_char(obj, '=', AKBASIC_TOK_LESS_THAN_EQUAL,
(void)match_next_char(obj, '>', AKBASIC_TOK_NOT_EQUAL, AKBASIC_TOK_LESS_THAN); AKBASIC_TOK_LESS_THAN, &matched));
if ( !matched ) {
PASS(errctx, match_next_char(obj, '>', AKBASIC_TOK_NOT_EQUAL,
AKBASIC_TOK_LESS_THAN, &matched));
} }
break; break;
case '>': case '>':
(void)match_next_char(obj, '=', AKBASIC_TOK_GREATER_THAN_EQUAL, AKBASIC_TOK_GREATER_THAN); PASS(errctx, match_next_char(obj, '=', AKBASIC_TOK_GREATER_THAN_EQUAL,
AKBASIC_TOK_GREATER_THAN, &matched));
break; break;
case '"': case '"':
obj->start = obj->current; obj->start = obj->current;
@@ -402,7 +495,10 @@ akerr_ErrorContext *akbasic_scanner_scan(akbasic_Runtime *obj, const char *line,
PASS(errctx, match_identifier(obj)); PASS(errctx, match_identifier(obj));
} else { } else {
char message[AKBASIC_MAX_LINE_LENGTH]; char message[AKBASIC_MAX_LINE_LENGTH];
snprintf(message, sizeof(message), "UNKNOWN TOKEN %c\n", c); int written = 0;
PASS(errctx, aksl_snprintf(&written, message, sizeof(message),
"UNKNOWN TOKEN %c\n", c));
PASS(errctx, akbasic_runtime_error(obj, AKBASIC_ERRCLASS_PARSE, message)); PASS(errctx, akbasic_runtime_error(obj, AKBASIC_ERRCLASS_PARSE, message));
obj->hasError = true; obj->hasError = true;
obj->start = obj->current; obj->start = obj->current;
@@ -426,10 +522,13 @@ akerr_ErrorContext *akbasic_scanner_scan(akbasic_Runtime *obj, const char *line,
* stores *that* as the program text. * stores *that* as the program text.
*/ */
int skip = obj->current; int skip = obj->current;
size_t tail = 0;
while ( obj->line[skip] == ' ' ) { while ( obj->line[skip] == ' ' ) {
skip += 1; skip += 1;
} }
memmove(obj->line, obj->line + skip, strlen(obj->line + skip) + 1); PASS(errctx, aksl_strlen(obj->line + skip, &tail));
PASS(errctx, aksl_memmove(obj->line, obj->line + skip, tail + 1));
obj->current = 0; obj->current = 0;
} else { } else {
PASS(errctx, get_lexeme(obj, lexeme, sizeof(lexeme))); PASS(errctx, get_lexeme(obj, lexeme, sizeof(lexeme)));
@@ -445,10 +544,10 @@ akerr_ErrorContext *akbasic_scanner_scan(akbasic_Runtime *obj, const char *line,
} }
if ( dest != NULL ) { if ( dest != NULL ) {
FAIL_ZERO_RETURN(errctx, (strlen(obj->line) < len), AKBASIC_ERR_BOUNDS, PASS(errctx, aksl_strlen(obj->line, &linelen));
FAIL_ZERO_RETURN(errctx, (linelen < len), AKBASIC_ERR_BOUNDS,
"Scanned line does not fit the caller's buffer"); "Scanned line does not fit the caller's buffer");
strncpy(dest, obj->line, len - 1); PASS(errctx, aksl_strcpy(dest, len, obj->line));
dest[len - 1] = '\0';
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }

View File

@@ -15,9 +15,8 @@
#include <ctype.h> #include <ctype.h>
#include <stdio.h> #include <stdio.h>
#include <string.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h>
#include <akgl/controller.h> #include <akgl/controller.h>
#include <akgl/draw.h> #include <akgl/draw.h>
@@ -32,15 +31,24 @@
/** @brief Bytes per row, including the terminator. */ /** @brief Bytes per row, including the terminator. */
#define SINK_MAX_COLUMNS 256 #define SINK_MAX_COLUMNS 256
/** @brief Scroll the grid up by one row, dropping the top one. */ /**
static void scroll(akbasic_AkglSink *state) * @brief Scroll the grid up by one row, dropping the top one.
*
* Returns a context rather than `void` because the row moves can fail and there
* was nowhere to say so. Everything on this chain -- scroll, newline,
* putchar_at, echo_line, edit_key -- returns one for the same reason, and the
* chain already ends at sink_write, sink_writeln and sink_readline, which
* returned one all along. See libakstdlib #38.
*/
static akerr_ErrorContext *scroll(akbasic_AkglSink *state)
{ {
PREPARE_ERROR(errctx);
int row = 0; int row = 0;
for ( row = 0; row < SINK_MAX_ROWS - 1; row++ ) { for ( row = 0; row < SINK_MAX_ROWS - 1; row++ ) {
memcpy(state->text[row], state->text[row + 1], SINK_MAX_COLUMNS); PASS(errctx, aksl_memcpy(state->text[row], state->text[row + 1], SINK_MAX_COLUMNS));
} }
memset(state->text[SINK_MAX_ROWS - 1], 0, SINK_MAX_COLUMNS); PASS(errctx, aksl_memset(state->text[SINK_MAX_ROWS - 1], 0, SINK_MAX_COLUMNS));
if ( state->cursorrow > 0 ) { if ( state->cursorrow > 0 ) {
state->cursorrow -= 1; state->cursorrow -= 1;
} }
@@ -59,13 +67,16 @@ static void scroll(akbasic_AkglSink *state)
} }
/** @brief Move to the start of the next row, scrolling if that runs off the end. */ /** @brief Move to the start of the next row, scrolling if that runs off the end. */
static void newline(akbasic_AkglSink *state) static akerr_ErrorContext *newline(akbasic_AkglSink *state)
{ {
PREPARE_ERROR(errctx);
state->cursorcol = 0; state->cursorcol = 0;
state->cursorrow += 1; state->cursorrow += 1;
if ( state->cursorrow >= state->rows || state->cursorrow >= SINK_MAX_ROWS ) { if ( state->cursorrow >= state->rows || state->cursorrow >= SINK_MAX_ROWS ) {
scroll(state); PASS(errctx, scroll(state));
} }
SUCCEED_RETURN(errctx);
} }
/** /**
@@ -89,16 +100,17 @@ static void newline(akbasic_AkglSink *state)
* cleared between rows, so the gap holds the tail of some longer row that used * cleared between rows, so the gap holds the tail of some longer row that used
* to be here. * to be here.
*/ */
static void putchar_at(akbasic_AkglSink *state, char c) static akerr_ErrorContext *putchar_at(akbasic_AkglSink *state, char c)
{ {
PREPARE_ERROR(errctx);
int col = 0; int col = 0;
if ( c == '\n' ) { if ( c == '\n' ) {
newline(state); PASS(errctx, newline(state));
return; SUCCEED_RETURN(errctx);
} }
if ( state->cursorcol >= state->columns || state->cursorcol >= SINK_MAX_COLUMNS - 1 ) { if ( state->cursorcol >= state->columns || state->cursorcol >= SINK_MAX_COLUMNS - 1 ) {
newline(state); PASS(errctx, newline(state));
} }
for ( col = 0; col < state->cursorcol; col++ ) { for ( col = 0; col < state->cursorcol; col++ ) {
if ( state->text[state->cursorrow][col] == '\0' ) { if ( state->text[state->cursorrow][col] == '\0' ) {
@@ -111,6 +123,7 @@ static void putchar_at(akbasic_AkglSink *state, char c)
state->text[state->cursorrow][state->cursorcol] = c; state->text[state->cursorrow][state->cursorcol] = c;
state->cursorcol += 1; state->cursorcol += 1;
state->text[state->cursorrow][state->cursorcol] = '\0'; state->text[state->cursorrow][state->cursorcol] = '\0';
SUCCEED_RETURN(errctx);
} }
static akerr_ErrorContext *sink_write(akbasic_TextSink *self, const char *text) static akerr_ErrorContext *sink_write(akbasic_TextSink *self, const char *text)
@@ -125,7 +138,7 @@ static akerr_ErrorContext *sink_write(akbasic_TextSink *self, const char *text)
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER, FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
"akgl sink has no state"); "akgl sink has no state");
for ( i = 0; text[i] != '\0'; i++ ) { for ( i = 0; text[i] != '\0'; i++ ) {
putchar_at(state, text[i]); PASS(errctx, putchar_at(state, text[i]));
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -137,7 +150,7 @@ static akerr_ErrorContext *sink_writeln(akbasic_TextSink *self, const char *text
PASS(errctx, sink_write(self, text)); PASS(errctx, sink_write(self, text));
state = (akbasic_AkglSink *)self->self; state = (akbasic_AkglSink *)self->self;
newline(state); PASS(errctx, newline(state));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -151,8 +164,9 @@ static akerr_ErrorContext *sink_writeln(akbasic_TextSink *self, const char *text
* memory, and the host draws it when it draws its frame -- so the cost is a * memory, and the host draws it when it draws its frame -- so the cost is a
* couple of memcpy-sized loops per keystroke. * couple of memcpy-sized loops per keystroke.
*/ */
static void echo_line(akbasic_AkglSink *state) static akerr_ErrorContext *echo_line(akbasic_AkglSink *state)
{ {
PREPARE_ERROR(errctx);
int i = 0; int i = 0;
int erasedto = 0; int erasedto = 0;
int endedat = 0; int endedat = 0;
@@ -160,14 +174,14 @@ static void echo_line(akbasic_AkglSink *state)
state->cursorrow = state->editrow; state->cursorrow = state->editrow;
state->cursorcol = state->editcol; state->cursorcol = state->editcol;
for ( i = 0; i < state->echolen; i++ ) { for ( i = 0; i < state->echolen; i++ ) {
putchar_at(state, ' '); PASS(errctx, putchar_at(state, ' '));
} }
erasedto = state->cursorrow; erasedto = state->cursorrow;
state->cursorrow = state->editrow; state->cursorrow = state->editrow;
state->cursorcol = state->editcol; state->cursorcol = state->editcol;
for ( i = 0; i < state->editlen; i++ ) { for ( i = 0; i < state->editlen; i++ ) {
putchar_at(state, state->editline[i]); PASS(errctx, putchar_at(state, state->editline[i]));
} }
endedat = state->cursorrow; endedat = state->cursorrow;
state->echolen = state->editlen; state->echolen = state->editlen;
@@ -186,6 +200,7 @@ static void echo_line(akbasic_AkglSink *state)
for ( i = endedat + 1; i <= erasedto && i < SINK_MAX_ROWS; i++ ) { for ( i = endedat + 1; i <= erasedto && i < SINK_MAX_ROWS; i++ ) {
state->text[i][0] = '\0'; state->text[i][0] = '\0';
} }
SUCCEED_RETURN(errctx);
} }
/** @brief Append one byte to the line being typed, if there is room for it. */ /** @brief Append one byte to the line being typed, if there is room for it. */
@@ -215,14 +230,15 @@ static void edit_append(akbasic_AkglSink *state, char c)
* Backspace and Escape are keys rather than characters and several of them * Backspace and Escape are keys rather than characters and several of them
* compose to text SDL would otherwise hand straight through. * compose to text SDL would otherwise hand straight through.
*/ */
static void edit_key(akbasic_AkglSink *state, const akgl_Keystroke *key, bool *submitted) static akerr_ErrorContext *edit_key(akbasic_AkglSink *state, const akgl_Keystroke *key, bool *submitted)
{ {
PREPARE_ERROR(errctx);
size_t i = 0; size_t i = 0;
if ( key->key == SDLK_RETURN || key->key == SDLK_KP_ENTER || if ( key->key == SDLK_RETURN || key->key == SDLK_KP_ENTER ||
key->key == '\r' || key->key == '\n' ) { key->key == '\r' || key->key == '\n' ) {
*submitted = true; *submitted = true;
return; SUCCEED_RETURN(errctx);
} }
if ( key->key == SDLK_BACKSPACE || key->key == '\b' || key->key == 0x7f ) { if ( key->key == SDLK_BACKSPACE || key->key == '\b' || key->key == 0x7f ) {
if ( state->editlen > 0 ) { if ( state->editlen > 0 ) {
@@ -235,15 +251,15 @@ static void edit_key(akbasic_AkglSink *state, const akgl_Keystroke *key, bool *s
*/ */
state->editlen -= 1; state->editlen -= 1;
state->editline[state->editlen] = '\0'; state->editline[state->editlen] = '\0';
echo_line(state); PASS(errctx, echo_line(state));
} }
return; SUCCEED_RETURN(errctx);
} }
if ( key->key == SDLK_ESCAPE || key->key == 0x1b ) { if ( key->key == SDLK_ESCAPE || key->key == 0x1b ) {
state->editlen = 0; state->editlen = 0;
state->editline[0] = '\0'; state->editline[0] = '\0';
echo_line(state); PASS(errctx, echo_line(state));
return; SUCCEED_RETURN(errctx);
} }
if ( key->text[0] != '\0' ) { if ( key->text[0] != '\0' ) {
@@ -257,8 +273,8 @@ static void edit_key(akbasic_AkglSink *state, const akgl_Keystroke *key, bool *s
edit_append(state, key->text[i]); edit_append(state, key->text[i]);
} }
} }
echo_line(state); PASS(errctx, echo_line(state));
return; SUCCEED_RETURN(errctx);
} }
/* /*
@@ -277,14 +293,15 @@ static void edit_key(akbasic_AkglSink *state, const akgl_Keystroke *key, bool *s
*/ */
if ( key->key >= 0x20 && key->key < 0x7f ) { if ( key->key >= 0x20 && key->key < 0x7f ) {
edit_append(state, (char)toupper((unsigned char)key->key)); edit_append(state, (char)toupper((unsigned char)key->key));
echo_line(state); PASS(errctx, echo_line(state));
return; SUCCEED_RETURN(errctx);
} }
/* /*
* Neither: a cursor key, a function key or a bare modifier. Not an editing * Neither: a cursor key, a function key or a bare modifier. Not an editing
* command here -- a script's own GET loop is what wants those. * command here -- a script's own GET loop is what wants those.
*/ */
SUCCEED_RETURN(errctx);
} }
/** /**
@@ -312,7 +329,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *edit_loop(akbasic_AkglSink *state, boo
*/ */
PASS(errctx, akgl_controller_poll_keystroke(&key, &available)); PASS(errctx, akgl_controller_poll_keystroke(&key, &available));
if ( available ) { if ( available ) {
edit_key(state, &key, &submitted); PASS(errctx, edit_key(state, &key, &submitted));
continue; continue;
} }
/* /*
@@ -375,9 +392,8 @@ static akerr_ErrorContext *sink_readline(akbasic_TextSink *self, char *dest, siz
if ( *eof ) { if ( *eof ) {
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
strncpy(dest, state->editline, len - 1); PASS(errctx, aksl_strncpy(dest, len, state->editline, len - 1));
dest[len - 1] = '\0'; PASS(errctx, newline(state));
newline(state);
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -391,7 +407,7 @@ static akerr_ErrorContext *sink_clear(akbasic_TextSink *self)
state = (akbasic_AkglSink *)self->self; state = (akbasic_AkglSink *)self->self;
FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER, FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
"akgl sink has no state"); "akgl sink has no state");
memset(state->text, 0, sizeof(state->text)); PASS(errctx, aksl_memset(state->text, 0, sizeof(state->text)));
state->cursorcol = 0; state->cursorcol = 0;
state->cursorrow = 0; state->cursorrow = 0;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
@@ -600,7 +616,7 @@ akerr_ErrorContext *akbasic_sink_init_akgl(akbasic_TextSink *obj, akbasic_AkglSi
"A %dx%d text area has no room for a %dx%d character", "A %dx%d text area has no room for a %dx%d character",
w, h, cellw, cellh); w, h, cellw, cellh);
memset(state, 0, sizeof(*state)); PASS(errctx, aksl_memset(state, 0, sizeof(*state)));
state->renderer = renderer; state->renderer = renderer;
state->font = font; state->font = font;
state->color.r = 0xff; state->color.r = 0xff;

View File

@@ -10,7 +10,6 @@
*/ */
#include <stdio.h> #include <stdio.h>
#include <string.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h> #include <akstdlib.h>
@@ -59,16 +58,40 @@ static akerr_ErrorContext *stdio_readline(akbasic_TextSink *self, char *dest, si
*eof = false; *eof = false;
dest[0] = '\0'; dest[0] = '\0';
if ( fgets(dest, (int)len, state->in) == NULL ) { /*
* The end of the stream is AKERR_EOF rather than a NULL return, so it is
* handled here and turned back into the *eof flag this sink promises. A
* genuine read error is left to propagate, which is what a NULL from
* fgets(3) could never be told apart from.
*/
ATTEMPT {
CATCH(errctx, aksl_fgets(dest, len, state->in, &used));
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_EOF) {
*eof = true; *eof = true;
} FINISH(errctx, true);
if ( *eof ) {
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
/*
* aksl_fgets(3)'s own contract: a full buffer with no trailing newline is
* how a caller spots a line longer than the buffer, because the rest of it
* is still sitting unread in the stream. Refusing here is what makes that
* true -- without it, the unread remainder is picked up by the *next*
* readline() as if it were its own statement, which does not fail, it just
* runs the wrong program. AKBASIC_MAX_LINE_LENGTH is small enough now that
* this is not a hypothetical: examples/breakout's own longest line used to
* clear the old 256-byte ceiling by more than half.
*/
FAIL_NONZERO_RETURN(errctx, (used == len - 1 && dest[used - 1] != '\n' && dest[used - 1] != '\r'),
AKBASIC_ERR_BOUNDS,
"Source line exceeds the %zu character limit", len - 1);
/* /*
* Strip the line terminator. The scanner treats \r and \n as end-of-line * Strip the line terminator. The scanner treats \r and \n as end-of-line
* anyway, but leaving them on would make a stored source line differ from * anyway, but leaving them on would make a stored source line differ from
* the same line typed at the REPL. * the same line typed at the REPL.
*/ */
used = strlen(dest);
while ( used > 0 && (dest[used - 1] == '\n' || dest[used - 1] == '\r') ) { while ( used > 0 && (dest[used - 1] == '\n' || dest[used - 1] == '\r') ) {
dest[used - 1] = '\0'; dest[used - 1] = '\0';
used -= 1; used -= 1;

View File

@@ -34,11 +34,11 @@
*/ */
#include <errno.h> #include <errno.h>
#include <string.h>
#include <SDL3_image/SDL_image.h> #include <SDL3_image/SDL_image.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h>
#include <akgl/error.h> #include <akgl/error.h>
#include <akgl/game.h> #include <akgl/game.h>
@@ -195,9 +195,9 @@ static akerr_ErrorContext *build_slot(akbasic_AkglSprites *state, int i, int wid
oldvisible = state->actors[i]->visible; oldvisible = state->actors[i]->visible;
} }
memset(spritename, 0, sizeof(spritename)); PASS(errctx, aksl_memset(spritename, 0, sizeof(spritename)));
memset(charname, 0, sizeof(charname)); PASS(errctx, aksl_memset(charname, 0, sizeof(charname)));
memset(actorname, 0, sizeof(actorname)); PASS(errctx, aksl_memset(actorname, 0, sizeof(actorname)));
SDL_snprintf(spritename, sizeof(spritename), "akbasic:sprite:%d", i + 1); SDL_snprintf(spritename, sizeof(spritename), "akbasic:sprite:%d", i + 1);
SDL_snprintf(charname, sizeof(charname), "akbasic:character:%d", i + 1); SDL_snprintf(charname, sizeof(charname), "akbasic:character:%d", i + 1);
SDL_snprintf(actorname, sizeof(actorname), "akbasic:actor:%d", i + 1); SDL_snprintf(actorname, sizeof(actorname), "akbasic:actor:%d", i + 1);
@@ -263,8 +263,8 @@ static akerr_ErrorContext *install_surface(akbasic_AkglSprites *state, int i, SD
* which is what makes the texture's ownership one rule rather than two. * which is what makes the texture's ownership one rule rather than two.
*/ */
PASS(errctx, akgl_heap_next_spritesheet(&state->sheets[i])); PASS(errctx, akgl_heap_next_spritesheet(&state->sheets[i]));
memset(state->sheets[i], 0, sizeof(*state->sheets[i])); PASS(errctx, aksl_memset(state->sheets[i], 0, sizeof(*state->sheets[i])));
memset(sheetname, 0, sizeof(sheetname)); PASS(errctx, aksl_memset(sheetname, 0, sizeof(sheetname)));
SDL_snprintf(sheetname, sizeof(sheetname), "akbasic:sheet:%d", i + 1); SDL_snprintf(sheetname, sizeof(sheetname), "akbasic:sheet:%d", i + 1);
SDL_strlcpy(state->sheets[i]->name, sheetname, sizeof(state->sheets[i]->name)); SDL_strlcpy(state->sheets[i]->name, sheetname, sizeof(state->sheets[i]->name));
state->sheets[i]->texture = texture; state->sheets[i]->texture = texture;
@@ -533,7 +533,7 @@ static akerr_ErrorContext *spr_shape(akbasic_SpriteBackend *self, int n, int kin
state->shapex2[i] = (float32_t)x2; state->shapex2[i] = (float32_t)x2;
state->shapey2[i] = (float32_t)y2; state->shapey2[i] = (float32_t)y2;
state->shapeexplicit[i] = (x2 > x1 && y2 > y1); state->shapeexplicit[i] = (x2 > x1 && y2 > y1);
memset(&state->syncedbox[i], 0, sizeof(state->syncedbox[i])); PASS(errctx, aksl_memset(&state->syncedbox[i], 0, sizeof(state->syncedbox[i])));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -703,7 +703,8 @@ static akerr_ErrorContext AKERR_NOIGNORE *run_scan(akbasic_AkglSprites *state)
for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) { for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
live[i] = slot_collidable(state, i); live[i] = slot_collidable(state, i);
synced[i] = false; synced[i] = false;
memset(&box[i], 0, sizeof(box[i])); /* PASS rather than CATCH, as everywhere else in this loop-heavy function. */
PASS(errctx, aksl_memset(&box[i], 0, sizeof(box[i])));
if ( !live[i] ) { if ( !live[i] ) {
continue; continue;
} }
@@ -745,7 +746,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *run_scan(akbasic_AkglSprites *state)
} }
state->lastmask = 0; state->lastmask = 0;
state->lastsolidmask = 0; state->lastsolidmask = 0;
memset(state->hascontact, 0, sizeof(state->hascontact)); PASS(errctx, aksl_memset(state->hascontact, 0, sizeof(state->hascontact)));
/* /*
* **Two phases, and the split is the whole performance story.** * **Two phases, and the split is the whole performance story.**
@@ -970,7 +971,7 @@ akerr_ErrorContext *akbasic_sprite_init_akgl(akbasic_SpriteBackend *obj, akbasic
/* Before anything else in libakgl, so every AKGL_ERR_* has a name to print. */ /* Before anything else in libakgl, so every AKGL_ERR_* has a name to print. */
PASS(errctx, akgl_error_init()); PASS(errctx, akgl_error_init());
memset(state, 0, sizeof(*state)); PASS(errctx, aksl_memset(state, 0, sizeof(*state)));
state->renderer = renderer; state->renderer = renderer;
state->graphics = graphics; state->graphics = graphics;
@@ -1012,7 +1013,7 @@ akerr_ErrorContext *akbasic_sprite_init_akgl(akbasic_SpriteBackend *obj, akbasic
&state->shapes[i], 0.0f, 0.0f, 0.0f)); &state->shapes[i], 0.0f, 0.0f, 0.0f));
} }
memset(obj, 0, sizeof(*obj)); PASS(errctx, aksl_memset(obj, 0, sizeof(*obj)));
obj->self = state; obj->self = state;
obj->define = spr_define; obj->define = spr_define;
obj->define_shape = spr_define_shape; obj->define_shape = spr_define_shape;

View File

@@ -9,9 +9,8 @@
* adaptor that has to turn a pattern into pixels. * adaptor that has to turn a pattern into pixels.
*/ */
#include <string.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h>
#include <akbasic/error.h> #include <akbasic/error.h>
#include <akbasic/sprite.h> #include <akbasic/sprite.h>
@@ -40,7 +39,7 @@ akerr_ErrorContext *akbasic_sprite_state_init(akbasic_SpriteState *obj)
int i = 0; int i = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL sprite state in init"); FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL sprite state in init");
memset(obj, 0, sizeof(*obj)); PASS(errctx, aksl_memset(obj, 0, sizeof(*obj)));
for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) { for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
obj->sprites[i].colorindex = SPRITE_COLOR_DEFAULT; obj->sprites[i].colorindex = SPRITE_COLOR_DEFAULT;
} }

View File

@@ -23,6 +23,7 @@
#include <string.h> #include <string.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h>
#include <akbasic/error.h> #include <akbasic/error.h>
#include <akbasic/runtime.h> #include <akbasic/runtime.h>
@@ -88,10 +89,22 @@ static const char *next_word(const char *cursor, char *dest, size_t len)
return cursor; return cursor;
} }
/** @brief Compare a word against a keyword. Verbs are case-insensitive here as everywhere. */ /**
static bool word_is(const char *word, const char *keyword) * @brief Compare a word against a keyword. Verbs are case-insensitive here as everywhere.
*
* The answer leaves through @p dest rather than the return value because the
* comparison itself can fail, and a `bool` has nowhere to put that. See
* libakstdlib #38.
*/
static akerr_ErrorContext *word_is(const char *word, const char *keyword, bool *dest)
{ {
return (strcmp(word, keyword) == 0); PREPARE_ERROR(errctx);
int cmp = 0;
FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER, "NULL dest in word_is");
PASS(errctx, aksl_strcmp(word, keyword, &cmp));
*dest = (cmp == 0);
SUCCEED_RETURN(errctx);
} }
/** /**
@@ -103,8 +116,9 @@ static bool word_is(const char *word, const char *keyword)
static akerr_ErrorContext *type_from_suffix(const char *name, akbasic_Type *dest) static akerr_ErrorContext *type_from_suffix(const char *name, akbasic_Type *dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
size_t len = strlen(name); size_t len = 0;
PASS(errctx, aksl_strlen(name, &len));
FAIL_ZERO_RETURN(errctx, (len >= 2), AKBASIC_ERR_SYNTAX, FAIL_ZERO_RETURN(errctx, (len >= 2), AKBASIC_ERR_SYNTAX,
"Field name \"%s\" carries no type suffix", name); "Field name \"%s\" carries no type suffix", name);
switch ( name[len - 1] ) { switch ( name[len - 1] ) {
@@ -125,7 +139,7 @@ akerr_ErrorContext *akbasic_structtype_table_init(akbasic_StructTypeTable *obj)
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL table in structtype init"); FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL table in structtype init");
memset(obj, 0, sizeof(*obj)); PASS(errctx, aksl_memset(obj, 0, sizeof(*obj)));
obj->count = 0; obj->count = 0;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -134,12 +148,17 @@ akerr_ErrorContext *akbasic_structtype_find(akbasic_StructTypeTable *obj, const
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
int i = 0; int i = 0;
int cmp = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL && name != NULL && dest != NULL), AKERR_NULLPOINTER, FAIL_ZERO_RETURN(errctx, (obj != NULL && name != NULL && dest != NULL), AKERR_NULLPOINTER,
"NULL argument in structtype find"); "NULL argument in structtype find");
*dest = -1; *dest = -1;
for ( i = 0; i < obj->count; i++ ) { for ( i = 0; i < obj->count; i++ ) {
if ( obj->types[i].used && strcmp(obj->types[i].name, name) == 0 ) { if ( !obj->types[i].used ) {
continue;
}
PASS(errctx, aksl_strcmp(obj->types[i].name, name, &cmp));
if ( cmp == 0 ) {
*dest = i; *dest = i;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -151,9 +170,14 @@ akerr_ErrorContext *akbasic_structtype_field(akbasic_StructTypeTable *obj, int t
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
char known[AKBASIC_MAX_STRING_LENGTH]; char known[AKBASIC_MAX_STRING_LENGTH];
const char *separator = NULL;
akbasic_StructType *type = NULL; akbasic_StructType *type = NULL;
size_t used = 0; size_t used = 0;
size_t namelen = 0;
size_t seplen = 0;
int written = 0;
int i = 0; int i = 0;
int cmp = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL && name != NULL && dest != NULL), AKERR_NULLPOINTER, FAIL_ZERO_RETURN(errctx, (obj != NULL && name != NULL && dest != NULL), AKERR_NULLPOINTER,
"NULL argument in structtype field"); "NULL argument in structtype field");
@@ -162,7 +186,8 @@ akerr_ErrorContext *akbasic_structtype_field(akbasic_StructTypeTable *obj, int t
type = &obj->types[typeindex]; type = &obj->types[typeindex];
for ( i = 0; i < type->fieldcount; i++ ) { for ( i = 0; i < type->fieldcount; i++ ) {
if ( strcmp(type->fields[i].name, name) == 0 ) { PASS(errctx, aksl_strcmp(type->fields[i].name, name, &cmp));
if ( cmp == 0 ) {
*dest = &type->fields[i]; *dest = &type->fields[i];
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -176,11 +201,20 @@ akerr_ErrorContext *akbasic_structtype_field(akbasic_StructTypeTable *obj, int t
*/ */
known[0] = '\0'; known[0] = '\0';
for ( i = 0; i < type->fieldcount; i++ ) { for ( i = 0; i < type->fieldcount; i++ ) {
int written = snprintf(known + used, sizeof(known) - used, "%s%s", separator = (i == 0 ? "" : ", ");
(i == 0 ? "" : ", "), type->fields[i].name); PASS(errctx, aksl_strlen(separator, &seplen));
if ( written < 0 || (size_t)written >= sizeof(known) - used ) { PASS(errctx, aksl_strlen(type->fields[i].name, &namelen));
/*
* The list is advisory, so a full buffer ends it rather than failing the
* lookup. aksl_snprintf treats truncation as an error, which is right
* everywhere the output matters -- so the fit is decided here, before the
* call, instead of being read back out of a return value afterwards.
*/
if ( used + seplen + namelen >= sizeof(known) ) {
break; break;
} }
PASS(errctx, aksl_snprintf(&written, known + used, sizeof(known) - used, "%s%s",
separator, type->fields[i].name));
used += (size_t)written; used += (size_t)written;
} }
FAIL_RETURN(errctx, AKBASIC_ERR_UNDEFINED, "%s has no field %s (%s)", FAIL_RETURN(errctx, AKBASIC_ERR_UNDEFINED, "%s has no field %s (%s)",
@@ -223,6 +257,8 @@ static akerr_ErrorContext *scan_names(akbasic_Runtime *obj)
int64_t i = 0; int64_t i = 0;
int open = -1; int open = -1;
int existing = 0; int existing = 0;
size_t namelen = 0;
bool matched = false;
const akbasic_Verb *verb = NULL; const akbasic_Verb *verb = NULL;
for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) { for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) {
@@ -231,9 +267,11 @@ static akerr_ErrorContext *scan_names(akbasic_Runtime *obj)
} }
cursor = next_word(skip_lineno(obj->source[i].code), word, sizeof(word)); cursor = next_word(skip_lineno(obj->source[i].code), word, sizeof(word));
if ( word_is(word, "END") ) { PASS(errctx, word_is(word, "END", &matched));
if ( matched ) {
(void)next_word(cursor, name, sizeof(name)); (void)next_word(cursor, name, sizeof(name));
if ( !word_is(name, "TYPE") ) { PASS(errctx, word_is(name, "TYPE", &matched));
if ( !matched ) {
continue; /* an ordinary END, not our terminator */ continue; /* an ordinary END, not our terminator */
} }
FAIL_ZERO_RETURN(errctx, (open >= 0), AKBASIC_ERR_SYNTAX, FAIL_ZERO_RETURN(errctx, (open >= 0), AKBASIC_ERR_SYNTAX,
@@ -242,7 +280,8 @@ static akerr_ErrorContext *scan_names(akbasic_Runtime *obj)
open = -1; open = -1;
continue; continue;
} }
if ( !word_is(word, "TYPE") ) { PASS(errctx, word_is(word, "TYPE", &matched));
if ( !matched ) {
continue; continue;
} }
@@ -268,16 +307,17 @@ static akerr_ErrorContext *scan_names(akbasic_Runtime *obj)
FAIL_ZERO_RETURN(errctx, (table->count < AKBASIC_MAX_STRUCT_TYPES), AKBASIC_ERR_BOUNDS, FAIL_ZERO_RETURN(errctx, (table->count < AKBASIC_MAX_STRUCT_TYPES), AKBASIC_ERR_BOUNDS,
"More than %d TYPE declarations", AKBASIC_MAX_STRUCT_TYPES); "More than %d TYPE declarations", AKBASIC_MAX_STRUCT_TYPES);
FAIL_ZERO_RETURN(errctx, (strlen(name) < AKBASIC_MAX_STRUCT_NAME), AKBASIC_ERR_BOUNDS, PASS(errctx, aksl_strlen(name, &namelen));
FAIL_ZERO_RETURN(errctx, (namelen < AKBASIC_MAX_STRUCT_NAME), AKBASIC_ERR_BOUNDS,
"TYPE name \"%s\" exceeds the %d character limit", "TYPE name \"%s\" exceeds the %d character limit",
name, AKBASIC_MAX_STRUCT_NAME - 1); name, AKBASIC_MAX_STRUCT_NAME - 1);
open = table->count; open = table->count;
memset(&table->types[open], 0, sizeof(table->types[open])); PASS(errctx, aksl_memset(&table->types[open], 0, sizeof(table->types[open])));
/* The memset is what terminates this: bounded at size - 1, the last byte /* aksl_strcpy always terminates and refuses rather than truncates, which
is the zero it already wrote. Refused above rather than truncated, is the behaviour this site already wanted -- two long names truncating
because two long names truncating to the same prefix would collide to the same prefix would collide silently. The length check above stays
silently -- and truncation is an error everywhere else here. */ because it names the limit in the message. */
strncpy(table->types[open].name, name, sizeof(table->types[open].name) - 1); PASS(errctx, aksl_strcpy(table->types[open].name, sizeof(table->types[open].name), name));
table->types[open].used = true; table->types[open].used = true;
table->types[open].firstline = i; table->types[open].firstline = i;
table->types[open].lastline = -1; table->types[open].lastline = -1;
@@ -302,34 +342,38 @@ static akerr_ErrorContext *parse_field(akbasic_StructTypeTable *table, akbasic_S
const char *cursor = NULL; const char *cursor = NULL;
akbasic_StructField *field = NULL; akbasic_StructField *field = NULL;
akbasic_Type valuetype = AKBASIC_TYPE_UNDEFINED; akbasic_Type valuetype = AKBASIC_TYPE_UNDEFINED;
size_t namelen = 0;
bool matched = false;
int referenced = 0; int referenced = 0;
int cmp = 0;
int i = 0; int i = 0;
cursor = next_word(skip_lineno(line), fieldname, sizeof(fieldname)); cursor = next_word(skip_lineno(line), fieldname, sizeof(fieldname));
if ( fieldname[0] == '\0' || fieldname[0] == '\'' ) { if ( fieldname[0] == '\0' || fieldname[0] == '\'' ) {
SUCCEED_RETURN(errctx); /* a blank line inside the block */ SUCCEED_RETURN(errctx); /* a blank line inside the block */
} }
if ( word_is(fieldname, "REM") ) { PASS(errctx, word_is(fieldname, "REM", &matched));
if ( matched ) {
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
FAIL_ZERO_RETURN(errctx, (type->fieldcount < AKBASIC_MAX_STRUCT_FIELDS), AKBASIC_ERR_BOUNDS, FAIL_ZERO_RETURN(errctx, (type->fieldcount < AKBASIC_MAX_STRUCT_FIELDS), AKBASIC_ERR_BOUNDS,
"TYPE %s declares more than %d fields", type->name, AKBASIC_MAX_STRUCT_FIELDS); "TYPE %s declares more than %d fields", type->name, AKBASIC_MAX_STRUCT_FIELDS);
for ( i = 0; i < type->fieldcount; i++ ) { for ( i = 0; i < type->fieldcount; i++ ) {
FAIL_NONZERO_RETURN(errctx, (strcmp(type->fields[i].name, fieldname) == 0), PASS(errctx, aksl_strcmp(type->fields[i].name, fieldname, &cmp));
AKBASIC_ERR_VALUE, FAIL_NONZERO_RETURN(errctx, (cmp == 0), AKBASIC_ERR_VALUE,
"TYPE %s declares %s twice", type->name, fieldname); "TYPE %s declares %s twice", type->name, fieldname);
} }
PASS(errctx, type_from_suffix(fieldname, &valuetype)); PASS(errctx, type_from_suffix(fieldname, &valuetype));
FAIL_ZERO_RETURN(errctx, (strlen(fieldname) < AKBASIC_MAX_STRUCT_NAME), AKBASIC_ERR_BOUNDS, PASS(errctx, aksl_strlen(fieldname, &namelen));
FAIL_ZERO_RETURN(errctx, (namelen < AKBASIC_MAX_STRUCT_NAME), AKBASIC_ERR_BOUNDS,
"Field name \"%s\" exceeds the %d character limit", "Field name \"%s\" exceeds the %d character limit",
fieldname, AKBASIC_MAX_STRUCT_NAME - 1); fieldname, AKBASIC_MAX_STRUCT_NAME - 1);
field = &type->fields[type->fieldcount]; field = &type->fields[type->fieldcount];
memset(field, 0, sizeof(*field)); PASS(errctx, aksl_memset(field, 0, sizeof(*field)));
/* Terminated by the memset above; refused rather than truncated, for the /* Refused rather than truncated, for the same reason a type name is. */
same reason a type name is. */ PASS(errctx, aksl_strcpy(field->name, sizeof(field->name), fieldname));
strncpy(field->name, fieldname, sizeof(field->name) - 1);
field->valuetype = valuetype; field->valuetype = valuetype;
field->typeindex = -1; field->typeindex = -1;
field->offset = -1; field->offset = -1;
@@ -354,13 +398,16 @@ static akerr_ErrorContext *parse_field(akbasic_StructTypeTable *table, akbasic_S
* not fit in one. So the declaration has to name it. * not fit in one. So the declaration has to name it.
*/ */
cursor = next_word(cursor, word, sizeof(word)); cursor = next_word(cursor, word, sizeof(word));
FAIL_ZERO_RETURN(errctx, word_is(word, "AS"), AKBASIC_ERR_SYNTAX, PASS(errctx, word_is(word, "AS", &matched));
FAIL_ZERO_RETURN(errctx, matched, AKBASIC_ERR_SYNTAX,
"Line %" PRId64 ": %s must name its type -- %s AS TYPENAME, or %s AS PTR TO TYPENAME", "Line %" PRId64 ": %s must name its type -- %s AS TYPENAME, or %s AS PTR TO TYPENAME",
lineno, fieldname, fieldname, fieldname); lineno, fieldname, fieldname, fieldname);
cursor = next_word(cursor, word, sizeof(word)); cursor = next_word(cursor, word, sizeof(word));
if ( word_is(word, "PTR") ) { PASS(errctx, word_is(word, "PTR", &matched));
if ( matched ) {
cursor = next_word(cursor, word, sizeof(word)); cursor = next_word(cursor, word, sizeof(word));
FAIL_ZERO_RETURN(errctx, word_is(word, "TO"), AKBASIC_ERR_SYNTAX, PASS(errctx, word_is(word, "TO", &matched));
FAIL_ZERO_RETURN(errctx, matched, AKBASIC_ERR_SYNTAX,
"Line %" PRId64 ": expected PTR TO TYPENAME", lineno); "Line %" PRId64 ": expected PTR TO TYPENAME", lineno);
cursor = next_word(cursor, word, sizeof(word)); cursor = next_word(cursor, word, sizeof(word));
field->kind = AKBASIC_FIELD_POINTER; field->kind = AKBASIC_FIELD_POINTER;
@@ -466,7 +513,7 @@ akerr_ErrorContext *akbasic_structtype_scan(akbasic_Runtime *obj)
} }
} }
for ( i = t; i < table->count; i++ ) { for ( i = t; i < table->count; i++ ) {
memset(&table->types[i], 0, sizeof(table->types[i])); PASS(errctx, aksl_memset(&table->types[i], 0, sizeof(table->types[i])));
} }
table->count = t; table->count = t;

View File

@@ -3,8 +3,6 @@
* @brief Implements the fixed-capacity open-addressed symbol table. * @brief Implements the fixed-capacity open-addressed symbol table.
*/ */
#include <string.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h> #include <akstdlib.h>
@@ -23,10 +21,13 @@ static akerr_ErrorContext *probe(akbasic_SymbolTable *obj, const char *key, int
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
uint32_t hashval = 0; uint32_t hashval = 0;
size_t keylen = 0;
int index = 0; int index = 0;
int cmp = 0;
int i = 0; int i = 0;
PASS(errctx, aksl_strhash_djb2((char *)key, strlen(key), &hashval)); PASS(errctx, aksl_strlen(key, &keylen));
PASS(errctx, aksl_strhash_djb2((char *)key, keylen, &hashval));
*found = false; *found = false;
index = (int)(hashval % (uint32_t)obj->capacity); index = (int)(hashval % (uint32_t)obj->capacity);
@@ -36,7 +37,8 @@ static akerr_ErrorContext *probe(akbasic_SymbolTable *obj, const char *key, int
*slot = probeidx; *slot = probeidx;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
if ( strcmp(obj->slots[probeidx].key, key) == 0 ) { PASS(errctx, aksl_strcmp(obj->slots[probeidx].key, key, &cmp));
if ( cmp == 0 ) {
*slot = probeidx; *slot = probeidx;
*found = true; *found = true;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
@@ -58,7 +60,7 @@ akerr_ErrorContext *akbasic_symtab_init(akbasic_SymbolTable *obj, int capacity)
"Symbol table capacity %d out of range 1..%d", "Symbol table capacity %d out of range 1..%d",
capacity, AKBASIC_SYMTAB_MAX_SLOTS); capacity, AKBASIC_SYMTAB_MAX_SLOTS);
memset(obj, 0, sizeof(*obj)); PASS(errctx, aksl_memset(obj, 0, sizeof(*obj)));
obj->capacity = capacity; obj->capacity = capacity;
obj->count = 0; obj->count = 0;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
@@ -67,6 +69,7 @@ akerr_ErrorContext *akbasic_symtab_init(akbasic_SymbolTable *obj, int capacity)
akerr_ErrorContext *akbasic_symtab_set(akbasic_SymbolTable *obj, const char *key, void *value, int64_t ivalue) akerr_ErrorContext *akbasic_symtab_set(akbasic_SymbolTable *obj, const char *key, void *value, int64_t ivalue)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
size_t keylen = 0;
int slot = 0; int slot = 0;
bool found = false; bool found = false;
@@ -74,7 +77,8 @@ akerr_ErrorContext *akbasic_symtab_set(akbasic_SymbolTable *obj, const char *key
"NULL symbol table in set"); "NULL symbol table in set");
FAIL_ZERO_RETURN(errctx, (key != NULL), AKERR_NULLPOINTER, FAIL_ZERO_RETURN(errctx, (key != NULL), AKERR_NULLPOINTER,
"NULL key in symbol table set"); "NULL key in symbol table set");
FAIL_ZERO_RETURN(errctx, (strlen(key) < AKBASIC_SYMTAB_MAX_KEY), PASS(errctx, aksl_strlen(key, &keylen));
FAIL_ZERO_RETURN(errctx, (keylen < AKBASIC_SYMTAB_MAX_KEY),
AKBASIC_ERR_BOUNDS, AKBASIC_ERR_BOUNDS,
"Symbol name '%s' exceeds %d characters", "Symbol name '%s' exceeds %d characters",
key, AKBASIC_SYMTAB_MAX_KEY - 1); key, AKBASIC_SYMTAB_MAX_KEY - 1);
@@ -82,8 +86,10 @@ akerr_ErrorContext *akbasic_symtab_set(akbasic_SymbolTable *obj, const char *key
PASS(errctx, probe(obj, key, &slot, &found)); PASS(errctx, probe(obj, key, &slot, &found));
if ( !found ) { if ( !found ) {
strncpy(obj->slots[slot].key, key, AKBASIC_SYMTAB_MAX_KEY - 1); /* aksl_strcpy always terminates, so the explicit terminator this
obj->slots[slot].key[AKBASIC_SYMTAB_MAX_KEY - 1] = '\0'; replaced is no longer needed. The length check above has already
refused anything that would not fit, so it cannot fail here. */
PASS(errctx, aksl_strcpy(obj->slots[slot].key, sizeof(obj->slots[slot].key), key));
obj->slots[slot].used = true; obj->slots[slot].used = true;
obj->count += 1; obj->count += 1;
} }
@@ -95,6 +101,7 @@ akerr_ErrorContext *akbasic_symtab_set(akbasic_SymbolTable *obj, const char *key
akerr_ErrorContext *akbasic_symtab_get(akbasic_SymbolTable *obj, const char *key, void **value, int64_t *ivalue) akerr_ErrorContext *akbasic_symtab_get(akbasic_SymbolTable *obj, const char *key, void **value, int64_t *ivalue)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
size_t keylen = 0;
int slot = 0; int slot = 0;
bool found = false; bool found = false;
@@ -102,7 +109,8 @@ akerr_ErrorContext *akbasic_symtab_get(akbasic_SymbolTable *obj, const char *key
"NULL symbol table in get"); "NULL symbol table in get");
FAIL_ZERO_RETURN(errctx, (key != NULL), AKERR_NULLPOINTER, FAIL_ZERO_RETURN(errctx, (key != NULL), AKERR_NULLPOINTER,
"NULL key in symbol table get"); "NULL key in symbol table get");
FAIL_ZERO_RETURN(errctx, (strlen(key) < AKBASIC_SYMTAB_MAX_KEY), PASS(errctx, aksl_strlen(key, &keylen));
FAIL_ZERO_RETURN(errctx, (keylen < AKBASIC_SYMTAB_MAX_KEY),
AKERR_KEY, AKERR_KEY,
"Symbol '%s' is not present", key); "Symbol '%s' is not present", key);
@@ -127,7 +135,7 @@ akerr_ErrorContext *akbasic_symtab_clear(akbasic_SymbolTable *obj)
"NULL symbol table in clear"); "NULL symbol table in clear");
capacity = obj->capacity; capacity = obj->capacity;
memset(obj, 0, sizeof(*obj)); PASS(errctx, aksl_memset(obj, 0, sizeof(*obj)));
obj->capacity = capacity; obj->capacity = capacity;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }

View File

@@ -16,10 +16,8 @@
* string. * string.
*/ */
#include <stdio.h>
#include <string.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h>
#include <akgl/error.h> #include <akgl/error.h>
#include <akgl/registry.h> #include <akgl/registry.h>
@@ -94,8 +92,7 @@ static akerr_ErrorContext *ui_dialog(akbasic_UiBackend *self, const char *text)
state->dialogtext[0] = '\0'; state->dialogtext[0] = '\0';
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
strncpy(state->dialogtext, text, sizeof(state->dialogtext) - 1); PASS(errctx, aksl_strcpy(state->dialogtext, sizeof(state->dialogtext), text));
state->dialogtext[sizeof(state->dialogtext) - 1] = '\0';
state->dialogopen = true; state->dialogopen = true;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -113,8 +110,7 @@ static akerr_ErrorContext *ui_label(akbasic_UiBackend *self, int slot, int ancho
state->labeltext[slot][0] = '\0'; state->labeltext[slot][0] = '\0';
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
strncpy(state->labeltext[slot], text, sizeof(state->labeltext[slot]) - 1); PASS(errctx, aksl_strcpy(state->labeltext[slot], sizeof(state->labeltext[slot]), text));
state->labeltext[slot][sizeof(state->labeltext[slot]) - 1] = '\0';
state->labelanchor[slot] = anchor; state->labelanchor[slot] = anchor;
state->labelset[slot] = true; state->labelset[slot] = true;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
@@ -143,8 +139,8 @@ static akerr_ErrorContext *ui_menu(akbasic_UiBackend *self, int slot, const char
for ( i = 0; i < count; i++ ) { for ( i = 0; i < count; i++ ) {
FAIL_ZERO_RETURN(errctx, (items[i] != NULL), AKERR_NULLPOINTER, FAIL_ZERO_RETURN(errctx, (items[i] != NULL), AKERR_NULLPOINTER,
"MENU entry %d is NULL", i + 1); "MENU entry %d is NULL", i + 1);
strncpy(state->menuitems[slot][i], items[i], sizeof(state->menuitems[slot][i]) - 1); PASS(errctx, aksl_strcpy(state->menuitems[slot][i], sizeof(state->menuitems[slot][i]),
state->menuitems[slot][i][sizeof(state->menuitems[slot][i]) - 1] = '\0'; items[i]));
state->menus[slot].items[i] = state->menuitems[slot][i]; state->menus[slot].items[i] = state->menuitems[slot][i];
} }
for ( i = count; i < AKBASIC_UI_MAX_MENU_ITEMS; i++ ) { for ( i = count; i < AKBASIC_UI_MAX_MENU_ITEMS; i++ ) {
@@ -348,9 +344,9 @@ akerr_ErrorContext *akbasic_ui_init_akgl(akbasic_UiBackend *obj, akbasic_AkglUi
"A %dx%d UI at %d points is not a UI", width, height, fontsize); "A %dx%d UI at %d points is not a UI", width, height, fontsize);
PASS(errctx, akgl_error_init()); PASS(errctx, akgl_error_init());
memset(state, 0, sizeof(*state)); PASS(errctx, aksl_memset(state, 0, sizeof(*state)));
state->renderer = renderer; state->renderer = renderer;
strncpy(state->fontname, UI_FONT_NAME, sizeof(state->fontname) - 1); PASS(errctx, aksl_strcpy(state->fontname, sizeof(state->fontname), UI_FONT_NAME));
/* /*
* The element ids, once. clay identifies an element by its string and keeps * The element ids, once. clay identifies an element by its string and keeps
@@ -359,13 +355,14 @@ akerr_ErrorContext *akbasic_ui_init_akgl(akbasic_UiBackend *obj, akbasic_AkglUi
* function that declares them. * function that declares them.
*/ */
for ( i = 0; i < AKBASIC_UI_MAX_LABELS; i++ ) { for ( i = 0; i < AKBASIC_UI_MAX_LABELS; i++ ) {
snprintf(state->labelid[i], sizeof(state->labelid[i]), "hud%d", i + 1); PASS(errctx, aksl_snprintf(&count, state->labelid[i], sizeof(state->labelid[i]),
"hud%d", i + 1));
} }
for ( i = 0; i < AKBASIC_UI_MAX_MENUS; i++ ) { for ( i = 0; i < AKBASIC_UI_MAX_MENUS; i++ ) {
snprintf(state->menuid[i], sizeof(state->menuid[i]), "menu%d", i + 1); PASS(errctx, aksl_snprintf(&count, state->menuid[i], sizeof(state->menuid[i]),
"menu%d", i + 1));
state->menus[i].id = state->menuid[i]; state->menus[i].id = state->menuid[i];
} }
(void)count;
/* /*
* **akgl_registry_init() does not initialize the font registry** -- its own * **akgl_registry_init() does not initialize the font registry** -- its own

View File

@@ -13,6 +13,7 @@
#include <string.h> #include <string.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h>
#include <akbasic/error.h> #include <akbasic/error.h>
#include <akbasic/value.h> #include <akbasic/value.h>
@@ -119,13 +120,17 @@ static akerr_ErrorContext *require_numeric(akbasic_Value *self, akbasic_Value *r
static akerr_ErrorContext *set_string(akbasic_Value *dest, const char *src) static akerr_ErrorContext *set_string(akbasic_Value *dest, const char *src)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
size_t srclen = 0;
FAIL_ZERO_RETURN(errctx, (strlen(src) < AKBASIC_MAX_STRING_LENGTH), PASS(errctx, aksl_strlen(src, &srclen));
FAIL_ZERO_RETURN(errctx, (srclen < AKBASIC_MAX_STRING_LENGTH),
AKBASIC_ERR_VALUE, AKBASIC_ERR_VALUE,
"String result of %zu characters exceeds the %d character limit", "String result of %zu characters exceeds the %d character limit",
strlen(src), AKBASIC_MAX_STRING_LENGTH - 1); srclen, AKBASIC_MAX_STRING_LENGTH - 1);
strncpy(dest->stringval, src, AKBASIC_MAX_STRING_LENGTH - 1); /* aksl_strcpy always terminates and refuses rather than truncates, which is
dest->stringval[AKBASIC_MAX_STRING_LENGTH - 1] = '\0'; the behaviour this site already wanted. The length check above stays
because it names the limit in the message. */
PASS(errctx, aksl_strcpy(dest->stringval, sizeof(dest->stringval), src));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -134,7 +139,7 @@ akerr_ErrorContext *akbasic_valuepool_init(akbasic_ValuePool *obj)
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL pool in init"); FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL pool in init");
memset(obj, 0, sizeof(*obj)); PASS(errctx, aksl_memset(obj, 0, sizeof(*obj)));
obj->next = 0; obj->next = 0;
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -201,7 +206,7 @@ akerr_ErrorContext *akbasic_value_clone(akbasic_Value *self, akbasic_Value *dest
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
dest->valuetype = self->valuetype; dest->valuetype = self->valuetype;
memcpy(dest->stringval, self->stringval, sizeof(dest->stringval)); PASS(errctx, aksl_memcpy(dest->stringval, self->stringval, sizeof(dest->stringval)));
dest->intval = self->intval; dest->intval = self->intval;
dest->floatval = self->floatval; dest->floatval = self->floatval;
dest->boolvalue = self->boolvalue; dest->boolvalue = self->boolvalue;
@@ -223,6 +228,7 @@ akerr_ErrorContext *akbasic_value_clone(akbasic_Value *self, akbasic_Value *dest
akerr_ErrorContext *akbasic_value_to_string(akbasic_Value *self, char *dest, size_t len) akerr_ErrorContext *akbasic_value_to_string(akbasic_Value *self, char *dest, size_t len)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
int written = 0;
FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER, "NULL value in to_string"); FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER, "NULL value in to_string");
FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER, "NULL destination in to_string"); FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER, "NULL destination in to_string");
@@ -230,20 +236,22 @@ akerr_ErrorContext *akbasic_value_to_string(akbasic_Value *self, char *dest, siz
switch ( self->valuetype ) { switch ( self->valuetype ) {
case AKBASIC_TYPE_STRING: case AKBASIC_TYPE_STRING:
snprintf(dest, len, "%s", self->stringval); PASS(errctx, aksl_snprintf(&written, dest, len, "%s", self->stringval));
break; break;
case AKBASIC_TYPE_INTEGER: case AKBASIC_TYPE_INTEGER:
snprintf(dest, len, "%" PRId64, self->intval); PASS(errctx, aksl_snprintf(&written, dest, len, "%" PRId64, self->intval));
break; break;
case AKBASIC_TYPE_FLOAT: case AKBASIC_TYPE_FLOAT:
snprintf(dest, len, "%f", self->floatval); PASS(errctx, aksl_snprintf(&written, dest, len, "%f", self->floatval));
break; break;
case AKBASIC_TYPE_BOOLEAN: case AKBASIC_TYPE_BOOLEAN:
/* Go's %t, which is "true"/"false" and not the numeric -1/0. */ /* Go's %t, which is "true"/"false" and not the numeric -1/0. */
snprintf(dest, len, "%s", (self->boolvalue == AKBASIC_TRUE ? "true" : "false")); PASS(errctx, aksl_snprintf(&written, dest, len, "%s",
(self->boolvalue == AKBASIC_TRUE ? "true" : "false")));
break; break;
default: default:
snprintf(dest, len, "(UNDEFINED STRING REPRESENTATION FOR %d)", (int)self->valuetype); PASS(errctx, aksl_snprintf(&written, dest, len, "(UNDEFINED STRING REPRESENTATION FOR %d)",
(int)self->valuetype));
break; break;
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
@@ -486,6 +494,7 @@ akerr_ErrorContext *akbasic_value_math_plus(akbasic_Value *self, akbasic_Value *
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
akbasic_Value *out = NULL; akbasic_Value *out = NULL;
char buf[AKBASIC_MAX_STRING_LENGTH * 2]; char buf[AKBASIC_MAX_STRING_LENGTH * 2];
int written = 0;
FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER, "NULL value in math plus"); FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER, "NULL value in math plus");
FAIL_ZERO_RETURN(errctx, (rval != NULL), AKERR_NULLPOINTER, "nil rval"); FAIL_ZERO_RETURN(errctx, (rval != NULL), AKERR_NULLPOINTER, "nil rval");
@@ -511,13 +520,16 @@ akerr_ErrorContext *akbasic_value_math_plus(akbasic_Value *self, akbasic_Value *
} else if ( self->valuetype == AKBASIC_TYPE_FLOAT ) { } else if ( self->valuetype == AKBASIC_TYPE_FLOAT ) {
out->floatval = self->floatval + rval_as_float(rval); out->floatval = self->floatval + rval_as_float(rval);
} else if ( self->valuetype == AKBASIC_TYPE_STRING && rval->valuetype == AKBASIC_TYPE_STRING ) { } else if ( self->valuetype == AKBASIC_TYPE_STRING && rval->valuetype == AKBASIC_TYPE_STRING ) {
snprintf(buf, sizeof(buf), "%s%s", self->stringval, rval->stringval); PASS(errctx, aksl_snprintf(&written, buf, sizeof(buf), "%s%s",
self->stringval, rval->stringval));
PASS(errctx, set_string(out, buf)); PASS(errctx, set_string(out, buf));
} else if ( self->valuetype == AKBASIC_TYPE_STRING && rval->valuetype == AKBASIC_TYPE_INTEGER ) { } else if ( self->valuetype == AKBASIC_TYPE_STRING && rval->valuetype == AKBASIC_TYPE_INTEGER ) {
snprintf(buf, sizeof(buf), "%s%" PRId64, self->stringval, rval->intval); PASS(errctx, aksl_snprintf(&written, buf, sizeof(buf), "%s%" PRId64,
self->stringval, rval->intval));
PASS(errctx, set_string(out, buf)); PASS(errctx, set_string(out, buf));
} else if ( self->valuetype == AKBASIC_TYPE_STRING && rval->valuetype == AKBASIC_TYPE_FLOAT ) { } else if ( self->valuetype == AKBASIC_TYPE_STRING && rval->valuetype == AKBASIC_TYPE_FLOAT ) {
snprintf(buf, sizeof(buf), "%s%f", self->stringval, rval->floatval); PASS(errctx, aksl_snprintf(&written, buf, sizeof(buf), "%s%f",
self->stringval, rval->floatval));
PASS(errctx, set_string(out, buf)); PASS(errctx, set_string(out, buf));
} else { } else {
FAIL_RETURN(errctx, AKBASIC_ERR_TYPE, "Invalid arithmetic operation"); FAIL_RETURN(errctx, AKBASIC_ERR_TYPE, "Invalid arithmetic operation");
@@ -586,14 +598,14 @@ akerr_ErrorContext *akbasic_value_math_multiply(akbasic_Value *self, akbasic_Val
*/ */
FAIL_NONZERO_RETURN(errctx, (rval->intval < 0), AKBASIC_ERR_VALUE, FAIL_NONZERO_RETURN(errctx, (rval->intval < 0), AKBASIC_ERR_VALUE,
"String multiplier %" PRId64 " must not be negative", rval->intval); "String multiplier %" PRId64 " must not be negative", rval->intval);
srclen = strlen((*dest)->stringval); PASS(errctx, aksl_strlen((*dest)->stringval, &srclen));
FAIL_NONZERO_RETURN(errctx, FAIL_NONZERO_RETURN(errctx,
(srclen != 0 && (uint64_t)rval->intval > (AKBASIC_MAX_STRING_LENGTH - 1) / srclen), (srclen != 0 && (uint64_t)rval->intval > (AKBASIC_MAX_STRING_LENGTH - 1) / srclen),
AKBASIC_ERR_VALUE, AKBASIC_ERR_VALUE,
"Repeated string of %zu x %" PRId64 " characters exceeds the %d character limit", "Repeated string of %zu x %" PRId64 " characters exceeds the %d character limit",
srclen, rval->intval, AKBASIC_MAX_STRING_LENGTH - 1); srclen, rval->intval, AKBASIC_MAX_STRING_LENGTH - 1);
for ( i = 0; i < rval->intval; i++ ) { for ( i = 0; i < rval->intval; i++ ) {
memcpy(buf + offset, (*dest)->stringval, srclen); PASS(errctx, aksl_memcpy(buf + offset, (*dest)->stringval, srclen));
offset += srclen; offset += srclen;
} }
buf[offset] = '\0'; buf[offset] = '\0';
@@ -619,6 +631,7 @@ akerr_ErrorContext *akbasic_value_math_multiply(akbasic_Value *self, akbasic_Val
akerr_ErrorContext *akbasic_value_less_than(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest) akerr_ErrorContext *akbasic_value_less_than(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
int cmp = 0;
PASS(errctx, binary_prologue(self, rval, scratch, dest)); PASS(errctx, binary_prologue(self, rval, scratch, dest));
if ( self->valuetype == AKBASIC_TYPE_INTEGER ) { if ( self->valuetype == AKBASIC_TYPE_INTEGER ) {
@@ -626,7 +639,8 @@ akerr_ErrorContext *akbasic_value_less_than(akbasic_Value *self, akbasic_Value *
} else if ( self->valuetype == AKBASIC_TYPE_FLOAT ) { } else if ( self->valuetype == AKBASIC_TYPE_FLOAT ) {
PASS(errctx, akbasic_value_set_bool(*dest, self->floatval < rval_as_float(rval))); PASS(errctx, akbasic_value_set_bool(*dest, self->floatval < rval_as_float(rval)));
} else { } else {
PASS(errctx, akbasic_value_set_bool(*dest, strcmp(self->stringval, rval->stringval) < 0)); PASS(errctx, aksl_strcmp(self->stringval, rval->stringval, &cmp));
PASS(errctx, akbasic_value_set_bool(*dest, cmp < 0));
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -634,6 +648,7 @@ akerr_ErrorContext *akbasic_value_less_than(akbasic_Value *self, akbasic_Value *
akerr_ErrorContext *akbasic_value_less_than_equal(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest) akerr_ErrorContext *akbasic_value_less_than_equal(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
int cmp = 0;
PASS(errctx, binary_prologue(self, rval, scratch, dest)); PASS(errctx, binary_prologue(self, rval, scratch, dest));
if ( self->valuetype == AKBASIC_TYPE_INTEGER ) { if ( self->valuetype == AKBASIC_TYPE_INTEGER ) {
@@ -641,7 +656,8 @@ akerr_ErrorContext *akbasic_value_less_than_equal(akbasic_Value *self, akbasic_V
} else if ( self->valuetype == AKBASIC_TYPE_FLOAT ) { } else if ( self->valuetype == AKBASIC_TYPE_FLOAT ) {
PASS(errctx, akbasic_value_set_bool(*dest, self->floatval <= rval_as_float(rval))); PASS(errctx, akbasic_value_set_bool(*dest, self->floatval <= rval_as_float(rval)));
} else { } else {
PASS(errctx, akbasic_value_set_bool(*dest, strcmp(self->stringval, rval->stringval) <= 0)); PASS(errctx, aksl_strcmp(self->stringval, rval->stringval, &cmp));
PASS(errctx, akbasic_value_set_bool(*dest, cmp <= 0));
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -649,6 +665,7 @@ akerr_ErrorContext *akbasic_value_less_than_equal(akbasic_Value *self, akbasic_V
akerr_ErrorContext *akbasic_value_greater_than(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest) akerr_ErrorContext *akbasic_value_greater_than(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
int cmp = 0;
PASS(errctx, binary_prologue(self, rval, scratch, dest)); PASS(errctx, binary_prologue(self, rval, scratch, dest));
if ( self->valuetype == AKBASIC_TYPE_INTEGER ) { if ( self->valuetype == AKBASIC_TYPE_INTEGER ) {
@@ -656,7 +673,8 @@ akerr_ErrorContext *akbasic_value_greater_than(akbasic_Value *self, akbasic_Valu
} else if ( self->valuetype == AKBASIC_TYPE_FLOAT ) { } else if ( self->valuetype == AKBASIC_TYPE_FLOAT ) {
PASS(errctx, akbasic_value_set_bool(*dest, self->floatval > rval_as_float(rval))); PASS(errctx, akbasic_value_set_bool(*dest, self->floatval > rval_as_float(rval)));
} else { } else {
PASS(errctx, akbasic_value_set_bool(*dest, strcmp(self->stringval, rval->stringval) > 0)); PASS(errctx, aksl_strcmp(self->stringval, rval->stringval, &cmp));
PASS(errctx, akbasic_value_set_bool(*dest, cmp > 0));
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -664,6 +682,7 @@ akerr_ErrorContext *akbasic_value_greater_than(akbasic_Value *self, akbasic_Valu
akerr_ErrorContext *akbasic_value_greater_than_equal(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest) akerr_ErrorContext *akbasic_value_greater_than_equal(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
int cmp = 0;
PASS(errctx, binary_prologue(self, rval, scratch, dest)); PASS(errctx, binary_prologue(self, rval, scratch, dest));
if ( self->valuetype == AKBASIC_TYPE_INTEGER ) { if ( self->valuetype == AKBASIC_TYPE_INTEGER ) {
@@ -671,7 +690,8 @@ akerr_ErrorContext *akbasic_value_greater_than_equal(akbasic_Value *self, akbasi
} else if ( self->valuetype == AKBASIC_TYPE_FLOAT ) { } else if ( self->valuetype == AKBASIC_TYPE_FLOAT ) {
PASS(errctx, akbasic_value_set_bool(*dest, self->floatval >= rval_as_float(rval))); PASS(errctx, akbasic_value_set_bool(*dest, self->floatval >= rval_as_float(rval)));
} else { } else {
PASS(errctx, akbasic_value_set_bool(*dest, strcmp(self->stringval, rval->stringval) >= 0)); PASS(errctx, aksl_strcmp(self->stringval, rval->stringval, &cmp));
PASS(errctx, akbasic_value_set_bool(*dest, cmp >= 0));
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -679,6 +699,7 @@ akerr_ErrorContext *akbasic_value_greater_than_equal(akbasic_Value *self, akbasi
akerr_ErrorContext *akbasic_value_is_equal(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest) akerr_ErrorContext *akbasic_value_is_equal(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
int cmp = 0;
PASS(errctx, binary_prologue(self, rval, scratch, dest)); PASS(errctx, binary_prologue(self, rval, scratch, dest));
if ( self->valuetype == AKBASIC_TYPE_INTEGER ) { if ( self->valuetype == AKBASIC_TYPE_INTEGER ) {
@@ -686,7 +707,8 @@ akerr_ErrorContext *akbasic_value_is_equal(akbasic_Value *self, akbasic_Value *r
} else if ( self->valuetype == AKBASIC_TYPE_FLOAT ) { } else if ( self->valuetype == AKBASIC_TYPE_FLOAT ) {
PASS(errctx, akbasic_value_set_bool(*dest, self->floatval == rval_as_float(rval))); PASS(errctx, akbasic_value_set_bool(*dest, self->floatval == rval_as_float(rval)));
} else { } else {
PASS(errctx, akbasic_value_set_bool(*dest, strcmp(self->stringval, rval->stringval) == 0)); PASS(errctx, aksl_strcmp(self->stringval, rval->stringval, &cmp));
PASS(errctx, akbasic_value_set_bool(*dest, cmp == 0));
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }
@@ -694,6 +716,7 @@ akerr_ErrorContext *akbasic_value_is_equal(akbasic_Value *self, akbasic_Value *r
akerr_ErrorContext *akbasic_value_is_not_equal(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest) akerr_ErrorContext *akbasic_value_is_not_equal(akbasic_Value *self, akbasic_Value *rval, akbasic_Value *scratch, akbasic_Value **dest)
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
int cmp = 0;
PASS(errctx, binary_prologue(self, rval, scratch, dest)); PASS(errctx, binary_prologue(self, rval, scratch, dest));
if ( self->valuetype == AKBASIC_TYPE_INTEGER ) { if ( self->valuetype == AKBASIC_TYPE_INTEGER ) {
@@ -701,7 +724,8 @@ akerr_ErrorContext *akbasic_value_is_not_equal(akbasic_Value *self, akbasic_Valu
} else if ( self->valuetype == AKBASIC_TYPE_FLOAT ) { } else if ( self->valuetype == AKBASIC_TYPE_FLOAT ) {
PASS(errctx, akbasic_value_set_bool(*dest, self->floatval != rval_as_float(rval))); PASS(errctx, akbasic_value_set_bool(*dest, self->floatval != rval_as_float(rval)));
} else { } else {
PASS(errctx, akbasic_value_set_bool(*dest, strcmp(self->stringval, rval->stringval) != 0)); PASS(errctx, aksl_strcmp(self->stringval, rval->stringval, &cmp));
PASS(errctx, akbasic_value_set_bool(*dest, cmp != 0));
} }
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }

View File

@@ -4,9 +4,9 @@
*/ */
#include <inttypes.h> #include <inttypes.h>
#include <string.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h>
#include <akbasic/error.h> #include <akbasic/error.h>
#include <akbasic/variable.h> #include <akbasic/variable.h>
@@ -53,7 +53,7 @@ akerr_ErrorContext *akbasic_variable_init(akbasic_Variable *obj, akbasic_ValuePo
"Array dimension count %d out of range 1..%d", "Array dimension count %d out of range 1..%d",
sizecount, AKBASIC_MAX_ARRAY_DEPTH); sizecount, AKBASIC_MAX_ARRAY_DEPTH);
namelen = strlen(obj->name); PASS(errctx, aksl_strlen(obj->name, &namelen));
FAIL_ZERO_RETURN(errctx, (namelen > 0), AKBASIC_ERR_VALUE, "Invalid variable name"); FAIL_ZERO_RETURN(errctx, (namelen > 0), AKBASIC_ERR_VALUE, "Invalid variable name");
/* Type comes from the suffix. A bare name keeps whatever type it had. */ /* Type comes from the suffix. A bare name keeps whatever type it had. */
@@ -202,16 +202,17 @@ akerr_ErrorContext *akbasic_variable_set_string(akbasic_Variable *obj, const cha
{ {
PREPARE_ERROR(errctx); PREPARE_ERROR(errctx);
akbasic_Value tmp; akbasic_Value tmp;
size_t valuelen = 0;
FAIL_ZERO_RETURN(errctx, (value != NULL), AKERR_NULLPOINTER, "NULL string in set_string"); FAIL_ZERO_RETURN(errctx, (value != NULL), AKERR_NULLPOINTER, "NULL string in set_string");
FAIL_ZERO_RETURN(errctx, (strlen(value) < AKBASIC_MAX_STRING_LENGTH), PASS(errctx, aksl_strlen(value, &valuelen));
FAIL_ZERO_RETURN(errctx, (valuelen < AKBASIC_MAX_STRING_LENGTH),
AKBASIC_ERR_VALUE, AKBASIC_ERR_VALUE,
"String of %zu characters exceeds the %d character limit", "String of %zu characters exceeds the %d character limit",
strlen(value), AKBASIC_MAX_STRING_LENGTH - 1); valuelen, AKBASIC_MAX_STRING_LENGTH - 1);
PASS(errctx, akbasic_value_zero(&tmp)); PASS(errctx, akbasic_value_zero(&tmp));
tmp.valuetype = AKBASIC_TYPE_STRING; tmp.valuetype = AKBASIC_TYPE_STRING;
strncpy(tmp.stringval, value, AKBASIC_MAX_STRING_LENGTH - 1); PASS(errctx, aksl_strcpy(tmp.stringval, sizeof(tmp.stringval), value));
tmp.stringval[AKBASIC_MAX_STRING_LENGTH - 1] = '\0';
PASS(errctx, akbasic_variable_set_subscript(obj, &tmp, subscripts, subscriptcount)); PASS(errctx, akbasic_variable_set_subscript(obj, &tmp, subscripts, subscriptcount));
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);
} }

View File

@@ -8,6 +8,7 @@
#include <string.h> #include <string.h>
#include <akerror.h> #include <akerror.h>
#include <akstdlib.h>
#include <akbasic/error.h> #include <akbasic/error.h>
#include <akbasic/verbs.h> #include <akbasic/verbs.h>
@@ -197,6 +198,28 @@ static const akbasic_Verb VERBS[] = {
#define VERB_COUNT ((int)(sizeof(VERBS) / sizeof(VERBS[0]))) #define VERB_COUNT ((int)(sizeof(VERBS) / sizeof(VERBS[0])))
/**
* @brief bsearch(3) comparator for the verb table.
*
* **This is the one call in `src/` that stays on raw `strcmp` on purpose.**
* Everything else that reaches past libakstdlib to a function that library wraps
* has been converted to the wrapper and propagates the error context; the ruling
* that required it is libakstdlib #38, and the rule is that a function which
* cannot report an error changes its own signature rather than swallowing one.
*
* That remedy is unavailable here, and only here. `bsearch(3)` fixes the
* comparator's signature at `int (*)(const void *, const void *)`, so there is
* no out-parameter to hand an `akerr_ErrorContext *` back through and no return
* value that is not already the ordering. Converting this one site means
* dropping `bsearch` for an in-house binary search that can propagate -- a
* separate change to a lookup that runs once per scanned identifier, and a
* decision that belongs in its own issue rather than riding along with a port.
*
* The bypass is safe as written: `key` is the caller's NUL-terminated `upper`
* buffer and `element->name` is a string literal in the static table above, so
* neither argument can be NULL and neither can be unterminated -- which is the
* whole of what `aksl_strcmp` would have checked.
*/
static int verb_compare(const void *key, const void *element) static int verb_compare(const void *key, const void *element)
{ {
const akbasic_Verb *verb = (const akbasic_Verb *)element; const akbasic_Verb *verb = (const akbasic_Verb *)element;
@@ -223,7 +246,7 @@ akerr_ErrorContext *akbasic_verb_lookup(const char *name, const akbasic_Verb **d
FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER, "NULL destination in verb lookup"); FAIL_ZERO_RETURN(errctx, (dest != NULL), AKERR_NULLPOINTER, "NULL destination in verb lookup");
*dest = NULL; *dest = NULL;
len = strlen(name); PASS(errctx, aksl_strlen(name, &len));
if ( len == 0 || len >= sizeof(upper) ) { if ( len == 0 || len >= sizeof(upper) ) {
/* Too long to be any verb we know. Not an error, just a miss. */ /* Too long to be any verb we know. Not an error, just a miss. */
SUCCEED_RETURN(errctx); SUCCEED_RETURN(errctx);

View File

@@ -13,6 +13,7 @@ int main(void)
akbasic_Variable *again = NULL; akbasic_Variable *again = NULL;
akbasic_Value *value = NULL; akbasic_Value *value = NULL;
int64_t lineno = 0; int64_t lineno = 0;
bool waiting = false;
int i = 0; int i = 0;
TEST_REQUIRE_OK(harness_start(NULL)); TEST_REQUIRE_OK(harness_start(NULL));
@@ -57,24 +58,34 @@ int main(void)
TEST_REQUIRE_STATUS(akbasic_runtime_prev_environment(&HARNESS_RUNTIME), TEST_REQUIRE_STATUS(akbasic_runtime_prev_environment(&HARNESS_RUNTIME),
AKBASIC_ERR_ENVIRONMENT); AKBASIC_ERR_ENVIRONMENT);
/* The wait mechanism, including the parent-chain search. */ /*
TEST_REQUIRE(!akbasic_environment_is_waiting_for_any(root), "a fresh environment waits for nothing"); * The wait mechanism, including the parent-chain search. Both predicates
* report through an out-parameter and return an error context, so each
* assertion is a TEST_REQUIRE_OK on the call and a TEST_REQUIRE on the
* answer. See libakstdlib #38.
*/
TEST_REQUIRE_OK(akbasic_environment_is_waiting_for_any(root, &waiting));
TEST_REQUIRE(!waiting, "a fresh environment waits for nothing");
TEST_REQUIRE_OK(akbasic_environment_wait_for_command(root, "NEXT")); TEST_REQUIRE_OK(akbasic_environment_wait_for_command(root, "NEXT"));
TEST_REQUIRE(akbasic_environment_is_waiting_for_any(root), "the wait must register"); TEST_REQUIRE_OK(akbasic_environment_is_waiting_for_any(root, &waiting));
TEST_REQUIRE(akbasic_environment_is_waiting_for(root, "NEXT"), "the wait must match by name"); TEST_REQUIRE(waiting, "the wait must register");
TEST_REQUIRE(!akbasic_environment_is_waiting_for(root, "DATA"), "a different verb must not match"); TEST_REQUIRE_OK(akbasic_environment_is_waiting_for(root, "NEXT", &waiting));
TEST_REQUIRE(waiting, "the wait must match by name");
TEST_REQUIRE_OK(akbasic_environment_is_waiting_for(root, "DATA", &waiting));
TEST_REQUIRE(!waiting, "a different verb must not match");
/* Two pending waits in one environment is a hard error, not a panic. */ /* Two pending waits in one environment is a hard error, not a panic. */
TEST_REQUIRE_STATUS(akbasic_environment_wait_for_command(root, "DATA"), AKBASIC_ERR_STATE); TEST_REQUIRE_STATUS(akbasic_environment_wait_for_command(root, "DATA"), AKBASIC_ERR_STATE);
TEST_REQUIRE_OK(akbasic_runtime_new_environment(&HARNESS_RUNTIME)); TEST_REQUIRE_OK(akbasic_runtime_new_environment(&HARNESS_RUNTIME));
child = HARNESS_RUNTIME.environment; child = HARNESS_RUNTIME.environment;
TEST_REQUIRE(akbasic_environment_is_waiting_for(child, "NEXT"), TEST_REQUIRE_OK(akbasic_environment_is_waiting_for(child, "NEXT", &waiting));
"a child must see its parent's wait"); TEST_REQUIRE(waiting, "a child must see its parent's wait");
TEST_REQUIRE_OK(akbasic_runtime_prev_environment(&HARNESS_RUNTIME)); TEST_REQUIRE_OK(akbasic_runtime_prev_environment(&HARNESS_RUNTIME));
TEST_REQUIRE_OK(akbasic_environment_stop_waiting(root, "NEXT")); TEST_REQUIRE_OK(akbasic_environment_stop_waiting(root, "NEXT"));
TEST_REQUIRE(!akbasic_environment_is_waiting_for_any(root), "stop_waiting must clear it"); TEST_REQUIRE_OK(akbasic_environment_is_waiting_for_any(root, &waiting));
TEST_REQUIRE(!waiting, "stop_waiting must clear it");
/* The per-line value pool is bounded and says so. */ /* The per-line value pool is bounded and says so. */
for ( i = 0; i < AKBASIC_MAX_VALUES; i++ ) { for ( i = 0; i < AKBASIC_MAX_VALUES; i++ ) {

View File

@@ -1,6 +1,6 @@
10 REM An array reference used as a function argument, and as one of several. 10 REM An array reference used as a function argument, and as one of several.
20 REM An identifier's subscript list used to hang off .right, which is also 20 REM An identifier's subscript list used to hang off .right, which is also
30 REM where an argument list chains its arguments -- so the arity counter walked 30 REM where an argument list chains its arguments; the arity counter walked
40 REM straight into the subscripts and refused the call. TODO.md section 4. 40 REM straight into the subscripts and refused the call. TODO.md section 4.
50 DIM C#(4) 50 DIM C#(4)
60 C#(1) = -9 60 C#(1) = -9

View File

@@ -1,6 +1,6 @@
10 REM FILTER has no device capability behind it -- akgl_audio_* synthesises and 10 REM FILTER has no device capability; akgl_audio_* synthesises and mixes but
20 REM mixes but has no filter stage, and SDL3 supplies no primitive to build one 20 REM has no filter stage, and SDL3 supplies no primitive to build one from.
30 REM from. It is refused rather than silently ignored, so a program that asked 30 REM It is refused rather than silently ignored, so a program that asked
40 REM for a low-pass finds out it did not get one. 40 REM for a low-pass finds out it did not get one.
50 PRINT "BEFORE" 50 PRINT "BEFORE"
60 FILTER 1000, 1, 0, 0, 5 60 FILTER 1000, 1, 0, 0, 5

View File

@@ -1,4 +1,4 @@
10 REM The standalone driver lends the script no audio device. ENVELOPE, VOL and 10 REM The standalone driver lends the script no audio device. ENVELOPE, VOL,
20 REM TEMPO only change interpreter state, so they work regardless; SOUND and 20 REM TEMPO only change interpreter state, so they work regardless; SOUND and
30 REM PLAY need the device and must name themselves when there is none. 30 REM PLAY need the device and must name themselves when there is none.
40 ENVELOPE 1, 5, 9, 12, 2 40 ENVELOPE 1, 5, 9, 12, 2

View File

@@ -1,6 +1,6 @@
10 REM A DEF call takes its environment from the pool, one per call, exactly as 10 REM A DEF call takes one environment from the pool, exactly as
20 REM GOSUB does. It used to be owned by the funcdef and reset on every call, 20 REM GOSUB does. It used to be owned by the funcdef and reset on every call,
30 REM which cost two silent defects: two calls in one expression shared a slot, 30 REM which cost two silent defects: calls in one expression shared a slot,
40 REM and recursion never came back at all. 40 REM and recursion never came back at all.
50 DEF FACT(N#) 50 DEF FACT(N#)
60 IF N# <= 1 THEN RETURN 1 60 IF N# <= 1 THEN RETURN 1
@@ -15,9 +15,9 @@
150 DEF DBL(N#) = N# * 2 150 DEF DBL(N#) = N# * 2
160 PRINT DBL(10) + DBL(1) 160 PRINT DBL(10) + DBL(1)
170 PRINT DBL(1) + DBL(10) + DBL(100) 170 PRINT DBL(1) + DBL(10) + DBL(100)
180 REM Depth answers to the environment pool now, so runaway recursion reports 180 REM Depth answers to the environment pool, so runaway recursion reports
190 REM "Environment pool exhausted" rather than hanging. It is not exercised here 190 REM "Environment pool exhausted" rather than hanging. It is not exercised
200 REM because the statement containing a failed call still prints a junk value 200 REM here because the failed statement still prints junk afterwards;
210 REM afterwards -- a separate, pre-existing defect recorded in TODO.md, and one 210 REM this is a separate defect in TODO.md, and one this golden file
220 REM this golden file would pin if it went in. tests/user_functions.c asserts 220 REM would pin if it went in. tests/user_functions.c asserts
230 REM the message instead. 230 REM the message instead.

View File

@@ -1,4 +1,4 @@
10 REM A range check reported through the driver, end to end. The program stops 10 REM A range check reported through the driver, end to end. The program
20 REM at the first error, so this file covers one; the rest of the checks are 20 REM at the first error, so this file covers one; the rest of the checks are
30 REM asserted in tests/graphics_verbs.c against the recording backend. 30 REM asserted in tests/graphics_verbs.c against the recording backend.
40 PRINT "BEFORE" 40 PRINT "BEFORE"

View File

@@ -1,5 +1,5 @@
10 REM The standalone driver lends the script no graphics device. 10 REM The standalone driver lends the script no graphics device.
20 REM COLOR, LOCATE and SCALE only touch interpreter state and must still work. 20 REM COLOR, LOCATE and SCALE touch interpreter state and must still work.
30 COLOR 1, 3 30 COLOR 1, 3
40 LOCATE 40, 50 40 LOCATE 40, 50
50 SCALE 1, 640, 400 50 SCALE 1, 640, 400

View File

@@ -1,5 +1,5 @@
10 REM Group B: the housekeeping verbs. None of these is in the Go reference -- 10 REM Group B: housekeeping verbs. None is in the Go reference --
20 REM they are on its own unimplemented list -- so what each one means here is 20 REM they are on its unimplemented list -- so each meaning here is
30 REM a decision, recorded in src/runtime_housekeeping.c beside the verb. 30 REM a decision, recorded in src/runtime_housekeeping.c beside the verb.
40 A# = 1 : B# = 2 40 A# = 1 : B# = 2
50 PRINT A# : PRINT B# 50 PRINT A# : PRINT B#
@@ -11,7 +11,7 @@
110 P#(2) = 7 : Q#(2) = 9 110 P#(2) = 7 : Q#(2) = 9
120 SWAP P#, Q# 120 SWAP P#, Q#
130 PRINT P#(2) : PRINT Q#(2) 130 PRINT P#(2) : PRINT Q#(2)
140 REM TRON prints each line number inline before the line runs, as a C128 does. 140 REM TRON prints each line number inline, as a C128 does.
150 TRON 150 TRON
160 PRINT "TRACED" 160 PRINT "TRACED"
170 TROFF 170 TROFF

View File

@@ -1,7 +1,7 @@
10 REM A leading zero is padding, not a radix. The reference selects base 8 for 10 REM A leading zero is padding, not a radix. The reference selected base 8
20 REM any lexeme starting with 0, so 010 printed 8 and 08 was a parse error -- 20 REM for lexemes starting with 0; 010 printed 8 and 08 was a parse error --
30 REM TODO.md section 6 item 10, fixed. Commodore BASIC has no octal literals. 30 REM TODO.md section 6 item 10 is fixed; Commodore BASIC has no octal.
40 REM 0x is the one prefix that changes the base, and it now reaches the scanner 40 REM 0x is the one prefix that changes the base, and now reaches the scanner
50 REM whole: that was section 6 item 15. 50 REM whole: that was section 6 item 15.
60 PRINT 010 60 PRINT 010
70 PRINT 08 70 PRINT 08

View File

@@ -1,12 +1,12 @@
10 REM A truth value carries its payload in boolvalue, not floatval. The three 10 REM A truth value carries its payload in boolvalue, not floatval. The three
20 REM numeric operators were `if ( INTEGER ) ... else <treat as float>`, and 20 REM numeric operators were `if ( INTEGER ) ... else <treat as float>`, and
30 REM that else was a catch-all rather than a float branch -- so a truth value 30 REM that else was a catch-all, not a float branch -- so a truth value
40 REM on the LEFT computed 0 - 1 into a field nothing reads, kept its BOOLEAN 40 REM on the LEFT computed 0 - 1 into a field nothing reads, kept its BOOLEAN
50 REM type, and printed `true` instead of -2. Silent, and wrong twice over. 50 REM type, and printed `true` instead of -2. Silent, and wrong twice over.
60 A# = 1 60 A# = 1
70 PRINT (A# == 1) 70 PRINT (A# == 1)
80 REM On the RIGHT it stays legal and stays -1, which is the same property that 80 REM On the RIGHT it stays legal and stays -1, the same property that
90 REM lets AND and OR double as logical operators. Refusing it here would have 90 REM lets AND and OR act as logical operators. Refusing it would have
100 REM broken every condition in the language. 100 REM broken every condition in the language.
110 PRINT 5 - (A# == 1) 110 PRINT 5 - (A# == 1)
120 PRINT 5 * (A# == 1) 120 PRINT 5 * (A# == 1)

View File

@@ -1,11 +1,11 @@
10 REM Statements separated by colons. The COLON token existed from the start of 10 REM Statements separated by colons. The COLON token existed from the start;
20 REM the port and nothing consumed it, so a line could hold only one statement. 20 REM nothing consumed it, so a line could hold only one statement.
30 PRINT "A" : PRINT "B" 30 PRINT "A" : PRINT "B"
40 A# = 1 : B# = 2 : PRINT A# + B# 40 A# = 1 : B# = 2 : PRINT A# + B#
50 REM An empty statement is not an error: a trailing separator, or a run of them. 50 REM Empty statements are valid: a trailing separator or runs of them.
60 PRINT "C" : 60 PRINT "C" :
70 PRINT "D" :: PRINT "E" 70 PRINT "D" :: PRINT "E"
80 REM Everything after THEN belongs to the condition, which is BASIC 7.0 and is 80 REM Everything after THEN belongs to the condition, as in BASIC 7.0, and is
90 REM not something the reference had an opinion about -- it never got here. 90 REM not something the reference had an opinion about -- it never got here.
100 IF 1 == 1 THEN PRINT "TRUE-1" : PRINT "TRUE-2" 100 IF 1 == 1 THEN PRINT "TRUE-1" : PRINT "TRUE-2"
110 IF 1 == 0 THEN PRINT "NEVER-1" : PRINT "NEVER-2" 110 IF 1 == 0 THEN PRINT "NEVER-1" : PRINT "NEVER-2"

View File

@@ -1,5 +1,5 @@
10 REM A field name is checked against a closed set the program declared, which 10 REM A field name is checked against the closed set the program declared;
20 REM is the one thing in this language whose valid spellings are written down. 20 REM this is the one thing whose valid spellings are written down.
30 REM A misspelled *variable* is still silent -- see the last two lines. 30 REM A misspelled *variable* is still silent -- see the last two lines.
40 TYPE RECT 40 TYPE RECT
50 W# 50 W#

View File

@@ -17,7 +17,7 @@
170 RETURN B@.W# 170 RETURN B@.W#
180 PRINT WIDEN(A@) 180 PRINT WIDEN(A@)
190 PRINT A@.W# 190 PRINT A@.W#
200 REM To change one on purpose, pass a pointer. Assignment copies a pointer's 200 REM To change one on purpose, pass a pointer. Assignment copies its
210 REM reference, so the callee is looking at the caller's own record. 210 REM reference, so the callee is looking at the caller's own record.
220 DIM Q@ AS PTR TO CRATE 220 DIM Q@ AS PTR TO CRATE
230 POINT Q@ AT A@ 230 POINT Q@ AT A@
@@ -37,7 +37,7 @@
370 N2@.COUNT# = 20 370 N2@.COUNT# = 20
380 POINT N1@.TAIL@ AT N2@ 380 POINT N1@.TAIL@ AT N2@
390 DEF TOTAL(P@ AS PTR TO NODE) 390 DEF TOTAL(P@ AS PTR TO NODE)
395 REM A pointer is true when it points at something, which is how a walk knows 395 REM A pointer is true when it points at something, so a walk knows
396 REM where the list ends. NOT is the bitwise operator here, so the test is 396 REM where the list ends. NOT is the bitwise operator here, so the test is
397 REM written the positive way round. 397 REM written the positive way round.
400 IF P@->TAIL@ THEN RETURN P@->COUNT# + TOTAL(P@->TAIL@) 400 IF P@->TAIL@ THEN RETURN P@->COUNT# + TOTAL(P@->TAIL@)

View File

@@ -1,4 +1,4 @@
10 REM A TYPE declares its fields; each takes its own type from its own suffix, 10 REM A TYPE declares its fields; each takes its type from its own suffix,
20 REM which is the same rule every other name in this language follows. 20 REM which is the same rule every other name in this language follows.
30 TYPE COORD 30 TYPE COORD
40 X# 40 X#

View File

@@ -1,6 +1,6 @@
10 REM A type name and a field name are bare words, and so is every verb, so 10 REM A type name and a field name are bare words, and so is every verb, so
20 REM they share a namespace whether we like it or not. Both are refused with 20 REM they share a namespace whether we like it or not. Both are refused with
30 REM the same rule the scanner already applies to variable names -- and a type 30 REM the scanner applies the same rule to variable names -- and a type
40 REM name is refused by the prescan, which can say so plainly rather than 40 REM name is refused by the prescan, which can say so plainly rather than
50 REM leaving the parser to report "Expected expression or literal". 50 REM leaving the parser to report "Expected expression or literal".
60 TYPE POINT 60 TYPE POINT

View File

@@ -1,6 +1,6 @@
10 REM This shows the waitingForCommand utility in the BasicEnvironment 10 REM This shows the waitingForCommand utility in the BasicEnvironment
11 REM when we have a nested for loop. The inner loop SHOULD execute, but 11 REM when we have a nested for loop. The inner loop SHOULD execute, but
12 REM the outer loop should NOT execute. Therefore, neither loop should execute. 12 REM the outer loop should NOT execute; neither loop should execute.
20 FOR I# = 1 TO 0 20 FOR I# = 1 TO 0
25 FOR J# = 2 TO 4 25 FOR J# = 2 TO 4
30 PRINT "waitingForCommand FAILS if this is seen" 30 PRINT "waitingForCommand FAILS if this is seen"
@@ -8,4 +8,4 @@
35 NEXT J# 35 NEXT J#
40 NEXT I# 40 NEXT I#
50 PRINT "SUCCESS" 50 PRINT "SUCCESS"
80 QUIT 80 QUIT

View File

@@ -1,6 +1,6 @@
10 PRINT MOD(10, 3) 10 PRINT MOD(10, 3)
20 PRINT MOD(12, 5) 20 PRINT MOD(12, 5)
30 PRINT MOD(4, 2) 30 PRINT MOD(4, 2)
40 REM MOD() ONLY WORKS WITH INTEGERS - RESULTS WITH FLOATING POINT ARE UNRELIABLE 40 REM MOD() ONLY WORKS WITH INTEGERS; FLOATING POINT RESULTS ARE UNRELIABLE
50 REM PRINT MOD(1.2, 0.4) 50 REM PRINT MOD(1.2, 0.4)
60 REM THERE IS NO ERROR THROWN HERE. JUST DONT DO IT. 60 REM THERE IS NO ERROR THROWN HERE. JUST DONT DO IT.

View File

@@ -97,9 +97,9 @@ static void test_scoped_loop_counter_costs_nothing(void)
/** /**
* @brief The pool is genuinely untouched, not merely large enough. * @brief The pool is genuinely untouched, not merely large enough.
* *
* Two hundred scope entries and then the pool's *entire* width -- four arrays, * Two hundred scope entries and then the pool's *entire* width -- two arrays,
* because AKBASIC_MAX_ARRAY_ELEMENTS caps any one of them at 1024 while the pool * because AKBASIC_MAX_ARRAY_ELEMENTS caps any one of them at 1024 while the pool
* holds 4096. One leaked slot and the fourth `DIM` has nowhere to go, which * holds 2048. One leaked slot and the second `DIM` has nowhere to go, which
* makes this the sharpest of the three and by far the cheapest: the two above * makes this the sharpest of the three and by far the cheapest: the two above
* prove a program finishes, this proves nothing at all was spent. The long ones * prove a program finishes, this proves nothing at all was spent. The long ones
* stay because they are the shape the defect was found in. * stay because they are the shape the defect was found in.
@@ -111,10 +111,8 @@ static void test_pool_is_untouched_by_scopes(void)
"30 NEXT T#\n" "30 NEXT T#\n"
"40 DIM A#(1024)\n" "40 DIM A#(1024)\n"
"50 DIM B#(1024)\n" "50 DIM B#(1024)\n"
"60 DIM C#(1024)\n" "70 B#(1023) = 7\n"
"70 DIM D#(1024)\n" "90 PRINT B#(1023)\n"
"80 D#(1023) = 7\n"
"90 PRINT D#(1023)\n"
"100 END\n" "100 END\n"
"110 LABEL SUBA\n" "110 LABEL SUBA\n"
"120 LOC# = 1\n" "120 LOC# = 1\n"