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>
This commit is contained in:
+358
@@ -0,0 +1,358 @@
|
||||
;; ----------------------------------------------------------------------
|
||||
;; crt0.s — Sprinter ESTEX C runtime startup, with argv parsing.
|
||||
;;
|
||||
;; What's special vs crt0_minimal.s:
|
||||
;; After gsinit and before calling main, we read the ESTEX command-line
|
||||
;; from the startup prefix (IX+0 = length, IX+1.. = ASCIIZ bytes) and
|
||||
;; tokenize it in place into a static argv[] table. argc → HL, argv → DE
|
||||
;; when we then `call _main`, which matches SDCC's __sdcccall(1) two-arg
|
||||
;; ABI. Programs declared as `int main(void)` simply ignore the input
|
||||
;; registers — no harm done — so this is the default crt0.
|
||||
;;
|
||||
;; Entry contract (per ESTEX EXEC):
|
||||
;; IX -> startup prefix:
|
||||
;; IX-3 = open file handle (only if EXE header.loader > 0)
|
||||
;; IX-2 = memory block id (released automatically on EXIT)
|
||||
;; IX-1 = current process level
|
||||
;; IX+0 = command-line length (1 byte)
|
||||
;; IX+1 = command-line bytes (ASCIIZ, up to 127 chars)
|
||||
;;
|
||||
;; Memory map: see compiler_approach memory file.
|
||||
;; ----------------------------------------------------------------------
|
||||
|
||||
.module crt0
|
||||
.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 — match the constants in argv parsing below.
|
||||
ARGV_MAX_ARGS = 16 ; including argv[0]
|
||||
ARGV_BUF_BYTES = 128 ; cmdline buffer size (ESTEX max + 1)
|
||||
|
||||
;; ___sdcc_heap_end (upper bound of the malloc free-list) lives in a
|
||||
;; separate per-program file (runtime/heap_top.s) so it can be regenerated
|
||||
;; with a custom value without re-assembling crt0. The default is set in
|
||||
;; that file (0xBB00); sprinter-cc may emit a different value when
|
||||
;; `--stack-size N` is given.
|
||||
|
||||
;; =========================================================================
|
||||
;; AREA ORDERING — emitted up-front so the linker walks them in this order.
|
||||
;; =========================================================================
|
||||
.area _HOME
|
||||
.area _CODE
|
||||
.area _INITIALIZER
|
||||
.area _GSINIT
|
||||
.area _GSFINAL
|
||||
|
||||
.area _DATA
|
||||
.area _INITIALIZED
|
||||
.area _BSEG
|
||||
.area _BSS
|
||||
.area _HEAP
|
||||
|
||||
;; =========================================================================
|
||||
;; Entry point — first instruction in _CODE, hence at --code-loc (0x4100).
|
||||
;; =========================================================================
|
||||
.area _CODE
|
||||
_start::
|
||||
ld sp, #0xBFFE
|
||||
ld (_estex_startup_ix), ix ; save IX prefix pointer
|
||||
|
||||
call gsinit
|
||||
|
||||
.ifdef DEBUG_RT
|
||||
;; tiny mode never self-allocates a W2 page (CODE+DATA both live in W2,
|
||||
;; which DSS itself maps for us). Publish 0 to the diagnostic flag —
|
||||
;; must run AFTER gsinit since BSS is zeroed there and we want a
|
||||
;; deterministic value visible to user code from main() onward.
|
||||
xor a
|
||||
ld (_w2_self_allocated), a
|
||||
.endif
|
||||
|
||||
;; Parse the ESTEX command-line into argv[]; populates _argc, _argv.
|
||||
call parse_argv
|
||||
|
||||
;; Replace argv[0] (empty string placeholder) with the basename of the
|
||||
;; running .EXE. Safe to skip if ESTEX APPINFO fails.
|
||||
call get_progname
|
||||
|
||||
;; Load argc/argv per SDCC __sdcccall(1): arg1 → HL, arg2 → DE.
|
||||
ld hl, (_argc)
|
||||
ld de, (_argv)
|
||||
call _main
|
||||
|
||||
;; SDCC's int return → DE. Low byte is the exit code.
|
||||
ld a, e
|
||||
ld b, a
|
||||
ld c, #0x41 ; ESTEX EXIT
|
||||
rst #0x10
|
||||
;; Should not return; halt loop just in case.
|
||||
1$: halt
|
||||
jr 1$
|
||||
|
||||
;; =========================================================================
|
||||
;; parse_argv — tokenize ESTEX cmdline into argv[].
|
||||
;;
|
||||
;; The prefix: (IX+0) = length; (IX+1...) = ASCIIZ bytes.
|
||||
;; We copy up to ARGV_BUF_BYTES-1 chars into our own buffer (so we can
|
||||
;; overwrite separators with NUL), strip leading whitespace (DSS quirk),
|
||||
;; then walk through tokens. argv[0] is set to an empty string because
|
||||
;; ESTEX doesn't pass the program name in the prefix.
|
||||
;; =========================================================================
|
||||
.area _CODE
|
||||
parse_argv::
|
||||
;; Copy cmdline body into argv_buf, NUL-terminate.
|
||||
ld hl, (_estex_startup_ix)
|
||||
ld a, (hl) ; A = cmdline length
|
||||
cp #ARGV_BUF_BYTES
|
||||
jr c, len_ok
|
||||
ld a, #ARGV_BUF_BYTES-1
|
||||
len_ok:
|
||||
ld c, a
|
||||
ld b, #0 ; BC = number of bytes to copy
|
||||
inc hl ; HL = body start (= IX+1)
|
||||
ld de, #argv_buf
|
||||
ld a, b
|
||||
or a, c
|
||||
jr Z, after_copy
|
||||
ldir ; DE = one past last copied byte
|
||||
after_copy:
|
||||
xor a
|
||||
ld (de), a ; NUL-terminate
|
||||
|
||||
;; Skip leading whitespace.
|
||||
ld hl, #argv_buf
|
||||
strip_ws:
|
||||
ld a, (hl)
|
||||
or a
|
||||
jr Z, no_args
|
||||
cp #0x20 ; ' '
|
||||
jr Z, strip_more
|
||||
cp #0x09 ; tab
|
||||
jr nz, have_first_token
|
||||
strip_more:
|
||||
inc hl
|
||||
jr strip_ws
|
||||
|
||||
no_args:
|
||||
;; Only argv[0] = "" — no real arguments.
|
||||
ld hl, #empty_str
|
||||
ld (argv_array), hl
|
||||
ld hl, #0
|
||||
ld (argv_array+2), hl ; argv[1] = NULL
|
||||
ld hl, #1
|
||||
ld (_argc), hl
|
||||
ld hl, #argv_array
|
||||
ld (_argv), hl
|
||||
ret
|
||||
|
||||
have_first_token:
|
||||
;; HL points at start of first real argument.
|
||||
;; argv[0] = empty string (placeholder for program name).
|
||||
ld de, #empty_str
|
||||
ld (argv_array), de
|
||||
|
||||
;; B = current argc (start at 1). Use B because HL/DE are busy.
|
||||
ld b, #1
|
||||
|
||||
token_loop:
|
||||
;; Bound check: argc < ARGV_MAX_ARGS
|
||||
ld a, b
|
||||
cp #ARGV_MAX_ARGS
|
||||
jr nc, tokens_done
|
||||
|
||||
;; argv[argc] = HL. Compute slot address = argv_array + argc*2.
|
||||
push hl
|
||||
ld a, b
|
||||
add a, a ; A = argc * 2
|
||||
ld e, a
|
||||
ld d, #0
|
||||
ld hl, #argv_array
|
||||
add hl, de ; HL = &argv[argc]
|
||||
pop de ; DE = current token pointer
|
||||
ld (hl), e
|
||||
inc hl
|
||||
ld (hl), d
|
||||
ex de, hl ; HL = token pointer again
|
||||
|
||||
inc b ; argc++
|
||||
|
||||
;; Advance past token (non-whitespace).
|
||||
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:
|
||||
;; Replace separator with NUL.
|
||||
xor a
|
||||
ld (hl), a
|
||||
inc hl
|
||||
|
||||
;; Skip extra whitespace.
|
||||
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:
|
||||
;; argv[argc] = NULL terminator.
|
||||
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
|
||||
;; Publish argc (16-bit, zero-extended from B).
|
||||
ld a, b
|
||||
ld (_argc), a
|
||||
xor a
|
||||
ld (_argc+1), a
|
||||
;; Publish argv = &argv_array[0]
|
||||
ld hl, #argv_array
|
||||
ld (_argv), hl
|
||||
ret
|
||||
|
||||
;; The empty string used as argv[0] placeholder — lives in code.
|
||||
empty_str:
|
||||
.db 0
|
||||
|
||||
;; =========================================================================
|
||||
;; get_progname — fetch the running program's full path via ESTEX APPINFO
|
||||
;; (subfn 2, $47) and set argv[0] to the basename portion.
|
||||
;;
|
||||
;; ESTEX APPINFO: A=err, C=$47, B=subfn, HL=buf → A=err, CF=1 on error.
|
||||
;; subfn 2 = full app path, e.g. "A:\PROGRAMS\ARGV_TES.EXE"
|
||||
;;
|
||||
;; On error or when no separator is found, we leave argv[0] as the empty
|
||||
;; string placeholder that parse_argv set.
|
||||
;; =========================================================================
|
||||
.area _CODE
|
||||
get_progname::
|
||||
push ix ; ESTEX clobbers IX
|
||||
ld hl, #progname_buf
|
||||
ld b, #2 ; subfn 2 = app_path
|
||||
ld c, #0x47
|
||||
rst #0x10
|
||||
pop ix
|
||||
jr c, gpn_skip ; APPINFO failed → keep empty argv[0]
|
||||
|
||||
;; First make sure the buffer is null-terminated (it should already be).
|
||||
;; Then scan forward to find the end-of-string, then scan backwards to
|
||||
;; the last directory separator ('\' or ':').
|
||||
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:
|
||||
;; HL points at the NUL. Walk backwards looking for '\' or ':'. Stop
|
||||
;; if we reach the start of the buffer — then the whole path is the
|
||||
;; basename.
|
||||
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 ; reached buffer start
|
||||
gpn_dec:
|
||||
dec hl
|
||||
ld a, (hl)
|
||||
cp #0x5C ; backslash
|
||||
jr Z, gpn_after_sep
|
||||
cp #0x3A ; colon (drive separator)
|
||||
jr Z, gpn_after_sep
|
||||
jr gpn_scan_back
|
||||
|
||||
gpn_after_sep:
|
||||
inc hl ; skip past the separator
|
||||
gpn_set:
|
||||
ld (argv_array), hl ; argv[0] = basename
|
||||
gpn_skip:
|
||||
ret
|
||||
|
||||
;; =========================================================================
|
||||
;; Runtime data (HOME's _DATA / _BSS)
|
||||
;; =========================================================================
|
||||
.area _DATA
|
||||
_estex_startup_ix::
|
||||
.ds 2
|
||||
|
||||
_argc::
|
||||
.ds 2
|
||||
_argv::
|
||||
.ds 2
|
||||
|
||||
.ifdef DEBUG_RT
|
||||
;; Runtime diagnostic: 0 = no extra W2 page was self-allocated by crt0
|
||||
;; (DSS gave us what we needed); 1 = crt0 had to allocate W2 itself.
|
||||
;; Only present when sprinter-cc is invoked with --debug.
|
||||
_w2_self_allocated::
|
||||
.ds 1
|
||||
.endif
|
||||
|
||||
.area _BSS
|
||||
argv_buf:
|
||||
.ds ARGV_BUF_BYTES
|
||||
argv_array:
|
||||
.ds (ARGV_MAX_ARGS + 1) * 2 ; +1 for trailing NULL pointer
|
||||
progname_buf:
|
||||
.ds 128 ; ESTEX APPINFO app_path target
|
||||
|
||||
;; =========================================================================
|
||||
;; gsinit — copy _INITIALIZER -> _INITIALIZED, then zero _BSS.
|
||||
;; =========================================================================
|
||||
.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
|
||||
Reference in New Issue
Block a user