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:
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
* file.c — minimal unbuffered FILE * implementation on top of the
|
||||
* POSIX-style fd I/O (open/read/write/lseek/close).
|
||||
*
|
||||
* No buffering: each fputc/fgetc maps to one read/write syscall. For
|
||||
* heavy-throughput code, prefer fread/fwrite with a sizable buffer or
|
||||
* the raw fd I/O directly.
|
||||
*
|
||||
* stdin/stdout/stderr are STATIC sentinel FILEs with fd=-1 and the
|
||||
* _F_CONIN/_F_CONOUT flags set; fputc/fgetc detect them and call
|
||||
* putchar()/getchar() (which already do CR/LF mapping and ESTEX calls).
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
|
||||
/* ---- console pseudo-streams ----------------------------------------*/
|
||||
static FILE _stdin = { -1, _F_READ | _F_CONIN };
|
||||
static FILE _stdout = { -2, _F_WRITE | _F_CONOUT };
|
||||
static FILE _stderr = { -3, _F_WRITE | _F_CONOUT };
|
||||
static FILE _stdaux = { -4, _F_WRITE | _F_CONOUT };
|
||||
static FILE _stdprn = { -5, _F_WRITE | _F_CONOUT };
|
||||
|
||||
FILE *const stdin = &_stdin;
|
||||
FILE *const stdout = &_stdout;
|
||||
FILE *const stderr = &_stderr;
|
||||
FILE *const stdaux = &_stdaux;
|
||||
FILE *const stdprn = &_stdprn;
|
||||
|
||||
/* ---- fopen / fclose -------------------------------------------------*/
|
||||
|
||||
/* Translate a fopen() mode string to the open() flags subset our
|
||||
* libc/io/open.c understands. Supported: r, w, a, with optional "+"
|
||||
* and trailing "b" (binary — we ignore as all I/O is binary).
|
||||
*/
|
||||
static int mode_to_flags(const char *mode, uint8_t *file_flags)
|
||||
{
|
||||
if (!mode || !*mode) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
int oflags = 0;
|
||||
uint8_t ff = 0;
|
||||
char base = *mode;
|
||||
int plus = 0;
|
||||
for (const char *p = mode + 1; *p; p++) {
|
||||
if (*p == '+') plus = 1;
|
||||
/* 'b' and 't' are ignored — all I/O is binary on Sprinter. */
|
||||
}
|
||||
switch (base) {
|
||||
case 'r':
|
||||
oflags = plus ? O_RDWR : O_RDONLY;
|
||||
ff = _F_READ | (plus ? _F_WRITE : 0);
|
||||
break;
|
||||
case 'w':
|
||||
oflags = (plus ? O_RDWR : O_WRONLY) | O_CREAT | O_TRUNC;
|
||||
ff = _F_WRITE | (plus ? _F_READ : 0);
|
||||
break;
|
||||
case 'a':
|
||||
oflags = (plus ? O_RDWR : O_WRONLY) | O_CREAT | O_APPEND;
|
||||
ff = _F_WRITE | _F_APPEND | (plus ? _F_READ : 0);
|
||||
break;
|
||||
default:
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
*file_flags = ff;
|
||||
return oflags;
|
||||
}
|
||||
|
||||
FILE *fopen(const char *path, const char *mode)
|
||||
{
|
||||
uint8_t ff;
|
||||
int oflags = mode_to_flags(mode, &ff);
|
||||
if (oflags < 0) return NULL;
|
||||
|
||||
int fd = open(path, oflags);
|
||||
if (fd < 0) return NULL;
|
||||
|
||||
FILE *fp = (FILE *)malloc(sizeof(FILE));
|
||||
if (!fp) {
|
||||
int saved = errno;
|
||||
close(fd);
|
||||
errno = saved ? saved : ENOMEM;
|
||||
return NULL;
|
||||
}
|
||||
fp->fd = fd;
|
||||
fp->flags = ff;
|
||||
return fp;
|
||||
}
|
||||
|
||||
int fclose(FILE *fp)
|
||||
{
|
||||
if (!fp) {
|
||||
errno = EBADF;
|
||||
return EOF;
|
||||
}
|
||||
/* Don't close stdin/stdout/stderr. */
|
||||
if (fp == &_stdin || fp == &_stdout || fp == &_stderr) {
|
||||
return 0;
|
||||
}
|
||||
int r = close(fp->fd);
|
||||
free(fp);
|
||||
return r < 0 ? EOF : 0;
|
||||
}
|
||||
|
||||
int fflush(FILE *fp)
|
||||
{
|
||||
/* Unbuffered — nothing to flush. */
|
||||
(void)fp;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- char-at-a-time -------------------------------------------------*/
|
||||
|
||||
int fputc(int c, FILE *fp)
|
||||
{
|
||||
if (!fp) { errno = EBADF; return EOF; }
|
||||
if (fp->flags & _F_CONOUT) {
|
||||
return putchar(c);
|
||||
}
|
||||
if (!(fp->flags & _F_WRITE)) { errno = EBADF; return EOF; }
|
||||
uint8_t ch = (uint8_t)c;
|
||||
if (write(fp->fd, &ch, 1) != 1) {
|
||||
fp->flags |= _F_ERROR;
|
||||
return EOF;
|
||||
}
|
||||
return (int)ch;
|
||||
}
|
||||
|
||||
int fgetc(FILE *fp)
|
||||
{
|
||||
if (!fp) { errno = EBADF; return EOF; }
|
||||
if (fp->flags & _F_CONIN) {
|
||||
return getchar();
|
||||
}
|
||||
if (!(fp->flags & _F_READ)) { errno = EBADF; return EOF; }
|
||||
uint8_t ch;
|
||||
int r = read(fp->fd, &ch, 1);
|
||||
if (r == 0) { fp->flags |= _F_EOF; return EOF; }
|
||||
if (r < 0) { fp->flags |= _F_ERROR; return EOF; }
|
||||
return (int)ch;
|
||||
}
|
||||
|
||||
/* ---- string-at-a-time ----------------------------------------------*/
|
||||
|
||||
int fputs(const char *s, FILE *fp)
|
||||
{
|
||||
if (!fp || !s) { errno = EBADF; return EOF; }
|
||||
if (fp->flags & _F_CONOUT) {
|
||||
while (*s) {
|
||||
if (putchar((unsigned char)*s++) == EOF) return EOF;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (!(fp->flags & _F_WRITE)) { errno = EBADF; return EOF; }
|
||||
size_t n = strlen(s);
|
||||
int w = write(fp->fd, s, (uint16_t)n);
|
||||
if (w < 0 || (size_t)w != n) {
|
||||
fp->flags |= _F_ERROR;
|
||||
return EOF;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
char *fgets(char *buf, int n, FILE *fp)
|
||||
{
|
||||
if (!buf || n < 2 || !fp) return NULL;
|
||||
int i = 0;
|
||||
while (i < n - 1) {
|
||||
int c = fgetc(fp);
|
||||
if (c == EOF) {
|
||||
if (i == 0) return NULL;
|
||||
break;
|
||||
}
|
||||
buf[i++] = (char)c;
|
||||
if (c == '\n') break;
|
||||
}
|
||||
buf[i] = '\0';
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* ---- block-at-a-time -----------------------------------------------*/
|
||||
|
||||
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *fp)
|
||||
{
|
||||
if (!ptr || !fp) return 0;
|
||||
if (size == 0 || nmemb == 0) return 0;
|
||||
if (fp->flags & _F_CONIN) {
|
||||
/* line-buffered console read — not very useful but functional. */
|
||||
char *p = (char *)ptr;
|
||||
size_t total = size * nmemb;
|
||||
for (size_t i = 0; i < total; i++) {
|
||||
int c = getchar();
|
||||
if (c == EOF) return i / size;
|
||||
p[i] = (char)c;
|
||||
}
|
||||
return nmemb;
|
||||
}
|
||||
if (!(fp->flags & _F_READ)) { errno = EBADF; return 0; }
|
||||
size_t total = size * nmemb;
|
||||
int r = read(fp->fd, ptr, (uint16_t)total);
|
||||
if (r < 0) { fp->flags |= _F_ERROR; return 0; }
|
||||
if ((size_t)r < total) fp->flags |= _F_EOF;
|
||||
return (size_t)r / size;
|
||||
}
|
||||
|
||||
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *fp)
|
||||
{
|
||||
if (!ptr || !fp) return 0;
|
||||
if (size == 0 || nmemb == 0) return 0;
|
||||
if (fp->flags & _F_CONOUT) {
|
||||
const char *p = (const char *)ptr;
|
||||
size_t total = size * nmemb;
|
||||
for (size_t i = 0; i < total; i++) {
|
||||
if (putchar((unsigned char)p[i]) == EOF) return i / size;
|
||||
}
|
||||
return nmemb;
|
||||
}
|
||||
if (!(fp->flags & _F_WRITE)) { errno = EBADF; return 0; }
|
||||
size_t total = size * nmemb;
|
||||
int w = write(fp->fd, ptr, (uint16_t)total);
|
||||
if (w < 0) { fp->flags |= _F_ERROR; return 0; }
|
||||
return (size_t)w / size;
|
||||
}
|
||||
|
||||
/* ---- positioning ---------------------------------------------------*/
|
||||
|
||||
int fseek(FILE *fp, long off, int whence)
|
||||
{
|
||||
if (!fp || (fp->flags & (_F_CONIN | _F_CONOUT))) {
|
||||
errno = EBADF;
|
||||
return -1;
|
||||
}
|
||||
long r = lseek(fp->fd, off, whence);
|
||||
if (r < 0) return -1;
|
||||
fp->flags &= (uint8_t)~_F_EOF;
|
||||
return 0;
|
||||
}
|
||||
|
||||
long ftell(FILE *fp)
|
||||
{
|
||||
if (!fp || (fp->flags & (_F_CONIN | _F_CONOUT))) {
|
||||
errno = EBADF;
|
||||
return -1L;
|
||||
}
|
||||
return lseek(fp->fd, 0L, SEEK_CUR);
|
||||
}
|
||||
|
||||
void rewind(FILE *fp)
|
||||
{
|
||||
if (!fp) return;
|
||||
fseek(fp, 0L, SEEK_SET);
|
||||
fp->flags &= (uint8_t)~(_F_EOF | _F_ERROR);
|
||||
}
|
||||
|
||||
/* ---- status --------------------------------------------------------*/
|
||||
|
||||
int feof (FILE *fp) { return fp && (fp->flags & _F_EOF) ? 1 : 0; }
|
||||
int ferror(FILE *fp) { return fp && (fp->flags & _F_ERROR) ? 1 : 0; }
|
||||
void clearerr(FILE *fp) {
|
||||
if (fp) fp->flags &= (uint8_t)~(_F_EOF | _F_ERROR);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* getchar via ESTEX RST 10h.
|
||||
*
|
||||
* ESTEX 0x30 (WAITKEY): blocks until a key, returns
|
||||
* A = scan code, D = position code, E = ASCII,
|
||||
* C = mode flags, B = shift flags.
|
||||
*
|
||||
* IX is preserved (RST 10h clobbers it; callers rely on it as frame pointer).
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
int getchar(void) __naked
|
||||
{
|
||||
__asm
|
||||
push ix
|
||||
ld c, #0x30 ; ESTEX WAITKEY
|
||||
rst #0x10
|
||||
pop ix
|
||||
ld a, e ; E = ASCII (already the low byte of our return DE)
|
||||
or a, a
|
||||
jr Z, no_ascii
|
||||
ld d, #0
|
||||
ret
|
||||
no_ascii:
|
||||
ld de, #-1
|
||||
ret
|
||||
__endasm;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* print_hex — print a single byte as two uppercase hex digits.
|
||||
*
|
||||
* No printf yet; this is what bare-metal debug looks like in stage 3.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <sprinter.h>
|
||||
|
||||
void print_hex(uint8_t v)
|
||||
{
|
||||
static const char digits[] = "0123456789ABCDEF";
|
||||
putchar(digits[(v >> 4) & 0x0F]);
|
||||
putchar(digits[v & 0x0F]);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* putchar — emit one character via ESTEX PUTCHAR ($5B).
|
||||
*
|
||||
* Turbo-C convention: this stdio.h function is the FAST path with NO
|
||||
* attribute control. Whatever ESTEX has cached for the cursor cell is
|
||||
* used (typically the shell's default colour). Translates '\n' to
|
||||
* CR LF for C-string semantics.
|
||||
*
|
||||
* For coloured output, use putch() / cputs() / cprintf() from <conio.h>
|
||||
* — those honour textattr / g_text_attr at the cost of being ~10× slower.
|
||||
*
|
||||
* SDCC __sdcccall(1): char arg in L (low byte of HL=int). Returns the
|
||||
* char in DE (SDCC int return).
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
int putchar(int c) __naked
|
||||
{
|
||||
(void)c;
|
||||
__asm
|
||||
ld a, l ; SDCC __sdcccall(1) int → HL
|
||||
push ix
|
||||
cp #0x0A
|
||||
jr nz, _pc_emit
|
||||
ld a, #0x0D ; CR before LF
|
||||
push af
|
||||
ld c, #0x5B
|
||||
rst #0x10
|
||||
pop af
|
||||
ld a, #0x0A
|
||||
_pc_emit:
|
||||
push af
|
||||
ld c, #0x5B
|
||||
rst #0x10
|
||||
pop af
|
||||
pop ix
|
||||
ld e, a
|
||||
ld d, #0
|
||||
ret
|
||||
__endasm;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* puts — C99 fputs(s, stdout) + '\n'.
|
||||
*
|
||||
* Turbo-C convention: stdio's `puts` is the FAST path with NO attribute
|
||||
* control — backed by ESTEX PCHARS ($5C). Cursor cell attributes are
|
||||
* whatever ESTEX has cached (usually the shell's default).
|
||||
*
|
||||
* For coloured output use cputs() / cprintf() from <conio.h>.
|
||||
*
|
||||
* Implementation notes:
|
||||
* - PCHARS does NOT translate '\n' to CR LF, so we copy the string
|
||||
* into a static buffer expanding each '\n' to CR LF, then append
|
||||
* the trailing CR LF before the NUL.
|
||||
* - Avoid trailing PUTCHAR after PCHARS — empirically that sometimes
|
||||
* drops the next char. Embed the line ending inside the PCHARS
|
||||
* buffer instead.
|
||||
* - Strings longer than the buffer fall back to per-char putchar so
|
||||
* we never silently truncate.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define PUTS_BUF_SIZE 256 /* body bytes before CR expansion */
|
||||
|
||||
static char puts_buf[PUTS_BUF_SIZE + 3]; /* +3 for trailing CR LF NUL */
|
||||
|
||||
static void pchars(const char *s) __naked
|
||||
{
|
||||
(void)s;
|
||||
__asm
|
||||
push ix
|
||||
ld c, #0x5C
|
||||
rst #0x10
|
||||
pop ix
|
||||
ret
|
||||
__endasm;
|
||||
}
|
||||
|
||||
int puts(const char *s)
|
||||
{
|
||||
uint16_t n = 0;
|
||||
uint16_t i = 0;
|
||||
|
||||
while (s[i] && n < PUTS_BUF_SIZE - 1) {
|
||||
char c = s[i++];
|
||||
if (c == '\n') {
|
||||
puts_buf[n++] = '\r';
|
||||
puts_buf[n++] = '\n';
|
||||
} else {
|
||||
puts_buf[n++] = c;
|
||||
}
|
||||
}
|
||||
|
||||
if (s[i]) {
|
||||
/* Overflow — char-by-char fallback so we never truncate. */
|
||||
for (uint16_t k = 0; s[k]; k++)
|
||||
putchar((unsigned char)s[k]);
|
||||
putchar('\n');
|
||||
return 0;
|
||||
}
|
||||
|
||||
puts_buf[n++] = '\r';
|
||||
puts_buf[n++] = '\n';
|
||||
puts_buf[n] = 0;
|
||||
|
||||
pchars(puts_buf);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* solid_helpers.c — small Solid-C compatibility helpers.
|
||||
*
|
||||
* Each function maps to the standard printf/sprintf machinery already
|
||||
* available from SDCC's z80.lib + our overrides. No new syscalls.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* ---- gets — dangerous but Solid-C provides it ---------------------- */
|
||||
char *gets(char *buf)
|
||||
{
|
||||
int i = 0;
|
||||
int c;
|
||||
for (;;) {
|
||||
c = getchar();
|
||||
if (c == EOF) {
|
||||
if (i == 0) return 0;
|
||||
break;
|
||||
}
|
||||
if (c == '\n' || c == '\r') break;
|
||||
buf[i++] = (char)c;
|
||||
}
|
||||
buf[i] = 0;
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* ---- decimal output: use printf %u ---------------------------------- */
|
||||
|
||||
void dec8(uint8_t v)
|
||||
{
|
||||
printf("%u", (unsigned)v);
|
||||
}
|
||||
|
||||
void dec16(uint16_t v)
|
||||
{
|
||||
printf("%u", (unsigned)v);
|
||||
}
|
||||
|
||||
void dec32(uint32_t v)
|
||||
{
|
||||
printf("%lu", (unsigned long)v);
|
||||
}
|
||||
|
||||
/* ---- hex output: zero-padded ---------------------------------------- */
|
||||
|
||||
void hex8(uint8_t v)
|
||||
{
|
||||
printf("%02X", (unsigned)v);
|
||||
}
|
||||
|
||||
void hex16(uint16_t v)
|
||||
{
|
||||
printf("%04X", (unsigned)v);
|
||||
}
|
||||
|
||||
void hex32(uint32_t v)
|
||||
{
|
||||
printf("%08lX", (unsigned long)v);
|
||||
}
|
||||
Reference in New Issue
Block a user