Files
snark13 c71e249a4e Add full compiler toolchain, libc, examples and reference docs
First substantive commit: the entire Sprinter C compiler tree on top of
the bare README+gitignore initial commit.

What's in here:
  bin/sprinter-cc        — driver script invoking SDCC + linker + mkexe
  libc/                  — Sprinter-specific libc layer over ESTEX/BIOS
                           (conio, gfx, io, mem, stdio + headers)
  runtime/               — crt0 variants (default/small/banked/minimal)
                           + heap + bank trampolines
  toolchain/             — mkexe (SprintEXE packer, C + tests)
  examples/              — 30 demo programs (gfx, file I/O, env, time, …)
  lib/Makefile           — builds the libc archive (sprinter.lib)
  docs/                  — converted Sprinter manuals + asm reference samples
  third_party/           — solid-c reference compiler dump + sdcc setup script
  release_docs/          — packaging / release notes

gitignore overhaul:
  • Drop dangerous blanket patterns: *.asm (would hide docs/samples/*.asm)
    and *.exe (case-insensitive match was hiding third_party/solid-c/*.EXE
    on macOS APFS).  Replaced with examples/*/*.{asm,exe,…} and lib/*.lib.
  • Restore tracking of toolchain/mkexe/tests/{one,big}.bin — those are
    INPUT fixtures, not build outputs.
  • Collapse the duplicated SDCC/C/Sdcc sections into one section per
    concern (build outputs / vendored / OS-junk).
  • Add .sprinter-cc-*/, build/ (catches lib/build/ too), .claude/.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 16:13:21 +03:00

396 lines
10 KiB
ArmAsm

;; ----------------------------------------------------------------------
;; crt0_small.s — Sprinter ESTEX C runtime startup for SMALL memory mode.
;;
;; Layout (small mode):
;; CODE at 0x4100 (W1)
;; DATA chained after code by the linker (W1, possibly spills into W2)
;; STACK at 0xBFFE (top of W2, valid after W2 page is mapped)
;; HEAP from end-of-BSS up to ___sdcc_heap_end (default 0xBB00)
;;
;; Size auto-detect:
;; For programs < ~14 KB, DSS allocates exactly ONE 16 KB page (W1). W2
;; is "page #FF" (read=0xFF, writes ignored) and we must allocate &
;; map a page ourselves. For programs that already span > 16 KB (the
;; linker emitted bytes above 0x7FFF), DSS allocates TWO pages on its
;; own — W2 already RAM — and we MUST NOT call SETWIN2, or we'd swap
;; the DSS-loaded code page out of W2.
;;
;; We auto-detect by writing a test byte at 0xBFFF (well above stack
;; pushes) and reading back. Match → W2 is RAM, skip allocation.
;; Mismatch → W2 is page #FF, allocate via ESTEX $3D + $3A.
;;
;; Chicken-and-egg:
;; DSS sets SP from the EXE header (0xBFFE) — in unmapped W2 for small
;; programs. Pushes silently fail; first `call` returns to 0xFFFF and
;; crashes. Boot stack lives in HOME (W1, real RAM) until W2 is ready.
;;
;; Why ESTEX $3A SETWIN2 not BIOS $C4 + OUT (0xC2):
;; BIOS calls require SP in W2 (0x8000..0xBFFF) — exactly what we don't
;; have yet. SETWIN2 is ESTEX, works with W1 stack.
;;
;; Entry contract — same as crt0.s.
;; ----------------------------------------------------------------------
.module crt0_small
.globl _main
;; Linker-emitted symbols (resolved at link time).
.globl s__INITIALIZER
.globl l__INITIALIZER
.globl s__INITIALIZED
.globl s__BSS
.globl l__BSS
;; Tunables — mirror crt0.s.
ARGV_MAX_ARGS = 16
ARGV_BUF_BYTES = 128
BOOT_STACK_BYTES = 32 ; enough for 2-3 nested RST 10h/RST 8 calls
;; =========================================================================
;; AREA ORDERING.
;; =========================================================================
.area _HOME
.area _CODE
.area _INITIALIZER
.area _GSINIT
.area _GSFINAL
.area _DATA
.area _INITIALIZED
.area _BSEG
.area _BSS
.area _HEAP
;; =========================================================================
;; Entry point — must be FIRST in _CODE so it lands at --code-loc (0x4100).
;; W2 is unmapped at this instant — no `call` allowed until we point SP at
;; boot_stack_top (in W1, declared just below).
;; =========================================================================
.area _CODE
_start::
;; Switch to the boot stack in W1. The DSS-set SP=0xBFFE may point
;; into unmapped W2 — pushing there silently fails and the first call
;; would return to 0xFFFF. Boot stack lives in HOME (W1, real RAM).
ld sp, #boot_stack_top
;; The DSS startup-prefix pointer is in IX. Stash it on the boot
;; stack — _estex_startup_ix is in W2 which may not be RAM yet.
push ix
;; ----- Step 1: detect whether W2 is already mapped. -----------------
;; Port 0xC2 is the W2 page register (0x82/A2/C2/E2 = W0/W1/W2/W3).
;; Reading it returns the page number currently selected for W2; page
;; 0xFF is the "unmapped" sentinel (read=0xFF / write-ignored). A
;; non-0xFF value means DSS already allocated and mapped a real RAM
;; page for W2 — this happens for programs whose HOME image extends
;; past 0x7FFF (DSS allocates as many pages as the image size demands).
in a, (#0xC2)
cp #0xFF
jr nz, w2_already_mapped
;; ----- Step 2a: W2 unmapped → allocate one page and map it. --------
;; ESTEX $3D GETMEM: B = npages → A = block id, CF = err.
ld b, #1
ld c, #0x3D
rst #0x10
jr c, w2_alloc_fail
;; A = block id of the freshly-allocated 1-page block.
;; ESTEX $3A SETWIN2: A = block id, B = page idx → maps to W2. This
;; is an ESTEX (RST 10h) call, not BIOS (RST 8) — BIOS would require
;; SP in W2, which is exactly what we don't have yet.
ld b, #0 ; page 0 of the 1-page block
ld c, #0x3A
rst #0x10
jr c, w2_alloc_fail
.ifdef DEBUG_RT
ld a, #1 ; flag: we self-allocated W2
jr w2_join
.endif
w2_already_mapped:
.ifdef DEBUG_RT
xor a ; flag: DSS already gave us W2
w2_join:
.endif
;; ----- Step 3: switch to the canonical SP at the top of W2. --------
;; Pop IX back from the boot stack first.
pop ix
ld sp, #0xBFFE
.ifdef DEBUG_RT
;; Save the self-alloc flag across gsinit (which clobbers BC/DE/HL and
;; zeroes BSS). SP is in W2 now, push is safe.
push af
.endif
;; ----- Step 5: save IX prefix in its canonical W2 slot. -------------
ld (_estex_startup_ix), ix
;; ----- Step 6: standard gsinit + argv flow (identical to crt0.s). --
call gsinit
.ifdef DEBUG_RT
pop af
ld (_w2_self_allocated), a
.endif
call parse_argv
call get_progname
ld hl, (_argc)
ld de, (_argv)
call _main
;; SDCC int return in DE. Low byte = exit code.
ld a, e
ld b, a
ld c, #0x41 ; ESTEX EXIT
rst #0x10
1$: halt
jr 1$
;; -------------------------------------------------------------------------
;; W2 allocation failed — there's not much we can do without DATA/BSS.
;; Try to abort via ESTEX EXIT with a non-zero code (no return).
;; -------------------------------------------------------------------------
w2_alloc_fail:
ld b, #0xFE ; arbitrary failure code
ld c, #0x41 ; ESTEX EXIT
rst #0x10
2$: halt
jr 2$
;; =========================================================================
;; Boot stack — reserved in HOME (W1, real RAM). Used only during the
;; first few instructions of _start, before W2 is mapped. After we switch
;; SP to 0xBFFE the boot stack memory is just dead bytes in HOME.
;; =========================================================================
boot_stack:
.ds BOOT_STACK_BYTES
boot_stack_top:
;; =========================================================================
;; parse_argv / get_progname / runtime data / gsinit — same as crt0.s.
;; Kept inline rather than factored out to keep crt0 build self-contained.
;; =========================================================================
.area _CODE
parse_argv::
ld hl, (_estex_startup_ix)
ld a, (hl)
cp #ARGV_BUF_BYTES
jr c, len_ok
ld a, #ARGV_BUF_BYTES-1
len_ok:
ld c, a
ld b, #0
inc hl
ld de, #argv_buf
ld a, b
or a, c
jr Z, after_copy
ldir
after_copy:
xor a
ld (de), a
ld hl, #argv_buf
strip_ws:
ld a, (hl)
or a
jr Z, no_args
cp #0x20
jr Z, strip_more
cp #0x09
jr nz, have_first_token
strip_more:
inc hl
jr strip_ws
no_args:
ld hl, #empty_str
ld (argv_array), hl
ld hl, #0
ld (argv_array+2), hl
ld hl, #1
ld (_argc), hl
ld hl, #argv_array
ld (_argv), hl
ret
have_first_token:
ld de, #empty_str
ld (argv_array), de
ld b, #1
token_loop:
ld a, b
cp #ARGV_MAX_ARGS
jr nc, tokens_done
push hl
ld a, b
add a, a
ld e, a
ld d, #0
ld hl, #argv_array
add hl, de
pop de
ld (hl), e
inc hl
ld (hl), d
ex de, hl
inc b
walk_token:
ld a, (hl)
or a
jr Z, tokens_done
cp #0x20
jr Z, end_of_token
cp #0x09
jr Z, end_of_token
inc hl
jr walk_token
end_of_token:
xor a
ld (hl), a
inc hl
skip_ws:
ld a, (hl)
or a
jr Z, tokens_done
cp #0x20
jr Z, skip_more
cp #0x09
jr Z, skip_more
jr token_loop
skip_more:
inc hl
jr skip_ws
tokens_done:
ld a, b
add a, a
ld e, a
ld d, #0
ld hl, #argv_array
add hl, de
ld (hl), #0
inc hl
ld (hl), #0
ld a, b
ld (_argc), a
xor a
ld (_argc+1), a
ld hl, #argv_array
ld (_argv), hl
ret
empty_str:
.db 0
.area _CODE
get_progname::
push ix
ld hl, #progname_buf
ld b, #2
ld c, #0x47
rst #0x10
pop ix
jr c, gpn_skip
ld hl, #progname_buf
gpn_find_end:
ld a, (hl)
or a, a
jr Z, gpn_at_end
inc hl
jr gpn_find_end
gpn_at_end:
ld de, #progname_buf
gpn_scan_back:
ld a, h
cp a, d
jr nz, gpn_dec
ld a, l
cp a, e
jr Z, gpn_set
gpn_dec:
dec hl
ld a, (hl)
cp #0x5C
jr Z, gpn_after_sep
cp #0x3A
jr Z, gpn_after_sep
jr gpn_scan_back
gpn_after_sep:
inc hl
gpn_set:
ld (argv_array), hl
gpn_skip:
ret
;; =========================================================================
;; Runtime data — same layout as crt0.s. Lives in DATA/BSS which the
;; linker places in W2 (--data-loc 0x8000).
;; =========================================================================
.area _DATA
_estex_startup_ix::
.ds 2
_argc::
.ds 2
_argv::
.ds 2
.area _BSS
argv_buf:
.ds ARGV_BUF_BYTES
argv_array:
.ds (ARGV_MAX_ARGS + 1) * 2
progname_buf:
.ds 128
.ifdef DEBUG_RT
;; Runtime diagnostic: 0 = DSS already mapped W2 for us (program > 16 KB,
;; or some other case); 1 = crt0 had to allocate and map W2 itself via
;; ESTEX $3D + $3A. Only present when sprinter-cc is invoked with --debug.
_w2_self_allocated::
.ds 1
.endif
;; =========================================================================
;; gsinit — copy _INITIALIZER -> _INITIALIZED, then zero _BSS. Identical
;; to crt0.s; runs AFTER W2 is mapped so writes to _INITIALIZED/_BSS land.
;; =========================================================================
.area _GSINIT
gsinit::
ld bc, #l__INITIALIZER
ld a, b
or a, c
jr Z, gsinit_bss
ld de, #s__INITIALIZED
ld hl, #s__INITIALIZER
ldir
gsinit_bss:
ld bc, #l__BSS
ld a, b
or a, c
jr Z, gsinit_done
ld hl, #s__BSS
ld (hl), #0
dec bc
ld a, b
or a, c
jr Z, gsinit_done
ld d, h
ld e, l
inc de
ldir
gsinit_done:
.area _GSFINAL
ret