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:
2026-06-03 16:13:21 +03:00
parent f542608b3f
commit c71e249a4e
404 changed files with 75155 additions and 58 deletions
+15
View File
@@ -0,0 +1,15 @@
CFLAGS ?= -O2 -Wall -Wextra -std=c99 -pedantic
CC ?= cc
all: mkexe
mkexe: mkexe.c
$(CC) $(CFLAGS) -o $@ mkexe.c
check: mkexe
./run-tests.sh
clean:
rm -f mkexe tests/*.actual tests/*.exe
.PHONY: all check clean
+485
View File
@@ -0,0 +1,485 @@
/*
* mkexe — convert a flat binary or Intel HEX image into a Sprinter ESTEX .EXE.
*
* Single-image mode (no banks):
* +00 3 bytes "EXE"
* +03 1 byte version (0)
* +04 4 bytes offset to image (0x00000200)
* +08 2 bytes loader size (0 — whole file at once)
* +0A 6 bytes reserved (0)
* +10 2 bytes load address
* +12 2 bytes start (PC)
* +14 2 bytes initial SP
* +16 490 bytes reserved (0)
* +200 ... contiguous image bytes
*
* Multi-bank mode (detected from IHX ELA records):
* header.loader = HOME image size (so ESTEX loads only HOME and keeps the
* .EXE file open with the handle in IX-3; crt0_banked.s
* then reads each bank with ESTEX READ)
* file layout: header (512) + HOME image + bank1 (16KB) + bank2 (16KB) + ...
*
* IHX convention from SDCC `-Wl-b _BANKn=0xNC000`:
* ELA = 0x00 → HOME (low16 = 0x4100..0x7FFF range)
* ELA = 0x01 → BANK1 (low16 = 0xC000..0xFFFF)
* ELA = 0x02 → BANK2
* ...
*
* Build: cc -O2 -Wall -Wextra -std=c99 -o mkexe mkexe.c
*/
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>
#define EXE_HEADER_SIZE 512
#define SEGMENT_SIZE 0x10000u
#define BANK_SIZE 0x4000u /* 16 KB per bank */
#define DEFAULT_BANK_BASE 0xC000u /* HUGE mode (banks live in window 3)*/
#define BIG_BANK_BASE 0x4000u /* BIG mode (banks live in window 1)*/
#define MAX_BANKS 15
#define HOME_SEG 0
#define DEFAULT_STACK 0xBFFEu
#define DEFAULT_LOAD 0x4100u
typedef struct {
uint8_t bytes[SEGMENT_SIZE];
uint8_t present[SEGMENT_SIZE];
uint32_t lo;
uint32_t hi;
int any;
} segment_t;
static segment_t segments[MAX_BANKS + 1];
static void segments_init(void) {
for (int i = 0; i <= MAX_BANKS; i++) {
memset(segments[i].bytes, 0, sizeof(segments[i].bytes));
memset(segments[i].present, 0, sizeof(segments[i].present));
segments[i].lo = 0xFFFFFFFFu;
segments[i].hi = 0;
segments[i].any = 0;
}
}
static int segment_set(unsigned seg, uint32_t addr, uint8_t v) {
if (seg > MAX_BANKS) {
fprintf(stderr, "mkexe: bank id %u exceeds MAX_BANKS=%d\n", seg, MAX_BANKS);
return -1;
}
if (addr >= SEGMENT_SIZE) {
fprintf(stderr, "mkexe: address 0x%X outside the 64K Z80 space\n", addr);
return -1;
}
segment_t *s = &segments[seg];
s->bytes[addr] = v;
s->present[addr] = 1;
if (addr < s->lo) s->lo = addr;
if (addr > s->hi) s->hi = addr;
s->any = 1;
return 0;
}
static int hex_nib(int c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
return -1;
}
static int hex_byte(const char *p) {
int h = hex_nib((unsigned char)p[0]);
int l = hex_nib((unsigned char)p[1]);
if (h < 0 || l < 0) return -1;
return (h << 4) | l;
}
static int load_ihx(const char *path) {
FILE *f = fopen(path, "rb");
if (!f) {
fprintf(stderr, "mkexe: cannot open '%s': %s\n", path, strerror(errno));
return -1;
}
char line[1024];
int rc = 0, lineno = 0;
unsigned seg = 0; /* upper 8 bits of 24-bit virtual addr */
uint32_t para_base = 0; /* paragraph base from type-02 records */
while (fgets(line, sizeof(line), f)) {
lineno++;
size_t n = strlen(line);
while (n > 0 && (line[n-1] == '\n' || line[n-1] == '\r' || line[n-1] == ' ')) {
line[--n] = 0;
}
if (n == 0) continue;
if (line[0] != ':') {
fprintf(stderr, "mkexe: %s:%d: expected ':' record start\n", path, lineno);
rc = -1; break;
}
if (n < 11) {
fprintf(stderr, "mkexe: %s:%d: record too short\n", path, lineno);
rc = -1; break;
}
int count = hex_byte(line + 1);
int aHi = hex_byte(line + 3);
int aLo = hex_byte(line + 5);
int type = hex_byte(line + 7);
if (count < 0 || aHi < 0 || aLo < 0 || type < 0) {
fprintf(stderr, "mkexe: %s:%d: bad hex in header\n", path, lineno);
rc = -1; break;
}
if (n < (size_t)(11 + 2 * count)) {
fprintf(stderr, "mkexe: %s:%d: truncated record\n", path, lineno);
rc = -1; break;
}
unsigned sum = (unsigned)count + (unsigned)aHi + (unsigned)aLo + (unsigned)type;
uint16_t addr = (uint16_t)((aHi << 8) | aLo);
if (type == 0x00) {
for (int i = 0; i < count; i++) {
int b = hex_byte(line + 9 + 2 * i);
if (b < 0) { rc = -1; goto done; }
sum += (unsigned)b;
uint32_t a = para_base + (uint32_t)addr + (uint32_t)i;
if (segment_set(seg, a, (uint8_t)b) < 0) { rc = -1; goto done; }
}
} else if (type == 0x01) {
int cc = hex_byte(line + 9);
if (cc < 0) { rc = -1; goto done; }
sum += (unsigned)cc;
if ((sum & 0xFF) != 0) {
fprintf(stderr, "mkexe: %s:%d: bad checksum on EOF record\n", path, lineno);
rc = -1; goto done;
}
break;
} else if (type == 0x02) {
int bHi = hex_byte(line + 9);
int bLo = hex_byte(line + 11);
if (bHi < 0 || bLo < 0) { rc = -1; goto done; }
sum += (unsigned)bHi + (unsigned)bLo;
para_base = ((uint32_t)((bHi << 8) | bLo)) << 4;
} else if (type == 0x04) {
int bHi = hex_byte(line + 9);
int bLo = hex_byte(line + 11);
if (bHi < 0 || bLo < 0) { rc = -1; goto done; }
sum += (unsigned)bHi + (unsigned)bLo;
uint32_t high = ((uint32_t)bHi << 8) | (uint32_t)bLo;
if (high > MAX_BANKS) {
fprintf(stderr, "mkexe: %s:%d: ELA upper16=0x%04X means bank id %u, exceeds MAX_BANKS=%d\n",
path, lineno, (unsigned)high, (unsigned)high, MAX_BANKS);
rc = -1; goto done;
}
seg = (unsigned)high;
} else if (type == 0x03 || type == 0x05) {
for (int i = 0; i < count; i++) {
int b = hex_byte(line + 9 + 2 * i);
if (b < 0) { rc = -1; goto done; }
sum += (unsigned)b;
}
} else {
fprintf(stderr, "mkexe: %s:%d: unsupported HEX type 0x%02X\n", path, lineno, type);
rc = -1; goto done;
}
int cc = hex_byte(line + 9 + 2 * count);
if (cc < 0) {
fprintf(stderr, "mkexe: %s:%d: missing checksum\n", path, lineno);
rc = -1; goto done;
}
sum += (unsigned)cc;
if ((sum & 0xFF) != 0) {
fprintf(stderr, "mkexe: %s:%d: checksum mismatch (sum=%02X)\n", path, lineno, sum & 0xFF);
rc = -1; goto done;
}
}
done:
fclose(f);
return rc;
}
static int load_bin(const char *path, uint32_t load_addr) {
FILE *f = fopen(path, "rb");
if (!f) {
fprintf(stderr, "mkexe: cannot open '%s': %s\n", path, strerror(errno));
return -1;
}
uint8_t buf[4096];
uint32_t addr = load_addr;
size_t n;
while ((n = fread(buf, 1, sizeof(buf), f)) > 0) {
for (size_t i = 0; i < n; i++) {
if (segment_set(HOME_SEG, addr++, buf[i]) < 0) { fclose(f); return -1; }
}
}
fclose(f);
return 0;
}
static int has_suffix(const char *s, const char *suf) {
size_t ls = strlen(s), lf = strlen(suf);
if (lf > ls) return 0;
return strcasecmp(s + ls - lf, suf) == 0;
}
static void usage(FILE *o) {
fprintf(o,
"mkexe — assemble a Sprinter ESTEX .EXE from an IHX or BIN image\n"
"\n"
"Usage:\n"
" mkexe [options] -o OUT.exe INPUT(.ihx|.bin)\n"
"\n"
"Options:\n"
" -o FILE output .exe path (required)\n"
" -L ADDR load address (default: lowest in .ihx, or 0x%04X for .bin)\n"
" -E ADDR entry point / start PC (default: same as load)\n"
" -S ADDR initial SP (default: 0x%04X)\n"
" -p PAD padding byte for gaps in image (default: 0xFF)\n"
" -B ADDR bank low-16 base (default: 0x%04X HUGE; pass 0x%04X BIG)\n"
" -v verbose\n"
" -h this help\n"
"\n"
"If the input .ihx contains ELA records (type 04) with high16 = N where\n"
"1 <= N <= %d, those bytes are packed as bank N (16 KB each, base 0x%04X).\n"
"The header.loader is then set to the HOME image size so ESTEX keeps the\n"
"file open for crt0_banked.s to read each bank with RST 10h C=13h.\n"
"\n"
"Addresses accept 0x.. / $.. / decimal.\n",
DEFAULT_LOAD, DEFAULT_STACK,
DEFAULT_BANK_BASE, BIG_BANK_BASE,
MAX_BANKS, DEFAULT_BANK_BASE);
}
static int parse_addr(const char *s, uint32_t *out) {
if (!s || !*s) return -1;
char *end = NULL;
int base = 10;
if (s[0] == '$') { s++; base = 16; }
else if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) { s += 2; base = 16; }
unsigned long v = strtoul(s, &end, base);
if (end == s || (end && *end)) return -1;
*out = (uint32_t)v;
return 0;
}
static void put16_le(uint8_t *p, uint16_t v) {
p[0] = (uint8_t)(v & 0xFF);
p[1] = (uint8_t)(v >> 8);
}
int main(int argc, char **argv) {
const char *out_path = NULL;
const char *in_path = NULL;
uint32_t load_addr = 0xFFFFFFFFu;
uint32_t start_addr = 0xFFFFFFFFu;
uint32_t stack_addr = DEFAULT_STACK;
uint32_t pad_byte = 0xFF;
uint32_t bank_base = DEFAULT_BANK_BASE;
int verbose = 0;
for (int i = 1; i < argc; i++) {
const char *a = argv[i];
if (!strcmp(a, "-h") || !strcmp(a, "--help")) { usage(stdout); return 0; }
if (!strcmp(a, "-v")) { verbose = 1; continue; }
if (!strcmp(a, "-o") && i + 1 < argc) { out_path = argv[++i]; continue; }
if (!strcmp(a, "-L") && i + 1 < argc) {
if (parse_addr(argv[++i], &load_addr) < 0) { fprintf(stderr, "mkexe: bad -L\n"); return 1; }
continue;
}
if (!strcmp(a, "-E") && i + 1 < argc) {
if (parse_addr(argv[++i], &start_addr) < 0) { fprintf(stderr, "mkexe: bad -E\n"); return 1; }
continue;
}
if (!strcmp(a, "-S") && i + 1 < argc) {
if (parse_addr(argv[++i], &stack_addr) < 0) { fprintf(stderr, "mkexe: bad -S\n"); return 1; }
continue;
}
if (!strcmp(a, "-p") && i + 1 < argc) {
if (parse_addr(argv[++i], &pad_byte) < 0) { fprintf(stderr, "mkexe: bad -p\n"); return 1; }
continue;
}
if (!strcmp(a, "-B") && i + 1 < argc) {
if (parse_addr(argv[++i], &bank_base) < 0) { fprintf(stderr, "mkexe: bad -B\n"); return 1; }
if (bank_base + BANK_SIZE > SEGMENT_SIZE) {
fprintf(stderr, "mkexe: -B 0x%X + 16KB exceeds 0x10000\n", bank_base);
return 1;
}
continue;
}
if (a[0] == '-') { fprintf(stderr, "mkexe: unknown option '%s'\n", a); usage(stderr); return 1; }
if (in_path) { fprintf(stderr, "mkexe: extra positional '%s'\n", a); return 1; }
in_path = a;
}
if (!in_path || !out_path) { usage(stderr); return 1; }
segments_init();
if (has_suffix(in_path, ".ihx") || has_suffix(in_path, ".hex")) {
if (load_ihx(in_path) < 0) return 2;
} else if (has_suffix(in_path, ".bin")) {
if (load_addr == 0xFFFFFFFFu) load_addr = DEFAULT_LOAD;
if (load_bin(in_path, load_addr) < 0) return 2;
} else {
fprintf(stderr, "mkexe: input must end with .ihx, .hex, or .bin\n");
return 1;
}
segment_t *home = &segments[HOME_SEG];
if (!home->any) {
fprintf(stderr, "mkexe: input has no HOME (segment 0) bytes\n");
return 2;
}
if (load_addr == 0xFFFFFFFFu) load_addr = home->lo;
if (start_addr == 0xFFFFFFFFu) start_addr = load_addr;
if (load_addr > 0xFFFFu) { fprintf(stderr, "mkexe: load address 0x%X > 0xFFFF\n", load_addr); return 1; }
if (start_addr > 0xFFFFu) { fprintf(stderr, "mkexe: start address 0x%X > 0xFFFF\n", start_addr); return 1; }
if (stack_addr > 0xFFFFu) { fprintf(stderr, "mkexe: stack address 0x%X > 0xFFFF\n", stack_addr); return 1; }
if (pad_byte > 0xFFu) { fprintf(stderr, "mkexe: pad byte 0x%X > 0xFF\n", pad_byte); return 1; }
if (home->lo < load_addr) {
fprintf(stderr, "mkexe: HOME has bytes at 0x%04X below requested load 0x%04X\n",
home->lo, load_addr);
return 2;
}
uint32_t home_size = home->hi + 1 - load_addr;
if (load_addr + home_size > 0x10000u) {
fprintf(stderr, "mkexe: HOME extends past 0xFFFF (load=0x%04X, size=0x%X)\n",
load_addr, home_size);
return 2;
}
/* Inventory banks. */
int max_bank = 0;
for (int i = 1; i <= MAX_BANKS; i++) {
if (segments[i].any) {
if (segments[i].lo < bank_base) {
/* Bytes below the bank base inside a BANK segment almost
certainly mean the *previous* bank overflowed 16 KB and
bled into this 64 KB virtual slot. Tell the user clearly. */
int prev = i - 1;
if (prev >= 1) {
fprintf(stderr,
"mkexe: BANK%d appears to overflow its 16 KB limit:\n"
" data lands at virtual 0x%X%04X (low16 < 0x%04X)\n"
" Move some code from BANK%d's .c files into a new bank.\n",
prev, i, segments[i].lo, bank_base, prev);
} else {
fprintf(stderr,
"mkexe: bank %d has bytes at 0x%04X, below the bank base 0x%04X\n",
i, segments[i].lo, bank_base);
}
return 2;
}
if (segments[i].hi >= bank_base + BANK_SIZE) {
fprintf(stderr,
"mkexe: BANK%d overflows its 16 KB limit:\n"
" last byte at 0x%04X (limit 0x%04X)\n",
i, segments[i].hi, bank_base + BANK_SIZE - 1);
return 2;
}
if (i > max_bank) max_bank = i;
}
}
/* HOME must not extend past 0xBFFF (W3 = bank territory in HUGE,
free in BIG but still off-limits to HOME). */
if (home->hi >= 0xC000u) {
fprintf(stderr,
"mkexe: HOME image extends to 0x%04X, past window 2 end (0xBFFF).\n"
" Code grew too big for HOME — move some .c files into a bank.\n",
home->hi);
return 2;
}
/* In BIG mode HOME starts at 0x8100 (W2). Reject if it accidentally
overlaps the bank window in W1 (0x4000..0x7FFF) — that means the
user passed a bad --code-loc and the image would clobber banks. */
if (bank_base == BIG_BANK_BASE && home->lo < 0x8000u) {
fprintf(stderr,
"mkexe: HOME image starts at 0x%04X in BIG mode, but banks own\n"
" 0x4000..0x7FFF — HOME must live in W2 (0x8000..0xBFFF).\n"
" Check --code-loc / -L.\n",
home->lo);
return 2;
}
/* Banks must be densely numbered from 1 so the loader index matches. */
for (int i = 1; i <= max_bank; i++) {
if (!segments[i].any) {
fprintf(stderr, "mkexe: bank %d is empty but bank %d exists — banks must be consecutive from 1\n",
i, max_bank);
return 2;
}
}
uint32_t loader_size = (max_bank > 0) ? home_size : 0u;
/* --- Write the EXE. --- */
uint8_t header[EXE_HEADER_SIZE];
memset(header, 0, sizeof(header));
header[0x00] = 'E';
header[0x01] = 'X';
header[0x02] = 'E';
header[0x03] = 0x00;
header[0x04] = 0x00;
header[0x05] = 0x02;
header[0x06] = 0x00;
header[0x07] = 0x00;
put16_le(header + 0x08, (uint16_t)loader_size);
put16_le(header + 0x10, (uint16_t)load_addr);
put16_le(header + 0x12, (uint16_t)start_addr);
put16_le(header + 0x14, (uint16_t)stack_addr);
FILE *out = fopen(out_path, "wb");
if (!out) {
fprintf(stderr, "mkexe: cannot open '%s' for writing: %s\n", out_path, strerror(errno));
return 2;
}
if (fwrite(header, 1, EXE_HEADER_SIZE, out) != EXE_HEADER_SIZE) {
fprintf(stderr, "mkexe: short write on header\n"); fclose(out); return 2;
}
/* HOME image */
for (uint32_t a = load_addr; a < load_addr + home_size; a++) {
uint8_t b = home->present[a] ? home->bytes[a] : (uint8_t)pad_byte;
if (fwrite(&b, 1, 1, out) != 1) {
fprintf(stderr, "mkexe: short write on HOME at 0x%04X\n", a);
fclose(out); return 2;
}
}
/* Banks — full 16 KB each, padding empty cells. */
for (int i = 1; i <= max_bank; i++) {
segment_t *bnk = &segments[i];
for (uint32_t a = bank_base; a < bank_base + BANK_SIZE; a++) {
uint8_t b = bnk->present[a] ? bnk->bytes[a] : (uint8_t)pad_byte;
if (fwrite(&b, 1, 1, out) != 1) {
fprintf(stderr, "mkexe: short write on bank %d at 0x%04X\n", i, a);
fclose(out); return 2;
}
}
}
fclose(out);
if (verbose) {
fprintf(stderr,
"mkexe: wrote %s\n"
" load=0x%04X start=0x%04X stack=0x%04X loader=0x%04X\n"
" HOME 0x%04X..0x%04X (%u bytes)\n",
out_path, load_addr, start_addr, stack_addr, loader_size,
home->lo, home->hi, home_size);
for (int i = 1; i <= max_bank; i++) {
fprintf(stderr, " BANK%-2d 0x%04X..0x%04X (%u live bytes, padded to 16 KB)\n",
i, segments[i].lo, segments[i].hi,
segments[i].hi + 1 - segments[i].lo);
}
uint32_t total = EXE_HEADER_SIZE + home_size + (uint32_t)max_bank * BANK_SIZE;
fprintf(stderr, " total .exe = %u bytes\n", total);
}
return 0;
}
+125
View File
@@ -0,0 +1,125 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
MKEXE=./mkexe
TESTS=tests
fail=0
total=0
assert_hex_eq() {
local file=$1 offset=$2 expected=$3 label=$4
local actual
actual=$(xxd -s "$offset" -l $((${#expected} / 2)) -p "$file")
if [ "$actual" != "$expected" ]; then
echo "FAIL: $label"
echo " file: $file"
echo " offset: $offset"
echo " expected: $expected"
echo " actual: $actual"
fail=$((fail + 1))
fi
}
run_case() {
local name=$1; shift
total=$((total + 1))
if "$@" >/dev/null 2>&1; then
:
else
echo "FAIL: $name (exit nonzero)"
fail=$((fail + 1))
return
fi
}
expect_failure() {
local name=$1; shift
total=$((total + 1))
if "$@" >/dev/null 2>&1; then
echo "FAIL: $name (expected nonzero exit, got success)"
fail=$((fail + 1))
fi
}
# Case 1: minimal .ihx with one byte RET at 0x4100
run_case "build-single-byte" \
$MKEXE -L 0x4100 -E 0x4100 -S 0xBFFE -o $TESTS/case1.exe $TESTS/hello.ihx
assert_hex_eq $TESTS/case1.exe 0 "455845" "case1: signature EXE"
assert_hex_eq $TESTS/case1.exe 3 "00" "case1: version 0"
assert_hex_eq $TESTS/case1.exe 4 "00020000" "case1: offset 0x00000200"
assert_hex_eq $TESTS/case1.exe 8 "0000" "case1: loader=0"
assert_hex_eq $TESTS/case1.exe 0x10 "0041" "case1: load=0x4100"
assert_hex_eq $TESTS/case1.exe 0x12 "0041" "case1: start=0x4100"
assert_hex_eq $TESTS/case1.exe 0x14 "febf" "case1: stack=0xBFFE"
assert_hex_eq $TESTS/case1.exe 0x200 "c9" "case1: image byte = RET (C9)"
# File size = 512 + 1
size=$(stat -f "%z" $TESTS/case1.exe 2>/dev/null || stat -c "%s" $TESTS/case1.exe)
if [ "$size" != "513" ]; then
echo "FAIL: case1: file size $size != 513"
fail=$((fail + 1))
fi
total=$((total + 1))
# Case 2: defaults — load and start auto-derived from .ihx, stack default = 0xBFFE
run_case "defaults-from-ihx" \
$MKEXE -o $TESTS/case2.exe $TESTS/hello.ihx
assert_hex_eq $TESTS/case2.exe 0x10 "0041" "case2: load defaults to 0x4100 (from ihx)"
assert_hex_eq $TESTS/case2.exe 0x12 "0041" "case2: start defaults to load"
assert_hex_eq $TESTS/case2.exe 0x14 "febf" "case2: stack defaults to 0xBFFE"
# Case 3: .bin input
printf '\xC9' > $TESTS/one.bin
run_case "build-from-bin" \
$MKEXE -L 0x4100 -o $TESTS/case3.exe $TESTS/one.bin
assert_hex_eq $TESTS/case3.exe 0x10 "0041" "case3: bin load=0x4100"
assert_hex_eq $TESTS/case3.exe 0x200 "c9" "case3: bin image"
# Case 4: image extending past 0xFFFF must fail
printf '\xFF%.0s' {1..4096} > $TESTS/big.bin
expect_failure "reject-past-FFFF" \
$MKEXE -L 0xF800 -o $TESTS/case4.exe $TESTS/big.bin
# Case 5: load above 0xFFFF must fail
expect_failure "reject-load-out-of-range" \
$MKEXE -L 0x10000 -o $TESTS/case5.exe $TESTS/one.bin
# Case 6: bad checksum in .ihx must fail
cat > $TESTS/bad.ihx <<'EOF'
:01410000C900
:00000001FF
EOF
expect_failure "reject-bad-checksum" \
$MKEXE -o $TESTS/case6.exe $TESTS/bad.ihx
# Case 7: multi-bank IHX → loader auto-set to HOME size, bank 1 appended (16 KB)
# ELA "0001" puts the next records into bank 1 (virtual 0x1C000+).
# Note: Intel HEX address fields are big-endian inside the record
# :01 C000 00 77 C8 means addr=0xC000 (record bytes "C000" = high then low)
cat > $TESTS/banked.ihx <<'EOF'
:01410000C9F5
:020000040001F9
:01C0000077C8
:00000001FF
EOF
run_case "banked-auto-loader" \
$MKEXE -L 0x4100 -o $TESTS/case7.exe $TESTS/banked.ihx
assert_hex_eq $TESTS/case7.exe 8 "0100" "case7: loader=HOME size (1 byte)"
size=$(stat -f "%z" $TESTS/case7.exe 2>/dev/null || stat -c "%s" $TESTS/case7.exe)
if [ "$size" != "16897" ]; then
echo "FAIL: case7: file size $size != 16897 (512 hdr + 1 HOME + 16384 bank1)"
fail=$((fail + 1))
fi
# Byte at file offset 0x201 (first byte of bank1) should be the 0x77 we put at bank1's 0xC000
assert_hex_eq $TESTS/case7.exe 0x201 "77" "case7: bank1 first byte"
total=$((total + 9))
if [ "$fail" -gt 0 ]; then
echo
echo "FAILED: $fail of $total assertions/cases"
exit 1
fi
echo "OK: $total cases passed"
+2
View File
@@ -0,0 +1,2 @@
:01410000C900
:00000001FF
+4
View File
@@ -0,0 +1,4 @@
:01410000C9F5
:020000040001F9
:01C0000077C8
:00000001FF
+1
View File
@@ -0,0 +1 @@
+2
View File
@@ -0,0 +1,2 @@
:01410000C9F5
:00000001FF
+1
View File
@@ -0,0 +1 @@