527d4a6a18
libc/mem/:
• Split bank_io.c into bank_io_w3.c (existing W3 helpers, base 0xC000,
port 0xE2) and bank_io_w1.c (new mirror through W1, base 0x4000,
port 0xA2). Two .rel files so DCE picks only the needed group:
a W3-only user pulls ~70 bytes instead of all 134. W1 variants are
`--memory tiny`-only (any other mode runs code from W1 or uses W1
for the banked-code segment, swapping it crashes).
• mem_alloc.c: add CF=err checks for mem_free_block and mem_get_page
(were silently ignored), per-function docstrings on alloc/free/
get_page/info, drop the confused "wait wrong order" comment in
mem_info. Header sprinter_mem.h gets matching per-function doc.
libc/stdio/:
• Add hex_print.c (hex8/hex16/hex32, ~26 bytes) and dec_print.c
(dec8/dec16/dec32, ~170 bytes) ported from solid-c STDLIB.ASM.
Replaces the printf("%u"/"%X") wrappers in solid_helpers.c that
dragged in the 3-5 KB printf machinery.
- hex* use the classic cp 10 / sbc 0x69 / daa nibble→ASCII trick;
hex8 self-calls for the high nibble, hex16/hex32 tail-call hex8.
- dec32 is the master routine; dec8/dec16 jump into shared entry
points (__dec_entry3 / __dec_entry5). 32-bit subtract-power-of-10
keeps the high 16 bits in HL alt (shadow set).
- DISCOVERY: ESTEX PUTCHAR ($5B) on our Sprinter build preserves
the main register set + IX but CLOBBERS the shadow set
(BC'/DE'/HL'). solid-c's original code assumed otherwise and
garbled output for values ≥ 6 digits. Fix: save/restore HL alt
around the RST 10 in _dec_emit_or_skip. Documented in
memory/estex_putchar_abi.md.
• file.c: drop stdaux/stdprn (no Sprinter printer API), change
stdin/stdout/stderr fd markers to 0/-1/-2 (positive fds clash with
ESTEX OPEN return values), add TODO header pointing at v2 buffered
FILE rewrite (see docs/TODO.md for the Solid-C reference struct).
bin/sprinter-cc:
• --memory big and --memory huge now always use crt0_banked.s (was:
only with --bank flags), matching docs/memory_modes_implemented.md.
When the user has no --bank flags, generate a tiny stub with
`const uint8_t n_banks = 0;` and assemble bank.s for _bank_pages.
Without this fix, openenv with --memory big could not see the
estex_file_handle symbol exported by crt0_banked.
examples/openenv:
• Add usage of estex_file_handle to confirm the crt0_banked startup-
info is reachable. Local extern decl — keeps the symbol out of
sprinter.h since it only exists in big/huge builds.
examples/dec_test:
• New regression test covering hex8/16/32 and dec8/16/32 across the
interesting boundary values.
.gitignore: add .kilo/ (editor session cache).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
290 lines
8.3 KiB
C
290 lines
8.3 KiB
C
/*
|
|
* file.c — *** PROVISIONAL *** minimal unbuffered FILE * implementation
|
|
* on top of POSIX-style fd I/O
|
|
* (open/read/write/lseek/close).
|
|
*
|
|
* ============================================================
|
|
* TODO (v2): replace with a proper BUFFERED implementation.
|
|
* See docs/TODO.md / Solid-C reference layout:
|
|
*
|
|
* typedef struct {
|
|
* uint flags; // +0..1 file status flags
|
|
* int level; // +2..3 empty/fill level of buffer
|
|
* char *curp; // +4..5 current active pointer
|
|
* int fd; // +6..7 underlying low-level fd
|
|
* char *buffer; // +8..9 data transfer buffer
|
|
* char hold; // +10 ungetc byte if no buffer
|
|
* short token; // +11..12 reserved
|
|
* char dummy; // +13 reserved
|
|
* } FILE;
|
|
*
|
|
* The current implementation maps each fputc/fgetc to one read/write
|
|
* syscall — fine for correctness checks, awful for throughput. Issues
|
|
* 3/4/5 from the stdio-review (fwrite short-write flag, fgets n=1,
|
|
* mode_to_flags break) are deferred until that rewrite.
|
|
* ============================================================
|
|
*
|
|
* stdin/stdout/stderr are STATIC sentinel FILEs flagged with
|
|
* _F_CONIN/_F_CONOUT; fputc/fgetc detect them and call putchar() /
|
|
* getchar() which already handle CR/LF translation and ESTEX calls.
|
|
*
|
|
* Their fd fields are 0 / -1 / -2. Negative values were chosen because
|
|
* ESTEX OPEN can return small positive fds (1, 2, …) for ordinary
|
|
* files — if we marked stdout/stderr with fd=1/2 a real file could
|
|
* collide with their identifier. fd=0 for stdin is kept (POSIX-style)
|
|
* because ESTEX does not return 0. Even so, none of these fd fields
|
|
* is ever passed to a syscall — the _F_CONIN/_F_CONOUT flags drive
|
|
* the dispatch.
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <fcntl.h>
|
|
#include <unistd.h>
|
|
#include <errno.h>
|
|
|
|
/* ---- console pseudo-streams ----------------------------------------*/
|
|
static FILE _stdin = { 0, _F_READ | _F_CONIN };
|
|
static FILE _stdout = { -1, _F_WRITE | _F_CONOUT };
|
|
static FILE _stderr = { -2, _F_WRITE | _F_CONOUT };
|
|
|
|
FILE *const stdin = &_stdin;
|
|
FILE *const stdout = &_stdout;
|
|
FILE *const stderr = &_stderr;
|
|
|
|
/* ---- 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);
|
|
}
|