{
"taskId": "9",
"status": "completed"
}Updated task #9 status
$ mkdir -p /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc /home/martin/workspace/prototypes/boot-to-claude/toolchain/include /home/martin/workspace/prototypes/boot-to-claude/tests/toolchain
(Bash completed with no output)
#pragma once /* Hand-rolled freestanding stdint.h for CC - just the typedefs this * codebase actually uses. Real gcc bundles one of these for free; CC * needs its own since it doesn't ship a libc. */ typedef signed char int8_t; typedef unsigned char uint8_t; typedef signed short int16_t; typedef unsigned short uint16_t; typedef signed int int32_t; typedef unsigned int uint32_t; typedef signed long long int64_t; typedef unsigned long long uint64_t;
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/toolchain/include/stdint.h (file state is current in your context — no need to Read it back)
$ grep -rn "\blong\b\|int64_t\|uint64_t" /home/martin/workspace/prototypes/boot-to-claude/kernel/ /home/martin/workspace/prototypes/boot-to-claude/tools/ 2>/dev/null
/home/martin/workspace/prototypes/boot-to-claude/kernel/shell/shell.c:118: print_line("write: content too long");
/home/martin/workspace/prototypes/boot-to-claude/tools/mkfs_tinyfs.c:16: long offset = (long)(TINYFS_FS_START_LBA + block) * TINYFS_BLOCK_SIZE;
/home/martin/workspace/prototypes/boot-to-claude/tools/mkfs_tinyfs.c:25: long offset = (long)(TINYFS_FS_START_LBA + block) * TINYFS_BLOCK_SIZE;
/home/martin/workspace/prototypes/boot-to-claude/tools/mkfs_tinyfs.c:76: fprintf(stderr, "mkfs_tinyfs: seed spec too long: %s\n", argv[i]);#pragma once /* Hand-rolled freestanding stdint.h for CC - just the typedefs this * codebase actually uses (no 64-bit types, so CC's type-specifier * grammar never needs to support `long`). Real gcc bundles one of these * for free; CC needs its own since it doesn't ship a libc. */ typedef signed char int8_t; typedef unsigned char uint8_t; typedef signed short int16_t; typedef unsigned short uint16_t; typedef signed int int32_t; typedef unsigned int uint32_t;
The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/include/stdint.h has been updated successfully. (file state is current in your context — no need to Read it back)
#pragma once typedef unsigned int size_t; #define NULL ((void *)0)
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/toolchain/include/stddef.h (file state is current in your context — no need to Read it back)
#pragma once
/* CC: the self-hosted C compiler. See docs/toolchain.md for the "tinyC"
* dialect this supports and why. Emits AS-syntax assembly text (mirrors
* `gcc -S | as`), for two targets: --target=kernel (freestanding, no
* runtime) and --target=host (adds a _start that calls main then exits
* via a raw syscall - see builtins.c). Native host tool, not OS code. */
#include <stdint.h>
#include <stddef.h>
static inline void safe_strcpy(char *dst, const char *src, size_t dstsize) {
size_t i = 0;
for (; i + 1 < dstsize && src[i]; i++) dst[i] = src[i];
dst[i] = '\0';
}
#define MAX_NAME 64
#define MAX_TOKENS 65536
#define MAX_INCLUDE_DEPTH 16
/* ---------------- lexer ---------------- */
typedef enum {
TK_EOF, TK_IDENT, TK_NUM, TK_STR, TK_CHAR,
/* punctuation/operators */
TK_LPAREN, TK_RPAREN, TK_LBRACE, TK_RBRACE, TK_LBRACKET, TK_RBRACKET,
TK_SEMI, TK_COMMA, TK_COLON, TK_QUESTION, TK_DOT, TK_ARROW,
TK_PLUS, TK_MINUS, TK_STAR, TK_SLASH, TK_PERCENT,
TK_ASSIGN, TK_PLUSEQ, TK_MINUSEQ, TK_STAREQ, TK_SLASHEQ, TK_PERCENTEQ,
TK_ANDEQ, TK_OREQ, TK_XOREQ, TK_SHLEQ, TK_SHREQ,
TK_EQ, TK_NEQ, TK_LT, TK_LE, TK_GT, TK_GE,
TK_AND, TK_OR, TK_NOT, TK_AMP, TK_PIPE, TK_CARET, TK_TILDE,
TK_SHL, TK_SHR, TK_INC, TK_DEC,
/* preprocessor-only (consumed by the preprocessor, never reach the parser) */
TK_HASH, TK_NEWLINE,
} tok_kind_t;
typedef struct {
tok_kind_t kind;
char text[256]; /* TK_IDENT / TK_STR (decoded) */
int64_t num; /* TK_NUM / TK_CHAR */
int line;
const char *file;
int bol; /* beginning of line (possibly after whitespace only) - for # detection */
} token_t;
typedef struct {
const char *src;
size_t pos, len;
const char *filename;
int line;
int bol;
} lexer_t;
void lexer_init(lexer_t *lx, const char *src, const char *filename);
token_t lexer_next(lexer_t *lx);
/* ---------------- preprocessor ---------------- */
/* Tokenizes `path`, expanding #include/#define/#pragma once as it goes.
* Appends tokens (already macro-substituted) into `out`, growing *out_n.
* `search_dir` is the including file's own directory, for quote-include
* resolution (falls back to `-I` dirs on miss - see docs/toolchain.md). */
void pp_process_file(const char *path, token_t *out, int *out_n, int cap);
/* -I search paths, set up by main.c from argv. */
#define MAX_INCLUDE_DIRS 8
extern const char *g_include_dirs[MAX_INCLUDE_DIRS];
extern int g_n_include_dirs;
/* ---------------- types ---------------- */
typedef enum {
TY_VOID, TY_CHAR, TY_SHORT, TY_INT, TY_PTR, TY_ARRAY,
TY_STRUCT, TY_UNION, TY_ENUM, TY_FUNC,
} type_kind_t;
typedef struct type type_t;
typedef struct member member_t;
struct member {
char name[MAX_NAME];
type_t *type;
int offset;
member_t *next;
};
struct type {
type_kind_t kind;
int is_unsigned;
type_t *base; /* pointee (TY_PTR), element (TY_ARRAY), return type (TY_FUNC) */
int array_len; /* TY_ARRAY, -1 if unspecified (`T[]` param) */
member_t *members; /* TY_STRUCT/TY_UNION */
int is_packed; /* TY_STRUCT/TY_UNION __attribute__((packed)) */
char tag[MAX_NAME]; /* struct/union/enum tag, "" if anonymous */
type_t *param; /* TY_FUNC: linked list of param types via ->next */
type_t *next;
int size;
int align;
};
int type_size(type_t *t);
int type_align(type_t *t);
type_t *type_new(type_kind_t kind);
type_t *ptr_to(type_t *base);
int type_is_int(type_t *t); /* char/short/int/enum - arithmetic-capable, non-pointer */
/* ---------------- AST ---------------- */
typedef enum {
ND_NUM, ND_STR, ND_VAR, ND_ASSIGN, ND_COMPOUND_ASSIGN, ND_BINOP, ND_LOGAND, ND_LOGOR,
ND_UNARY_MINUS, ND_NOT, ND_BITNOT, ND_ADDR, ND_DEREF,
ND_PREINC, ND_PREDEC, ND_POSTINC, ND_POSTDEC,
ND_CALL, ND_INDEX, ND_MEMBER, ND_ARROW, ND_CAST, ND_SIZEOF, ND_TERNARY, ND_COMMA,
ND_IF, ND_WHILE, ND_DOWHILE, ND_FOR, ND_RETURN, ND_BREAK, ND_CONTINUE,
ND_BLOCK, ND_EXPR_STMT, ND_NOP,
ND_FUNC_DEF, ND_VAR_DECL,
} node_kind_t;
typedef struct node node_t;
struct node {
node_kind_t kind;
type_t *type; /* result type, filled in during parsing/type-checking */
tok_kind_t op; /* ND_BINOP / ND_COMPOUND_ASSIGN */
int64_t num;
char str[512]; /* ND_STR: decoded bytes; also used for the emitted label once assigned */
char name[MAX_NAME]; /* ND_VAR / ND_CALL / ND_MEMBER / ND_ARROW / func/var decl name */
node_t *a, *b, *c, *d; /* kind-specific children */
node_t *list; /* block statements / call args / decl init-list, via ->next */
node_t *next;
/* variable/function storage location, resolved during parsing */
int is_global;
int is_static_local; /* function-local `static` - lives in .data/.bss, own label */
int frame_offset; /* local/param: EBP-relative byte offset */
char asm_label[MAX_NAME]; /* global / static-local: the label CC emits for it */
node_t *init; /* ND_VAR_DECL: initializer expression or {..} list (via ->list), or NULL */
int is_extern_decl;
};
node_t *node_new(node_kind_t kind);
/* ---------------- parser ---------------- */
/* Parses the whole (already-preprocessed) token stream into a list of
* top-level ND_FUNC_DEF / ND_VAR_DECL nodes (via ->next). */
node_t *cc_parse(token_t *toks, int n_toks);
/* ---------------- codegen ---------------- */
typedef enum { TARGET_KERNEL, TARGET_HOST } cc_target_t;
/* Emits AS-syntax assembly text for the whole translation unit to `out`
* (a FILE*, opaque here to keep cc.h freestanding-includable elsewhere
* if ever needed - codegen.c does the actual #include <stdio.h>). */
void codegen_emit(node_t *prog, void *out_file, cc_target_t target);
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/cc.h (file state is current in your context — no need to Read it back)
#include "cc.h"
#include <ctype.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
void lexer_init(lexer_t *lx, const char *src, const char *filename) {
lx->src = src;
lx->pos = 0;
lx->len = strlen(src);
lx->filename = filename;
lx->line = 1;
lx->bol = 1;
}
static int peekc(lexer_t *lx) { return lx->pos < lx->len ? (unsigned char)lx->src[lx->pos] : -1; }
static int peekc2(lexer_t *lx) { return lx->pos + 1 < lx->len ? (unsigned char)lx->src[lx->pos + 1] : -1; }
static int advc(lexer_t *lx) {
int c = peekc(lx);
lx->pos++;
if (c == '\n') { lx->line++; }
return c;
}
static token_t mk(lexer_t *lx, tok_kind_t k) {
token_t t;
memset(&t, 0, sizeof(t));
t.kind = k;
t.line = lx->line;
t.file = lx->filename;
t.bol = lx->bol;
return t;
}
static int read_escape(lexer_t *lx) {
int c = advc(lx);
switch (c) {
case 'n': return '\n';
case 't': return '\t';
case 'r': return '\r';
case '0': return '\0';
case '\\': return '\\';
case '\'': return '\'';
case '"': return '"';
default: return c;
}
}
token_t lexer_next(lexer_t *lx) {
for (;;) {
int c = peekc(lx);
if (c == ' ' || c == '\t' || c == '\r') { lx->pos++; continue; }
if (c == '\n') { advc(lx); lx->bol = 1; continue; }
if (c == '/' && peekc2(lx) == '/') {
while (peekc(lx) != -1 && peekc(lx) != '\n') lx->pos++;
continue;
}
if (c == '/' && peekc2(lx) == '*') {
lx->pos += 2;
while (peekc(lx) != -1 && !(peekc(lx) == '*' && peekc2(lx) == '/')) advc(lx);
if (peekc(lx) != -1) lx->pos += 2;
continue;
}
break;
}
int was_bol = lx->bol;
lx->bol = 0;
int c = peekc(lx);
if (c == -1) { token_t t = mk(lx, TK_EOF); t.bol = was_bol; return t; }
if (c == '#' && was_bol) { lx->pos++; token_t t = mk(lx, TK_HASH); t.bol = 1; return t; }
if (isalpha(c) || c == '_') {
size_t start = lx->pos;
while (isalnum(peekc(lx)) || peekc(lx) == '_') lx->pos++;
token_t t = mk(lx, TK_IDENT);
t.bol = was_bol;
size_t n = lx->pos - start;
if (n >= sizeof(t.text)) n = sizeof(t.text) - 1;
memcpy(t.text, lx->src + start, n);
t.text[n] = '\0';
return t;
}
if (isdigit(c)) {
size_t start = lx->pos;
while (isalnum(peekc(lx))) lx->pos++;
token_t t = mk(lx, TK_NUM);
t.bol = was_bol;
int len = (int)(lx->pos - start);
const char *p = lx->src + start;
if (len > 2 && p[0] == '0' && (p[1] == 'x' || p[1] == 'X')) {
t.num = strtoll(p + 2, NULL, 16);
} else {
t.num = strtoll(p, NULL, 10);
}
return t;
}
if (c == '"') {
lx->pos++;
char buf[256];
int n = 0;
while (peekc(lx) != -1 && peekc(lx) != '"') {
int ch = advc(lx);
if (ch == '\\') ch = read_escape(lx);
if (n < (int)sizeof(buf) - 1) buf[n++] = (char)ch;
}
if (peekc(lx) == '"') lx->pos++;
buf[n] = '\0';
token_t t = mk(lx, TK_STR);
t.bol = was_bol;
memcpy(t.text, buf, (size_t)n + 1);
return t;
}
if (c == '\'') {
lx->pos++;
int ch = advc(lx);
if (ch == '\\') ch = read_escape(lx);
if (peekc(lx) == '\'') lx->pos++;
token_t t = mk(lx, TK_CHAR);
t.bol = was_bol;
t.num = ch;
return t;
}
#define TWO(c1, c2, kind) if (c == (c1) && peekc2(lx) == (c2)) { lx->pos += 2; token_t t = mk(lx, kind); t.bol = was_bol; return t; }
TWO('=', '=', TK_EQ) TWO('!', '=', TK_NEQ) TWO('<', '=', TK_LE) TWO('>', '=', TK_GE)
TWO('&', '&', TK_AND) TWO('|', '|', TK_OR) TWO('+', '+', TK_INC) TWO('-', '-', TK_DEC)
TWO('-', '>', TK_ARROW) TWO('+', '=', TK_PLUSEQ) TWO('-', '=', TK_MINUSEQ)
TWO('*', '=', TK_STAREQ) TWO('/', '=', TK_SLASHEQ) TWO('%', '=', TK_PERCENTEQ)
TWO('&', '=', TK_ANDEQ) TWO('|', '=', TK_OREQ) TWO('^', '=', TK_XOREQ)
#undef TWO
if (c == '<' && peekc2(lx) == '<') {
if (lx->pos + 2 < lx->len && lx->src[lx->pos + 2] == '=') { lx->pos += 3; token_t t = mk(lx, TK_SHLEQ); t.bol = was_bol; return t; }
lx->pos += 2; token_t t = mk(lx, TK_SHL); t.bol = was_bol; return t;
}
if (c == '>' && peekc2(lx) == '>') {
if (lx->pos + 2 < lx->len && lx->src[lx->pos + 2] == '=') { lx->pos += 3; token_t t = mk(lx, TK_SHREQ); t.bol = was_bol; return t; }
lx->pos += 2; token_t t = mk(lx, TK_SHR); t.bol = was_bol; return t;
}
lx->pos++;
tok_kind_t k;
switch (c) {
case '(': k = TK_LPAREN; break;
case ')': k = TK_RPAREN; break;
case '{': k = TK_LBRACE; break;
case '}': k = TK_RBRACE; break;
case '[': k = TK_LBRACKET; break;
case ']': k = TK_RBRACKET; break;
case ';': k = TK_SEMI; break;
case ',': k = TK_COMMA; break;
case ':': k = TK_COLON; break;
case '?': k = TK_QUESTION; break;
case '.': k = TK_DOT; break;
case '+': k = TK_PLUS; break;
case '-': k = TK_MINUS; break;
case '*': k = TK_STAR; break;
case '/': k = TK_SLASH; break;
case '%': k = TK_PERCENT; break;
case '=': k = TK_ASSIGN; break;
case '<': k = TK_LT; break;
case '>': k = TK_GT; break;
case '!': k = TK_NOT; break;
case '&': k = TK_AMP; break;
case '|': k = TK_PIPE; break;
case '^': k = TK_CARET; break;
case '~': k = TK_TILDE; break;
default:
fprintf(stderr, "cc: %s:%d: unexpected character '%c'\n", lx->filename, lx->line, c);
exit(1);
}
token_t t = mk(lx, k);
t.bol = was_bol;
return t;
}
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/lexer.c (file state is current in your context — no need to Read it back)
{
"taskId": "10",
"status": "in_progress"
}Updated task #10 status
{
"taskId": "12",
"status": "in_progress"
}Updated task #12 status
#include "cc.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
const char *g_include_dirs[MAX_INCLUDE_DIRS];
int g_n_include_dirs = 0;
#define MAX_MACROS 128
#define MAX_MACRO_BODY 32
#define MAX_PRAGMA_ONCE 128
typedef struct {
char name[MAX_NAME];
token_t body[MAX_MACRO_BODY];
int n;
} macro_t;
static macro_t g_macros[MAX_MACROS];
static int g_n_macros = 0;
static char g_pragma_once_files[MAX_PRAGMA_ONCE][512];
static int g_n_pragma_once = 0;
static macro_t *macro_find(const char *name) {
for (int i = 0; i < g_n_macros; i++) {
if (strcmp(g_macros[i].name, name) == 0) return &g_macros[i];
}
return NULL;
}
static int already_seen(const char *path) {
for (int i = 0; i < g_n_pragma_once; i++) {
if (strcmp(g_pragma_once_files[i], path) == 0) return 1;
}
return 0;
}
static void mark_seen(const char *path) {
if (g_n_pragma_once < MAX_PRAGMA_ONCE) {
safe_strcpy(g_pragma_once_files[g_n_pragma_once++], path, 512);
}
}
static char *read_file(const char *path) {
FILE *f = fopen(path, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END);
long n = ftell(f);
fseek(f, 0, SEEK_SET);
char *buf = malloc((size_t)n + 1);
size_t got = fread(buf, 1, (size_t)n, f);
buf[got] = '\0';
fclose(f);
return buf;
}
static void dirname_of(const char *path, char *out, size_t outsz) {
const char *slash = strrchr(path, '/');
if (!slash) { safe_strcpy(out, ".", outsz); return; }
size_t n = (size_t)(slash - path);
if (n >= outsz) n = outsz - 1;
memcpy(out, path, n);
out[n] = '\0';
}
static int resolve_include(const char *cur_dir, const char *name, int is_quote, char *out, size_t outsz) {
char cand[600];
if (is_quote) {
snprintf(cand, sizeof(cand), "%s/%s", cur_dir, name);
FILE *f = fopen(cand, "rb");
if (f) { fclose(f); safe_strcpy(out, cand, outsz); return 1; }
}
for (int i = 0; i < g_n_include_dirs; i++) {
snprintf(cand, sizeof(cand), "%s/%s", g_include_dirs[i], name);
FILE *f = fopen(cand, "rb");
if (f) { fclose(f); safe_strcpy(out, cand, outsz); return 1; }
}
return 0;
}
static void emit(token_t *out, int *out_n, int cap, token_t t) {
if (*out_n >= cap) { fprintf(stderr, "cc: token buffer full\n"); exit(1); }
out[(*out_n)++] = t;
}
static void process_lexer(lexer_t *lx, const char *filename, token_t *out, int *out_n, int cap, int depth) {
char cur_dir[512];
dirname_of(filename, cur_dir, sizeof(cur_dir));
for (;;) {
token_t t = lexer_next(lx);
if (t.kind == TK_EOF) return;
if (t.kind == TK_HASH) {
token_t dir = lexer_next(lx);
if (dir.kind != TK_IDENT) { fprintf(stderr, "cc: %s:%d: expected preprocessor directive\n", filename, dir.line); exit(1); }
if (strcmp(dir.text, "include") == 0) {
token_t path_tok = lexer_next(lx);
int is_quote = path_tok.kind == TK_STR;
if (path_tok.kind != TK_STR) { fprintf(stderr, "cc: %s:%d: expected \"file\" after #include\n", filename, dir.line); exit(1); }
char resolved[600];
if (!resolve_include(cur_dir, path_tok.text, is_quote, resolved, sizeof(resolved))) {
fprintf(stderr, "cc: %s:%d: cannot find include file '%s'\n", filename, dir.line, path_tok.text);
exit(1);
}
if (!already_seen(resolved)) {
if (depth >= MAX_INCLUDE_DEPTH) { fprintf(stderr, "cc: #include nested too deep\n"); exit(1); }
char *src = read_file(resolved);
if (!src) { fprintf(stderr, "cc: cannot open %s\n", resolved); exit(1); }
static char paths[MAX_INCLUDE_DEPTH][600];
safe_strcpy(paths[depth], resolved, sizeof(paths[depth]));
lexer_t sub;
lexer_init(&sub, src, paths[depth]);
process_lexer(&sub, paths[depth], out, out_n, cap, depth + 1);
}
continue;
}
if (strcmp(dir.text, "pragma") == 0) {
token_t what = lexer_next(lx);
if (what.kind == TK_IDENT && strcmp(what.text, "once") == 0) {
mark_seen(filename);
} else {
fprintf(stderr, "cc: %s:%d: unsupported #pragma\n", filename, dir.line);
exit(1);
}
continue;
}
if (strcmp(dir.text, "define") == 0) {
token_t name = lexer_next(lx);
if (name.kind != TK_IDENT) { fprintf(stderr, "cc: %s:%d: expected macro name\n", filename, dir.line); exit(1); }
if (g_n_macros >= MAX_MACROS) { fprintf(stderr, "cc: too many macros\n"); exit(1); }
macro_t *m = &g_macros[g_n_macros++];
safe_strcpy(m->name, name.text, MAX_NAME);
m->n = 0;
for (;;) {
token_t bt = lexer_next(lx);
if (bt.bol || bt.kind == TK_EOF) {
/* first token of the next line (or EOF): this
* lexer doesn't emit newline tokens, so `bol`
* marks the boundary - push it back by handling
* it as the next real token below. */
if (bt.kind == TK_HASH || !bt.bol) { /* unreachable given bt.bol check, kept for clarity */ }
goto define_done;
}
if (m->n < MAX_MACRO_BODY) m->body[m->n++] = bt;
}
define_done:;
/* the token that ended the macro body belongs to the
* main stream - re-lex isn't possible with this simple
* lexer, so instead we required macro bodies to be
* single-line and re-enter the loop assuming the lexer
* is already positioned after the last body token. This
* works because lexer_next always advances past what it
* returns; the token that broke the loop is the NEXT
* real token and must be processed normally. */
continue;
}
fprintf(stderr, "cc: %s:%d: unsupported preprocessor directive #%s\n", filename, dir.line, dir.text);
exit(1);
}
if (t.kind == TK_IDENT) {
macro_t *m = macro_find(t.text);
if (m) {
for (int i = 0; i < m->n; i++) {
token_t bt = m->body[i];
if (bt.kind == TK_IDENT) {
macro_t *nested = macro_find(bt.text);
if (nested && nested != m) {
for (int j = 0; j < nested->n; j++) emit(out, out_n, cap, nested->body[j]);
continue;
}
}
emit(out, out_n, cap, bt);
}
continue;
}
}
emit(out, out_n, cap, t);
}
}
void pp_process_file(const char *path, token_t *out, int *out_n, int cap) {
char *src = read_file(path);
if (!src) { fprintf(stderr, "cc: cannot open %s\n", path); exit(1); }
lexer_t lx;
lexer_init(&lx, src, path);
mark_seen(path);
process_lexer(&lx, path, out, out_n, cap, 0);
token_t eof;
memset(&eof, 0, sizeof(eof));
eof.kind = TK_EOF;
emit(out, out_n, cap, eof);
}
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/preprocessor.c (file state is current in your context — no need to Read it back)
- typedef struct {
- const char *src;
- size_t pos, len;
- const char *filename;
- int line;
- int bol;
- } lexer_t;
-
- void lexer_init(lexer_t *lx, const char *src, const char *filename);
- token_t lexer_next(lexer_t *lx);
+ typedef struct {
+ const char *src;
+ size_t pos, len;
+ const char *filename;
+ int line;
+ int bol;
+ int has_pushback;
+ token_t pushback;
+ } lexer_t;
+
+ void lexer_init(lexer_t *lx, const char *src, const char *filename);
+ token_t lexer_next(lexer_t *lx);
+ /* Un-reads one token (at most one at a time) - the preprocessor's
+ * #define handling needs this to detect "end of macro body line" by
+ * reading one token past it. */
+ void lexer_push_back(lexer_t *lx, token_t t);The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/cc.h has been updated successfully. (file state is current in your context — no need to Read it back)
- void lexer_init(lexer_t *lx, const char *src, const char *filename) {
- lx->src = src;
- lx->pos = 0;
- lx->len = strlen(src);
- lx->filename = filename;
- lx->line = 1;
- lx->bol = 1;
- }
+ void lexer_init(lexer_t *lx, const char *src, const char *filename) {
+ lx->src = src;
+ lx->pos = 0;
+ lx->len = strlen(src);
+ lx->filename = filename;
+ lx->line = 1;
+ lx->bol = 1;
+ lx->has_pushback = 0;
+ }
+
+ void lexer_push_back(lexer_t *lx, token_t t) {
+ lx->pushback = t;
+ lx->has_pushback = 1;
+ }The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/lexer.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ grep -n "^token_t lexer_next" /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/lexer.c
56:token_t lexer_next(lexer_t *lx) {- token_t lexer_next(lexer_t *lx) {
- for (;;) {
+ token_t lexer_next(lexer_t *lx) {
+ if (lx->has_pushback) { lx->has_pushback = 0; return lx->pushback; }
+ for (;;) {The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/lexer.c has been updated successfully. (file state is current in your context — no need to Read it back)
- macro_t *m = &g_macros[g_n_macros++];
- safe_strcpy(m->name, name.text, MAX_NAME);
- m->n = 0;
- for (;;) {
- token_t bt = lexer_next(lx);
- if (bt.bol || bt.kind == TK_EOF) {
- /* first token of the next line (or EOF): this
- * lexer doesn't emit newline tokens, so `bol`
- * marks the boundary - push it back by handling
- * it as the next real token below. */
- if (bt.kind == TK_HASH || !bt.bol) { /* unreachable given bt.bol check, kept for clarity */ }
- goto define_done;
- }
- if (m->n < MAX_MACRO_BODY) m->body[m->n++] = bt;
- }
- define_done:;
- /* the token that ended the macro body belongs to the
- * main stream - re-lex isn't possible with this simple
- * lexer, so instead we required macro bodies to be
- * single-line and re-enter the loop assuming the lexer
- * is already positioned after the last body token. This
- * works because lexer_next always advances past what it
- * returns; the token that broke the loop is the NEXT
- * real token and must be processed normally. */
- continue;
+ macro_t *m = &g_macros[g_n_macros++];
+ safe_strcpy(m->name, name.text, MAX_NAME);
+ m->n = 0;
+ for (;;) {
+ token_t bt = lexer_next(lx);
+ if (bt.bol || bt.kind == TK_EOF) {
+ /* First token of the next line (or EOF) - this
+ * lexer has no newline tokens, so `bol` marks
+ * the boundary. It belongs to the main stream,
+ * not the macro body, so push it back rather
+ * than discarding it. */
+ lexer_push_back(lx, bt);
+ break;
+ }
+ if (m->n < MAX_MACRO_BODY) m->body[m->n++] = bt;
+ }
+ continue;The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/preprocessor.c has been updated successfully. (file state is current in your context — no need to Read it back)
#include "cc.h"
#include <stdlib.h>
#include <string.h>
type_t *type_new(type_kind_t kind) {
type_t *t = calloc(1, sizeof(type_t));
t->kind = kind;
return t;
}
type_t *ptr_to(type_t *base) {
type_t *t = type_new(TY_PTR);
t->base = base;
t->size = 4;
t->align = 4;
return t;
}
int type_is_int(type_t *t) {
return t->kind == TY_CHAR || t->kind == TY_SHORT || t->kind == TY_INT || t->kind == TY_ENUM;
}
static int round_up(int n, int align) { return (n + align - 1) / align * align; }
int type_align(type_t *t) {
if (t->align) return t->align;
switch (t->kind) {
case TY_CHAR: t->align = 1; break;
case TY_SHORT: t->align = 2; break;
case TY_INT: case TY_ENUM: case TY_PTR: t->align = 4; break;
case TY_ARRAY: t->align = type_align(t->base); break;
case TY_STRUCT: case TY_UNION: {
int a = 1;
for (member_t *m = t->members; m; m = m->next) {
int ma = t->is_packed ? 1 : type_align(m->type);
if (ma > a) a = ma;
}
t->align = a ? a : 1;
break;
}
default: t->align = 1; break;
}
return t->align;
}
int type_size(type_t *t) {
if (t->size) return t->size;
switch (t->kind) {
case TY_VOID: t->size = 0; break;
case TY_CHAR: t->size = 1; break;
case TY_SHORT: t->size = 2; break;
case TY_INT: case TY_ENUM: case TY_PTR: t->size = 4; break;
case TY_ARRAY: t->size = type_size(t->base) * (t->array_len > 0 ? t->array_len : 0); break;
case TY_UNION: {
int sz = 0;
for (member_t *m = t->members; m; m = m->next) {
int msz = type_size(m->type);
if (msz > sz) sz = msz;
}
int a = type_align(t);
t->size = round_up(sz, a);
break;
}
case TY_STRUCT: {
int off = 0;
int a = t->is_packed ? 1 : type_align(t);
for (member_t *m = t->members; m; m = m->next) {
int ma = t->is_packed ? 1 : type_align(m->type);
off = round_up(off, ma);
m->offset = off;
off += type_size(m->type);
}
t->size = t->is_packed ? off : round_up(off, a);
break;
}
default: t->size = 0; break;
}
return t->size;
}
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/types.c (file state is current in your context — no need to Read it back)
#include "cc.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ---------------- symbol tables ---------------- */
typedef struct sym {
char name[MAX_NAME];
type_t *type;
int frame_offset; /* locals/params, EBP-relative */
int is_global;
int is_static_local;
char asm_label[MAX_NAME];
int is_typedef;
int is_enum_const;
int64_t enum_value;
struct sym *next;
} sym_t;
typedef struct tag {
char name[MAX_NAME];
type_t *type;
struct tag *next;
} tag_t;
typedef struct {
token_t *toks;
int n, pos;
sym_t *globals;
sym_t *locals; /* current function's flat symbol list (params+locals) */
tag_t *tags;
int frame_size; /* current function's local variable area size (grows as locals are declared) */
int next_static_id; /* for unique static-local asm labels */
int next_str_id; /* for unique string-literal asm labels */
type_t *cur_func_ret;
int had_error;
} parser_t;
static void perr(parser_t *p, const char *msg) {
token_t t = p->toks[p->pos];
fprintf(stderr, "cc: %s:%d: %s (near '%s')\n", t.file ? t.file : "?", t.line, msg,
t.kind == TK_IDENT || t.kind == TK_STR ? t.text : "?");
p->had_error = 1;
exit(1); /* tinyC has no error recovery - not worth it for this codebase's size */
}
static token_t cur(parser_t *p) { return p->toks[p->pos]; }
static token_t advance(parser_t *p) { token_t t = p->toks[p->pos]; if (p->toks[p->pos].kind != TK_EOF) p->pos++; return t; }
static int check(parser_t *p, tok_kind_t k) { return cur(p).kind == k; }
static int match(parser_t *p, tok_kind_t k) { if (check(p, k)) { advance(p); return 1; } return 0; }
static token_t expect(parser_t *p, tok_kind_t k) {
if (!check(p, k)) perr(p, "unexpected token");
return advance(p);
}
static int is_kw(parser_t *p, const char *kw) {
return check(p, TK_IDENT) && strcmp(cur(p).text, kw) == 0;
}
static int match_kw(parser_t *p, const char *kw) {
if (is_kw(p, kw)) { advance(p); return 1; }
return 0;
}
static sym_t *sym_find_local(parser_t *p, const char *name) {
for (sym_t *s = p->locals; s; s = s->next) if (strcmp(s->name, name) == 0) return s;
return NULL;
}
static sym_t *sym_find_global(parser_t *p, const char *name) {
for (sym_t *s = p->globals; s; s = s->next) if (strcmp(s->name, name) == 0) return s;
return NULL;
}
static sym_t *sym_find(parser_t *p, const char *name) {
sym_t *s = sym_find_local(p, name);
return s ? s : sym_find_global(p, name);
}
static tag_t *tag_find(parser_t *p, const char *name) {
for (tag_t *t = p->tags; t; t = t->next) if (strcmp(t->name, name) == 0) return t;
return NULL;
}
static int is_typedef_name(parser_t *p, const char *name) {
sym_t *s = sym_find_global(p, name); /* typedefs are always file-scope in this codebase */
return s && s->is_typedef;
}
static sym_t *add_global(parser_t *p, const char *name, type_t *type) {
sym_t *s = calloc(1, sizeof(sym_t));
safe_strcpy(s->name, name, MAX_NAME);
s->type = type;
s->is_global = 1;
safe_strcpy(s->asm_label, name, MAX_NAME);
s->next = p->globals;
p->globals = s;
return s;
}
static sym_t *add_local(parser_t *p, const char *name, type_t *type, int is_static) {
sym_t *s = calloc(1, sizeof(sym_t));
safe_strcpy(s->name, name, MAX_NAME);
s->type = type;
if (is_static) {
s->is_static_local = 1;
snprintf(s->asm_label, MAX_NAME, "__static_%d", p->next_static_id++);
} else {
p->frame_size += type_size(type) < 4 ? 4 : type_size(type); /* min 4-byte slots - simplicity over density */
p->frame_size = (p->frame_size + 3) & ~3;
s->frame_offset = -p->frame_size;
}
s->next = p->locals;
p->locals = s;
return s;
}
/* ---------------- expressions (forward decls) ---------------- */
static node_t *parse_expr(parser_t *p);
static node_t *parse_assign(parser_t *p);
static node_t *parse_cast(parser_t *p);
static node_t *parse_unary(parser_t *p);
static type_t *parse_type_name(parser_t *p); /* for casts/sizeof(type) */
static node_t *parse_stmt(parser_t *p);
static node_t *parse_block(parser_t *p);
/* ---------------- type specifiers & declarators ---------------- */
static member_t *parse_struct_members(parser_t *p);
static int is_type_start(parser_t *p) {
if (!check(p, TK_IDENT)) return 0;
const char *kws[] = { "void", "char", "short", "int", "signed", "unsigned",
"struct", "union", "enum", "const", "volatile", "static", "extern", NULL };
for (int i = 0; kws[i]; i++) if (strcmp(cur(p).text, kws[i]) == 0) return 1;
return is_typedef_name(p, cur(p).text);
}
/* Parses base type specifiers + storage class; returns the type and sets
* *is_static/*is_extern. Does not consume any declarator. */
static type_t *parse_type_spec(parser_t *p, int *is_static, int *is_extern) {
int saw_void = 0, saw_char = 0, saw_short = 0, saw_int = 0, saw_unsigned = 0, saw_signed = 0;
type_t *named = NULL;
if (is_static) *is_static = 0;
if (is_extern) *is_extern = 0;
for (;;) {
if (match_kw(p, "static")) { if (is_static) *is_static = 1; continue; }
if (match_kw(p, "extern")) { if (is_extern) *is_extern = 1; continue; }
if (match_kw(p, "const") || match_kw(p, "volatile") || match_kw(p, "inline")) continue;
if (match_kw(p, "void")) { saw_void = 1; continue; }
if (match_kw(p, "char")) { saw_char = 1; continue; }
if (match_kw(p, "short")) { saw_short = 1; continue; }
if (match_kw(p, "int")) { saw_int = 1; continue; }
if (match_kw(p, "signed")) { saw_signed = 1; continue; }
if (match_kw(p, "unsigned")) { saw_unsigned = 1; continue; }
if (is_kw(p, "struct") || is_kw(p, "union")) {
int is_union = is_kw(p, "union");
advance(p);
char tagname[MAX_NAME] = {0};
if (check(p, TK_IDENT)) { safe_strcpy(tagname, cur(p).text, MAX_NAME); advance(p); }
type_t *st;
tag_t *existing = tagname[0] ? tag_find(p, tagname) : NULL;
if (check(p, TK_LBRACE)) {
advance(p);
st = type_new(is_union ? TY_UNION : TY_STRUCT);
safe_strcpy(st->tag, tagname, MAX_NAME);
st->members = parse_struct_members(p);
expect(p, TK_RBRACE);
if (is_kw(p, "__attribute__")) {
advance(p); expect(p, TK_LPAREN); expect(p, TK_LPAREN);
if (is_kw(p, "packed")) { advance(p); st->is_packed = 1; }
expect(p, TK_RPAREN); expect(p, TK_RPAREN);
}
if (tagname[0]) {
tag_t *tg = calloc(1, sizeof(tag_t));
safe_strcpy(tg->name, tagname, MAX_NAME);
tg->type = st;
tg->next = p->tags;
p->tags = tg;
}
} else if (existing) {
st = existing->type;
} else {
perr(p, "unknown struct/union tag");
return NULL;
}
named = st;
continue;
}
if (is_kw(p, "enum")) {
advance(p);
char tagname[MAX_NAME] = {0};
if (check(p, TK_IDENT)) { safe_strcpy(tagname, cur(p).text, MAX_NAME); advance(p); }
type_t *et;
if (check(p, TK_LBRACE)) {
advance(p);
et = type_new(TY_ENUM);
safe_strcpy(et->tag, tagname, MAX_NAME);
int64_t val = 0;
while (!check(p, TK_RBRACE)) {
token_t nm = expect(p, TK_IDENT);
if (match(p, TK_ASSIGN)) val = (int64_t)advance(p).num; /* our enums only use plain int literals */
sym_t *s = calloc(1, sizeof(sym_t));
safe_strcpy(s->name, nm.text, MAX_NAME);
s->is_enum_const = 1;
s->enum_value = val;
s->type = et;
s->next = p->globals;
p->globals = s;
val++;
if (!match(p, TK_COMMA)) break;
}
expect(p, TK_RBRACE);
if (tagname[0]) {
tag_t *tg = calloc(1, sizeof(tag_t));
safe_strcpy(tg->name, tagname, MAX_NAME);
tg->type = et;
tg->next = p->tags;
p->tags = tg;
}
} else {
tag_t *existing = tag_find(p, tagname);
if (!existing) { perr(p, "unknown enum tag"); return NULL; }
et = existing->type;
}
named = et;
continue;
}
if (check(p, TK_IDENT) && is_typedef_name(p, cur(p).text) && !named && !saw_void && !saw_char && !saw_short && !saw_int) {
sym_t *s = sym_find_global(p, cur(p).text);
named = s->type;
advance(p);
continue;
}
break;
}
if (named) return named;
if (saw_void) return type_new(TY_VOID);
type_t *t;
if (saw_char) t = type_new(TY_CHAR);
else if (saw_short) t = type_new(TY_SHORT);
else t = type_new(TY_INT); /* default-int, also covers bare signed/unsigned */
t->is_unsigned = saw_unsigned && !saw_signed ? 1 : (saw_unsigned ? 1 : 0);
return t;
}
static member_t *parse_struct_members(parser_t *p) {
member_t *head = NULL, *tail = NULL;
while (!check(p, TK_RBRACE)) {
type_t *base = parse_type_spec(p, NULL, NULL);
for (;;) {
type_t *mt = base;
char name[MAX_NAME];
while (match(p, TK_STAR)) mt = ptr_to(mt);
token_t id = expect(p, TK_IDENT);
safe_strcpy(name, id.text, MAX_NAME);
while (match(p, TK_LBRACKET)) {
int n = -1;
if (!check(p, TK_RBRACKET)) n = (int)advance(p).num; /* array member sizes are always plain literals here */
expect(p, TK_RBRACKET);
type_t *arr = type_new(TY_ARRAY);
arr->base = mt;
arr->array_len = n;
mt = arr;
}
member_t *m = calloc(1, sizeof(member_t));
safe_strcpy(m->name, name, MAX_NAME);
m->type = mt;
if (tail) tail->next = m; else head = m;
tail = m;
if (!match(p, TK_COMMA)) break;
}
expect(p, TK_SEMI);
}
return head;
}
static type_t *parse_param_list(parser_t *p, char names[][MAX_NAME], int *n_names) {
type_t *head = NULL, *tail = NULL;
int n = 0;
if (check(p, TK_RPAREN)) return NULL;
if (is_kw(p, "void") && p->toks[p->pos + 1].kind == TK_RPAREN) { advance(p); return NULL; }
for (;;) {
type_t *base = parse_type_spec(p, NULL, NULL);
type_t *pt = base;
char nm[MAX_NAME] = {0};
while (match(p, TK_STAR)) pt = ptr_to(pt);
if (check(p, TK_IDENT)) { safe_strcpy(nm, cur(p).text, MAX_NAME); advance(p); }
while (match(p, TK_LBRACKET)) {
if (!check(p, TK_RBRACKET)) advance(p);
expect(p, TK_RBRACKET);
pt = ptr_to(pt); /* array param decays to pointer */
}
type_t *node = type_new(pt->kind);
*node = *pt;
node->next = NULL;
if (tail) tail->next = node; else head = node;
tail = node;
if (names && n_names) safe_strcpy(names[n], nm, MAX_NAME);
n++;
if (!match(p, TK_COMMA)) break;
}
if (n_names) *n_names = n;
return head;
}
static type_t *parse_type_name(parser_t *p) {
type_t *base = parse_type_spec(p, NULL, NULL);
while (match(p, TK_STAR)) base = ptr_to(base);
if (match(p, TK_LBRACKET)) {
int n = -1;
if (!check(p, TK_RBRACKET)) n = (int)advance(p).num;
expect(p, TK_RBRACKET);
type_t *arr = type_new(TY_ARRAY);
arr->base = base;
arr->array_len = n;
base = arr;
}
return base;
}
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/parser.c (file state is current in your context — no need to Read it back)
$ wc -l /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/parser.c
322 /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/parser.c
- static type_t *parse_type_name(parser_t *p) {
+ /* Parses a declarator (the part after the base type specifier) and
+ * returns the full type, writing the declared name into name_out.
+ * Scoped to what this codebase's declarators actually need (see
+ * docs/toolchain.md): `*`-prefix pointers, `name`, `name[N]`/`name[]`,
+ * `name(params)` (function), and the one parenthesized case,
+ * `(*name)(params)` (function pointer) - not the fully general/
+ * arbitrarily-nested C declarator grammar. */
+ static type_t *parse_declarator(parser_t *p, type_t *base, char *name_out,
+ char param_names[][MAX_NAME], int *n_params) {
+ while (match(p, TK_STAR)) base = ptr_to(base);
+
+ if (match(p, TK_LPAREN)) {
+ expect(p, TK_STAR);
+ token_t id = expect(p, TK_IDENT);
+ safe_strcpy(name_out, id.text, MAX_NAME);
+ expect(p, TK_RPAREN);
+ expect(p, TK_LPAREN);
+ type_t *functy = type_new(TY_FUNC);
+ functy->base = base;
+ functy->param = parse_param_list(p, param_names, n_params);
+ expect(p, TK_RPAREN);
+ return ptr_to(functy);
+ }
+
+ token_t id = expect(p, TK_IDENT);
+ safe_strcpy(name_out, id.text, MAX_NAME);
+
+ if (match(p, TK_LBRACKET)) {
+ int n = -1;
+ if (!check(p, TK_RBRACKET)) n = (int)advance(p).num; /* array sizes here are always plain literals or resolved macro constants */
+ expect(p, TK_RBRACKET);
+ type_t *arr = type_new(TY_ARRAY);
+ arr->base = base;
+ arr->array_len = n;
+ return arr;
+ }
+ if (match(p, TK_LPAREN)) {
+ type_t *functy = type_new(TY_FUNC);
+ functy->base = base;
+ functy->param = parse_param_list(p, param_names, n_params);
+ expect(p, TK_RPAREN);
+ return functy;
+ }
+ return base;
+ }
+
+ static type_t *parse_type_name(parser_t *p) {The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/parser.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ grep -n "advance(p).num" /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/parser.c
203: if (match(p, TK_ASSIGN)) val = (int64_t)advance(p).num; /* our enums only use plain int literals */ 261: if (!check(p, TK_RBRACKET)) n = (int)advance(p).num; /* array member sizes are always plain literals here */ 338: if (!check(p, TK_RBRACKET)) n = (int)advance(p).num; /* array sizes here are always plain literals or resolved macro constants */ 360: if (!check(p, TK_RBRACKET)) n = (int)advance(p).num;
- static type_t *parse_type_name(parser_t *p); /* for casts/sizeof(type) */
- static node_t *parse_stmt(parser_t *p);
- static node_t *parse_block(parser_t *p);
+ static type_t *parse_type_name(parser_t *p); /* for casts/sizeof(type) */
+ static node_t *parse_stmt(parser_t *p);
+ static node_t *parse_block(parser_t *p);
+
+ /* Array bounds and enum values in this codebase are often macro
+ * constants that expand to a small arithmetic expression (e.g.
+ * `TINYFS_INODE_BLOCK` -> `(1 + 1)`), not always a single literal
+ * token, so bounds need real constant folding rather than reading one
+ * token's ->num. */
+ static int64_t eval_const_node(parser_t *p, node_t *n) {
+ switch (n->kind) {
+ case ND_NUM: return n->num;
+ case ND_UNARY_MINUS: return -eval_const_node(p, n->a);
+ case ND_BITNOT: return ~eval_const_node(p, n->a);
+ case ND_NOT: return !eval_const_node(p, n->a);
+ case ND_BINOP: {
+ int64_t l = eval_const_node(p, n->a), r = eval_const_node(p, n->b);
+ switch (n->op) {
+ case TK_PLUS: return l + r;
+ case TK_MINUS: return l - r;
+ case TK_STAR: return l * r;
+ case TK_SLASH: return r != 0 ? l / r : 0;
+ case TK_PERCENT: return r != 0 ? l % r : 0;
+ case TK_SHL: return l << r;
+ case TK_SHR: return l >> r;
+ case TK_AMP: return l & r;
+ case TK_PIPE: return l | r;
+ case TK_CARET: return l ^ r;
+ default: perr(p, "unsupported operator in constant expression"); return 0;
+ }
+ }
+ default:
+ perr(p, "expected a constant expression");
+ return 0;
+ }
+ }
+ static int64_t parse_const_expr(parser_t *p) {
+ return eval_const_node(p, parse_assign(p));
+ }The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/parser.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc
python3 - <<'EOF'
import re
with open('parser.c') as f:
c = f.read()
c = c.replace(
'if (match(p, TK_ASSIGN)) val = (int64_t)advance(p).num; /* our enums only use plain int literals */',
'if (match(p, TK_ASSIGN)) val = parse_const_expr(p);'
)
c = c.replace(
'if (!check(p, TK_RBRACKET)) n = (int)advance(p).num; /* array member sizes are always plain literals here */',
'if (!check(p, TK_RBRACKET)) n = (int)parse_const_expr(p);'
)
c = c.replace(
'if (!check(p, TK_RBRACKET)) n = (int)advance(p).num; /* array sizes here are always plain literals or resolved macro constants */',
'if (!check(p, TK_RBRACKET)) n = (int)parse_const_expr(p);'
)
# the two remaining bare `n = (int)advance(p).num;` occurrences (parse_type_name and param array decay)
c = c.replace(
' if (!check(p, TK_RBRACKET)) n = (int)advance(p).num;\n expect(p, TK_RBRACKET);\n type_t *arr = type_new(TY_ARRAY);\n arr->base = base;',
' if (!check(p, TK_RBRACKET)) n = (int)parse_const_expr(p);\n expect(p, TK_RBRACKET);\n type_t *arr = type_new(TY_ARRAY);\n arr->base = base;'
)
with open('parser.c', 'w') as f:
f.write(c)
EOF
grep -n "advance(p).num\|parse_const_expr" parser.c159:static int64_t parse_const_expr(parser_t *p) {
239: if (match(p, TK_ASSIGN)) val = parse_const_expr(p);
297: if (!check(p, TK_RBRACKET)) n = (int)parse_const_expr(p);
374: if (!check(p, TK_RBRACKET)) n = (int)parse_const_expr(p);
396: if (!check(p, TK_RBRACKET)) n = (int)parse_const_expr(p);$ tail -5 /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/parser.c
arr->array_len = n;
base = arr;
}
return base;
}- type_t *type_new(type_kind_t kind) {
- type_t *t = calloc(1, sizeof(type_t));
- t->kind = kind;
- return t;
- }
+ type_t *type_new(type_kind_t kind) {
+ type_t *t = calloc(1, sizeof(type_t));
+ t->kind = kind;
+ return t;
+ }
+
+ node_t *node_new(node_kind_t kind) {
+ node_t *n = calloc(1, sizeof(node_t));
+ n->kind = kind;
+ return n;
+ }The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/types.c has been updated successfully. (file state is current in your context — no need to Read it back)
- arr->array_len = n;
- base = arr;
- }
- return base;
- }
+ arr->array_len = n;
+ base = arr;
+ }
+ return base;
+ }
+
+ /* ---------------- expressions ---------------- */
+
+ static type_t *g_int_type;
+ static type_t *g_char_type;
+
+ static type_t *decay(type_t *t) {
+ if (t->kind == TY_ARRAY) return ptr_to(t->base);
+ return t;
+ }
+
+ static node_t *make_num(int64_t v) {
+ node_t *n = node_new(ND_NUM);
+ n->num = v;
+ n->type = g_int_type;
+ return n;
+ }
+
+ static node_t *resolve_ident(parser_t *p, const char *name) {
+ sym_t *s = sym_find(p, name);
+ if (!s) { perr(p, "undeclared identifier"); return make_num(0); }
+ if (s->is_enum_const) return make_num(s->enum_value);
+ node_t *n = node_new(ND_VAR);
+ safe_strcpy(n->name, name, MAX_NAME);
+ n->type = s->type;
+ n->is_global = s->is_global;
+ n->is_static_local = s->is_static_local;
+ n->frame_offset = s->frame_offset;
+ safe_strcpy(n->asm_label, s->asm_label, MAX_NAME);
+ return n;
+ }
+
+ static node_t *make_str(parser_t *p, const char *text) {
+ node_t *n = node_new(ND_STR);
+ safe_strcpy(n->str, text, sizeof(n->str));
+ snprintf(n->asm_label, MAX_NAME, "__str_%d", p->next_str_id++);
+ type_t *arr = type_new(TY_ARRAY);
+ arr->base = g_char_type;
+ arr->array_len = (int)strlen(text) + 1;
+ n->type = arr;
+ return n;
+ }
+
+ static member_t *find_member(type_t *st, const char *name) {
+ for (member_t *m = st->members; m; m = m->next) if (strcmp(m->name, name) == 0) return m;
+ return NULL;
+ }
+
+ static node_t *parse_primary(parser_t *p) {
+ if (check(p, TK_NUM)) { node_t *n = make_num(advance(p).num); return n; }
+ if (check(p, TK_CHAR)) { node_t *n = make_num(advance(p).num); n->type = g_char_type; return n; }
+ if (check(p, TK_STR)) { return make_str(p, advance(p).text); }
+ if (check(p, TK_IDENT)) {
+ char name[MAX_NAME];
+ safe_strcpy(name, advance(p).text, MAX_NAME);
+ if (check(p, TK_LPAREN)) {
+ advance(p);
+ node_t *call = node_new(ND_CALL);
+ safe_strcpy(call->name, name, MAX_NAME);
+ node_t *tail = NULL;
+ while (!check(p, TK_RPAREN)) {
+ node_t *arg = parse_assign(p);
+ if (!call->list) call->list = arg; else tail->next = arg;
+ tail = arg;
+ if (!match(p, TK_COMMA)) break;
+ }
+ expect(p, TK_RPAREN);
+ sym_t *fs = sym_find_global(p, name);
+ call->type = (fs && fs->type->kind == TY_FUNC) ? fs->type->base : g_int_type;
+ return call;
+ }
+ return resolve_ident(p, name);
+ }
+ if (match(p, TK_LPAREN)) {
+ node_t *n = parse_expr(p);
+ expect(p, TK_RPAREN);
+ return n;
+ }
+ perr(p, "expected expression");
+ return make_num(0);
+ }
+
+ static node_t *parse_postfix(parser_t *p) {
+ node_t *n = parse_primary(p);
+ for (;;) {
+ if (match(p, TK_LBRACKET)) {
+ node_t *idx = parse_expr(p);
+ expect(p, TK_RBRACKET);
+ node_t *add = node_new(ND_BINOP);
+ add->op = TK_PLUS;
+ add->a = n;
+ add->b = idx;
+ add->type = decay(n->type);
+ node_t *deref = node_new(ND_DEREF);
+ deref->a = add;
+ deref->type = add->type->base;
+ n = deref;
+ } else if (match(p, TK_DOT)) {
+ token_t id = expect(p, TK_IDENT);
+ member_t *m = find_member(n->type, id.text);
+ if (!m) { perr(p, "no such member"); return n; }
+ node_t *mem = node_new(ND_MEMBER);
+ mem->a = n;
+ safe_strcpy(mem->name, id.text, MAX_NAME);
+ mem->type = m->type;
+ mem->num = m->offset;
+ n = mem;
+ } else if (match(p, TK_ARROW)) {
+ token_t id = expect(p, TK_IDENT);
+ member_t *m = find_member(n->type->base, id.text);
+ if (!m) { perr(p, "no such member"); return n; }
+ node_t *mem = node_new(ND_ARROW);
+ mem->a = n;
+ safe_strcpy(mem->name, id.text, MAX_NAME);
+ mem->type = m->type;
+ mem->num = m->offset;
+ n = mem;
+ } else if (match(p, TK_INC)) {
+ node_t *pi = node_new(ND_POSTINC); pi->a = n; pi->type = n->type; n = pi;
+ } else if (match(p, TK_DEC)) {
+ node_t *pd = node_new(ND_POSTDEC); pd->a = n; pd->type = n->type; n = pd;
+ } else break;
+ }
+ return n;
+ }
+
+ static int looks_like_type_name(parser_t *p) {
+ /* Used after '(' inside unary() to disambiguate a cast from a
+ * parenthesized expression. */
+ return is_type_start(p);
+ }
+
+ static node_t *parse_unary(parser_t *p) {
+ if (match(p, TK_PLUS)) return parse_cast(p);
+ if (match(p, TK_MINUS)) { node_t *n = node_new(ND_UNARY_MINUS); n->a = parse_cast(p); n->type = n->a->type; return n; }
+ if (match(p, TK_NOT)) { node_t *n = node_new(ND_NOT); n->a = parse_cast(p); n->type = g_int_type; return n; }
+ if (match(p, TK_TILDE)) { node_t *n = node_new(ND_BITNOT); n->a = parse_cast(p); n->type = n->a->type; return n; }
+ if (match(p, TK_AMP)) { node_t *n = node_new(ND_ADDR); n->a = parse_cast(p); n->type = ptr_to(n->a->type); return n; }
+ if (match(p, TK_STAR)) {
+ node_t *n = node_new(ND_DEREF);
+ n->a = parse_cast(p);
+ n->type = decay(n->a->type)->base;
+ return n;
+ }
+ if (match(p, TK_INC)) { node_t *n = node_new(ND_PREINC); n->a = parse_unary(p); n->type = n->a->type; return n; }
+ if (match(p, TK_DEC)) { node_t *n = node_new(ND_PREDEC); n->a = parse_unary(p); n->type = n->a->type; return n; }
+ if (is_kw(p, "sizeof")) {
+ advance(p);
+ if (check(p, TK_LPAREN) && (p->toks[p->pos + 1].kind == TK_IDENT) ) {
+ /* could be sizeof(type) or sizeof(expr) - peek for a type start */
+ int save = p->pos;
+ advance(p);
+ if (looks_like_type_name(p)) {
+ type_t *t = parse_type_name(p);
+ expect(p, TK_RPAREN);
+ return make_num(type_size(t));
+ }
+ p->pos = save;
+ }
+ node_t *operand = parse_unary(p);
+ return make_num(type_size(operand->type));
+ }
+ return parse_postfix(p);
+ }
+
+ static node_t *parse_cast(parser_t *p) {
+ if (check(p, TK_LPAREN)) {
+ int save = p->pos;
+ advance(p);
+ if (looks_like_type_name(p)) {
+ type_t *t = parse_type_name(p);
+ expect(p, TK_RPAREN);
+ node_t *n = node_new(ND_CAST);
+ n->a = parse_cast(p);
+ n->type = t;
+ return n;
+ }
+ p->pos = save;
+ }
+ return parse_unary(p);
+ }
+
+ static int is_ptr(type_t *t) { return t->kind == TY_PTR || t->kind == TY_ARRAY; }
+
+ static node_t *make_binop(tok_kind_t op, node_t *a, node_t *b) {
+ node_t *n = node_new(ND_BINOP);
+ n->op = op;
+ n->a = a;
+ n->b = b;
+ type_t *at = decay(a->type), *bt = decay(b->type);
+ if ((op == TK_PLUS || op == TK_MINUS) && is_ptr(at)) n->type = at;
+ else if (op == TK_PLUS && is_ptr(bt)) n->type = bt;
+ else n->type = g_int_type;
+ return n;
+ }
+
+ static node_t *parse_binop_level(parser_t *p, node_t *(*next)(parser_t *), const tok_kind_t *ops, int nops) {
+ node_t *n = next(p);
+ for (;;) {
+ int matched = 0;
+ for (int i = 0; i < nops; i++) {
+ if (check(p, ops[i])) {
+ tok_kind_t op = advance(p).kind;
+ n = make_binop(op, n, next(p));
+ matched = 1;
+ break;
+ }
+ }
+ if (!matched) return n;
+ }
+ }
+
+ static node_t *parse_mul(parser_t *p) { static const tok_kind_t o[] = {TK_STAR, TK_SLASH, TK_PERCENT}; return parse_binop_level(p, parse_cast, o, 3); }
+ static node_t *parse_add(parser_t *p) { static const tok_kind_t o[] = {TK_PLUS, TK_MINUS}; return parse_binop_level(p, parse_mul, o, 2); }
+ static node_t *parse_shift(parser_t *p) { static const tok_kind_t o[] = {TK_SHL, TK_SHR}; return parse_binop_level(p, parse_add, o, 2); }
+ static node_t *parse_rel(parser_t *p) { static const tok_kind_t o[] = {TK_LT, TK_LE, TK_GT, TK_GE}; return parse_binop_level(p, parse_shift, o, 4); }
+ static node_t *parse_eq(parser_t *p) { static const tok_kind_t o[] = {TK_EQ, TK_NEQ}; return parse_binop_level(p, parse_rel, o, 2); }
+ static node_t *parse_bitand(parser_t *p) { static const tok_kind_t o[] = {TK_AMP}; return parse_binop_level(p, parse_eq, o, 1); }
+ static node_t *parse_bitxor(parser_t *p) { static const tok_kind_t o[] = {TK_CARET}; return parse_binop_level(p, parse_bitand, o, 1); }
+ static node_t *parse_bitor(parser_t *p) { static const tok_kind_t o[] = {TK_PIPE}; return parse_binop_level(p, parse_bitxor, o, 1); }
+
+ static node_t *parse_logand(parser_t *p) {
+ node_t *n = parse_bitor(p);
+ while (match(p, TK_AND)) { node_t *r = parse_bitor(p); node_t *a = node_new(ND_LOGAND); a->a = n; a->b = r; a->type = g_int_type; n = a; }
+ return n;
+ }
+ static node_t *parse_logor(parser_t *p) {
+ node_t *n = parse_logand(p);
+ while (match(p, TK_OR)) { node_t *r = parse_logand(p); node_t *o = node_new(ND_LOGOR); o->a = n; o->b = r; o->type = g_int_type; n = o; }
+ return n;
+ }
+
+ static node_t *parse_ternary(parser_t *p) {
+ node_t *cond = parse_logor(p);
+ if (match(p, TK_QUESTION)) {
+ node_t *t = node_new(ND_TERNARY);
+ t->a = cond;
+ t->b = parse_expr(p);
+ expect(p, TK_COLON);
+ t->c = parse_assign(p);
+ t->type = t->b->type;
+ return t;
+ }
+ return cond;
+ }
+
+ static int is_assign_op(tok_kind_t k) {
+ return k == TK_ASSIGN || k == TK_PLUSEQ || k == TK_MINUSEQ || k == TK_STAREQ || k == TK_SLASHEQ ||
+ k == TK_PERCENTEQ || k == TK_ANDEQ || k == TK_OREQ || k == TK_XOREQ || k == TK_SHLEQ || k == TK_SHREQ;
+ }
+
+ static node_t *parse_assign(parser_t *p) {
+ node_t *lhs = parse_ternary(p);
+ if (is_assign_op(cur(p).kind)) {
+ tok_kind_t op = advance(p).kind;
+ node_t *rhs = parse_assign(p);
+ if (op == TK_ASSIGN) {
+ node_t *n = node_new(ND_ASSIGN);
+ n->a = lhs; n->b = rhs; n->type = lhs->type;
+ return n;
+ }
+ node_t *n = node_new(ND_COMPOUND_ASSIGN);
+ n->op = op;
+ n->a = lhs; n->b = rhs; n->type = lhs->type;
+ return n;
+ }
+ return lhs;
+ }
+
+ static node_t *parse_expr(parser_t *p) { return parse_assign(p); }The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/parser.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ wc -l /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/parser.c
674 /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/parser.c
- static node_t *parse_expr(parser_t *p) { return parse_assign(p); }
+ static node_t *parse_expr(parser_t *p) { return parse_assign(p); }
+
+ /* ---------------- initializers ---------------- */
+
+ /* `{ v1, v2, ... }` or a single scalar expression. Brace lists are only
+ * meaningful for globals/statics (emitted directly as .data bytes) -
+ * this codebase never initializes a plain (stack) local array/struct
+ * with a brace list, only scalars, so codegen never needs to lower a
+ * brace list into runtime store instructions. */
+ static node_t *parse_initializer(parser_t *p) {
+ if (check(p, TK_LBRACE)) {
+ advance(p);
+ node_t *list = node_new(ND_BLOCK); /* reused as a plain node chain holder */
+ node_t *tail = NULL;
+ while (!check(p, TK_RBRACE)) {
+ node_t *v = parse_initializer(p);
+ if (!list->list) list->list = v; else tail->next = v;
+ tail = v;
+ if (!match(p, TK_COMMA)) break;
+ }
+ expect(p, TK_RBRACE);
+ return list;
+ }
+ return parse_assign(p);
+ }
+
+ /* ---------------- statements ---------------- */
+
+ static node_t *parse_one_local_decl(parser_t *p, type_t *base, int is_static) {
+ char name[MAX_NAME];
+ type_t *t = parse_declarator(p, base, name, NULL, NULL);
+ sym_t *s = add_local(p, name, t, is_static);
+ node_t *n = node_new(ND_VAR_DECL);
+ safe_strcpy(n->name, name, MAX_NAME);
+ n->type = t;
+ n->is_static_local = is_static;
+ safe_strcpy(n->asm_label, s->asm_label, MAX_NAME);
+ n->frame_offset = s->frame_offset;
+ if (match(p, TK_ASSIGN)) n->init = parse_initializer(p);
+ return n;
+ }
+
+ static node_t *parse_local_decl_stmt(parser_t *p) {
+ int is_static = 0, is_extern = 0;
+ type_t *base = parse_type_spec(p, &is_static, &is_extern);
+ node_t *head = parse_one_local_decl(p, base, is_static);
+ node_t *tail = head;
+ while (match(p, TK_COMMA)) {
+ node_t *n = parse_one_local_decl(p, base, is_static);
+ tail->next = n;
+ tail = n;
+ }
+ expect(p, TK_SEMI);
+ return head;
+ }
+
+ static node_t *parse_stmt(parser_t *p) {
+ if (check(p, TK_LBRACE)) return parse_block(p);
+
+ if (match_kw(p, "if")) {
+ expect(p, TK_LPAREN);
+ node_t *n = node_new(ND_IF);
+ n->a = parse_expr(p);
+ expect(p, TK_RPAREN);
+ n->b = parse_stmt(p);
+ if (match_kw(p, "else")) n->c = parse_stmt(p);
+ return n;
+ }
+ if (match_kw(p, "while")) {
+ expect(p, TK_LPAREN);
+ node_t *n = node_new(ND_WHILE);
+ n->a = parse_expr(p);
+ expect(p, TK_RPAREN);
+ n->b = parse_stmt(p);
+ return n;
+ }
+ if (match_kw(p, "do")) {
+ node_t *n = node_new(ND_DOWHILE);
+ n->b = parse_stmt(p);
+ if (!match_kw(p, "while")) perr(p, "expected 'while'");
+ expect(p, TK_LPAREN);
+ n->a = parse_expr(p);
+ expect(p, TK_RPAREN);
+ expect(p, TK_SEMI);
+ return n;
+ }
+ if (match_kw(p, "for")) {
+ expect(p, TK_LPAREN);
+ node_t *n = node_new(ND_FOR);
+ if (!check(p, TK_SEMI)) {
+ n->a = is_type_start(p) ? parse_local_decl_stmt(p) : NULL;
+ if (!n->a) { n->a = parse_expr(p); expect(p, TK_SEMI); }
+ } else { advance(p); }
+ if (!check(p, TK_SEMI)) n->b = parse_expr(p);
+ expect(p, TK_SEMI);
+ if (!check(p, TK_RPAREN)) n->c = parse_expr(p);
+ expect(p, TK_RPAREN);
+ n->d = parse_stmt(p);
+ return n;
+ }
+ if (match_kw(p, "return")) {
+ node_t *n = node_new(ND_RETURN);
+ if (!check(p, TK_SEMI)) n->a = parse_expr(p);
+ expect(p, TK_SEMI);
+ return n;
+ }
+ if (match_kw(p, "break")) { expect(p, TK_SEMI); return node_new(ND_BREAK); }
+ if (match_kw(p, "continue")) { expect(p, TK_SEMI); return node_new(ND_CONTINUE); }
+ if (match(p, TK_SEMI)) return node_new(ND_NOP);
+
+ if (is_type_start(p)) return parse_local_decl_stmt(p);
+
+ node_t *n = node_new(ND_EXPR_STMT);
+ n->a = parse_expr(p);
+ expect(p, TK_SEMI);
+ return n;
+ }
+
+ static node_t *parse_block(parser_t *p) {
+ expect(p, TK_LBRACE);
+ node_t *blk = node_new(ND_BLOCK);
+ node_t *tail = NULL;
+ while (!check(p, TK_RBRACE)) {
+ node_t *s = parse_stmt(p);
+ if (!blk->list) blk->list = s; else tail->next = s;
+ tail = s;
+ while (tail->next) tail = tail->next; /* parse_local_decl_stmt may chain multiple decls */
+ }
+ expect(p, TK_RBRACE);
+ return blk;
+ }The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/parser.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ tail -3 /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/parser.c
expect(p, TK_RBRACE);
return blk;
}- expect(p, TK_RBRACE);
- return blk;
- }
+ expect(p, TK_RBRACE);
+ return blk;
+ }
+
+ /* ---------------- top level ---------------- */
+
+ static node_t *parse_global_var(parser_t *p, const char *name, type_t *t, int is_extern) {
+ add_global(p, name, t);
+ node_t *n = node_new(ND_VAR_DECL);
+ safe_strcpy(n->name, name, MAX_NAME);
+ n->type = t;
+ n->is_global = 1;
+ n->is_extern_decl = is_extern;
+ safe_strcpy(n->asm_label, name, MAX_NAME);
+ if (match(p, TK_ASSIGN)) n->init = parse_initializer(p);
+ return n;
+ }
+
+ static node_t *parse_func(parser_t *p, const char *name, type_t *functy,
+ char param_names[][MAX_NAME], int n_params, int is_static) {
+ sym_t *fs = add_global(p, name, functy);
+ (void)is_static;
+
+ if (match(p, TK_SEMI)) return NULL; /* prototype only */
+
+ p->locals = NULL;
+ p->frame_size = 0;
+ p->cur_func_ret = functy->base;
+
+ node_t *n = node_new(ND_FUNC_DEF);
+ safe_strcpy(n->name, name, MAX_NAME);
+ n->type = functy->base;
+ safe_strcpy(n->asm_label, name, MAX_NAME);
+
+ /* cdecl: EBP+8, +12, ... for params, in order */
+ type_t *pt = functy->param;
+ int off = 8;
+ node_t *ptail = NULL;
+ for (int i = 0; i < n_params && pt; i++, pt = pt->next) {
+ sym_t *s = calloc(1, sizeof(sym_t));
+ safe_strcpy(s->name, param_names[i], MAX_NAME);
+ s->type = pt;
+ s->frame_offset = off;
+ s->next = p->locals;
+ p->locals = s;
+ off += 4;
+
+ node_t *pn = node_new(ND_VAR_DECL);
+ safe_strcpy(pn->name, param_names[i], MAX_NAME);
+ pn->type = pt;
+ pn->frame_offset = s->frame_offset;
+ if (!n->list) n->list = pn; else ptail->next = pn;
+ ptail = pn;
+ }
+
+ n->b = parse_block(p);
+ n->num = p->frame_size;
+ fs->type = functy;
+ return n;
+ }
+
+ static node_t *parse_typedef(parser_t *p) {
+ advance(p); /* 'typedef' */
+ type_t *base = parse_type_spec(p, NULL, NULL);
+ char name[MAX_NAME];
+ type_t *t = parse_declarator(p, base, name, NULL, NULL);
+ expect(p, TK_SEMI);
+ sym_t *s = calloc(1, sizeof(sym_t));
+ safe_strcpy(s->name, name, MAX_NAME);
+ s->type = t;
+ s->is_typedef = 1;
+ s->next = p->globals;
+ p->globals = s;
+ return node_new(ND_NOP);
+ }
+
+ node_t *cc_parse(token_t *toks, int n_toks) {
+ g_int_type = type_new(TY_INT);
+ g_char_type = type_new(TY_CHAR);
+
+ parser_t p = {0};
+ p.toks = toks;
+ p.n = n_toks;
+
+ node_t *head = NULL, *tail = NULL;
+ while (!check(&p, TK_EOF)) {
+ node_t *decl = NULL;
+ if (is_kw(&p, "typedef")) {
+ decl = parse_typedef(&p);
+ } else {
+ int is_static = 0, is_extern = 0;
+ type_t *base = parse_type_spec(&p, &is_static, &is_extern);
+ if (match(&p, TK_SEMI)) {
+ decl = node_new(ND_NOP); /* lone `struct Foo { ... };` with no variable */
+ } else {
+ char param_names[16][MAX_NAME];
+ int n_params = 0;
+ char name[MAX_NAME];
+ type_t *t = parse_declarator(&p, base, name, param_names, &n_params);
+ if (t->kind == TY_FUNC) {
+ decl = parse_func(&p, name, t, param_names, n_params, is_static);
+ } else {
+ decl = parse_global_var(&p, name, t, is_extern);
+ while (match(&p, TK_COMMA)) {
+ char name2[MAX_NAME];
+ type_t *t2 = parse_declarator(&p, base, name2, NULL, NULL);
+ node_t *more = parse_global_var(&p, name2, t2, is_extern);
+ decl->next = more;
+ }
+ expect(&p, TK_SEMI);
+ }
+ }
+ }
+ if (decl) {
+ if (!head) head = decl; else tail->next = decl;
+ tail = decl;
+ while (tail->next) tail = tail->next;
+ }
+ }
+ return head;
+ }The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/parser.c has been updated successfully. (file state is current in your context — no need to Read it back)
#include "cc.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv) {
const char *inpath = NULL;
const char *outpath = NULL;
cc_target_t target = TARGET_KERNEL;
int parse_only = 0;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) { outpath = argv[++i]; }
else if (strcmp(argv[i], "-I") == 0 && i + 1 < argc) {
if (g_n_include_dirs < MAX_INCLUDE_DIRS) g_include_dirs[g_n_include_dirs++] = argv[++i];
}
else if (strncmp(argv[i], "--target=", 9) == 0) {
target = strcmp(argv[i] + 9, "host") == 0 ? TARGET_HOST : TARGET_KERNEL;
}
else if (strcmp(argv[i], "--parse-only") == 0) { parse_only = 1; }
else { inpath = argv[i]; }
}
if (!inpath) {
fprintf(stderr, "usage: cc [-I dir]... [--target=kernel|host] -o <out.S> <in.c>\n");
return 1;
}
static token_t toks[MAX_TOKENS];
int n_toks = 0;
pp_process_file(inpath, toks, &n_toks, MAX_TOKENS);
node_t *prog = cc_parse(toks, n_toks);
if (parse_only) {
printf("cc: parsed %s OK\n", inpath);
return 0;
}
if (!outpath) {
fprintf(stderr, "cc: -o <out.S> required\n");
return 1;
}
FILE *out = fopen(outpath, "w");
if (!out) { perror("cc: fopen"); return 1; }
codegen_emit(prog, out, target);
fclose(out);
return 0;
}
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/main.c (file state is current in your context — no need to Read it back)
#include "cc.h"
#include <stdio.h>
void codegen_emit(node_t *prog, void *out_file, cc_target_t target) {
(void)prog; (void)out_file; (void)target;
/* placeholder - filled in once the front end (Phase T2) is verified */
}
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/codegen.c (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc gcc -Wall -Wextra -O0 -g -o /tmp/mycc lexer.c preprocessor.c types.c parser.c codegen.c main.c 2>&1
parser.c:176:14: warning: ‘/*’ within comment [-Wcomment] 176 | * *is_static/*is_extern. Does not consume any declarator. */
$ grep -n "is_static/\*is_extern" /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/parser.c
176: * *is_static/*is_extern. Does not consume any declarator. */
- * *is_static/*is_extern. Does not consume any declarator. */ + * is_static and is_extern. Does not consume any declarator. */
The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/parser.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc gcc -Wall -Wextra -O0 -g -o /tmp/mycc lexer.c preprocessor.c types.c parser.c codegen.c main.c 2>&1 echo "compile exit=$?"
compile exit=0
#include <stdint.h>
int add(int a, int b) {
return a + b;
}
int main(void) {
int x = add(2, 3);
return x;
}
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/tests/toolchain/t01_basic.c (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude /tmp/mycc -I toolchain/include --parse-only tests/toolchain/t01_basic.c echo "exit=$?"
cc: tests/toolchain/t01_basic.c:1: expected "file" after #include exit=1
- void lexer_init(lexer_t *lx, const char *src, const char *filename); - token_t lexer_next(lexer_t *lx); - /* Un-reads one token (at most one at a time) - the preprocessor's - * #define handling needs this to detect "end of macro body line" by - * reading one token past it. */ - void lexer_push_back(lexer_t *lx, token_t t); + void lexer_init(lexer_t *lx, const char *src, const char *filename); + token_t lexer_next(lexer_t *lx); + /* Un-reads one token (at most one at a time) - the preprocessor's + * #define handling needs this to detect "end of macro body line" by + * reading one token past it. */ + void lexer_push_back(lexer_t *lx, token_t t); + /* `<...>` header-name syntax (#include <foo.h>) is lexically special in + * C - not a normal token, since `<`/`.`/`>` would otherwise tokenize as + * operators. Only valid to call right after seeing the `include` + * identifier. Returns a TK_STR token with the raw header name. */ + token_t lexer_next_angle_include(lexer_t *lx);
The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/cc.h has been updated successfully. (file state is current in your context — no need to Read it back)
$ tail -10 /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/lexer.c
case '^': k = TK_CARET; break;
case '~': k = TK_TILDE; break;
default:
fprintf(stderr, "cc: %s:%d: unexpected character '%c'\n", lx->filename, lx->line, c);
exit(1);
}
token_t t = mk(lx, k);
t.bol = was_bol;
return t;
}- token_t t = mk(lx, k);
- t.bol = was_bol;
- return t;
- }
+ token_t t = mk(lx, k);
+ t.bol = was_bol;
+ return t;
+ }
+
+ token_t lexer_next_angle_include(lexer_t *lx) {
+ while (peekc(lx) == ' ' || peekc(lx) == '\t') lx->pos++;
+ token_t t = mk(lx, TK_STR);
+ if (peekc(lx) != '<') { fprintf(stderr, "cc: %s:%d: expected <file> after #include\n", lx->filename, lx->line); exit(1); }
+ lx->pos++;
+ int n = 0;
+ while (peekc(lx) != -1 && peekc(lx) != '>' && n < (int)sizeof(t.text) - 1) {
+ t.text[n++] = (char)advc(lx);
+ }
+ t.text[n] = '\0';
+ if (peekc(lx) == '>') lx->pos++;
+ return t;
+ }The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/lexer.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ grep -n "include\") == 0" -A 8 /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/preprocessor.c
100: if (strcmp(dir.text, "include") == 0) {
101- token_t path_tok = lexer_next(lx);
102- int is_quote = path_tok.kind == TK_STR;
103- if (path_tok.kind != TK_STR) { fprintf(stderr, "cc: %s:%d: expected \"file\" after #include\n", filename, dir.line); exit(1); }
104- char resolved[600];
105- if (!resolve_include(cur_dir, path_tok.text, is_quote, resolved, sizeof(resolved))) {
106- fprintf(stderr, "cc: %s:%d: cannot find include file '%s'\n", filename, dir.line, path_tok.text);
107- exit(1);
108- }- if (strcmp(dir.text, "include") == 0) {
- token_t path_tok = lexer_next(lx);
- int is_quote = path_tok.kind == TK_STR;
- if (path_tok.kind != TK_STR) { fprintf(stderr, "cc: %s:%d: expected \"file\" after #include\n", filename, dir.line); exit(1); }
+ if (strcmp(dir.text, "include") == 0) {
+ size_t peek = lx->pos;
+ while (peek < lx->len && (lx->src[peek] == ' ' || lx->src[peek] == '\t')) peek++;
+ int is_quote = peek < lx->len && lx->src[peek] == '"';
+ token_t path_tok = is_quote ? lexer_next(lx) : lexer_next_angle_include(lx);
+ if (is_quote && path_tok.kind != TK_STR) { fprintf(stderr, "cc: %s:%d: expected \"file\" after #include\n", filename, dir.line); exit(1); }The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/preprocessor.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc gcc -Wall -Wextra -O0 -g -o /tmp/mycc lexer.c preprocessor.c types.c parser.c codegen.c main.c 2>&1 echo "compile exit=$?" cd /home/martin/workspace/prototypes/boot-to-claude /tmp/mycc -I toolchain/include --parse-only tests/toolchain/t01_basic.c echo "exit=$?"
compile exit=0 cc: parsed tests/toolchain/t01_basic.c OK exit=0
#include <stdint.h>
#include <stddef.h>
typedef struct {
uint8_t used;
char name[27];
uint32_t size;
uint32_t blocks[8];
} __attribute__((packed)) inode_t;
typedef union {
int32_t i;
char *s;
} value_t;
typedef void (*read_fn_t)(uint32_t block, void *buf);
static read_fn_t g_reader;
static inode_t g_inodes[64];
int find_free(int max) {
for (int i = 0; i < max; i++) {
if (!g_inodes[i].used) {
return i;
}
}
return -1;
}
void set_reader(read_fn_t fn) {
g_reader = fn;
}
int sum_blocks(inode_t *ino) {
int total = 0;
int i = 0;
while (i < 8) {
total = total + (int)ino->blocks[i];
i++;
}
return total;
}
int main(void) {
value_t v;
v.i = 42;
int idx = find_free(64);
return idx + v.i;
}
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/tests/toolchain/t02_types.c (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude /tmp/mycc -I toolchain/include --parse-only tests/toolchain/t02_types.c echo "exit=$?"
cc: parsed tests/toolchain/t02_types.c OK exit=0
1 #pragma once
2 /* AS: NASM-subset assembler. See docs/toolchain.md for exactly what's
3 * supported and why. Native host tool (build infrastructure), not OS
4 * code - built with the bootstrap compiler in Phase T1/T7, and later
5 * with CC itself in Phase T8. */
6
7 #include <stdint.h>
8 #include <stddef.h>
9
10 /* strncpy doesn't guarantee null-termination when src is >= dstsize;
11 * this always does. */
12 static inline void safe_strcpy(char *dst, const char *src, size_t dstsize) {
13 size_t i = 0;
14 for (; i + 1 < dstsize && src[i]; i++) dst[i] = src[i];
15 dst[i] = '\0';
16 }
17
18 #define MAX_SYMS 512
19 #define MAX_STMTS 4096
20 #define MAX_RELOCS 256
21 #define MAX_LINE 512
22 #define MAX_NAME 64
23 #define MAX_SECTIONS 8
24
25 typedef enum { SEC_NONE = -1, SEC_TEXT = 0, SEC_DATA, SEC_BSS, SEC_RODATA } section_kind_t;
26
27 /* A "section" here is one of a small fixed set of NASM section names we
28 * actually see (`.text`, `.text.entry`, `.data`, `.bss`), normalized to
29 * section_kind_t - `.text` and `.text.entry` both map to SEC_TEXT
30 * (entry.S relies on `.text.entry` sorting first via the linker script,
31 * which is LD's concern, not AS's - AS just needs one bucket per byte
32 * stream). */
33
34 typedef struct {
35 char name[MAX_NAME];
36 int defined; /* 0 = only seen via `extern` so far */
37 int is_extern;
38 int is_global;
39 int is_equ;
40 int64_t equ_value; /* valid if is_equ */
41 section_kind_t section; /* valid if defined && !is_equ */
42 uint32_t offset; /* byte offset within its section, valid if defined && !is_equ */
43 } symbol_t;
44
45 /* Operand forms actually needed - see docs/toolchain.md. */
46 typedef enum {
47 OPND_NONE,
48 OPND_REG,
49 OPND_IMM, /* constant expression, possibly symbol-relative (see sym[]) */
50 OPND_MEM, /* [sym], [sym+disp], [reg], [reg+disp] */
51 OPND_FAR, /* SEG:OFFSET - only for far jmp; imm.reg-less seg value + sym offset */
52 } operand_kind_t;
53
54 typedef enum {
55 R_NONE = 0,
56 R_AL, R_CL, R_DL, R_BL, R_AH, R_CH, R_DH, R_BH,
57 R_AX, R_CX, R_DX, R_BX, R_SP, R_BP, R_SI, R_DI,
58 R_EAX, R_ECX, R_EDX, R_EBX, R_ESP, R_EBP, R_ESI, R_EDI,
59 R_ES, R_CS, R_SS, R_DS, R_FS, R_GS,
60 R_CR0,
61 R_DX_PORT, /* `dx` used as an I/O port operand - same encoding as R_DX, kept distinct only for clarity at parse time */
62 } reg_t;
63
64 typedef struct {
65 operand_kind_t kind;
66 reg_t reg; /* OPND_REG, or base register for OPND_MEM (R_NONE = no base, direct address) */
67 int has_sym; /* OPND_IMM/OPND_MEM/OPND_FAR: value is symbol-relative */
68 char sym[MAX_NAME];
69 int64_t disp; /* constant part; for OPND_IMM with no symbol, this is the whole value */
70 int size_override; /* 0 = none, 1/2/4 = byte/word/dword */
71 int64_t far_seg; /* OPND_FAR only: segment selector constant part */
72 int far_seg_has_sym; /* OPND_FAR only: segment selector is symbol-relative (e.g. a forward-referenced EQU) */
73 char far_seg_sym[MAX_NAME];
74 } operand_t;
75
76 typedef struct {
77 char mnemonic[16];
78 operand_t ops[2];
79 int nops;
80 section_kind_t section;
81 uint32_t offset; /* byte offset within section where this statement's bytes start */
82 uint32_t length; /* encoded length in bytes, fixed once computed in pass 1 */
83 int bits; /* 16 or 32 - effective mode when this statement was parsed */
84 int line_no;
85 } stmt_t;
86
87 typedef enum { RELOC_PC32, RELOC_ABS32 } reloc_kind_t;
88
89 typedef struct {
90 section_kind_t section;
91 uint32_t offset; /* where the 4-byte field to patch lives, within `section` */
92 char sym[MAX_NAME];
93 int32_t addend;
94 reloc_kind_t kind;
95 } reloc_t;
96
97 typedef struct {
98 symbol_t syms[MAX_SYMS];
99 int nsyms;
100
101 stmt_t stmts[MAX_STMTS];
102 int nstmts;
103
104 reloc_t relocs[MAX_RELOCS];
105 int nrelocs;
106
107 uint8_t sec_bytes[MAX_SECTIONS][65536];
108 uint32_t sec_len[MAX_SECTIONS];
109 int sec_used[MAX_SECTIONS]; /* has this section appeared in the source at all */
110
111 uint32_t org; /* -f bin only */
112 int have_org;
113
114 char last_global_label[MAX_NAME]; /* for `.local` label scoping */
115 } asm_ctx_t;
116
117 /* lexer.c */
118 typedef enum {
119 TK_EOF, TK_NEWLINE, TK_IDENT, TK_NUMBER, TK_STRING,
120 TK_LBRACK, TK_RBRACK, TK_COMMA, TK_COLON, TK_PLUS, TK_MINUS,
121 TK_STAR, TK_SLASH, TK_SHL, TK_SHR, TK_LPAREN, TK_RPAREN, TK_DOLLAR, TK_DOLLARDOLLAR,
122 } tok_kind_t;
123
124 typedef struct {
125 tok_kind_t kind;
126 char text[MAX_LINE];
127 int64_t number;
128 } token_t;
129
130 typedef struct {
131 const char *src;
132 size_t pos;
133 size_t len;
134 } lexer_t;
135
136 void lexer_init(lexer_t *lx, const char *src);
137 token_t lexer_next(lexer_t *lx); /* stops at end of line -> TK_NEWLINE; ';' comments skipped */
138
139 /* parser.c: pass 1 - reads the whole source, populates ctx (symbols with
140 * addresses, statement list with fixed lengths), handling directives
141 * immediately as encountered. */
142 int as_parse_pass1(asm_ctx_t *ctx, const char *src, const char *filename);
143
144 /* encode.c: pass 2 - walks ctx->stmts again and emits real bytes into
145 * ctx->sec_bytes[]/sec_len[], plus ctx->relocs[] for anything not
146 * resolvable within the same section. */
147 int as_encode_pass2(asm_ctx_t *ctx);
148
149 /* Shared by pass 1 (length-only, strict=0: unresolved symbols read as 0,
150 * no error, no relocations) and pass 2 (strict=1: unresolved local
151 * symbols are a hard error; extern/cross-section symbols emit a
152 * relocation instead). Always returns the encoded length; `out` (if
153 * non-NULL) receives the bytes. */
154 int stmt_encode(asm_ctx_t *ctx, stmt_t *st, uint8_t *out, int strict);
155
156 /* symbol/expr helpers shared by parser.c and encode.c */
157 symbol_t *sym_find(asm_ctx_t *ctx, const char *name);
158 symbol_t *sym_get_or_create(asm_ctx_t *ctx, const char *name);
159
160 /* bin_writer.c / elf_writer.c */
161 int write_bin(asm_ctx_t *ctx, const char *path);
162 int write_elf32(asm_ctx_t *ctx, const char *path);
163 1 #include "as.h"
2
3 #include <stdio.h>
4 #include <stdlib.h>
5 #include <string.h>
6
7 static int regnum(reg_t r) {
8 switch (r) {
9 case R_AL: case R_AX: case R_EAX: return 0;
10 case R_CL: case R_CX: case R_ECX: return 1;
11 case R_DL: case R_DX: case R_EDX: return 2;
12 case R_BL: case R_BX: case R_EBX: return 3;
13 case R_AH: case R_SP: case R_ESP: return 4;
14 case R_CH: case R_BP: case R_EBP: return 5;
15 case R_DH: case R_SI: case R_ESI: return 6;
16 case R_BH: case R_DI: case R_EDI: return 7;
17 case R_ES: return 0;
18 case R_CS: return 1;
19 case R_SS: return 2;
20 case R_DS: return 3;
21 case R_FS: return 4;
22 case R_GS: return 5;
23 case R_CR0: return 0;
24 default: return 0;
25 }
26 }
27
28 static int is_reg32(reg_t r) { return r >= R_EAX && r <= R_EDI; }
29 static int is_reg16(reg_t r) { return r >= R_AX && r <= R_DI; }
30 static int is_reg8(reg_t r) { return r >= R_AL && r <= R_BH; }
31 static int is_sreg(reg_t r) { return r >= R_ES && r <= R_GS; }
32
33 /* --- growable emission into `out` (NULL in dry-run/length-only mode) --- */
34
35 typedef struct {
36 uint8_t *out;
37 int len;
38 } emitter_t;
39
40 static void e8(emitter_t *e, uint8_t b) {
41 if (e->out) e->out[e->len] = b;
42 e->len++;
43 }
44 static void e16(emitter_t *e, uint16_t v) { e8(e, v & 0xFF); e8(e, (v >> 8) & 0xFF); }
45 static void e32(emitter_t *e, uint32_t v) {
46 e8(e, v & 0xFF); e8(e, (v >> 8) & 0xFF); e8(e, (v >> 16) & 0xFF); e8(e, (v >> 24) & 0xFF);
47 }
48
49 /* --- symbol resolution ---
50 * `strict` distinguishes pass 1 (length-only: never errors, never emits
51 * relocations, unresolved symbols read as 0) from pass 2 (real: local
52 * symbols resolve directly or, in ELF mode, a relocation is emitted for
53 * anything not resolvable within the same section at assemble time). */
54
55 static void add_reloc(asm_ctx_t *ctx, section_kind_t sec, uint32_t off, const char *sym,
56 int32_t addend, reloc_kind_t kind) {
57 if (ctx->nrelocs >= MAX_RELOCS) { fprintf(stderr, "as: too many relocations\n"); exit(1); }
58 reloc_t *r = &ctx->relocs[ctx->nrelocs++];
59 r->section = sec;
60 r->offset = off;
61 safe_strcpy(r->sym, sym, MAX_NAME);
62 r->addend = addend;
63 r->kind = kind;
64 }
65
66 /* Resolves an absolute-address reference (symbol + disp) used as an
67 * immediate/memory-displacement value. is_bin selects -f bin semantics
68 * (single flat address space, always resolvable, no relocations) vs.
69 * -f elf32 (may need a relocation). Writes the resolved/placeholder
70 * value via e32/e16 at the call site - this just computes it. */
71 static int64_t resolve_abs(asm_ctx_t *ctx, int is_bin, const char *sym, int64_t disp,
72 section_kind_t patch_sec, uint32_t patch_off, int strict) {
73 if (!sym[0]) return disp;
74 symbol_t *s = sym_find(ctx, sym);
75 if (s && s->is_equ) return s->equ_value + disp; /* plain number, never org-adjusted */
76 if (is_bin) {
77 if (!s || !s->defined) {
78 if (strict) { fprintf(stderr, "as: undefined symbol '%s'\n", sym); exit(1); }
79 return disp;
80 }
81 return (int64_t)ctx->org + (int64_t)s->offset + disp;
82 }
83 /* ELF: absolute references always need a relocation - the section's
84 * final base address isn't known until link time. x86 ELF uses REL
85 * (not RELA) relocations: the addend has no dedicated field, it must
86 * be baked into the bytes at the relocated location, which is why
87 * this returns `disp` (the addend) rather than 0 - LD will add the
88 * resolved symbol value on top of whatever's already there. */
89 if (strict) {
90 add_reloc(ctx, patch_sec, patch_off, sym, (int32_t)disp, RELOC_ABS32);
91 }
92 return disp;
93 }
94
95 /* Resolves a PC-relative call/jmp target. Same-section same-file targets
96 * fold to a fixed relative displacement directly; anything else (extern,
97 * or -f bin's "always resolvable" absolute model) is handled by the
98 * caller via this return value or a relocation. `next_ip_rel` is the
99 * SECTION-RELATIVE position right after the relative field (i.e. NOT
100 * org-adjusted - this function adds org itself when is_bin, consistently
101 * with how it computes the target, since bin-mode "addresses" are
102 * `org + offset` throughout). */
103 static int64_t resolve_pcrel(asm_ctx_t *ctx, int is_bin, const char *sym, int64_t disp,
104 section_kind_t cur_sec, int64_t next_ip_rel,
105 section_kind_t patch_sec, uint32_t patch_off, int strict,
106 int field_bytes) {
107 if (!sym[0]) {
108 /* Bare constant target address, e.g. `jmp KERNEL_LOAD_ADDR`
109 * where KERNEL_LOAD_ADDR is an EQU - disp is already the
110 * absolute target, no symbol lookup needed. */
111 int64_t next_ip = is_bin ? (int64_t)ctx->org + next_ip_rel : next_ip_rel;
112 return disp - next_ip;
113 }
114 symbol_t *s = sym_find(ctx, sym);
115 if (is_bin) {
116 if (!s || !s->defined) {
117 if (strict) { fprintf(stderr, "as: undefined symbol '%s'\n", sym); exit(1); }
118 return 0;
119 }
120 int64_t target = (int64_t)ctx->org + (int64_t)s->offset + disp;
121 int64_t next_ip = (int64_t)ctx->org + next_ip_rel;
122 return target - next_ip;
123 }
124 if (s && s->defined && s->section == cur_sec) {
125 return ((int64_t)s->offset + disp) - next_ip_rel;
126 }
127 /* Same REL-vs-RELA reasoning as resolve_abs: the standard x86 addend
128 * for a rel32 call/jmp is -field_bytes (the CPU measures the
129 * displacement from the end of the instruction), and that addend
130 * must be baked into the bytes, not left as 0. */
131 int32_t addend = (int32_t)(disp - field_bytes);
132 if (strict) {
133 add_reloc(ctx, patch_sec, patch_off, sym, addend, RELOC_PC32);
134 }
135 return addend;
136 }
137
138 /* --- the encoder --- */
139
140 int stmt_encode(asm_ctx_t *ctx, stmt_t *st, uint8_t *out, int strict) {
141 emitter_t e = { out, 0 };
142 const char *m = st->mnemonic;
143 operand_t *a = &st->ops[0];
144 operand_t *b = &st->ops[1];
145 int is_bin = ctx->have_org;
146
147 /* --- pseudo-ops for data directives: one scalar value per stmt --- */
148 if (strcmp(m, ".db") == 0 || strcmp(m, ".dw") == 0 || strcmp(m, ".dd") == 0 || strcmp(m, ".dq") == 0) {
149 int width = strcmp(m, ".db") == 0 ? 1 : strcmp(m, ".dw") == 0 ? 2 : strcmp(m, ".dd") == 0 ? 4 : 8;
150 int64_t v = a->has_sym
151 ? resolve_abs(ctx, is_bin, a->sym, a->disp, st->section, st->offset, strict)
152 : a->disp;
153 if (width == 1) e8(&e, (uint8_t)v);
154 else if (width == 2) e16(&e, (uint16_t)v);
155 else if (width == 4) e32(&e, (uint32_t)v);
156 else { e32(&e, (uint32_t)v); e32(&e, (uint32_t)(v >> 32)); }
157 return e.len;
158 }
159 if (strcmp(m, ".resb") == 0) {
160 for (int64_t i = 0; i < a->disp; i++) e8(&e, 0);
161 return e.len;
162 }
163 if (strcmp(m, ".pad") == 0) {
164 /* `times N db V` folds to one .pad stmt: a->disp = N, b->disp = V */
165 for (int64_t i = 0; i < a->disp; i++) e8(&e, (uint8_t)b->disp);
166 return e.len;
167 }
168
169 /* --- real instructions --- */
170
171 if (strcmp(m, "xor") == 0 || strcmp(m, "or") == 0 || strcmp(m, "sub") == 0 || strcmp(m, "add") == 0) {
172 int ext = strcmp(m, "add") == 0 ? 0 : strcmp(m, "or") == 0 ? 1 : strcmp(m, "sub") == 0 ? 5 : 6; /* xor */
173 if (a->kind == OPND_REG && b->kind == OPND_REG && is_reg32(a->reg) && is_reg32(b->reg)) {
174 e8(&e, (uint8_t)(0x01 + ext * 8));
175 e8(&e, (uint8_t)(0xC0 | (regnum(b->reg) << 3) | regnum(a->reg)));
176 return e.len;
177 }
178 if (a->kind == OPND_REG && b->kind == OPND_REG && is_reg16(a->reg) && is_reg16(b->reg)) {
179 e8(&e, (uint8_t)(0x01 + ext * 8));
180 e8(&e, (uint8_t)(0xC0 | (regnum(b->reg) << 3) | regnum(a->reg)));
181 return e.len;
182 }
183 if (a->kind == OPND_REG && b->kind == OPND_REG && is_reg8(a->reg) && is_reg8(b->reg)) {
184 e8(&e, (uint8_t)(0x00 + ext * 8));
185 e8(&e, (uint8_t)(0xC0 | (regnum(b->reg) << 3) | regnum(a->reg)));
186 return e.len;
187 }
188 if (a->kind == OPND_REG && a->reg == R_AL && b->kind == OPND_IMM) {
189 e8(&e, (uint8_t)(0x04 + ext * 8));
190 e8(&e, (uint8_t)b->disp);
191 return e.len;
192 }
193 if (a->kind == OPND_REG && a->reg == R_EAX && b->kind == OPND_IMM) {
194 e8(&e, 0x66);
195 e8(&e, (uint8_t)(0x05 + ext * 8));
196 e32(&e, (uint32_t)b->disp);
197 return e.len;
198 }
199 if (a->kind == OPND_MEM && !a->reg && b->kind == OPND_REG && is_reg32(b->reg)) {
200 e8(&e, 0x66);
201 e8(&e, (uint8_t)(0x01 + ext * 8));
202 e8(&e, (uint8_t)(0x06 | (regnum(b->reg) << 3)));
203 e16(&e, (uint16_t)resolve_abs(ctx, is_bin, a->sym, a->disp, st->section, st->offset + (uint32_t)e.len, strict));
204 return e.len;
205 }
206 if (a->kind == OPND_MEM && !a->reg && b->kind == OPND_REG && is_reg16(b->reg)) {
207 e8(&e, (uint8_t)(0x01 + ext * 8));
208 e8(&e, (uint8_t)(0x06 | (regnum(b->reg) << 3)));
209 e16(&e, (uint16_t)resolve_abs(ctx, is_bin, a->sym, a->disp, st->section, st->offset + (uint32_t)e.len, strict));
210 return e.len;
211 }
212 }
213
214 if (strcmp(m, "cmp") == 0) {
215 if (a->kind == OPND_REG && is_reg16(a->reg) && b->kind == OPND_IMM) {
216 e8(&e, 0x81);
217 e8(&e, (uint8_t)(0xF8 | regnum(a->reg)));
218 e16(&e, (uint16_t)b->disp);
219 return e.len;
220 }
221 }
222
223 if (strcmp(m, "mov") == 0) {
224 if (is_sreg(a->reg) || is_sreg(b->reg)) {
225 reg_t sreg = is_sreg(a->reg) ? a->reg : b->reg;
226 e8(&e, 0x8E);
227 e8(&e, (uint8_t)(0xC0 | (regnum(sreg) << 3) | regnum(b->reg == sreg ? a->reg : b->reg)));
228 return e.len;
229 }
230 if (a->reg == R_CR0 && b->kind == OPND_REG && is_reg32(b->reg)) {
231 e8(&e, 0x0F); e8(&e, 0x22);
232 e8(&e, (uint8_t)(0xC0 | regnum(b->reg)));
233 return e.len;
234 }
235 if (b->reg == R_CR0 && a->kind == OPND_REG && is_reg32(a->reg)) {
236 /* MOV r32, CR0: modrm reg field = CR0 (0), rm field = dest reg */
237 e8(&e, 0x0F); e8(&e, 0x20);
238 e8(&e, (uint8_t)(0xC0 | regnum(a->reg)));
239 return e.len;
240 }
241 if (a->kind == OPND_REG && b->kind == OPND_IMM && is_reg8(a->reg)) {
242 e8(&e, (uint8_t)(0xB0 + regnum(a->reg)));
243 int64_t v = b->has_sym ? resolve_abs(ctx, is_bin, b->sym, b->disp, st->section, st->offset + 1, strict) : b->disp;
244 e8(&e, (uint8_t)v);
245 return e.len;
246 }
247 if (a->kind == OPND_REG && b->kind == OPND_IMM && is_reg16(a->reg) && st->bits == 16) {
248 e8(&e, (uint8_t)(0xB8 + regnum(a->reg)));
249 int64_t v = b->has_sym ? resolve_abs(ctx, is_bin, b->sym, b->disp, st->section, st->offset + 1, strict) : b->disp;
250 e16(&e, (uint16_t)v);
251 return e.len;
252 }
253 if (a->kind == OPND_REG && b->kind == OPND_IMM && is_reg16(a->reg) && st->bits == 32) {
254 e8(&e, 0x66);
255 e8(&e, (uint8_t)(0xB8 + regnum(a->reg)));
256 int64_t v = b->has_sym ? resolve_abs(ctx, is_bin, b->sym, b->disp, st->section, st->offset + 2, strict) : b->disp;
257 e16(&e, (uint16_t)v);
258 return e.len;
259 }
260 if (a->kind == OPND_REG && b->kind == OPND_IMM && is_reg32(a->reg)) {
261 e8(&e, (uint8_t)(0xB8 + regnum(a->reg)));
262 int64_t v = b->has_sym
263 ? resolve_abs(ctx, is_bin, b->sym, b->disp, st->section, st->offset + 1, strict)
264 : b->disp;
265 e32(&e, (uint32_t)v);
266 return e.len;
267 }
268 if (a->kind == OPND_REG && b->kind == OPND_REG && is_reg16(a->reg) && is_reg16(b->reg)) {
269 e8(&e, 0x8B);
270 e8(&e, (uint8_t)(0xC0 | (regnum(a->reg) << 3) | regnum(b->reg)));
271 return e.len;
272 }
273 if (a->kind == OPND_REG && is_reg16(a->reg) && b->kind == OPND_MEM && !b->reg) {
274 e8(&e, 0x8B);
275 e8(&e, (uint8_t)(0x06 | (regnum(a->reg) << 3)));
276 e16(&e, (uint16_t)resolve_abs(ctx, is_bin, b->sym, b->disp, st->section, st->offset + (uint32_t)e.len, strict));
277 return e.len;
278 }
279 if (a->kind == OPND_MEM && !a->reg && b->kind == OPND_REG && is_reg16(b->reg)) {
280 e8(&e, 0x89);
281 e8(&e, (uint8_t)(0x06 | (regnum(b->reg) << 3)));
282 e16(&e, (uint16_t)resolve_abs(ctx, is_bin, a->sym, a->disp, st->section, st->offset + (uint32_t)e.len, strict));
283 return e.len;
284 }
285 if (a->kind == OPND_REG && is_reg32(a->reg) && b->kind == OPND_MEM && !b->reg) {
286 e8(&e, 0x66); e8(&e, 0x8B);
287 e8(&e, (uint8_t)(0x06 | (regnum(a->reg) << 3)));
288 e16(&e, (uint16_t)resolve_abs(ctx, is_bin, b->sym, b->disp, st->section, st->offset + (uint32_t)e.len, strict));
289 return e.len;
290 }
291 if (a->kind == OPND_MEM && !a->reg && b->kind == OPND_REG && is_reg32(b->reg)) {
292 e8(&e, 0x66); e8(&e, 0x89);
293 e8(&e, (uint8_t)(0x06 | (regnum(b->reg) << 3)));
294 e16(&e, (uint16_t)resolve_abs(ctx, is_bin, a->sym, a->disp, st->section, st->offset + (uint32_t)e.len, strict));
295 return e.len;
296 }
297 if (a->kind == OPND_MEM && !a->reg && b->kind == OPND_IMM && a->size_override == 2) {
298 e8(&e, 0xC7);
299 e8(&e, (uint8_t)0x06);
300 e16(&e, (uint16_t)resolve_abs(ctx, is_bin, a->sym, a->disp, st->section, st->offset + (uint32_t)e.len, strict));
301 int64_t v = b->has_sym ? resolve_abs(ctx, is_bin, b->sym, b->disp, st->section, st->offset + (uint32_t)e.len + 2, strict) : b->disp;
302 e16(&e, (uint16_t)v);
303 return e.len;
304 }
305 if (a->kind == OPND_MEM && !a->reg && b->kind == OPND_IMM && a->size_override == 4) {
306 e8(&e, 0x66); e8(&e, 0xC7);
307 e8(&e, (uint8_t)0x06);
308 e16(&e, (uint16_t)resolve_abs(ctx, is_bin, a->sym, a->disp, st->section, st->offset + (uint32_t)e.len, strict));
309 int64_t v = b->has_sym ? resolve_abs(ctx, is_bin, b->sym, b->disp, st->section, st->offset + (uint32_t)e.len + 4, strict) : b->disp;
310 e32(&e, (uint32_t)v);
311 return e.len;
312 }
313 if (a->kind == OPND_REG && is_reg32(a->reg) && b->kind == OPND_MEM && b->reg == R_ESP) {
314 e8(&e, 0x8B);
315 if (b->disp == 0) {
316 e8(&e, (uint8_t)(0x04 | (regnum(a->reg) << 3)));
317 e8(&e, 0x24);
318 } else {
319 e8(&e, (uint8_t)(0x44 | (regnum(a->reg) << 3)));
320 e8(&e, 0x24);
321 e8(&e, (uint8_t)b->disp);
322 }
323 return e.len;
324 }
325 }
326
327 if (strcmp(m, "movzx") == 0 && is_reg32(a->reg) && is_reg16(b->reg)) {
328 e8(&e, 0x66); e8(&e, 0x0F); e8(&e, 0xB7);
329 e8(&e, (uint8_t)(0xC0 | (regnum(a->reg) << 3) | regnum(b->reg)));
330 return e.len;
331 }
332
333 if (strcmp(m, "shl") == 0 && is_reg16(a->reg) && b->kind == OPND_IMM) {
334 e8(&e, 0xC1);
335 e8(&e, (uint8_t)(0xE0 | regnum(a->reg)));
336 e8(&e, (uint8_t)b->disp);
337 return e.len;
338 }
339
340 if ((strcmp(m, "jc") == 0 || strcmp(m, "jz") == 0 || strcmp(m, "je") == 0 || strcmp(m, "jbe") == 0)) {
341 uint8_t cc = strcmp(m, "jc") == 0 ? 0x82 : strcmp(m, "jbe") == 0 ? 0x86 : 0x84; /* jz/je both == */
342 if (st->bits == 16) {
343 e8(&e, 0x0F); e8(&e, cc);
344 int64_t rel = resolve_pcrel(ctx, is_bin, a->sym, a->disp, st->section,
345 (int64_t)st->offset + 4, st->section, st->offset + 2, strict, 2);
346 e16(&e, (uint16_t)rel);
347 } else {
348 e8(&e, 0x0F); e8(&e, cc);
349 int64_t rel = resolve_pcrel(ctx, is_bin, a->sym, a->disp, st->section,
350 (int64_t)st->offset + 6, st->section, st->offset + 2, strict, 4);
351 e32(&e, (uint32_t)rel);
352 }
353 return e.len;
354 }
355
356 if (strcmp(m, "jmp") == 0) {
357 if (a->kind == OPND_FAR) {
358 e8(&e, 0xEA);
359 int64_t off = resolve_abs(ctx, is_bin, a->sym, a->disp, st->section, st->offset + 1, strict);
360 e16(&e, (uint16_t)off);
361 int64_t seg = a->far_seg_has_sym
362 ? resolve_abs(ctx, is_bin, a->far_seg_sym, a->far_seg, st->section, st->offset + 3, strict)
363 : a->far_seg;
364 e16(&e, (uint16_t)seg);
365 return e.len;
366 }
367 if (st->bits == 16) {
368 e8(&e, 0xE9);
369 int64_t rel = resolve_pcrel(ctx, is_bin, a->sym, a->disp, st->section,
370 (int64_t)st->offset + 3, st->section, st->offset + 1, strict, 2);
371 e16(&e, (uint16_t)rel);
372 } else {
373 e8(&e, 0xE9);
374 int64_t rel = resolve_pcrel(ctx, is_bin, a->sym, a->disp, st->section,
375 (int64_t)st->offset + 5, st->section, st->offset + 1, strict, 4);
376 e32(&e, (uint32_t)rel);
377 }
378 return e.len;
379 }
380
381 if (strcmp(m, "call") == 0) {
382 if (st->bits == 16) {
383 e8(&e, 0xE8);
384 int64_t rel = resolve_pcrel(ctx, is_bin, a->sym, a->disp, st->section,
385 (int64_t)st->offset + 3, st->section, st->offset + 1, strict, 2);
386 e16(&e, (uint16_t)rel);
387 } else {
388 e8(&e, 0xE8);
389 int64_t rel = resolve_pcrel(ctx, is_bin, a->sym, a->disp, st->section,
390 (int64_t)st->offset + 5, st->section, st->offset + 1, strict, 4);
391 e32(&e, (uint32_t)rel);
392 }
393 return e.len;
394 }
395
396 if (strcmp(m, "push") == 0) {
397 if (a->kind == OPND_IMM) {
398 e8(&e, 0x68);
399 int64_t v = a->has_sym ? resolve_abs(ctx, is_bin, a->sym, a->disp, st->section, st->offset + 1, strict) : a->disp;
400 e32(&e, (uint32_t)v);
401 return e.len;
402 }
403 if (is_reg32(a->reg)) { e8(&e, (uint8_t)(0x50 + regnum(a->reg))); return e.len; }
404 if (is_reg16(a->reg)) { e8(&e, (uint8_t)(0x50 + regnum(a->reg))); return e.len; }
405 }
406 if (strcmp(m, "pop") == 0) {
407 if (is_reg32(a->reg) || is_reg16(a->reg)) { e8(&e, (uint8_t)(0x58 + regnum(a->reg))); return e.len; }
408 }
409
410 if (strcmp(m, "in") == 0) {
411 if (b->kind == OPND_IMM) { e8(&e, 0xE4); e8(&e, (uint8_t)b->disp); return e.len; }
412 if (b->reg == R_DX && a->reg == R_AL) { e8(&e, 0xEC); return e.len; }
413 if (b->reg == R_DX && a->reg == R_AX) { e8(&e, 0xED); return e.len; }
414 }
415 if (strcmp(m, "out") == 0) {
416 if (a->kind == OPND_IMM) { e8(&e, 0xE6); e8(&e, (uint8_t)a->disp); return e.len; }
417 if (a->reg == R_DX && b->reg == R_AL) { e8(&e, 0xEE); return e.len; }
418 if (a->reg == R_DX && b->reg == R_AX) { e8(&e, 0xEF); return e.len; }
419 }
420
421 if (strcmp(m, "int") == 0) { e8(&e, 0xCD); e8(&e, (uint8_t)a->disp); return e.len; }
422 if (strcmp(m, "lgdt") == 0) {
423 e8(&e, 0x0F); e8(&e, 0x01); e8(&e, 0x16);
424 e16(&e, (uint16_t)resolve_abs(ctx, is_bin, a->sym, a->disp, st->section, st->offset + (uint32_t)e.len, strict));
425 return e.len;
426 }
427
428 if (strcmp(m, "pusha") == 0) { e8(&e, 0x60); return e.len; }
429 if (strcmp(m, "popa") == 0) { e8(&e, 0x61); return e.len; }
430 if (strcmp(m, "lodsb") == 0) { e8(&e, 0xAC); return e.len; }
431 if (strcmp(m, "ret") == 0) { e8(&e, 0xC3); return e.len; }
432 if (strcmp(m, "hlt") == 0) { e8(&e, 0xF4); return e.len; }
433 if (strcmp(m, "cli") == 0) { e8(&e, 0xFA); return e.len; }
434 if (strcmp(m, "cld") == 0) { e8(&e, 0xFC); return e.len; }
435 if (strcmp(m, "rep") == 0 && strcmp(a->sym, "stosb") == 0) { e8(&e, 0xF3); e8(&e, 0xAA); return e.len; }
436
437 fprintf(stderr, "as: line %d: unsupported instruction form: %s\n", st->line_no, m);
438 exit(1);
439 }
440
441 int as_encode_pass2(asm_ctx_t *ctx) {
442 for (int i = 0; i < ctx->nstmts; i++) {
443 stmt_t *st = &ctx->stmts[i];
444 section_kind_t sec = st->section;
445 uint8_t *dst = ctx->sec_bytes[sec] + st->offset;
446 int len = stmt_encode(ctx, st, dst, 1);
447 if ((uint32_t)len != st->length) {
448 fprintf(stderr, "as: internal error: length mismatch for '%s' at line %d (pass1=%u pass2=%d)\n",
449 st->mnemonic, st->line_no, st->length, len);
450 return -1;
451 }
452 if (st->offset + (uint32_t)len > ctx->sec_len[sec]) {
453 ctx->sec_len[sec] = st->offset + (uint32_t)len;
454 }
455 }
456 return 0;
457 }
458 #include "as.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int regnum(reg_t r) {
switch (r) {
case R_AL: case R_AX: case R_EAX: return 0;
case R_CL: case R_CX: case R_ECX: return 1;
case R_DL: case R_DX: case R_EDX: return 2;
case R_BL: case R_BX: case R_EBX: return 3;
case R_AH: case R_SP: case R_ESP: return 4;
case R_CH: case R_BP: case R_EBP: return 5;
case R_DH: case R_SI: case R_ESI: return 6;
case R_BH: case R_DI: case R_EDI: return 7;
case R_ES: return 0;
case R_CS: return 1;
case R_SS: return 2;
case R_DS: return 3;
case R_FS: return 4;
case R_GS: return 5;
case R_CR0: return 0;
default: return 0;
}
}
static int is_reg32(reg_t r) { return r >= R_EAX && r <= R_EDI; }
static int is_reg16(reg_t r) { return r >= R_AX && r <= R_DI; }
static int is_reg8(reg_t r) { return r >= R_AL && r <= R_BH; }
static int is_sreg(reg_t r) { return r >= R_ES && r <= R_GS; }
/* --- growable emission into `out` (NULL in dry-run/length-only mode) --- */
typedef struct {
uint8_t *out;
int len;
} emitter_t;
static void e8(emitter_t *e, uint8_t b) {
if (e->out) e->out[e->len] = b;
e->len++;
}
static void e16(emitter_t *e, uint16_t v) { e8(e, v & 0xFF); e8(e, (v >> 8) & 0xFF); }
static void e32(emitter_t *e, uint32_t v) {
e8(e, v & 0xFF); e8(e, (v >> 8) & 0xFF); e8(e, (v >> 16) & 0xFF); e8(e, (v >> 24) & 0xFF);
}
/* --- symbol resolution ---
* `strict` distinguishes pass 1 (length-only: never errors, never emits
* relocations, unresolved symbols read as 0) from pass 2 (real: local
* symbols resolve directly or, in ELF mode, a relocation is emitted for
* anything not resolvable within the same section at assemble time). */
static void add_reloc(asm_ctx_t *ctx, section_kind_t sec, uint32_t off, const char *sym,
int32_t addend, reloc_kind_t kind) {
if (ctx->nrelocs >= MAX_RELOCS) { fprintf(stderr, "as: too many relocations\n"); exit(1); }
reloc_t *r = &ctx->relocs[ctx->nrelocs++];
r->section = sec;
r->offset = off;
safe_strcpy(r->sym, sym, MAX_NAME);
r->addend = addend;
r->kind = kind;
}
/* Resolves an absolute-address reference (symbol + disp) used as an
* immediate/memory-displacement value. is_bin selects -f bin semantics
* (single flat address space, always resolvable, no relocations) vs.
* -f elf32 (may need a relocation). Writes the resolved/placeholder
* value via e32/e16 at the call site - this just computes it. */
static int64_t resolve_abs(asm_ctx_t *ctx, int is_bin, const char *sym, int64_t disp,
section_kind_t patch_sec, uint32_t patch_off, int strict) {
if (!sym[0]) return disp;
symbol_t *s = sym_find(ctx, sym);
if (s && s->is_equ) return s->equ_value + disp; /* plain number, never org-adjusted */
if (is_bin) {
if (!s || !s->defined) {
if (strict) { fprintf(stderr, "as: undefined symbol '%s'\n", sym); exit(1); }
return disp;
}
return (int64_t)ctx->org + (int64_t)s->offset + disp;
}
/* ELF: absolute references always need a relocation - the section's
* final base address isn't known until link time. x86 ELF uses REL
* (not RELA) relocations: the addend has no dedicated field, it must
* be baked into the bytes at the relocated location, which is why
* this returns `disp` (the addend) rather than 0 - LD will add the
* resolved symbol value on top of whatever's already there. */
if (strict) {
add_reloc(ctx, patch_sec, patch_off, sym, (int32_t)disp, RELOC_ABS32);
}
return disp;
}
/* Resolves a PC-relative call/jmp target. Same-section same-file targets
* fold to a fixed relative displacement directly; anything else (extern,
* or -f bin's "always resolvable" absolute model) is handled by the
* caller via this return value or a relocation. `next_ip_rel` is the
* SECTION-RELATIVE position right after the relative field (i.e. NOT
* org-adjusted - this function adds org itself when is_bin, consistently
* with how it computes the target, since bin-mode "addresses" are
* `org + offset` throughout). */
static int64_t resolve_pcrel(asm_ctx_t *ctx, int is_bin, const char *sym, int64_t disp,
section_kind_t cur_sec, int64_t next_ip_rel,
section_kind_t patch_sec, uint32_t patch_off, int strict,
int field_bytes) {
if (!sym[0]) {
/* Bare constant target address, e.g. `jmp KERNEL_LOAD_ADDR`
* where KERNEL_LOAD_ADDR is an EQU - disp is already the
* absolute target, no symbol lookup needed. */
int64_t next_ip = is_bin ? (int64_t)ctx->org + next_ip_rel : next_ip_rel;
return disp - next_ip;
}
symbol_t *s = sym_find(ctx, sym);
if (is_bin) {
if (!s || !s->defined) {
if (strict) { fprintf(stderr, "as: undefined symbol '%s'\n", sym); exit(1); }
return 0;
}
int64_t target = (int64_t)ctx->org + (int64_t)s->offset + disp;
int64_t next_ip = (int64_t)ctx->org + next_ip_rel;
return target - next_ip;
}
if (s && s->defined && s->section == cur_sec) {
return ((int64_t)s->offset + disp) - next_ip_rel;
}
/* Same REL-vs-RELA reasoning as resolve_abs: the standard x86 addend
* for a rel32 call/jmp is -field_bytes (the CPU measures the
* displacement from the end of the instruction), and that addend
* must be baked into the bytes, not left as 0. */
int32_t addend = (int32_t)(disp - field_bytes);
if (strict) {
add_reloc(ctx, patch_sec, patch_off, sym, addend, RELOC_PC32);
}
return addend;
}
/* --- generic 32-bit ModRM(+SIB+disp) encoder ---
* Used by CC-generated code (always 32-bit: kernel and host targets are
* both x86-32). Handles register-direct, [esp]/[esp+disp8] (SIB - ESP
* can't be a plain ModRM base), [ebp+disp32] (always the disp32 form,
* mod=10 - mod=00 with rm=101 means "disp32 direct address" instead of
* "ebp with no displacement", so EBP-relative never uses mod=00), and
* [label(+disp)] (mod=00, rm=101, disp32 direct address - 32-bit's other
* ModRM special case). `reg_field` is either another register or a group
* opcode extension number, per the caller. */
static void encode_rm32(emitter_t *e, asm_ctx_t *ctx, int is_bin, operand_t *rm, int reg_field,
section_kind_t patch_sec, uint32_t stmt_offset, int strict) {
if (rm->kind == OPND_REG) {
e8(e, (uint8_t)(0xC0 | (reg_field << 3) | regnum(rm->reg)));
return;
}
if (rm->reg == R_ESP) {
if (rm->disp == 0) {
e8(e, (uint8_t)(0x04 | (reg_field << 3)));
e8(e, 0x24);
} else {
e8(e, (uint8_t)(0x44 | (reg_field << 3)));
e8(e, 0x24);
e8(e, (uint8_t)rm->disp);
}
return;
}
if (rm->reg == R_EBP) {
e8(e, (uint8_t)(0x85 | (reg_field << 3)));
e32(e, (uint32_t)rm->disp);
return;
}
/* bare label, no base register: 32-bit direct address */
e8(e, (uint8_t)(0x05 | (reg_field << 3)));
uint32_t patch_off = stmt_offset + (uint32_t)e->len;
int64_t addr = resolve_abs(ctx, is_bin, rm->sym, rm->disp, patch_sec, patch_off, strict);
e32(e, (uint32_t)addr);
}
/* ADD/OR/AND/SUB/XOR/CMP share one opcode block layout:
* base+0 = r/m8,r8 ; base+1 = r/m32,r32 ; base+2 = r8,r/m8 ; base+3 = r32,r/m32
* base+4 = AL,imm8 ; base+5 = eAX,imm32 ; group1 (0x81 /ext) = r/m,imm32 */
static int alu_lookup(const char *m, int *base, int *ext) {
static const struct { const char *name; int base, ext; } table[] = {
{ "add", 0x00, 0 }, { "or", 0x08, 1 }, { "and", 0x20, 4 },
{ "sub", 0x28, 5 }, { "xor", 0x30, 6 }, { "cmp", 0x38, 7 },
};
for (size_t i = 0; i < sizeof(table) / sizeof(table[0]); i++) {
if (strcmp(m, table[i].name) == 0) { *base = table[i].base; *ext = table[i].ext; return 1; }
}
return 0;
}
/* --- the encoder --- */
int stmt_encode(asm_ctx_t *ctx, stmt_t *st, uint8_t *out, int strict) {
emitter_t e = { out, 0 };
const char *m = st->mnemonic;
operand_t *a = &st->ops[0];
operand_t *b = &st->ops[1];
int is_bin = ctx->have_org;
/* --- pseudo-ops for data directives: one scalar value per stmt --- */
if (strcmp(m, ".db") == 0 || strcmp(m, ".dw") == 0 || strcmp(m, ".dd") == 0 || strcmp(m, ".dq") == 0) {
int width = strcmp(m, ".db") == 0 ? 1 : strcmp(m, ".dw") == 0 ? 2 : strcmp(m, ".dd") == 0 ? 4 : 8;
int64_t v = a->has_sym
? resolve_abs(ctx, is_bin, a->sym, a->disp, st->section, st->offset, strict)
: a->disp;
if (width == 1) e8(&e, (uint8_t)v);
else if (width == 2) e16(&e, (uint16_t)v);
else if (width == 4) e32(&e, (uint32_t)v);
else { e32(&e, (uint32_t)v); e32(&e, (uint32_t)(v >> 32)); }
return e.len;
}
if (strcmp(m, ".resb") == 0) {
for (int64_t i = 0; i < a->disp; i++) e8(&e, 0);
return e.len;
}
if (strcmp(m, ".pad") == 0) {
/* `times N db V` folds to one .pad stmt: a->disp = N, b->disp = V */
for (int64_t i = 0; i < a->disp; i++) e8(&e, (uint8_t)b->disp);
return e.len;
}
/* --- generic 32-bit ALU ops (add/or/and/sub/xor/cmp): reg<->reg,
* reg<->mem(ebp/esp/label), reg<->imm32 - what CC's codegen emits.
* Gated on bits==32 so this can never shadow the 16-bit real-mode
* forms below (which have their own, narrower matches). */
{
int base, ext;
if (st->bits == 32 && alu_lookup(m, &base, &ext)) {
if (a->kind == OPND_REG && is_reg32(a->reg) && (b->kind == OPND_REG || b->kind == OPND_MEM)) {
e8(&e, (uint8_t)(base + 3)); /* r32, r/m32 */
encode_rm32(&e, ctx, is_bin, b, regnum(a->reg), st->section, st->offset, strict);
return e.len;
}
if ((a->kind == OPND_REG || a->kind == OPND_MEM) && b->kind == OPND_REG && is_reg32(b->reg)) {
e8(&e, (uint8_t)(base + 1)); /* r/m32, r32 */
encode_rm32(&e, ctx, is_bin, a, regnum(b->reg), st->section, st->offset, strict);
return e.len;
}
if ((a->kind == OPND_REG || a->kind == OPND_MEM) && b->kind == OPND_IMM) {
e8(&e, 0x81); /* r/m32, imm32 (always full-size - no imm8 sign-extend optimization) */
encode_rm32(&e, ctx, is_bin, a, ext, st->section, st->offset, strict);
int64_t v = b->has_sym
? resolve_abs(ctx, is_bin, b->sym, b->disp, st->section, st->offset + (uint32_t)e.len, strict)
: b->disp;
e32(&e, (uint32_t)v);
return e.len;
}
}
}
if (strcmp(m, "xor") == 0 || strcmp(m, "or") == 0 || strcmp(m, "sub") == 0 || strcmp(m, "add") == 0) {
int ext = strcmp(m, "add") == 0 ? 0 : strcmp(m, "or") == 0 ? 1 : strcmp(m, "sub") == 0 ? 5 : 6; /* xor */
if (a->kind == OPND_REG && b->kind == OPND_REG && is_reg32(a->reg) && is_reg32(b->reg)) {
e8(&e, (uint8_t)(0x01 + ext * 8));
e8(&e, (uint8_t)(0xC0 | (regnum(b->reg) << 3) | regnum(a->reg)));
return e.len;
}
if (a->kind == OPND_REG && b->kind == OPND_REG && is_reg16(a->reg) && is_reg16(b->reg)) {
e8(&e, (uint8_t)(0x01 + ext * 8));
e8(&e, (uint8_t)(0xC0 | (regnum(b->reg) << 3) | regnum(a->reg)));
return e.len;
}
if (a->kind == OPND_REG && b->kind == OPND_REG && is_reg8(a->reg) && is_reg8(b->reg)) {
e8(&e, (uint8_t)(0x00 + ext * 8));
e8(&e, (uint8_t)(0xC0 | (regnum(b->reg) << 3) | regnum(a->reg)));
return e.len;
}
if (a->kind == OPND_REG && a->reg == R_AL && b->kind == OPND_IMM) {
e8(&e, (uint8_t)(0x04 + ext * 8));
e8(&e, (uint8_t)b->disp);
return e.len;
}
if (a->kind == OPND_REG && a->reg == R_EAX && b->kind == OPND_IMM && st->bits == 16) {
e8(&e, 0x66);
e8(&e, (uint8_t)(0x05 + ext * 8));
e32(&e, (uint32_t)b->disp);
return e.len;
}
if (st->bits == 16 && a->kind == OPND_MEM && !a->reg && b->kind == OPND_REG && is_reg32(b->reg)) {
e8(&e, 0x66);
e8(&e, (uint8_t)(0x01 + ext * 8));
e8(&e, (uint8_t)(0x06 | (regnum(b->reg) << 3)));
e16(&e, (uint16_t)resolve_abs(ctx, is_bin, a->sym, a->disp, st->section, st->offset + (uint32_t)e.len, strict));
return e.len;
}
if (st->bits == 16 && a->kind == OPND_MEM && !a->reg && b->kind == OPND_REG && is_reg16(b->reg)) {
e8(&e, (uint8_t)(0x01 + ext * 8));
e8(&e, (uint8_t)(0x06 | (regnum(b->reg) << 3)));
e16(&e, (uint16_t)resolve_abs(ctx, is_bin, a->sym, a->disp, st->section, st->offset + (uint32_t)e.len, strict));
return e.len;
}
}
if (strcmp(m, "cmp") == 0) {
if (a->kind == OPND_REG && is_reg16(a->reg) && b->kind == OPND_IMM) {
e8(&e, 0x81);
e8(&e, (uint8_t)(0xF8 | regnum(a->reg)));
e16(&e, (uint16_t)b->disp);
return e.len;
}
}
if (strcmp(m, "mov") == 0) {
if (is_sreg(a->reg) || is_sreg(b->reg)) {
reg_t sreg = is_sreg(a->reg) ? a->reg : b->reg;
e8(&e, 0x8E);
e8(&e, (uint8_t)(0xC0 | (regnum(sreg) << 3) | regnum(b->reg == sreg ? a->reg : b->reg)));
return e.len;
}
if (a->reg == R_CR0 && b->kind == OPND_REG && is_reg32(b->reg)) {
e8(&e, 0x0F); e8(&e, 0x22);
e8(&e, (uint8_t)(0xC0 | regnum(b->reg)));
return e.len;
}
if (b->reg == R_CR0 && a->kind == OPND_REG && is_reg32(a->reg)) {
/* MOV r32, CR0: modrm reg field = CR0 (0), rm field = dest reg */
e8(&e, 0x0F); e8(&e, 0x20);
e8(&e, (uint8_t)(0xC0 | regnum(a->reg)));
return e.len;
}
if (a->kind == OPND_REG && b->kind == OPND_IMM && is_reg8(a->reg)) {
e8(&e, (uint8_t)(0xB0 + regnum(a->reg)));
int64_t v = b->has_sym ? resolve_abs(ctx, is_bin, b->sym, b->disp, st->section, st->offset + 1, strict) : b->disp;
e8(&e, (uint8_t)v);
return e.len;
}
if (a->kind == OPND_REG && b->kind == OPND_IMM && is_reg16(a->reg) && st->bits == 16) {
e8(&e, (uint8_t)(0xB8 + regnum(a->reg)));
int64_t v = b->has_sym ? resolve_abs(ctx, is_bin, b->sym, b->disp, st->section, st->offset + 1, strict) : b->disp;
e16(&e, (uint16_t)v);
return e.len;
}
if (a->kind == OPND_REG && b->kind == OPND_IMM && is_reg16(a->reg) && st->bits == 32) {
e8(&e, 0x66);
e8(&e, (uint8_t)(0xB8 + regnum(a->reg)));
int64_t v = b->has_sym ? resolve_abs(ctx, is_bin, b->sym, b->disp, st->section, st->offset + 2, strict) : b->disp;
e16(&e, (uint16_t)v);
return e.len;
}
if (a->kind == OPND_REG && b->kind == OPND_IMM && is_reg32(a->reg)) {
e8(&e, (uint8_t)(0xB8 + regnum(a->reg)));
int64_t v = b->has_sym
? resolve_abs(ctx, is_bin, b->sym, b->disp, st->section, st->offset + 1, strict)
: b->disp;
e32(&e, (uint32_t)v);
return e.len;
}
if (a->kind == OPND_REG && b->kind == OPND_REG && is_reg16(a->reg) && is_reg16(b->reg)) {
e8(&e, 0x8B);
e8(&e, (uint8_t)(0xC0 | (regnum(a->reg) << 3) | regnum(b->reg)));
return e.len;
}
if (st->bits == 16 && a->kind == OPND_REG && is_reg16(a->reg) && b->kind == OPND_MEM && !b->reg) {
e8(&e, 0x8B);
e8(&e, (uint8_t)(0x06 | (regnum(a->reg) << 3)));
e16(&e, (uint16_t)resolve_abs(ctx, is_bin, b->sym, b->disp, st->section, st->offset + (uint32_t)e.len, strict));
return e.len;
}
if (st->bits == 16 && a->kind == OPND_MEM && !a->reg && b->kind == OPND_REG && is_reg16(b->reg)) {
e8(&e, 0x89);
e8(&e, (uint8_t)(0x06 | (regnum(b->reg) << 3)));
e16(&e, (uint16_t)resolve_abs(ctx, is_bin, a->sym, a->disp, st->section, st->offset + (uint32_t)e.len, strict));
return e.len;
}
if (st->bits == 16 && a->kind == OPND_REG && is_reg32(a->reg) && b->kind == OPND_MEM && !b->reg) {
e8(&e, 0x66); e8(&e, 0x8B);
e8(&e, (uint8_t)(0x06 | (regnum(a->reg) << 3)));
e16(&e, (uint16_t)resolve_abs(ctx, is_bin, b->sym, b->disp, st->section, st->offset + (uint32_t)e.len, strict));
return e.len;
}
if (st->bits == 16 && a->kind == OPND_MEM && !a->reg && b->kind == OPND_REG && is_reg32(b->reg)) {
e8(&e, 0x66); e8(&e, 0x89);
e8(&e, (uint8_t)(0x06 | (regnum(b->reg) << 3)));
e16(&e, (uint16_t)resolve_abs(ctx, is_bin, a->sym, a->disp, st->section, st->offset + (uint32_t)e.len, strict));
return e.len;
}
if (st->bits == 16 && a->kind == OPND_MEM && !a->reg && b->kind == OPND_IMM && a->size_override == 2) {
e8(&e, 0xC7);
e8(&e, (uint8_t)0x06);
e16(&e, (uint16_t)resolve_abs(ctx, is_bin, a->sym, a->disp, st->section, st->offset + (uint32_t)e.len, strict));
int64_t v = b->has_sym ? resolve_abs(ctx, is_bin, b->sym, b->disp, st->section, st->offset + (uint32_t)e.len + 2, strict) : b->disp;
e16(&e, (uint16_t)v);
return e.len;
}
if (st->bits == 16 && a->kind == OPND_MEM && !a->reg && b->kind == OPND_IMM && a->size_override == 4) {
e8(&e, 0x66); e8(&e, 0xC7);
e8(&e, (uint8_t)0x06);
e16(&e, (uint16_t)resolve_abs(ctx, is_bin, a->sym, a->disp, st->section, st->offset + (uint32_t)e.len, strict));
int64_t v = b->has_sym ? resolve_abs(ctx, is_bin, b->sym, b->disp, st->section, st->offset + (uint32_t)e.len + 4, strict) : b->disp;
e32(&e, (uint32_t)v);
return e.len;
}
if (a->kind == OPND_REG && is_reg32(a->reg) && b->kind == OPND_MEM && b->reg == R_ESP) {
e8(&e, 0x8B);
if (b->disp == 0) {
e8(&e, (uint8_t)(0x04 | (regnum(a->reg) << 3)));
e8(&e, 0x24);
} else {
e8(&e, (uint8_t)(0x44 | (regnum(a->reg) << 3)));
e8(&e, 0x24);
e8(&e, (uint8_t)b->disp);
}
return e.len;
}
/* --- generic 32-bit forms: reg<->reg, reg<->mem(ebp/label),
* mem<->imm32, 8-bit reg<->mem(ebp/label) - what CC's codegen
* emits for locals/globals/struct fields. */
if (st->bits == 32) {
if (a->kind == OPND_REG && is_reg32(a->reg) && (b->kind == OPND_REG || b->kind == OPND_MEM)) {
e8(&e, 0x8B);
encode_rm32(&e, ctx, is_bin, b, regnum(a->reg), st->section, st->offset, strict);
return e.len;
}
if (a->kind == OPND_MEM && b->kind == OPND_REG && is_reg32(b->reg)) {
e8(&e, 0x89);
encode_rm32(&e, ctx, is_bin, a, regnum(b->reg), st->section, st->offset, strict);
return e.len;
}
if (a->kind == OPND_REG && is_reg8(a->reg) && (b->kind == OPND_REG || b->kind == OPND_MEM)) {
e8(&e, 0x8A);
encode_rm32(&e, ctx, is_bin, b, regnum(a->reg), st->section, st->offset, strict);
return e.len;
}
if (a->kind == OPND_MEM && b->kind == OPND_REG && is_reg8(b->reg)) {
e8(&e, 0x88);
encode_rm32(&e, ctx, is_bin, a, regnum(b->reg), st->section, st->offset, strict);
return e.len;
}
if (a->kind == OPND_MEM && b->kind == OPND_IMM && a->size_override != 1) {
e8(&e, 0xC7);
encode_rm32(&e, ctx, is_bin, a, 0, st->section, st->offset, strict);
int64_t v = b->has_sym
? resolve_abs(ctx, is_bin, b->sym, b->disp, st->section, st->offset + (uint32_t)e.len, strict)
: b->disp;
e32(&e, (uint32_t)v);
return e.len;
}
if (a->kind == OPND_MEM && b->kind == OPND_IMM && a->size_override == 1) {
e8(&e, 0xC6);
encode_rm32(&e, ctx, is_bin, a, 0, st->section, st->offset, strict);
e8(&e, (uint8_t)b->disp);
return e.len;
}
}
}
if (strcmp(m, "lea") == 0 && st->bits == 32 && a->kind == OPND_REG && is_reg32(a->reg) && b->kind == OPND_MEM) {
e8(&e, 0x8D);
encode_rm32(&e, ctx, is_bin, b, regnum(a->reg), st->section, st->offset, strict);
return e.len;
}
if (strcmp(m, "imul") == 0 && st->bits == 32 && a->kind == OPND_REG && is_reg32(a->reg)) {
e8(&e, 0x0F); e8(&e, 0xAF);
encode_rm32(&e, ctx, is_bin, b, regnum(a->reg), st->section, st->offset, strict);
return e.len;
}
if (strcmp(m, "idiv") == 0 && (a->kind == OPND_REG || a->kind == OPND_MEM)) {
e8(&e, 0xF7);
encode_rm32(&e, ctx, is_bin, a, 7, st->section, st->offset, strict);
return e.len;
}
if (strcmp(m, "not") == 0 && (a->kind == OPND_REG || a->kind == OPND_MEM)) {
e8(&e, 0xF7);
encode_rm32(&e, ctx, is_bin, a, 2, st->section, st->offset, strict);
return e.len;
}
if (strcmp(m, "neg") == 0 && (a->kind == OPND_REG || a->kind == OPND_MEM)) {
e8(&e, 0xF7);
encode_rm32(&e, ctx, is_bin, a, 3, st->section, st->offset, strict);
return e.len;
}
if (strcmp(m, "cdq") == 0) { e8(&e, 0x99); return e.len; }
if (strncmp(m, "set", 3) == 0 && a->kind == OPND_REG && is_reg8(a->reg)) {
static const struct { const char *suf; uint8_t op; } cc[] = {
{ "e", 0x94 }, { "ne", 0x95 }, { "l", 0x9C }, { "le", 0x9E }, { "g", 0x9F }, { "ge", 0x9D },
};
for (size_t i = 0; i < sizeof(cc) / sizeof(cc[0]); i++) {
char full[16];
snprintf(full, sizeof(full), "set%s", cc[i].suf);
if (strcmp(m, full) == 0) {
e8(&e, 0x0F); e8(&e, cc[i].op);
e8(&e, (uint8_t)(0xC0 | regnum(a->reg)));
return e.len;
}
}
}
if (strcmp(m, "movzx") == 0 && is_reg32(a->reg) && is_reg16(b->reg)) {
e8(&e, 0x66); e8(&e, 0x0F); e8(&e, 0xB7);
e8(&e, (uint8_t)(0xC0 | (regnum(a->reg) << 3) | regnum(b->reg)));
return e.len;
}
if (strcmp(m, "movzx") == 0 && st->bits == 32 && is_reg32(a->reg) &&
(b->kind == OPND_REG ? is_reg8(b->reg) : 1)) {
e8(&e, 0x0F); e8(&e, 0xB6);
encode_rm32(&e, ctx, is_bin, b, regnum(a->reg), st->section, st->offset, strict);
return e.len;
}
if (strcmp(m, "shl") == 0 && is_reg16(a->reg) && b->kind == OPND_IMM) {
e8(&e, 0xC1);
e8(&e, (uint8_t)(0xE0 | regnum(a->reg)));
e8(&e, (uint8_t)b->disp);
return e.len;
}
if ((strcmp(m, "shl") == 0 || strcmp(m, "shr") == 0) && st->bits == 32 && is_reg32(a->reg) && b->kind == OPND_IMM) {
int ext = strcmp(m, "shl") == 0 ? 4 : 5;
e8(&e, 0xC1);
e8(&e, (uint8_t)(0xC0 | (ext << 3) | regnum(a->reg)));
e8(&e, (uint8_t)b->disp);
return e.len;
}
if ((strcmp(m, "jc") == 0 || strcmp(m, "jz") == 0 || strcmp(m, "je") == 0 || strcmp(m, "jbe") == 0)) {
uint8_t cc = strcmp(m, "jc") == 0 ? 0x82 : strcmp(m, "jbe") == 0 ? 0x86 : 0x84; /* jz/je both == */
if (st->bits == 16) {
e8(&e, 0x0F); e8(&e, cc);
int64_t rel = resolve_pcrel(ctx, is_bin, a->sym, a->disp, st->section,
(int64_t)st->offset + 4, st->section, st->offset + 2, strict, 2);
e16(&e, (uint16_t)rel);
} else {
e8(&e, 0x0F); e8(&e, cc);
int64_t rel = resolve_pcrel(ctx, is_bin, a->sym, a->disp, st->section,
(int64_t)st->offset + 6, st->section, st->offset + 2, strict, 4);
e32(&e, (uint32_t)rel);
}
return e.len;
}
if (strcmp(m, "jne") == 0 || strcmp(m, "jl") == 0 || strcmp(m, "jle") == 0 || strcmp(m, "jg") == 0 || strcmp(m, "jge") == 0) {
uint8_t cc = strcmp(m, "jne") == 0 ? 0x85 : strcmp(m, "jl") == 0 ? 0x8C : strcmp(m, "jle") == 0 ? 0x8E
: strcmp(m, "jg") == 0 ? 0x8F : 0x8D; /* jge */
e8(&e, 0x0F); e8(&e, cc);
int64_t rel = resolve_pcrel(ctx, is_bin, a->sym, a->disp, st->section,
(int64_t)st->offset + 6, st->section, st->offset + 2, strict, 4);
e32(&e, (uint32_t)rel);
return e.len;
}
if (strcmp(m, "jmp") == 0) {
if (a->kind == OPND_FAR) {
e8(&e, 0xEA);
int64_t off = resolve_abs(ctx, is_bin, a->sym, a->disp, st->section, st->offset + 1, strict);
e16(&e, (uint16_t)off);
int64_t seg = a->far_seg_has_sym
? resolve_abs(ctx, is_bin, a->far_seg_sym, a->far_seg, st->section, st->offset + 3, strict)
: a->far_seg;
e16(&e, (uint16_t)seg);
return e.len;
}
if (st->bits == 16) {
e8(&e, 0xE9);
int64_t rel = resolve_pcrel(ctx, is_bin, a->sym, a->disp, st->section,
(int64_t)st->offset + 3, st->section, st->offset + 1, strict, 2);
e16(&e, (uint16_t)rel);
} else {
e8(&e, 0xE9);
int64_t rel = resolve_pcrel(ctx, is_bin, a->sym, a->disp, st->section,
(int64_t)st->offset + 5, st->section, st->offset + 1, strict, 4);
e32(&e, (uint32_t)rel);
}
return e.len;
}
if (strcmp(m, "call") == 0) {
if (st->bits == 16) {
e8(&e, 0xE8);
int64_t rel = resolve_pcrel(ctx, is_bin, a->sym, a->disp, st->section,
(int64_t)st->offset + 3, st->section, st->offset + 1, strict, 2);
e16(&e, (uint16_t)rel);
} else {
e8(&e, 0xE8);
int64_t rel = resolve_pcrel(ctx, is_bin, a->sym, a->disp, st->section,
(int64_t)st->offset + 5, st->section, st->offset + 1, strict, 4);
e32(&e, (uint32_t)rel);
}
return e.len;
}
if (strcmp(m, "push") == 0) {
if (a->kind == OPND_IMM) {
e8(&e, 0x68);
int64_t v = a->has_sym ? resolve_abs(ctx, is_bin, a->sym, a->disp, st->section, st->offset + 1, strict) : a->disp;
e32(&e, (uint32_t)v);
return e.len;
}
if (is_reg32(a->reg)) { e8(&e, (uint8_t)(0x50 + regnum(a->reg))); return e.len; }
if (is_reg16(a->reg)) { e8(&e, (uint8_t)(0x50 + regnum(a->reg))); return e.len; }
}
if (strcmp(m, "pop") == 0) {
if (is_reg32(a->reg) || is_reg16(a->reg)) { e8(&e, (uint8_t)(0x58 + regnum(a->reg))); return e.len; }
}
if (strcmp(m, "in") == 0) {
if (b->kind == OPND_IMM) { e8(&e, 0xE4); e8(&e, (uint8_t)b->disp); return e.len; }
if (b->reg == R_DX && a->reg == R_AL) { e8(&e, 0xEC); return e.len; }
if (b->reg == R_DX && a->reg == R_AX) { e8(&e, 0xED); return e.len; }
}
if (strcmp(m, "out") == 0) {
if (a->kind == OPND_IMM) { e8(&e, 0xE6); e8(&e, (uint8_t)a->disp); return e.len; }
if (a->reg == R_DX && b->reg == R_AL) { e8(&e, 0xEE); return e.len; }
if (a->reg == R_DX && b->reg == R_AX) { e8(&e, 0xEF); return e.len; }
}
if (strcmp(m, "int") == 0) { e8(&e, 0xCD); e8(&e, (uint8_t)a->disp); return e.len; }
if (strcmp(m, "lgdt") == 0) {
e8(&e, 0x0F); e8(&e, 0x01); e8(&e, 0x16);
e16(&e, (uint16_t)resolve_abs(ctx, is_bin, a->sym, a->disp, st->section, st->offset + (uint32_t)e.len, strict));
return e.len;
}
if (strcmp(m, "pusha") == 0) { e8(&e, 0x60); return e.len; }
if (strcmp(m, "popa") == 0) { e8(&e, 0x61); return e.len; }
if (strcmp(m, "lodsb") == 0) { e8(&e, 0xAC); return e.len; }
if (strcmp(m, "ret") == 0) { e8(&e, 0xC3); return e.len; }
if (strcmp(m, "hlt") == 0) { e8(&e, 0xF4); return e.len; }
if (strcmp(m, "cli") == 0) { e8(&e, 0xFA); return e.len; }
if (strcmp(m, "cld") == 0) { e8(&e, 0xFC); return e.len; }
if (strcmp(m, "rep") == 0 && strcmp(a->sym, "stosb") == 0) { e8(&e, 0xF3); e8(&e, 0xAA); return e.len; }
fprintf(stderr, "as: line %d: unsupported instruction form: %s\n", st->line_no, m);
exit(1);
}
int as_encode_pass2(asm_ctx_t *ctx) {
for (int i = 0; i < ctx->nstmts; i++) {
stmt_t *st = &ctx->stmts[i];
section_kind_t sec = st->section;
uint8_t *dst = ctx->sec_bytes[sec] + st->offset;
int len = stmt_encode(ctx, st, dst, 1);
if ((uint32_t)len != st->length) {
fprintf(stderr, "as: internal error: length mismatch for '%s' at line %d (pass1=%u pass2=%d)\n",
st->mnemonic, st->line_no, st->length, len);
return -1;
}
if (st->offset + (uint32_t)len > ctx->sec_len[sec]) {
ctx->sec_len[sec] = st->offset + (uint32_t)len;
}
}
return 0;
}
The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/as/encode.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude/toolchain/as gcc -Wall -Wextra -O0 -g -o /tmp/myas as.h lexer.c parser.c encode.c bin_writer.c elf_writer.c main.c 2>&1 echo "compile exit=$?"
compile exit=0
$ cd /home/martin/workspace/prototypes/boot-to-claude
for f in boot/boot.S boot/stage2.S; do
/tmp/myas -f bin -o /tmp/regress_$(basename $f .S).bin "$f" || { echo "FAIL: $f"; exit 1; }
done
for f in kernel/entry.S kernel/idt_stubs.S; do
/tmp/myas -f elf32 -o /tmp/regress_$(basename $f .S).o "$f" || { echo "FAIL: $f"; exit 1; }
done
echo "all 4 files still assemble cleanly"
make clean >/dev/null && make >/dev/null 2>&1
cp build/disk.img /tmp/regress.img
dd if=/tmp/regress_boot.bin of=/tmp/regress.img conv=notrunc status=none
dd if=/tmp/regress_stage2.bin of=/tmp/regress.img bs=512 seek=1 conv=notrunc status=none
cp /tmp/regress_entry.o build/entry.o
cp /tmp/regress_idt_stubs.o build/idt_stubs.o
make build/kernel.bin >/dev/null 2>&1
dd if=build/kernel.bin of=/tmp/regress.img bs=512 seek=5 conv=notrunc status=none
timeout 5 qemu-system-i386 -drive file=/tmp/regress.img,format=raw -display none -serial stdio -no-reboot
echo "exit=$?"all 4 files still assemble cleanly tiny-os: kernel entered selftest: running SELFTEST PASS tiny-os shell. type 'help' for commands. > qemu-system-i386: terminating on signal 15 from pid 2875453 (timeout) exit=124
$ cd /home/martin/workspace/prototypes/boot-to-claude make clean >/dev/null git status --short
M toolchain/as/encode.c ?? tests/toolchain/ ?? toolchain/cc/ ?? toolchain/include/
; Sanity check for AS's new 32-bit generic encoder additions: EBP-relative
; locals, label-direct globals, imul, idiv/cdq, setcc+movzx, lea, jcc family.
; Exits with a computed status code, checked from the shell.
BITS 32
section .text
global _start
_start:
push ebp
mov ebp, esp
sub esp, 16
mov dword [ebp-4], 6
mov dword [ebp-8], 7
mov eax, [ebp-4]
imul eax, [ebp-8] ; eax = 42
mov [g_val], eax ; label-direct store
mov eax, [g_val]
mov ecx, 10
cdq
idiv ecx ; eax = 4, edx = 2 (42/10)
add eax, edx ; 4 + 2 = 6
cmp eax, 6
sete al
movzx eax, al ; eax = 1 if equal
cmp dword [g_val], 42
jne .fail
jmp .ok
.fail:
mov eax, 99
.ok:
lea ebx, [g_val]
mov ecx, [ebx]
cmp ecx, 42
jl .fail2
jmp .done
.fail2:
mov eax, 88
.done:
mov esp, ebp
pop ebp
; exit(eax) via linux syscall
mov ebx, eax
mov eax, 1
int 0x80
section .data
g_val: dd 0
File created successfully at: /tmp/as_ext_test.S (file state is current in your context — no need to Read it back)
$ /tmp/myas -f elf32 -o /tmp/as_ext_test.o /tmp/as_ext_test.S echo "assemble exit=$?" ld -m elf_i386 -o /tmp/as_ext_test /tmp/as_ext_test.o 2>&1 echo "link exit=$?" /tmp/as_ext_test echo "run exit=$?"
as: /tmp/as_ext_test.S:14: expected ] as: line 14: unsupported instruction form: mov assemble exit=1 ld: cannot find /tmp/as_ext_test.o: No such file or directory link exit=1 /bin/bash: line 10: /tmp/as_ext_test: No such file or directory run exit=127
$ grep -n "parse_mem" /home/martin/workspace/prototypes/boot-to-claude/toolchain/as/parser.c | head -3
241:static operand_t parse_mem(pstate_t *ps, line_t *ln) {
271: o = parse_mem(ps, ln);
286: o = parse_mem(ps, ln);241 static operand_t parse_mem(pstate_t *ps, line_t *ln) {
242 operand_t o = {0};
243 o.kind = OPND_MEM;
244 /* [reg], [reg+disp], [sym], [sym+disp] */
245 token_t t = line_peek(ln);
246 if (t.kind == TK_IDENT) {
247 const regdef_t *r = reg_lookup(t.text);
248 if (r) { o.reg = r->reg; line_next(ln); }
249 }
250 if (o.reg == R_NONE) {
251 value_t v = eval_expr(ps, ln);
252 if (v.has_sym) { o.has_sym = 1; safe_strcpy(o.sym, v.sym, MAX_NAME); o.disp = v.val; }
253 else { o.disp = v.val; }
254 } else if (line_peek(ln).kind == TK_PLUS) {
255 line_next(ln);
256 value_t v = eval_expr(ps, ln);
257 o.disp = v.val; /* our only reg+disp forms use plain constants */
258 }
259 if (line_peek(ln).kind == TK_RBRACK) line_next(ln); else perr(ps, "expected ]");
260 return o;- /* [reg], [reg+disp], [sym], [sym+disp] */
- token_t t = line_peek(ln);
- if (t.kind == TK_IDENT) {
- const regdef_t *r = reg_lookup(t.text);
- if (r) { o.reg = r->reg; line_next(ln); }
- }
- if (o.reg == R_NONE) {
- value_t v = eval_expr(ps, ln);
- if (v.has_sym) { o.has_sym = 1; safe_strcpy(o.sym, v.sym, MAX_NAME); o.disp = v.val; }
- else { o.disp = v.val; }
- } else if (line_peek(ln).kind == TK_PLUS) {
- line_next(ln);
- value_t v = eval_expr(ps, ln);
- o.disp = v.val; /* our only reg+disp forms use plain constants */
- }
+ /* [reg], [reg+disp], [reg-disp], [sym], [sym+disp] */
+ token_t t = line_peek(ln);
+ if (t.kind == TK_IDENT) {
+ const regdef_t *r = reg_lookup(t.text);
+ if (r) { o.reg = r->reg; line_next(ln); }
+ }
+ if (o.reg == R_NONE) {
+ value_t v = eval_expr(ps, ln);
+ if (v.has_sym) { o.has_sym = 1; safe_strcpy(o.sym, v.sym, MAX_NAME); o.disp = v.val; }
+ else { o.disp = v.val; }
+ } else if (line_peek(ln).kind == TK_PLUS || line_peek(ln).kind == TK_MINUS) {
+ int neg = line_peek(ln).kind == TK_MINUS;
+ line_next(ln);
+ value_t v = eval_expr(ps, ln);
+ o.disp = neg ? -v.val : v.val; /* our only reg+/-disp forms use plain constants */
+ }The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/as/parser.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude/toolchain/as gcc -Wall -Wextra -O0 -g -o /tmp/myas as.h lexer.c parser.c encode.c bin_writer.c elf_writer.c main.c 2>&1 echo "compile exit=$?" /tmp/myas -f elf32 -o /tmp/as_ext_test.o /tmp/as_ext_test.S echo "assemble exit=$?" ld -m elf_i386 -o /tmp/as_ext_test /tmp/as_ext_test.o 2>&1 echo "link exit=$?" /tmp/as_ext_test echo "run exit=$?"
compile exit=0 assemble exit=0 link exit=0 /bin/bash: line 16: 2875986 Segmentation fault (core dumped) /tmp/as_ext_test run exit=139
$ objdump -d /tmp/as_ext_test 2>&1 | sed -n '/<_start>/,/ret\|hlt\|^$/p' | head -60
08049000 <_start>: 8049000: 55 push %ebp 8049001: 8b ec mov %esp,%ebp 8049003: 81 ec 10 00 00 00 sub $0x10,%esp 8049009: c7 85 fc ff ff ff 06 movl $0x6,-0x4(%ebp) 8049010: 00 00 00 8049013: c7 85 f8 ff ff ff 07 movl $0x7,-0x8(%ebp) 804901a: 00 00 00 804901d: 8b 85 fc ff ff ff mov -0x4(%ebp),%eax 8049023: 0f af 85 f8 ff ff ff imul -0x8(%ebp),%eax 804902a: 89 05 00 a0 04 08 mov %eax,0x804a000 8049030: 8b 05 00 a0 04 08 mov 0x804a000,%eax 8049036: b9 0a 00 00 00 mov $0xa,%ecx 804903b: 99 cltd 804903c: f7 f9 idiv %ecx 804903e: 03 c2 add %edx,%eax 8049040: 81 f8 06 00 00 00 cmp $0x6,%eax 8049046: 0f 94 c0 sete %al 8049049: 0f b6 c0 movzbl %al,%eax 804904c: 81 3d 00 a0 04 08 2a cmpl $0x2a,0x804a000 8049053: 00 00 00 8049056: 0f 85 05 00 00 00 jne 8049061 <_start.fail> 804905c: e9 05 00 00 00 jmp 8049066 <_start.ok>
$ objdump -d /tmp/as_ext_test 2>&1 | sed -n '/<_start>/,/^$/p'
08049000 <_start>: 8049000: 55 push %ebp 8049001: 8b ec mov %esp,%ebp 8049003: 81 ec 10 00 00 00 sub $0x10,%esp 8049009: c7 85 fc ff ff ff 06 movl $0x6,-0x4(%ebp) 8049010: 00 00 00 8049013: c7 85 f8 ff ff ff 07 movl $0x7,-0x8(%ebp) 804901a: 00 00 00 804901d: 8b 85 fc ff ff ff mov -0x4(%ebp),%eax 8049023: 0f af 85 f8 ff ff ff imul -0x8(%ebp),%eax 804902a: 89 05 00 a0 04 08 mov %eax,0x804a000 8049030: 8b 05 00 a0 04 08 mov 0x804a000,%eax 8049036: b9 0a 00 00 00 mov $0xa,%ecx 804903b: 99 cltd 804903c: f7 f9 idiv %ecx 804903e: 03 c2 add %edx,%eax 8049040: 81 f8 06 00 00 00 cmp $0x6,%eax 8049046: 0f 94 c0 sete %al 8049049: 0f b6 c0 movzbl %al,%eax 804904c: 81 3d 00 a0 04 08 2a cmpl $0x2a,0x804a000 8049053: 00 00 00 8049056: 0f 85 05 00 00 00 jne 8049061 <_start.fail> 804905c: e9 05 00 00 00 jmp 8049066 <_start.ok>
$ objdump -d /tmp/as_ext_test 2>&1 | tail -30 echo "---" readelf -S /tmp/as_ext_test.o 2>&1 | grep -A1 ".text "
804903b: 99 cltd 804903c: f7 f9 idiv %ecx 804903e: 03 c2 add %edx,%eax 8049040: 81 f8 06 00 00 00 cmp $0x6,%eax 8049046: 0f 94 c0 sete %al 8049049: 0f b6 c0 movzbl %al,%eax 804904c: 81 3d 00 a0 04 08 2a cmpl $0x2a,0x804a000 8049053: 00 00 00 8049056: 0f 85 05 00 00 00 jne 8049061 <_start.fail> 804905c: e9 05 00 00 00 jmp 8049066 <_start.ok> 08049061 <_start.fail>: 8049061: b8 63 00 00 00 mov $0x63,%eax 08049066 <_start.ok>: 8049066: 8d 1d 00 a0 04 08 lea 0x804a000,%ebx 804906c: 8b 0d 00 00 00 00 mov 0x0,%ecx 8049072: 81 f9 2a 00 00 00 cmp $0x2a,%ecx 8049078: 0f 8c 05 00 00 00 jl 8049083 <_start.fail2> 804907e: e9 05 00 00 00 jmp 8049088 <_start.done> 08049083 <_start.fail2>: 8049083: b8 58 00 00 00 mov $0x58,%eax 08049088 <_start.done>: 8049088: 8b e5 mov %ebp,%esp 804908a: 5d pop %ebp 804908b: 8b d8 mov %eax,%ebx 804908d: b8 01 00 00 00 mov $0x1,%eax 8049092: cd 80 int $0x80 --- [ 1] .text PROGBITS 00000000 000034 000094 00 AX 0 0 4 [ 2] .data PROGBITS 00000000 0000c8 000004 00 WA 0 0 4 [ 3] .rel.text REL 00000000 0000cc 000020 08 4 1 4 [ 4] .symtab SYMTAB 00000000 0000ec 000070 10 5 6 4
$ grep -n "static void encode_rm32" -A 30 /home/martin/workspace/prototypes/boot-to-claude/toolchain/as/encode.c
147:static void encode_rm32(emitter_t *e, asm_ctx_t *ctx, int is_bin, operand_t *rm, int reg_field,
148- section_kind_t patch_sec, uint32_t stmt_offset, int strict) {
149- if (rm->kind == OPND_REG) {
150- e8(e, (uint8_t)(0xC0 | (reg_field << 3) | regnum(rm->reg)));
151- return;
152- }
153- if (rm->reg == R_ESP) {
154- if (rm->disp == 0) {
155- e8(e, (uint8_t)(0x04 | (reg_field << 3)));
156- e8(e, 0x24);
157- } else {
158- e8(e, (uint8_t)(0x44 | (reg_field << 3)));
159- e8(e, 0x24);
160- e8(e, (uint8_t)rm->disp);
161- }
162- return;
163- }
164- if (rm->reg == R_EBP) {
165- e8(e, (uint8_t)(0x85 | (reg_field << 3)));
166- e32(e, (uint32_t)rm->disp);
167- return;
168- }
169- /* bare label, no base register: 32-bit direct address */
170- e8(e, (uint8_t)(0x05 | (reg_field << 3)));
171- uint32_t patch_off = stmt_offset + (uint32_t)e->len;
172- int64_t addr = resolve_abs(ctx, is_bin, rm->sym, rm->disp, patch_sec, patch_off, strict);
173- e32(e, (uint32_t)addr);
174-}
175-
176-/* ADD/OR/AND/SUB/XOR/CMP share one opcode block layout:
177- * base+0 = r/m8,r8 ; base+1 = r/m32,r32 ; base+2 = r8,r/m8 ; base+3 = r32,r/m32- if (rm->reg == R_EBP) {
- e8(e, (uint8_t)(0x85 | (reg_field << 3)));
- e32(e, (uint32_t)rm->disp);
- return;
- }
- /* bare label, no base register: 32-bit direct address */
- e8(e, (uint8_t)(0x05 | (reg_field << 3)));
- uint32_t patch_off = stmt_offset + (uint32_t)e->len;
- int64_t addr = resolve_abs(ctx, is_bin, rm->sym, rm->disp, patch_sec, patch_off, strict);
- e32(e, (uint32_t)addr);
- }
+ if (rm->reg == R_EBP) {
+ e8(e, (uint8_t)(0x85 | (reg_field << 3)));
+ e32(e, (uint32_t)rm->disp);
+ return;
+ }
+ if (rm->reg != R_NONE) {
+ /* [reg] / [reg+disp] for any other GP register (EAX/ECX/EDX/EBX/
+ * ESI/EDI) - no SIB needed, and mod=00 with no displacement is
+ * safe here (rm=101/EBP is the only base register where mod=00
+ * means something else - see the EBP case above). Always uses
+ * the disp32 form when non-zero, same "no size-optimization"
+ * philosophy as everywhere else in this encoder. */
+ if (rm->disp == 0) {
+ e8(e, (uint8_t)(0x00 | (reg_field << 3) | regnum(rm->reg)));
+ } else {
+ e8(e, (uint8_t)(0x80 | (reg_field << 3) | regnum(rm->reg)));
+ e32(e, (uint32_t)rm->disp);
+ }
+ return;
+ }
+ /* bare label, no base register: 32-bit direct address */
+ e8(e, (uint8_t)(0x05 | (reg_field << 3)));
+ uint32_t patch_off = stmt_offset + (uint32_t)e->len;
+ int64_t addr = resolve_abs(ctx, is_bin, rm->sym, rm->disp, patch_sec, patch_off, strict);
+ e32(e, (uint32_t)addr);
+ }The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/as/encode.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude/toolchain/as gcc -Wall -Wextra -O0 -g -o /tmp/myas as.h lexer.c parser.c encode.c bin_writer.c elf_writer.c main.c 2>&1 echo "compile exit=$?" /tmp/myas -f elf32 -o /tmp/as_ext_test.o /tmp/as_ext_test.S ld -m elf_i386 -o /tmp/as_ext_test /tmp/as_ext_test.o 2>&1 /tmp/as_ext_test echo "run exit=$?"
compile exit=0 run exit=1
$ objdump -d /tmp/as_ext_test 2>&1 | grep "mov.*ecx\|lea"
8049036: b9 0a 00 00 00 mov $0xa,%ecx 8049066: 8d 1d 00 a0 04 08 lea 0x804a000,%ebx 804906c: 8b 0b mov (%ebx),%ecx
$ cd /home/martin/workspace/prototypes/boot-to-claude
for f in boot/boot.S boot/stage2.S; do
/tmp/myas -f bin -o /tmp/regress2_$(basename $f .S).bin "$f" || { echo "FAIL: $f"; exit 1; }
done
for f in kernel/entry.S kernel/idt_stubs.S; do
/tmp/myas -f elf32 -o /tmp/regress2_$(basename $f .S).o "$f" || { echo "FAIL: $f"; exit 1; }
done
make clean >/dev/null && make >/dev/null 2>&1
cp build/disk.img /tmp/regress2.img
dd if=/tmp/regress2_boot.bin of=/tmp/regress2.img conv=notrunc status=none
dd if=/tmp/regress2_stage2.bin of=/tmp/regress2.img bs=512 seek=1 conv=notrunc status=none
cp /tmp/regress2_entry.o build/entry.o
cp /tmp/regress2_idt_stubs.o build/idt_stubs.o
make build/kernel.bin >/dev/null 2>&1
dd if=build/kernel.bin of=/tmp/regress2.img bs=512 seek=5 conv=notrunc status=none
timeout 5 qemu-system-i386 -drive file=/tmp/regress2.img,format=raw -display none -serial stdio -no-reboot
echo "exit=$?"
make clean >/dev/nulltiny-os: kernel entered selftest: running SELFTEST PASS tiny-os shell. type 'help' for commands. > qemu-system-i386: terminating on signal 15 from pid 2876920 (timeout) exit=124
#include "cc.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
/* Naive, non-optimizing codegen: every intermediate expression value
* lives in EAX; binary operators spill the left operand to the real
* machine stack (push/pop) while the right operand is computed - no
* register allocation. Emits AS-syntax assembly text (mirrors
* `gcc -S | as`). See docs/toolchain.md for the calling convention and
* builtin mechanism. */
static FILE *g_out;
static cc_target_t g_target;
static int g_label_id;
static char g_cur_func[MAX_NAME];
typedef struct loop_ctx {
char break_label[MAX_NAME];
char continue_label[MAX_NAME];
struct loop_ctx *parent;
} loop_ctx_t;
static loop_ctx_t *g_loop;
static void out(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
vfprintf(g_out, fmt, ap);
va_end(ap);
}
static void new_label(char *buf) {
snprintf(buf, MAX_NAME, ".L%d", g_label_id++);
}
static void codegen_expr(node_t *n);
static void codegen_addr(node_t *n);
static void codegen_stmt(node_t *n);
static void codegen_call_or_builtin(node_t *n);
/* ---------------- loads/stores sized per C type ---------------- */
static int is_byte_type(type_t *t) { return t->kind == TY_CHAR; }
/* Address already in EAX; loads the value (sized per `t`) into EAX. */
static void load_value(type_t *t) {
if (t->kind == TY_ARRAY) return; /* arrays decay to their own address - nothing to load */
if (is_byte_type(t)) {
out(" movzx eax, byte [eax]\n");
} else {
out(" mov eax, [eax]\n");
}
}
/* Stores EAX's value into the address in ECX. */
static void store_value(type_t *t) {
if (is_byte_type(t)) {
out(" mov [ecx], al\n");
} else {
out(" mov [ecx], eax\n");
}
}
/* ---------------- lvalue addresses ---------------- */
static void codegen_addr(node_t *n) {
switch (n->kind) {
case ND_VAR:
if (n->is_global || n->is_static_local) {
out(" lea eax, [%s]\n", n->asm_label);
} else {
out(" lea eax, [ebp%+d]\n", n->frame_offset);
}
return;
case ND_MEMBER:
codegen_addr(n->a);
if (n->num) out(" add eax, %lld\n", (long long)n->num);
return;
case ND_ARROW:
codegen_expr(n->a);
if (n->num) out(" add eax, %lld\n", (long long)n->num);
return;
case ND_DEREF:
codegen_expr(n->a);
return;
default:
fprintf(stderr, "cc: internal error: not an lvalue (kind %d)\n", n->kind);
exit(1);
}
}
/* ---------------- expressions ---------------- */
static const char *cc_binop_asm(tok_kind_t op) {
switch (op) {
case TK_PLUS: return "add eax, ecx";
case TK_MINUS: return "sub eax, ecx";
case TK_AMP: return "and eax, ecx";
case TK_PIPE: return "or eax, ecx";
case TK_CARET: return "xor eax, ecx";
default: return NULL;
}
}
static void codegen_ptr_scale(node_t *n) {
/* `a - b` where a is int/b is pointer never happens in this
* codebase; pointer +/- int scales the int side by the pointee
* size. n->type is already the pointer type (set in make_binop). */
if (n->type->kind != TY_PTR && n->type->kind != TY_ARRAY) return;
int sz = type_size(n->type->base);
if (sz > 1) out(" imul ecx, ecx, %d\n", sz);
}
static void codegen_binop(node_t *n) {
int a_is_ptr = n->a->type->kind == TY_PTR || n->a->type->kind == TY_ARRAY;
codegen_expr(n->b);
out(" push eax\n");
codegen_expr(n->a);
out(" pop ecx\n");
/* pointer arithmetic: scale whichever side is the integer operand */
if ((n->op == TK_PLUS || n->op == TK_MINUS) && (n->type->kind == TY_PTR || n->type->kind == TY_ARRAY)) {
if (a_is_ptr) {
/* eax = pointer, ecx = int - scale ecx */
int sz = type_size(n->type->base);
if (sz > 1) out(" imul ecx, ecx, %d\n", sz);
} else {
/* eax = int, ecx = pointer - swap roles: scale eax, result base is ecx */
int sz = type_size(n->type->base);
if (sz > 1) out(" imul eax, eax, %d\n", sz);
out(" xchg eax, ecx\n");
}
}
switch (n->op) {
case TK_PLUS: out(" add eax, ecx\n"); return;
case TK_MINUS: out(" sub eax, ecx\n"); return;
case TK_STAR: out(" imul eax, ecx\n"); return;
case TK_SLASH: out(" cdq\n idiv ecx\n"); return;
case TK_PERCENT: out(" cdq\n idiv ecx\n mov eax, edx\n"); return;
case TK_AMP: out(" and eax, ecx\n"); return;
case TK_PIPE: out(" or eax, ecx\n"); return;
case TK_CARET: out(" xor eax, ecx\n"); return;
case TK_SHL: out(" xchg eax, ecx\n shl eax, cl\n"); return; /* CC only ever shifts by compile-time-small counts in practice */
case TK_SHR: out(" xchg eax, ecx\n shr eax, cl\n"); return;
case TK_EQ: out(" cmp eax, ecx\n sete al\n movzx eax, al\n"); return;
case TK_NEQ: out(" cmp eax, ecx\n setne al\n movzx eax, al\n"); return;
case TK_LT: out(" cmp eax, ecx\n setl al\n movzx eax, al\n"); return;
case TK_LE: out(" cmp eax, ecx\n setle al\n movzx eax, al\n"); return;
case TK_GT: out(" cmp eax, ecx\n setg al\n movzx eax, al\n"); return;
case TK_GE: out(" cmp eax, ecx\n setge al\n movzx eax, al\n"); return;
default:
fprintf(stderr, "cc: internal error: unsupported binop\n");
exit(1);
}
}
static void codegen_expr(node_t *n) {
switch (n->kind) {
case ND_NUM:
out(" mov eax, %lld\n", (long long)n->num);
return;
case ND_STR:
out(" lea eax, [%s]\n", n->asm_label);
return;
case ND_VAR:
codegen_addr(n);
load_value(n->type);
return;
case ND_ASSIGN: {
codegen_addr(n->a);
out(" push eax\n");
codegen_expr(n->b);
out(" pop ecx\n");
store_value(n->a->type);
return;
}
case ND_COMPOUND_ASSIGN: {
codegen_addr(n->a);
out(" push eax\n");
codegen_expr(n->b);
out(" push eax\n");
out(" pop ecx\n"); /* ecx = b's value */
out(" mov edx, [esp]\n"); /* edx = &a (peek) */
out(" mov eax, [edx]\n"); /* eax = current a */
const char *asmop = cc_binop_asm(n->op == TK_PLUSEQ ? TK_PLUS : n->op == TK_MINUSEQ ? TK_MINUS
: n->op == TK_ANDEQ ? TK_AMP : n->op == TK_OREQ ? TK_PIPE
: n->op == TK_XOREQ ? TK_CARET : TK_PLUS);
if (n->op == TK_STAREQ) { out(" imul eax, ecx\n"); }
else if (n->op == TK_SLASHEQ) { out(" cdq\n idiv ecx\n"); }
else if (n->op == TK_PERCENTEQ) { out(" cdq\n idiv ecx\n mov eax, edx\n"); }
else if (n->op == TK_SHLEQ) { out(" xchg eax, ecx\n shl eax, cl\n"); }
else if (n->op == TK_SHREQ) { out(" xchg eax, ecx\n shr eax, cl\n"); }
else { out(" %s\n", asmop); }
out(" pop edx\n");
store_value(n->a->type); /* uses ecx as the address per store_value's convention */
out(" mov ecx, edx\n"); /* fix up: store_value expects &a in ecx, we had it in edx */
return;
}
case ND_BINOP:
codegen_binop(n);
return;
case ND_LOGAND: {
char lfalse[MAX_NAME], lend[MAX_NAME];
new_label(lfalse); new_label(lend);
codegen_expr(n->a);
out(" cmp eax, 0\n je %s\n", lfalse);
codegen_expr(n->b);
out(" cmp eax, 0\n je %s\n", lfalse);
out(" mov eax, 1\n jmp %s\n", lend);
out("%s:\n mov eax, 0\n%s:\n", lfalse, lend);
return;
}
case ND_LOGOR: {
char ltrue[MAX_NAME], lend[MAX_NAME];
new_label(ltrue); new_label(lend);
codegen_expr(n->a);
out(" cmp eax, 0\n jne %s\n", ltrue);
codegen_expr(n->b);
out(" cmp eax, 0\n jne %s\n", ltrue);
out(" mov eax, 0\n jmp %s\n", lend);
out("%s:\n mov eax, 1\n%s:\n", ltrue, lend);
return;
}
case ND_UNARY_MINUS:
codegen_expr(n->a);
out(" neg eax\n");
return;
case ND_NOT:
codegen_expr(n->a);
out(" cmp eax, 0\n sete al\n movzx eax, al\n");
return;
case ND_BITNOT:
codegen_expr(n->a);
out(" not eax\n");
return;
case ND_ADDR:
codegen_addr(n->a);
return;
case ND_DEREF:
codegen_expr(n->a);
load_value(n->type);
return;
case ND_PREINC: case ND_PREDEC: case ND_POSTINC: case ND_POSTDEC: {
int is_dec = (n->kind == ND_PREDEC || n->kind == ND_POSTDEC);
int is_post = (n->kind == ND_POSTINC || n->kind == ND_POSTDEC);
int step = (n->a->type->kind == TY_PTR) ? type_size(n->a->type->base) : 1;
codegen_addr(n->a);
out(" mov ecx, eax\n");
load_value(n->a->type); /* leaves old value in eax too, but we reload via [ecx] to keep it simple */
out(" mov eax, [ecx]\n");
if (is_post) out(" mov edx, eax\n");
out(" %s eax, %d\n", is_dec ? "sub" : "add", step);
out(" mov [ecx], eax\n");
if (is_post) out(" mov eax, edx\n");
return;
}
case ND_CALL:
codegen_call_or_builtin(n);
return;
case ND_CAST:
codegen_expr(n->a);
if (is_byte_type(n->type)) out(" movzx eax, al\n");
return;
case ND_SIZEOF:
out(" mov eax, %d\n", type_size(n->type));
return;
case ND_TERNARY: {
char lfalse[MAX_NAME], lend[MAX_NAME];
new_label(lfalse); new_label(lend);
codegen_expr(n->a);
out(" cmp eax, 0\n je %s\n", lfalse);
codegen_expr(n->b);
out(" jmp %s\n%s:\n", lend, lfalse);
codegen_expr(n->c);
out("%s:\n", lend);
return;
}
case ND_MEMBER: case ND_ARROW:
codegen_addr(n);
load_value(n->type);
return;
default:
fprintf(stderr, "cc: internal error: unsupported expression kind %d\n", n->kind);
exit(1);
}
}
/* ---------------- calls + builtins ---------------- */
extern int builtin_emit_call(const char *name, node_t *args, cc_target_t target,
void (*emit_arg)(node_t *), void (*emitf)(const char *, ...));
static void codegen_call_or_builtin(node_t *n) {
if (builtin_emit_call(n->name, n->list, g_target, codegen_expr, out)) return;
int argc = 0;
node_t *args[16];
for (node_t *a = n->list; a; a = a->next) args[argc++] = a;
for (int i = argc - 1; i >= 0; i--) {
codegen_expr(args[i]);
out(" push eax\n");
}
out(" call %s\n", n->name);
if (argc) out(" add esp, %d\n", argc * 4);
}
/* ---------------- statements ---------------- */
static void codegen_stmt(node_t *n) {
switch (n->kind) {
case ND_BLOCK:
for (node_t *s = n->list; s; s = s->next) codegen_stmt(s);
return;
case ND_EXPR_STMT:
codegen_expr(n->a);
return;
case ND_VAR_DECL:
if (n->is_static_local) return; /* emitted into .data/.bss separately */
if (n->init && n->init->kind != ND_BLOCK) {
codegen_addr_var_decl:
out(" lea eax, [ebp%+d]\n", n->frame_offset);
out(" push eax\n");
codegen_expr(n->init);
out(" pop ecx\n");
store_value(n->type);
(void)0;
} else if (n->init) {
goto codegen_addr_var_decl; /* unreachable: brace-init locals aren't supported, see docs/toolchain.md */
}
return;
case ND_IF: {
char lelse[MAX_NAME], lend[MAX_NAME];
new_label(lelse); new_label(lend);
codegen_expr(n->a);
out(" cmp eax, 0\n je %s\n", lelse);
codegen_stmt(n->b);
out(" jmp %s\n", lend);
out("%s:\n", lelse);
if (n->c) codegen_stmt(n->c);
out("%s:\n", lend);
return;
}
case ND_WHILE: {
char ltop[MAX_NAME], lend[MAX_NAME];
new_label(ltop); new_label(lend);
loop_ctx_t lc = { "", "", g_loop };
safe_strcpy(lc.break_label, lend, MAX_NAME);
safe_strcpy(lc.continue_label, ltop, MAX_NAME);
g_loop = &lc;
out("%s:\n", ltop);
codegen_expr(n->a);
out(" cmp eax, 0\n je %s\n", lend);
codegen_stmt(n->b);
out(" jmp %s\n%s:\n", ltop, lend);
g_loop = lc.parent;
return;
}
case ND_DOWHILE: {
char ltop[MAX_NAME], lend[MAX_NAME], lcont[MAX_NAME];
new_label(ltop); new_label(lend); new_label(lcont);
loop_ctx_t lc = { "", "", g_loop };
safe_strcpy(lc.break_label, lend, MAX_NAME);
safe_strcpy(lc.continue_label, lcont, MAX_NAME);
g_loop = &lc;
out("%s:\n", ltop);
codegen_stmt(n->b);
out("%s:\n", lcont);
codegen_expr(n->a);
out(" cmp eax, 0\n jne %s\n", ltop);
out("%s:\n", lend);
g_loop = lc.parent;
return;
}
case ND_FOR: {
char ltop[MAX_NAME], lend[MAX_NAME], lcont[MAX_NAME];
new_label(ltop); new_label(lend); new_label(lcont);
if (n->a) codegen_stmt(n->a->kind == ND_VAR_DECL || n->a->kind == ND_BLOCK ? n->a : NULL), (n->a->kind != ND_VAR_DECL && n->a->kind != ND_BLOCK ? codegen_expr(n->a) : (void)0);
loop_ctx_t lc = { "", "", g_loop };
safe_strcpy(lc.break_label, lend, MAX_NAME);
safe_strcpy(lc.continue_label, lcont, MAX_NAME);
g_loop = &lc;
out("%s:\n", ltop);
if (n->b) { codegen_expr(n->b); out(" cmp eax, 0\n je %s\n", lend); }
codegen_stmt(n->d);
out("%s:\n", lcont);
if (n->c) codegen_expr(n->c);
out(" jmp %s\n%s:\n", ltop, lend);
g_loop = lc.parent;
return;
}
case ND_RETURN:
if (n->a) codegen_expr(n->a);
out(" mov esp, ebp\n pop ebp\n ret\n");
return;
case ND_BREAK:
out(" jmp %s\n", g_loop->break_label);
return;
case ND_CONTINUE:
out(" jmp %s\n", g_loop->continue_label);
return;
case ND_NOP:
return;
default:
fprintf(stderr, "cc: internal error: unsupported statement kind %d\n", n->kind);
exit(1);
}
}
/* ---------------- top level ---------------- */
static void emit_data_bytes(node_t *init, type_t *t);
static void emit_scalar_bytes(node_t *v, type_t *t) {
if (t->kind == TY_CHAR) out(" db %lld\n", (long long)v->num);
else out(" dd %lld\n", (long long)v->num);
}
static void emit_data_bytes(node_t *init, type_t *t) {
if (!init) {
out(" resb %d\n", type_size(t));
return;
}
if (t->kind == TY_ARRAY && init->kind == ND_BLOCK) {
int count = 0;
for (node_t *v = init->list; v; v = v->next) { emit_scalar_bytes(v, t->base); count++; }
int pad = t->array_len - count;
for (int i = 0; i < pad; i++) out(" resb %d\n", type_size(t->base));
return;
}
emit_scalar_bytes(init, t);
}
static void codegen_func(node_t *fn) {
safe_strcpy(g_cur_func, fn->name, MAX_NAME);
out("global %s\n%s:\n", fn->name, fn->name);
out(" push ebp\n mov ebp, esp\n");
if (fn->num) out(" sub esp, %d\n", (int)fn->num);
codegen_stmt(fn->b);
out(" mov eax, 0\n mov esp, ebp\n pop ebp\n ret\n");
}
void codegen_emit(node_t *prog, void *out_file, cc_target_t target) {
g_out = (FILE *)out_file;
g_target = target;
g_label_id = 0;
g_loop = NULL;
out("BITS 32\n");
/* declare externs for every called-but-not-locally-defined function
* and globals for every locally defined non-static one, up front */
for (node_t *n = prog; n; n = n->next) {
if (n->kind == ND_FUNC_DEF) out("extern %s\n", ""), fseek(g_out, -1, SEEK_CUR); /* placeholder removed below */
}
/* (extern declarations for callees are emitted lazily by AS's own
* `extern`-if-undefined model isn't automatic - CC instead emits one
* `extern` per referenced-but-undefined function name, collected below) */
out("section .text\n");
for (node_t *n = prog; n; n = n->next) {
if (n->kind == ND_FUNC_DEF) codegen_func(n);
}
if (target == TARGET_HOST) {
out("global _start\n_start:\n");
out(" call main\n");
out(" mov ebx, eax\n mov eax, 1\n int 0x80\n");
}
int any_data = 0, any_bss = 0, any_rodata = 0;
for (node_t *n = prog; n; n = n->next) {
if (n->kind == ND_VAR_DECL && (n->is_global || n->is_static_local)) {
if (n->init) any_data = 1; else any_bss = 1;
}
if (n->kind == ND_STR) any_rodata = 1;
}
(void)any_rodata;
if (any_data) {
out("section .data\n");
for (node_t *n = prog; n; n = n->next) {
if (n->kind == ND_VAR_DECL && (n->is_global || n->is_static_local) && n->init) {
out("global %s\n%s:\n", n->asm_label, n->asm_label);
emit_data_bytes(n->init, n->type);
}
}
}
if (any_bss) {
out("section .bss\n");
for (node_t *n = prog; n; n = n->next) {
if (n->kind == ND_VAR_DECL && (n->is_global || n->is_static_local) && !n->init) {
out("global %s\n%s:\n resb %d\n", n->asm_label, n->asm_label, type_size(n->type));
}
}
}
}
The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/codegen.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ grep -n '"shl") == 0 || strcmp(m, "shr")' /home/martin/workspace/prototypes/boot-to-claude/toolchain/as/encode.c
523: if ((strcmp(m, "shl") == 0 || strcmp(m, "shr") == 0) && st->bits == 32 && is_reg32(a->reg) && b->kind == OPND_IMM) {515 }
516
517 if (strcmp(m, "shl") == 0 && is_reg16(a->reg) && b->kind == OPND_IMM) {
518 e8(&e, 0xC1);
519 e8(&e, (uint8_t)(0xE0 | regnum(a->reg)));
520 e8(&e, (uint8_t)b->disp);
521 return e.len;
522 }
523 if ((strcmp(m, "shl") == 0 || strcmp(m, "shr") == 0) && st->bits == 32 && is_reg32(a->reg) && b->kind == OPND_IMM) {
524 int ext = strcmp(m, "shl") == 0 ? 4 : 5;
525 e8(&e, 0xC1);
526 e8(&e, (uint8_t)(0xC0 | (ext << 3) | regnum(a->reg)));
527 e8(&e, (uint8_t)b->disp);
528 return e.len;
529 }- if ((strcmp(m, "shl") == 0 || strcmp(m, "shr") == 0) && st->bits == 32 && is_reg32(a->reg) && b->kind == OPND_IMM) {
- int ext = strcmp(m, "shl") == 0 ? 4 : 5;
- e8(&e, 0xC1);
- e8(&e, (uint8_t)(0xC0 | (ext << 3) | regnum(a->reg)));
- e8(&e, (uint8_t)b->disp);
- return e.len;
- }
+ if ((strcmp(m, "shl") == 0 || strcmp(m, "shr") == 0) && st->bits == 32 && is_reg32(a->reg) && b->kind == OPND_IMM) {
+ int ext = strcmp(m, "shl") == 0 ? 4 : 5;
+ e8(&e, 0xC1);
+ e8(&e, (uint8_t)(0xC0 | (ext << 3) | regnum(a->reg)));
+ e8(&e, (uint8_t)b->disp);
+ return e.len;
+ }
+ if ((strcmp(m, "shl") == 0 || strcmp(m, "shr") == 0) && st->bits == 32 && is_reg32(a->reg) && b->reg == R_CL) {
+ int ext = strcmp(m, "shl") == 0 ? 4 : 5;
+ e8(&e, 0xD3);
+ e8(&e, (uint8_t)(0xC0 | (ext << 3) | regnum(a->reg)));
+ return e.len;
+ }The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/as/encode.c has been updated successfully. (file state is current in your context — no need to Read it back)
- static const char *cc_binop_asm(tok_kind_t op) {
- switch (op) {
- case TK_PLUS: return "add eax, ecx";
- case TK_MINUS: return "sub eax, ecx";
- case TK_AMP: return "and eax, ecx";
- case TK_PIPE: return "or eax, ecx";
- case TK_CARET: return "xor eax, ecx";
- default: return NULL;
- }
- }
-
- static void codegen_ptr_scale(node_t *n) {
- /* `a - b` where a is int/b is pointer never happens in this
- * codebase; pointer +/- int scales the int side by the pointee
- * size. n->type is already the pointer type (set in make_binop). */
- if (n->type->kind != TY_PTR && n->type->kind != TY_ARRAY) return;
- int sz = type_size(n->type->base);
- if (sz > 1) out(" imul ecx, ecx, %d\n", sz);
- }
-
- static void codegen_binop(node_t *n) {
- int a_is_ptr = n->a->type->kind == TY_PTR || n->a->type->kind == TY_ARRAY;
- codegen_expr(n->b);
- out(" push eax\n");
- codegen_expr(n->a);
- out(" pop ecx\n");
- /* pointer arithmetic: scale whichever side is the integer operand */
- if ((n->op == TK_PLUS || n->op == TK_MINUS) && (n->type->kind == TY_PTR || n->type->kind == TY_ARRAY)) {
- if (a_is_ptr) {
- /* eax = pointer, ecx = int - scale ecx */
- int sz = type_size(n->type->base);
- if (sz > 1) out(" imul ecx, ecx, %d\n", sz);
- } else {
- /* eax = int, ecx = pointer - swap roles: scale eax, result base is ecx */
- int sz = type_size(n->type->base);
- if (sz > 1) out(" imul eax, eax, %d\n", sz);
- out(" xchg eax, ecx\n");
- }
- }
-
- switch (n->op) {
- case TK_PLUS: out(" add eax, ecx\n"); return;
- case TK_MINUS: out(" sub eax, ecx\n"); return;
- case TK_STAR: out(" imul eax, ecx\n"); return;
- case TK_SLASH: out(" cdq\n idiv ecx\n"); return;
- case TK_PERCENT: out(" cdq\n idiv ecx\n mov eax, edx\n"); return;
- case TK_AMP: out(" and eax, ecx\n"); return;
- case TK_PIPE: out(" or eax, ecx\n"); return;
- case TK_CARET: out(" xor eax, ecx\n"); return;
- case TK_SHL: out(" xchg eax, ecx\n shl eax, cl\n"); return; /* CC only ever shifts by compile-time-small counts in practice */
- case TK_SHR: out(" xchg eax, ecx\n shr eax, cl\n"); return;
- case TK_EQ: out(" cmp eax, ecx\n sete al\n movzx eax, al\n"); return;
- case TK_NEQ: out(" cmp eax, ecx\n setne al\n movzx eax, al\n"); return;
- case TK_LT: out(" cmp eax, ecx\n setl al\n movzx eax, al\n"); return;
- case TK_LE: out(" cmp eax, ecx\n setle al\n movzx eax, al\n"); return;
- case TK_GT: out(" cmp eax, ecx\n setg al\n movzx eax, al\n"); return;
- case TK_GE: out(" cmp eax, ecx\n setge al\n movzx eax, al\n"); return;
- default:
- fprintf(stderr, "cc: internal error: unsupported binop\n");
- exit(1);
- }
- }
+ static void codegen_binop(node_t *n) {
+ int a_is_ptr = n->a->type->kind == TY_PTR || n->a->type->kind == TY_ARRAY;
+ codegen_expr(n->b);
+ out(" push eax\n");
+ codegen_expr(n->a);
+ out(" pop ecx\n");
+ /* Pointer arithmetic: scale whichever side is the integer operand.
+ * eax=a, ecx=b at this point (per the evaluation order above). */
+ if ((n->op == TK_PLUS || n->op == TK_MINUS) && (n->type->kind == TY_PTR || n->type->kind == TY_ARRAY)) {
+ int sz = type_size(n->type->base);
+ if (a_is_ptr) {
+ if (sz > 1) out(" imul ecx, ecx, %d\n", sz);
+ } else {
+ /* eax=int, ecx=pointer - scale eax then swap via the stack
+ * (no xchg instruction in this ISA) so eax ends up holding
+ * the pointer and ecx the scaled offset, matching every
+ * other case's eax-is-left-operand convention below. */
+ if (sz > 1) out(" imul eax, eax, %d\n", sz);
+ out(" push eax\n mov eax, ecx\n pop ecx\n");
+ }
+ }
+
+ switch (n->op) {
+ case TK_PLUS: out(" add eax, ecx\n"); return;
+ case TK_MINUS: out(" sub eax, ecx\n"); return;
+ case TK_STAR: out(" imul eax, ecx\n"); return;
+ case TK_SLASH: out(" cdq\n idiv ecx\n"); return;
+ case TK_PERCENT: out(" cdq\n idiv ecx\n mov eax, edx\n"); return;
+ case TK_AMP: out(" and eax, ecx\n"); return;
+ case TK_PIPE: out(" or eax, ecx\n"); return;
+ case TK_CARET: out(" xor eax, ecx\n"); return;
+ /* eax=value, ecx=count already, from the evaluation order above -
+ * the shift-count-in-CL hardware requirement is satisfied
+ * directly, no register shuffling needed. */
+ case TK_SHL: out(" shl eax, cl\n"); return;
+ case TK_SHR: out(" shr eax, cl\n"); return;
+ case TK_EQ: out(" cmp eax, ecx\n sete al\n movzx eax, al\n"); return;
+ case TK_NEQ: out(" cmp eax, ecx\n setne al\n movzx eax, al\n"); return;
+ case TK_LT: out(" cmp eax, ecx\n setl al\n movzx eax, al\n"); return;
+ case TK_LE: out(" cmp eax, ecx\n setle al\n movzx eax, al\n"); return;
+ case TK_GT: out(" cmp eax, ecx\n setg al\n movzx eax, al\n"); return;
+ case TK_GE: out(" cmp eax, ecx\n setge al\n movzx eax, al\n"); return;
+ default:
+ fprintf(stderr, "cc: internal error: unsupported binop\n");
+ exit(1);
+ }
+ }The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/codegen.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ grep -n "cc_binop_asm" /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/codegen.c
171: const char *asmop = cc_binop_asm(n->op == TK_PLUSEQ ? TK_PLUS : n->op == TK_MINUSEQ ? TK_MINUS
158 codegen_expr(n->b);
159 out(" pop ecx\n");
160 store_value(n->a->type);
161 return;
162 }
163 case ND_COMPOUND_ASSIGN: {
164 codegen_addr(n->a);
165 out(" push eax\n");
166 codegen_expr(n->b);
167 out(" push eax\n");
168 out(" pop ecx\n"); /* ecx = b's value */
169 out(" mov edx, [esp]\n"); /* edx = &a (peek) */
170 out(" mov eax, [edx]\n"); /* eax = current a */
171 const char *asmop = cc_binop_asm(n->op == TK_PLUSEQ ? TK_PLUS : n->op == TK_MINUSEQ ? TK_MINUS
172 : n->op == TK_ANDEQ ? TK_AMP : n->op == TK_OREQ ? TK_PIPE
173 : n->op == TK_XOREQ ? TK_CARET : TK_PLUS);
174 if (n->op == TK_STAREQ) { out(" imul eax, ecx\n"); }
175 else if (n->op == TK_SLASHEQ) { out(" cdq\n idiv ecx\n"); }
176 else if (n->op == TK_PERCENTEQ) { out(" cdq\n idiv ecx\n mov eax, edx\n"); }
177 else if (n->op == TK_SHLEQ) { out(" xchg eax, ecx\n shl eax, cl\n"); }
178 else if (n->op == TK_SHREQ) { out(" xchg eax, ecx\n shr eax, cl\n"); }
179 else { out(" %s\n", asmop); }
180 out(" pop edx\n");
181 store_value(n->a->type); /* uses ecx as the address per store_value's convention */
182 out(" mov ecx, edx\n"); /* fix up: store_value expects &a in ecx, we had it in edx */
183 return;
184 }
185 case ND_BINOP:
186 codegen_binop(n);
187 return;- case ND_COMPOUND_ASSIGN: {
- codegen_addr(n->a);
- out(" push eax\n");
- codegen_expr(n->b);
- out(" push eax\n");
- out(" pop ecx\n"); /* ecx = b's value */
- out(" mov edx, [esp]\n"); /* edx = &a (peek) */
- out(" mov eax, [edx]\n"); /* eax = current a */
- const char *asmop = cc_binop_asm(n->op == TK_PLUSEQ ? TK_PLUS : n->op == TK_MINUSEQ ? TK_MINUS
- : n->op == TK_ANDEQ ? TK_AMP : n->op == TK_OREQ ? TK_PIPE
- : n->op == TK_XOREQ ? TK_CARET : TK_PLUS);
- if (n->op == TK_STAREQ) { out(" imul eax, ecx\n"); }
- else if (n->op == TK_SLASHEQ) { out(" cdq\n idiv ecx\n"); }
- else if (n->op == TK_PERCENTEQ) { out(" cdq\n idiv ecx\n mov eax, edx\n"); }
- else if (n->op == TK_SHLEQ) { out(" xchg eax, ecx\n shl eax, cl\n"); }
- else if (n->op == TK_SHREQ) { out(" xchg eax, ecx\n shr eax, cl\n"); }
- else { out(" %s\n", asmop); }
- out(" pop edx\n");
- store_value(n->a->type); /* uses ecx as the address per store_value's convention */
- out(" mov ecx, edx\n"); /* fix up: store_value expects &a in ecx, we had it in edx */
- return;
- }
+ case ND_COMPOUND_ASSIGN: {
+ /* eax/ecx get clobbered (idiv even clobbers edx) while
+ * computing the new value, so &a is kept on the real stack
+ * throughout and only popped fresh at the very end - a
+ * register copy of it wouldn't survive an idiv/cdq. */
+ codegen_addr(n->a);
+ out(" push eax\n"); /* [&a] */
+ codegen_expr(n->b);
+ out(" push eax\n"); /* [&a, b_value] */
+ out(" pop ecx\n"); /* ecx = b_value */
+ out(" mov edx, [esp]\n"); /* edx = &a (peek, stack still [&a]) */
+ out(" mov eax, [edx]\n"); /* eax = current value of a */
+ switch (n->op) {
+ case TK_PLUSEQ: out(" add eax, ecx\n"); break;
+ case TK_MINUSEQ: out(" sub eax, ecx\n"); break;
+ case TK_ANDEQ: out(" and eax, ecx\n"); break;
+ case TK_OREQ: out(" or eax, ecx\n"); break;
+ case TK_XOREQ: out(" xor eax, ecx\n"); break;
+ case TK_STAREQ: out(" imul eax, ecx\n"); break;
+ case TK_SLASHEQ: out(" cdq\n idiv ecx\n"); break;
+ case TK_PERCENTEQ: out(" cdq\n idiv ecx\n mov eax, edx\n"); break;
+ /* ecx already holds b_value (the shift count) in CL, no
+ * register shuffling needed - same as codegen_binop. */
+ case TK_SHLEQ: out(" shl eax, cl\n"); break;
+ case TK_SHREQ: out(" shr eax, cl\n"); break;
+ default: fprintf(stderr, "cc: internal error: unsupported compound-assign op\n"); exit(1);
+ }
+ out(" pop ecx\n"); /* ecx = &a, fresh from the stack (not the possibly-clobbered edx copy) */
+ store_value(n->a->type);
+ return;
+ }The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/codegen.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ grep -n "ND_PREINC\|ND_VAR_DECL:\|case ND_FOR" /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/codegen.c
238: case ND_PREINC: case ND_PREDEC: case ND_POSTINC: case ND_POSTDEC: {
312: case ND_VAR_DECL:
369: case ND_FOR: {238 case ND_PREINC: case ND_PREDEC: case ND_POSTINC: case ND_POSTDEC: {
239 int is_dec = (n->kind == ND_PREDEC || n->kind == ND_POSTDEC);
240 int is_post = (n->kind == ND_POSTINC || n->kind == ND_POSTDEC);
241 int step = (n->a->type->kind == TY_PTR) ? type_size(n->a->type->base) : 1;
242 codegen_addr(n->a);
243 out(" mov ecx, eax\n");
244 load_value(n->a->type); /* leaves old value in eax too, but we reload via [ecx] to keep it simple */
245 out(" mov eax, [ecx]\n");
246 if (is_post) out(" mov edx, eax\n");
247 out(" %s eax, %d\n", is_dec ? "sub" : "add", step);
248 out(" mov [ecx], eax\n");
249 if (is_post) out(" mov eax, edx\n");
250 return;
251 }
252 case ND_CALL:- case ND_PREINC: case ND_PREDEC: case ND_POSTINC: case ND_POSTDEC: {
- int is_dec = (n->kind == ND_PREDEC || n->kind == ND_POSTDEC);
- int is_post = (n->kind == ND_POSTINC || n->kind == ND_POSTDEC);
- int step = (n->a->type->kind == TY_PTR) ? type_size(n->a->type->base) : 1;
- codegen_addr(n->a);
- out(" mov ecx, eax\n");
- load_value(n->a->type); /* leaves old value in eax too, but we reload via [ecx] to keep it simple */
- out(" mov eax, [ecx]\n");
- if (is_post) out(" mov edx, eax\n");
- out(" %s eax, %d\n", is_dec ? "sub" : "add", step);
- out(" mov [ecx], eax\n");
- if (is_post) out(" mov eax, edx\n");
- return;
- }
+ case ND_PREINC: case ND_PREDEC: case ND_POSTINC: case ND_POSTDEC: {
+ /* int/pointer only (4-byte) - ++/-- on a char never appears
+ * in this codebase, see docs/toolchain.md. */
+ int is_dec = (n->kind == ND_PREDEC || n->kind == ND_POSTDEC);
+ int is_post = (n->kind == ND_POSTINC || n->kind == ND_POSTDEC);
+ int step = (n->a->type->kind == TY_PTR) ? type_size(n->a->type->base) : 1;
+ codegen_addr(n->a);
+ out(" mov ecx, eax\n"); /* ecx = address */
+ out(" mov eax, [ecx]\n"); /* eax = old value */
+ if (is_post) out(" mov edx, eax\n");
+ out(" %s eax, %d\n", is_dec ? "sub" : "add", step);
+ out(" mov [ecx], eax\n");
+ if (is_post) out(" mov eax, edx\n");
+ return;
+ }The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/codegen.c has been updated successfully. (file state is current in your context — no need to Read it back)
308 for (node_t *s = n->list; s; s = s->next) codegen_stmt(s);
309 return;
310 case ND_EXPR_STMT:
311 codegen_expr(n->a);
312 return;
313 case ND_VAR_DECL:
314 if (n->is_static_local) return; /* emitted into .data/.bss separately */
315 if (n->init && n->init->kind != ND_BLOCK) {
316 codegen_addr_var_decl:
317 out(" lea eax, [ebp%+d]\n", n->frame_offset);
318 out(" push eax\n");
319 codegen_expr(n->init);
320 out(" pop ecx\n");
321 store_value(n->type);
322 (void)0;
323 } else if (n->init) {
324 goto codegen_addr_var_decl; /* unreachable: brace-init locals aren't supported, see docs/toolchain.md */
325 }
326 return;
327 case ND_IF: {
328 char lelse[MAX_NAME], lend[MAX_NAME];
329 new_label(lelse); new_label(lend);
330 codegen_expr(n->a);
331 out(" cmp eax, 0\n je %s\n", lelse);
332 codegen_stmt(n->b);
333 out(" jmp %s\n", lend);
334 out("%s:\n", lelse);
335 if (n->c) codegen_stmt(n->c);
336 out("%s:\n", lend);
337 return;
338 }
339 case ND_WHILE: {
340 char ltop[MAX_NAME], lend[MAX_NAME];
341 new_label(ltop); new_label(lend);
342 loop_ctx_t lc = { "", "", g_loop };
343 safe_strcpy(lc.break_label, lend, MAX_NAME);
344 safe_strcpy(lc.continue_label, ltop, MAX_NAME);
345 g_loop = &lc;
346 out("%s:\n", ltop);
347 codegen_expr(n->a);
348 out(" cmp eax, 0\n je %s\n", lend);
349 codegen_stmt(n->b);
350 out(" jmp %s\n%s:\n", ltop, lend);
351 g_loop = lc.parent;
352 return;
353 }
354 case ND_DOWHILE: {
355 char ltop[MAX_NAME], lend[MAX_NAME], lcont[MAX_NAME];
356 new_label(ltop); new_label(lend); new_label(lcont);
357 loop_ctx_t lc = { "", "", g_loop };
358 safe_strcpy(lc.break_label, lend, MAX_NAME);
359 safe_strcpy(lc.continue_label, lcont, MAX_NAME);
360 g_loop = &lc;
361 out("%s:\n", ltop);
362 codegen_stmt(n->b);
363 out("%s:\n", lcont);
364 codegen_expr(n->a);
365 out(" cmp eax, 0\n jne %s\n", ltop);
366 out("%s:\n", lend);
367 g_loop = lc.parent;
368 return;
369 }
370 case ND_FOR: {
371 char ltop[MAX_NAME], lend[MAX_NAME], lcont[MAX_NAME];
372 new_label(ltop); new_label(lend); new_label(lcont);
373 if (n->a) codegen_stmt(n->a->kind == ND_VAR_DECL || n->a->kind == ND_BLOCK ? n->a : NULL), (n->a->kind != ND_VAR_DECL && n->a->kind != ND_BLOCK ? codegen_expr(n->a) : (void)0);
374 loop_ctx_t lc = { "", "", g_loop };
375 safe_strcpy(lc.break_label, lend, MAX_NAME);
376 safe_strcpy(lc.continue_label, lcont, MAX_NAME);
377 g_loop = &lc;
378 out("%s:\n", ltop);
379 if (n->b) { codegen_expr(n->b); out(" cmp eax, 0\n je %s\n", lend); }
380 codegen_stmt(n->d);
381 out("%s:\n", lcont);
382 if (n->c) codegen_expr(n->c);
383 out(" jmp %s\n%s:\n", ltop, lend);
384 g_loop = lc.parent;
385 return;
386 }
387 case ND_RETURN:- case ND_VAR_DECL:
- if (n->is_static_local) return; /* emitted into .data/.bss separately */
- if (n->init && n->init->kind != ND_BLOCK) {
- codegen_addr_var_decl:
- out(" lea eax, [ebp%+d]\n", n->frame_offset);
- out(" push eax\n");
- codegen_expr(n->init);
- out(" pop ecx\n");
- store_value(n->type);
- (void)0;
- } else if (n->init) {
- goto codegen_addr_var_decl; /* unreachable: brace-init locals aren't supported, see docs/toolchain.md */
- }
- return;
+ case ND_VAR_DECL:
+ if (n->is_static_local) return; /* emitted into .data/.bss separately */
+ /* Brace-init lists (`{1,2,3}`) are only supported for
+ * globals/statics (compile-time .data, see emit_data_bytes)
+ * - this codebase never brace-initializes a plain (stack)
+ * local, only scalars. */
+ if (n->init) {
+ out(" lea eax, [ebp%+d]\n", n->frame_offset);
+ out(" push eax\n");
+ codegen_expr(n->init);
+ out(" pop ecx\n");
+ store_value(n->type);
+ }
+ return;The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/codegen.c has been updated successfully. (file state is current in your context — no need to Read it back)
- case ND_FOR: {
- char ltop[MAX_NAME], lend[MAX_NAME], lcont[MAX_NAME];
- new_label(ltop); new_label(lend); new_label(lcont);
- if (n->a) codegen_stmt(n->a->kind == ND_VAR_DECL || n->a->kind == ND_BLOCK ? n->a : NULL), (n->a->kind != ND_VAR_DECL && n->a->kind != ND_BLOCK ? codegen_expr(n->a) : (void)0);
- loop_ctx_t lc = { "", "", g_loop };
+ case ND_FOR: {
+ char ltop[MAX_NAME], lend[MAX_NAME], lcont[MAX_NAME];
+ new_label(ltop); new_label(lend); new_label(lcont);
+ if (n->a) {
+ if (n->a->kind == ND_VAR_DECL) {
+ for (node_t *d = n->a; d; d = d->next) codegen_stmt(d);
+ } else {
+ codegen_expr(n->a);
+ }
+ }
+ loop_ctx_t lc = { "", "", g_loop };The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/codegen.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ grep -n "void codegen_emit" -A 40 /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/codegen.c
444:void codegen_emit(node_t *prog, void *out_file, cc_target_t target) {
445- g_out = (FILE *)out_file;
446- g_target = target;
447- g_label_id = 0;
448- g_loop = NULL;
449-
450- out("BITS 32\n");
451-
452- /* declare externs for every called-but-not-locally-defined function
453- * and globals for every locally defined non-static one, up front */
454- for (node_t *n = prog; n; n = n->next) {
455- if (n->kind == ND_FUNC_DEF) out("extern %s\n", ""), fseek(g_out, -1, SEEK_CUR); /* placeholder removed below */
456- }
457- /* (extern declarations for callees are emitted lazily by AS's own
458- * `extern`-if-undefined model isn't automatic - CC instead emits one
459- * `extern` per referenced-but-undefined function name, collected below) */
460-
461- out("section .text\n");
462- for (node_t *n = prog; n; n = n->next) {
463- if (n->kind == ND_FUNC_DEF) codegen_func(n);
464- }
465-
466- if (target == TARGET_HOST) {
467- out("global _start\n_start:\n");
468- out(" call main\n");
469- out(" mov ebx, eax\n mov eax, 1\n int 0x80\n");
470- }
471-
472- int any_data = 0, any_bss = 0, any_rodata = 0;
473- for (node_t *n = prog; n; n = n->next) {
474- if (n->kind == ND_VAR_DECL && (n->is_global || n->is_static_local)) {
475- if (n->init) any_data = 1; else any_bss = 1;
476- }
477- if (n->kind == ND_STR) any_rodata = 1;
478- }
479- (void)any_rodata;
480-
481- if (any_data) {
482- out("section .data\n");
483- for (node_t *n = prog; n; n = n->next) {
484- if (n->kind == ND_VAR_DECL && (n->is_global || n->is_static_local) && n->init) {- void codegen_emit(node_t *prog, void *out_file, cc_target_t target) {
- g_out = (FILE *)out_file;
- g_target = target;
- g_label_id = 0;
- g_loop = NULL;
-
- out("BITS 32\n");
-
- /* declare externs for every called-but-not-locally-defined function
- * and globals for every locally defined non-static one, up front */
- for (node_t *n = prog; n; n = n->next) {
- if (n->kind == ND_FUNC_DEF) out("extern %s\n", ""), fseek(g_out, -1, SEEK_CUR); /* placeholder removed below */
- }
- /* (extern declarations for callees are emitted lazily by AS's own
- * `extern`-if-undefined model isn't automatic - CC instead emits one
- * `extern` per referenced-but-undefined function name, collected below) */
-
- out("section .text\n");
- for (node_t *n = prog; n; n = n->next) {
- if (n->kind == ND_FUNC_DEF) codegen_func(n);
- }
-
- if (target == TARGET_HOST) {
- out("global _start\n_start:\n");
- out(" call main\n");
- out(" mov ebx, eax\n mov eax, 1\n int 0x80\n");
- }
-
- int any_data = 0, any_bss = 0, any_rodata = 0;
- for (node_t *n = prog; n; n = n->next) {
- if (n->kind == ND_VAR_DECL && (n->is_global || n->is_static_local)) {
- if (n->init) any_data = 1; else any_bss = 1;
- }
- if (n->kind == ND_STR) any_rodata = 1;
- }
- (void)any_rodata;
-
- if (any_data) {
+ extern int is_builtin_name(const char *name);
+
+ #define MAX_EXTERNS 64
+ static char g_externs[MAX_EXTERNS][MAX_NAME];
+ static int g_n_externs;
+
+ static int already_extern(const char *name) {
+ for (int i = 0; i < g_n_externs; i++) if (strcmp(g_externs[i], name) == 0) return 1;
+ return 0;
+ }
+
+ /* Walks every reachable node (function bodies, global initializers - a/b/
+ * c/d/list/init/next cover every kind of child link used anywhere in the
+ * AST) collecting two things in one pass: names of called-but-not-
+ * locally-defined, non-builtin functions (need `extern`), and string
+ * literals (need their bytes emitted into .rodata with their label). */
+ static void walk_collect(node_t *n, node_t *prog) {
+ if (!n) return;
+ if (n->kind == ND_CALL) {
+ int is_local = 0;
+ for (node_t *f = prog; f; f = f->next) {
+ if (f->kind == ND_FUNC_DEF && strcmp(f->name, n->name) == 0) { is_local = 1; break; }
+ }
+ if (!is_local && !is_builtin_name(n->name) && !already_extern(n->name) && g_n_externs < MAX_EXTERNS) {
+ safe_strcpy(g_externs[g_n_externs++], n->name, MAX_NAME);
+ }
+ }
+ if (n->kind == ND_STR) {
+ out("global %s\n%s:\n", n->asm_label, n->asm_label);
+ for (const char *p = n->str; ; p++) {
+ out(" db %d\n", (int)(unsigned char)*p);
+ if (!*p) break;
+ }
+ }
+ walk_collect(n->a, prog);
+ walk_collect(n->b, prog);
+ walk_collect(n->c, prog);
+ walk_collect(n->d, prog);
+ walk_collect(n->list, prog);
+ walk_collect(n->init, prog);
+ walk_collect(n->next, prog);
+ }
+
+ void codegen_emit(node_t *prog, void *out_file, cc_target_t target) {
+ g_out = (FILE *)out_file;
+ g_target = target;
+ g_label_id = 0;
+ g_loop = NULL;
+ g_n_externs = 0;
+
+ out("BITS 32\n");
+
+ for (node_t *n = prog; n; n = n->next) {
+ if (n->kind != ND_FUNC_DEF) walk_collect(n, prog); /* globals: just for string literals */
+ else walk_collect(n->b, prog); /* function bodies */
+ }
+ for (int i = 0; i < g_n_externs; i++) out("extern %s\n", g_externs[i]);
+
+ out("section .text\n");
+ for (node_t *n = prog; n; n = n->next) {
+ if (n->kind == ND_FUNC_DEF) codegen_func(n);
+ }
+
+ if (target == TARGET_HOST) {
+ out("global _start\n_start:\n");
+ out(" call main\n");
+ out(" mov ebx, eax\n mov eax, 1\n int 0x80\n");
+ }
+
+ int any_data = 0, any_bss = 0;
+ for (node_t *n = prog; n; n = n->next) {
+ if (n->kind == ND_VAR_DECL && (n->is_global || n->is_static_local)) {
+ if (n->init) any_data = 1; else any_bss = 1;
+ }
+ }
+
+ if (any_data) {The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/codegen.c has been updated successfully. (file state is current in your context — no need to Read it back)
444 extern int is_builtin_name(const char *name);
445
446 #define MAX_EXTERNS 64
447 static char g_externs[MAX_EXTERNS][MAX_NAME];
448 static int g_n_externs;
449
450 static int already_extern(const char *name) {
451 for (int i = 0; i < g_n_externs; i++) if (strcmp(g_externs[i], name) == 0) return 1;
452 return 0;
453 }
454
455 /* Walks every reachable node (function bodies, global initializers - a/b/
456 * c/d/list/init/next cover every kind of child link used anywhere in the
457 * AST) collecting two things in one pass: names of called-but-not-
458 * locally-defined, non-builtin functions (need `extern`), and string
459 * literals (need their bytes emitted into .rodata with their label). */
460 static void walk_collect(node_t *n, node_t *prog) {
461 if (!n) return;
462 if (n->kind == ND_CALL) {
463 int is_local = 0;
464 for (node_t *f = prog; f; f = f->next) {
465 if (f->kind == ND_FUNC_DEF && strcmp(f->name, n->name) == 0) { is_local = 1; break; }
466 }
467 if (!is_local && !is_builtin_name(n->name) && !already_extern(n->name) && g_n_externs < MAX_EXTERNS) {
468 safe_strcpy(g_externs[g_n_externs++], n->name, MAX_NAME);
469 }
470 }
471 if (n->kind == ND_STR) {
472 out("global %s\n%s:\n", n->asm_label, n->asm_label);
473 for (const char *p = n->str; ; p++) {
474 out(" db %d\n", (int)(unsigned char)*p);
475 if (!*p) break;
476 }
477 }
478 walk_collect(n->a, prog);
479 walk_collect(n->b, prog);
480 walk_collect(n->c, prog);
481 walk_collect(n->d, prog);
482 walk_collect(n->list, prog);
483 walk_collect(n->init, prog);
484 walk_collect(n->next, prog);
485 }
486
487 void codegen_emit(node_t *prog, void *out_file, cc_target_t target) {
488 g_out = (FILE *)out_file;
489 g_target = target;
490 g_label_id = 0;
491 g_loop = NULL;
492 g_n_externs = 0;
493
494 out("BITS 32\n");
495
496 for (node_t *n = prog; n; n = n->next) {
497 if (n->kind != ND_FUNC_DEF) walk_collect(n, prog); /* globals: just for string literals */
498 else walk_collect(n->b, prog); /* function bodies */
499 }
500 for (int i = 0; i < g_n_externs; i++) out("extern %s\n", g_externs[i]);
501
502 out("section .text\n");
503 for (node_t *n = prog; n; n = n->next) {- /* Walks every reachable node (function bodies, global initializers - a/b/
- * c/d/list/init/next cover every kind of child link used anywhere in the
- * AST) collecting two things in one pass: names of called-but-not-
- * locally-defined, non-builtin functions (need `extern`), and string
- * literals (need their bytes emitted into .rodata with their label). */
- static void walk_collect(node_t *n, node_t *prog) {
- if (!n) return;
- if (n->kind == ND_CALL) {
- int is_local = 0;
- for (node_t *f = prog; f; f = f->next) {
- if (f->kind == ND_FUNC_DEF && strcmp(f->name, n->name) == 0) { is_local = 1; break; }
- }
- if (!is_local && !is_builtin_name(n->name) && !already_extern(n->name) && g_n_externs < MAX_EXTERNS) {
- safe_strcpy(g_externs[g_n_externs++], n->name, MAX_NAME);
- }
- }
- if (n->kind == ND_STR) {
- out("global %s\n%s:\n", n->asm_label, n->asm_label);
- for (const char *p = n->str; ; p++) {
- out(" db %d\n", (int)(unsigned char)*p);
- if (!*p) break;
- }
- }
- walk_collect(n->a, prog);
- walk_collect(n->b, prog);
- walk_collect(n->c, prog);
- walk_collect(n->d, prog);
- walk_collect(n->list, prog);
- walk_collect(n->init, prog);
- walk_collect(n->next, prog);
- }
-
- void codegen_emit(node_t *prog, void *out_file, cc_target_t target) {
- g_out = (FILE *)out_file;
- g_target = target;
- g_label_id = 0;
- g_loop = NULL;
- g_n_externs = 0;
-
- out("BITS 32\n");
-
- for (node_t *n = prog; n; n = n->next) {
- if (n->kind != ND_FUNC_DEF) walk_collect(n, prog); /* globals: just for string literals */
- else walk_collect(n->b, prog); /* function bodies */
- }
- for (int i = 0; i < g_n_externs; i++) out("extern %s\n", g_externs[i]);
-
- out("section .text\n");
- for (node_t *n = prog; n; n = n->next) {
- if (n->kind == ND_FUNC_DEF) codegen_func(n);
- }
+ #define MAX_STRINGS 256
+ static node_t *g_strings[MAX_STRINGS];
+ static int g_n_strings;
+
+ /* Walks every reachable node (function bodies, global initializers - a/b/
+ * c/d/list/init/next cover every kind of child link used anywhere in the
+ * AST), collecting (not yet emitting - the caller isn't in the right
+ * section context here) two things in one pass: names of called-but-not-
+ * locally-defined, non-builtin functions (need `extern`), and string
+ * literal nodes (need their bytes emitted into a data section later). */
+ static void walk_collect(node_t *n, node_t *prog) {
+ if (!n) return;
+ if (n->kind == ND_CALL) {
+ int is_local = 0;
+ for (node_t *f = prog; f; f = f->next) {
+ if (f->kind == ND_FUNC_DEF && strcmp(f->name, n->name) == 0) { is_local = 1; break; }
+ }
+ if (!is_local && !is_builtin_name(n->name) && !already_extern(n->name) && g_n_externs < MAX_EXTERNS) {
+ safe_strcpy(g_externs[g_n_externs++], n->name, MAX_NAME);
+ }
+ }
+ if (n->kind == ND_STR && g_n_strings < MAX_STRINGS) {
+ g_strings[g_n_strings++] = n;
+ }
+ walk_collect(n->a, prog);
+ walk_collect(n->b, prog);
+ walk_collect(n->c, prog);
+ walk_collect(n->d, prog);
+ walk_collect(n->list, prog);
+ walk_collect(n->init, prog);
+ walk_collect(n->next, prog);
+ }
+
+ void codegen_emit(node_t *prog, void *out_file, cc_target_t target) {
+ g_out = (FILE *)out_file;
+ g_target = target;
+ g_label_id = 0;
+ g_loop = NULL;
+ g_n_externs = 0;
+ g_n_strings = 0;
+
+ out("BITS 32\n");
+
+ for (node_t *n = prog; n; n = n->next) {
+ if (n->kind != ND_FUNC_DEF) walk_collect(n, prog); /* globals: just for string literals in initializers */
+ else walk_collect(n->b, prog); /* function bodies */
+ }
+ for (int i = 0; i < g_n_externs; i++) out("extern %s\n", g_externs[i]);
+
+ out("section .text\n");
+ for (node_t *n = prog; n; n = n->next) {
+ if (n->kind == ND_FUNC_DEF) codegen_func(n);
+ }
+
+ if (g_n_strings) {
+ out("section .rodata\n");
+ for (int i = 0; i < g_n_strings; i++) {
+ node_t *s = g_strings[i];
+ out("global %s\n%s:\n", s->asm_label, s->asm_label);
+ for (const char *p = s->str; ; p++) {
+ out(" db %d\n", (int)(unsigned char)*p);
+ if (!*p) break;
+ }
+ }
+ }The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/codegen.c has been updated successfully. (file state is current in your context — no need to Read it back)
495
496 out("BITS 32\n");
497
498 for (node_t *n = prog; n; n = n->next) {
499 if (n->kind != ND_FUNC_DEF) walk_collect(n, prog); /* globals: just for string literals in initializers */
500 else walk_collect(n->b, prog); /* function bodies */
501 }
502 for (int i = 0; i < g_n_externs; i++) out("extern %s\n", g_externs[i]);
503
504 out("section .text\n");
505 for (node_t *n = prog; n; n = n->next) {
506 if (n->kind == ND_FUNC_DEF) codegen_func(n);
507 }
508
509 if (g_n_strings) {
510 out("section .rodata\n");
511 for (int i = 0; i < g_n_strings; i++) {
512 node_t *s = g_strings[i];
513 out("global %s\n%s:\n", s->asm_label, s->asm_label);
514 for (const char *p = s->str; ; p++) {
515 out(" db %d\n", (int)(unsigned char)*p);
516 if (!*p) break;
517 }
518 }
519 }
520
521 if (target == TARGET_HOST) {
522 out("global _start\n_start:\n");
523 out(" call main\n");
524 out(" mov ebx, eax\n mov eax, 1\n int 0x80\n");
525 }
526
527 int any_data = 0, any_bss = 0;
528 for (node_t *n = prog; n; n = n->next) {
529 if (n->kind == ND_VAR_DECL && (n->is_global || n->is_static_local)) {
530 if (n->init) any_data = 1; else any_bss = 1;
531 }
532 }
533
534 if (any_data) {
535 out("section .data\n");
536 for (node_t *n = prog; n; n = n->next) {
537 if (n->kind == ND_VAR_DECL && (n->is_global || n->is_static_local) && n->init) {
538 out("global %s\n%s:\n", n->asm_label, n->asm_label);
539 emit_data_bytes(n->init, n->type);
540 }
541 }
542 }
543 if (any_bss) {
544 out("section .bss\n");- out("section .text\n");
- for (node_t *n = prog; n; n = n->next) {
- if (n->kind == ND_FUNC_DEF) codegen_func(n);
- }
-
- if (g_n_strings) {
- out("section .rodata\n");
- for (int i = 0; i < g_n_strings; i++) {
- node_t *s = g_strings[i];
- out("global %s\n%s:\n", s->asm_label, s->asm_label);
- for (const char *p = s->str; ; p++) {
- out(" db %d\n", (int)(unsigned char)*p);
- if (!*p) break;
- }
- }
- }
-
- if (target == TARGET_HOST) {
- out("global _start\n_start:\n");
- out(" call main\n");
- out(" mov ebx, eax\n mov eax, 1\n int 0x80\n");
- }
-
- int any_data = 0, any_bss = 0;
+ out("section .text\n");
+ for (node_t *n = prog; n; n = n->next) {
+ if (n->kind == ND_FUNC_DEF) codegen_func(n);
+ }
+ if (target == TARGET_HOST) {
+ /* Still inside `section .text` - must stay there, not after the
+ * .rodata switch below. */
+ out("global _start\n_start:\n");
+ out(" call main\n");
+ out(" mov ebx, eax\n mov eax, 1\n int 0x80\n");
+ }
+
+ if (g_n_strings) {
+ out("section .rodata\n");
+ for (int i = 0; i < g_n_strings; i++) {
+ node_t *s = g_strings[i];
+ out("global %s\n%s:\n", s->asm_label, s->asm_label);
+ for (const char *p = s->str; ; p++) {
+ out(" db %d\n", (int)(unsigned char)*p);
+ if (!*p) break;
+ }
+ }
+ }
+
+ int any_data = 0, any_bss = 0;The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/codegen.c has been updated successfully. (file state is current in your context — no need to Read it back)
#include "cc.h"
#include <stdio.h>
#include <string.h>
/* The fixed builtin list this compiler supports, in place of a general
* GCC-extended-inline-asm parser (see docs/toolchain.md for why - the
* kernel's actual inline-asm usage reduces to exactly these ~7
* operations). Each maps 1:1 to a short, fixed instruction sequence;
* arguments are evaluated left-to-right via the normal expression
* codegen, matching how a real function call would evaluate them,
* then moved into the fixed registers x86 port I/O and syscalls
* require. `emit_arg(node)` is codegen_expr (result in EAX);
* `emitf(fmt, ...)` writes a line of assembly text. */
typedef void (*emit_arg_fn)(node_t *);
typedef void (*emitf_fn)(const char *, ...);
static node_t *nth(node_t *args, int i) {
for (; args && i > 0; args = args->next, i--) {}
return args;
}
int is_builtin_name(const char *name) {
static const char *names[] = {
"__builtin_inb", "__builtin_outb", "__builtin_inw", "__builtin_outw",
"__builtin_lidt", "__builtin_gdt_flush", "__builtin_hlt",
"__sys_open", "__sys_read", "__sys_write", "__sys_lseek", "__sys_close", "__sys_exit",
NULL,
};
for (int i = 0; names[i]; i++) if (strcmp(name, names[i]) == 0) return 1;
return 0;
}
/* Returns 1 if `name` was a builtin (and emitted code for it), 0
* otherwise (ordinary call - the caller falls back to a normal `call`). */
int builtin_emit_call(const char *name, node_t *args, cc_target_t target,
emit_arg_fn emit_arg, emitf_fn emitf) {
(void)target;
/* --- kernel port I/O + privileged instructions --- */
if (strcmp(name, "__builtin_outb") == 0) {
emit_arg(nth(args, 0)); emitf(" push eax\n"); /* port */
emit_arg(nth(args, 1)); emitf(" mov ecx, eax\n"); /* value */
emitf(" pop edx\n mov eax, ecx\n mov edx, edx\n");
emitf(" xchg eax, edx\n"); /* eax=port, edx=value - need dx=port, al=value: fix up below */
/* Simpler, unambiguous sequence, ignoring the scratch above: */
return 1;
}
if (strcmp(name, "__builtin_inb") == 0) {
emit_arg(nth(args, 0)); /* port */
emitf(" mov edx, eax\n in al, dx\n movzx eax, al\n");
return 1;
}
if (strcmp(name, "__builtin_outw") == 0) {
emit_arg(nth(args, 0)); emitf(" push eax\n");
emit_arg(nth(args, 1)); emitf(" mov ecx, eax\n");
emitf(" pop edx\n mov eax, ecx\n out dx, ax\n");
return 1;
}
if (strcmp(name, "__builtin_inw") == 0) {
emit_arg(nth(args, 0));
emitf(" mov edx, eax\n in ax, dx\n movzx eax, ax\n");
return 1;
}
if (strcmp(name, "__builtin_hlt") == 0) { emitf(" hlt\n"); return 1; }
if (strcmp(name, "__builtin_lidt") == 0) {
emit_arg(nth(args, 0));
emitf(" mov ecx, eax\n lidt [ecx]\n");
return 1;
}
if (strcmp(name, "__builtin_gdt_flush") == 0) {
emit_arg(nth(args, 0));
emitf(" lgdt [eax]\n");
emitf(" mov ax, 0x10\n mov ds, ax\n mov es, ax\n mov fs, ax\n mov gs, ax\n mov ss, ax\n");
emitf(" jmp 0x08:%s\n", ".Lgdtflush_next");
emitf("%s:\n", ".Lgdtflush_next");
return 1;
}
/* --- raw Linux syscalls (host target only - see docs/toolchain.md) --- */
if (strcmp(name, "__sys_exit") == 0) {
emit_arg(nth(args, 0)); emitf(" mov ebx, eax\n mov eax, 1\n int 0x80\n");
return 1;
}
if (strcmp(name, "__sys_open") == 0 || strcmp(name, "__sys_close") == 0) {
int nr = strcmp(name, "__sys_open") == 0 ? 5 : 6;
if (nr == 6) {
emit_arg(nth(args, 0)); emitf(" mov ebx, eax\n mov eax, 6\n int 0x80\n");
return 1;
}
emit_arg(nth(args, 0)); emitf(" push eax\n");
emit_arg(nth(args, 1)); emitf(" push eax\n");
emitf(" mov edx, 0o644\n pop ecx\n pop ebx\n mov eax, 5\n int 0x80\n");
return 1;
}
if (strcmp(name, "__sys_read") == 0 || strcmp(name, "__sys_write") == 0) {
int nr = strcmp(name, "__sys_read") == 0 ? 3 : 4;
emit_arg(nth(args, 0)); emitf(" push eax\n");
emit_arg(nth(args, 1)); emitf(" push eax\n");
emit_arg(nth(args, 2)); emitf(" mov edx, eax\n pop ecx\n pop ebx\n mov eax, %d\n int 0x80\n", nr);
return 1;
}
if (strcmp(name, "__sys_lseek") == 0) {
emit_arg(nth(args, 0)); emitf(" push eax\n");
emit_arg(nth(args, 1)); emitf(" push eax\n");
emit_arg(nth(args, 2)); emitf(" mov edx, eax\n pop ecx\n pop ebx\n mov eax, 19\n int 0x80\n");
return 1;
}
return 0;
}
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/builtins.c (file state is current in your context — no need to Read it back)
#include "cc.h"
#include <stdio.h>
#include <string.h>
/* The fixed builtin list this compiler supports, in place of a general
* GCC-extended-inline-asm parser (see docs/toolchain.md for why - the
* kernel's actual inline-asm usage reduces to exactly these ~7
* operations). Each maps 1:1 to a short, fixed instruction sequence;
* arguments are evaluated left-to-right via the normal expression
* codegen, matching how a real function call would evaluate them,
* then moved into the fixed registers x86 port I/O and syscalls
* require. `emit_arg(node)` is codegen_expr (result in EAX);
* `emitf(fmt, ...)` writes a line of assembly text. */
typedef void (*emit_arg_fn)(node_t *);
typedef void (*emitf_fn)(const char *, ...);
static int g_builtin_label_id;
static node_t *nth(node_t *args, int i) {
for (; args && i > 0; args = args->next, i--) {}
return args;
}
int is_builtin_name(const char *name) {
static const char *names[] = {
"__builtin_inb", "__builtin_outb", "__builtin_inw", "__builtin_outw",
"__builtin_lidt", "__builtin_gdt_flush", "__builtin_hlt",
"__sys_open", "__sys_read", "__sys_write", "__sys_lseek", "__sys_close", "__sys_exit",
NULL,
};
for (int i = 0; names[i]; i++) if (strcmp(name, names[i]) == 0) return 1;
return 0;
}
/* Returns 1 if `name` was a builtin (and emitted code for it), 0
* otherwise (ordinary call - the caller falls back to a normal `call`). */
int builtin_emit_call(const char *name, node_t *args, cc_target_t target,
emit_arg_fn emit_arg, emitf_fn emitf) {
(void)target;
/* --- kernel port I/O + privileged instructions --- */
if (strcmp(name, "__builtin_outb") == 0) {
emit_arg(nth(args, 0)); emitf(" push eax\n"); /* port */
emit_arg(nth(args, 1)); emitf(" mov ecx, eax\n"); /* value */
emitf(" pop eax\n"); /* eax = port */
emitf(" mov edx, eax\n"); /* edx = port (dx usable) */
emitf(" mov eax, ecx\n"); /* eax = value (al usable) */
emitf(" out dx, al\n");
return 1;
}
if (strcmp(name, "__builtin_inb") == 0) {
emit_arg(nth(args, 0)); /* port */
emitf(" mov edx, eax\n in al, dx\n movzx eax, al\n");
return 1;
}
if (strcmp(name, "__builtin_outw") == 0) {
emit_arg(nth(args, 0)); emitf(" push eax\n");
emit_arg(nth(args, 1)); emitf(" mov ecx, eax\n");
emitf(" pop eax\n mov edx, eax\n mov eax, ecx\n out dx, ax\n");
return 1;
}
if (strcmp(name, "__builtin_inw") == 0) {
emit_arg(nth(args, 0));
emitf(" mov edx, eax\n in ax, dx\n movzx eax, ax\n");
return 1;
}
if (strcmp(name, "__builtin_hlt") == 0) { emitf(" hlt\n"); return 1; }
if (strcmp(name, "__builtin_lidt") == 0) {
emit_arg(nth(args, 0)); /* pointer to the idt descriptor */
emitf(" mov ecx, eax\n lidt [ecx]\n");
return 1;
}
if (strcmp(name, "__builtin_gdt_flush") == 0) {
/* Loads the GDT, reloads every data/stack segment to the flat
* data selector (0x10, matching gdt.c's layout), and far-jumps
* to the flat code selector (0x08) to reload CS - the one
* sequence in the kernel that inherently can't be a single
* instruction. A unique label per call site (own counter,
* ".Lbi" prefix so it can never collide with codegen.c's own
* ".L" control-flow labels) since this can be called more than
* once in a translation unit. */
char lbl[MAX_NAME];
snprintf(lbl, sizeof(lbl), ".Lbi%d", g_builtin_label_id++);
emit_arg(nth(args, 0)); /* pointer to the gdt descriptor */
emitf(" mov ecx, eax\n lgdt [ecx]\n");
emitf(" mov ax, 0x10\n mov ds, ax\n mov es, ax\n mov fs, ax\n mov gs, ax\n mov ss, ax\n");
emitf(" jmp 0x08:%s\n", lbl);
emitf("%s:\n", lbl);
return 1;
}
/* --- raw Linux syscalls (host target only - see docs/toolchain.md) --- */
if (strcmp(name, "__sys_exit") == 0) {
emit_arg(nth(args, 0)); emitf(" mov ebx, eax\n mov eax, 1\n int 0x80\n");
return 1;
}
if (strcmp(name, "__sys_close") == 0) {
emit_arg(nth(args, 0)); emitf(" mov ebx, eax\n mov eax, 6\n int 0x80\n");
return 1;
}
if (strcmp(name, "__sys_open") == 0) {
emit_arg(nth(args, 0)); emitf(" push eax\n"); /* path */
emit_arg(nth(args, 1)); emitf(" push eax\n"); /* flags */
emitf(" mov edx, 420\n"); /* mode 0644, used only when O_CREAT is set */
emitf(" pop ecx\n pop ebx\n mov eax, 5\n int 0x80\n");
return 1;
}
if (strcmp(name, "__sys_read") == 0 || strcmp(name, "__sys_write") == 0) {
int nr = strcmp(name, "__sys_read") == 0 ? 3 : 4;
emit_arg(nth(args, 0)); emitf(" push eax\n"); /* fd */
emit_arg(nth(args, 1)); emitf(" push eax\n"); /* buf */
emit_arg(nth(args, 2)); emitf(" mov edx, eax\n"); /* count */
emitf(" pop ecx\n pop ebx\n mov eax, %d\n int 0x80\n", nr);
return 1;
}
if (strcmp(name, "__sys_lseek") == 0) {
emit_arg(nth(args, 0)); emitf(" push eax\n"); /* fd */
emit_arg(nth(args, 1)); emitf(" push eax\n"); /* offset */
emit_arg(nth(args, 2)); emitf(" mov edx, eax\n"); /* whence */
emitf(" pop ecx\n pop ebx\n mov eax, 19\n int 0x80\n");
return 1;
}
return 0;
}
The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/builtins.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc gcc -Wall -Wextra -O0 -g -o /tmp/mycc lexer.c preprocessor.c types.c parser.c codegen.c builtins.c main.c 2>&1
Exit code 1
codegen.c: In function ‘out’:
codegen.c:28:5: error: implicit declaration of function ‘va_start’ [-Wimplicit-function-declaration]
28 | va_start(ap, fmt);
| ^~~~~~~~
codegen.c:30:5: error: implicit declaration of function ‘va_end’ [-Wimplicit-function-declaration]
30 | va_end(ap);
| ^~~~~~- #include "cc.h" - - #include <stdio.h> - #include <string.h> - #include <stdlib.h> + #include "cc.h" + + #include <stdio.h> + #include <stdarg.h> + #include <string.h> + #include <stdlib.h>
The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/codegen.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc gcc -Wall -Wextra -O0 -g -o /tmp/mycc lexer.c preprocessor.c types.c parser.c codegen.c builtins.c main.c 2>&1
(Bash completed with no output)
int add(int a, int b) {
return a + b;
}
int main(void) {
int x = add(2, 3);
return x;
}
File created successfully at: /tmp/t_simple.c (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude /tmp/mycc --target=host -o /tmp/t_simple.S /tmp/t_simple.c echo "compile exit=$?" cat /tmp/t_simple.S
compile exit=0
BITS 32
section .text
global add
add:
push ebp
mov ebp, esp
lea eax, [ebp+12]
mov eax, [eax]
push eax
lea eax, [ebp+8]
mov eax, [eax]
pop ecx
add eax, ecx
mov esp, ebp
pop ebp
ret
mov eax, 0
mov esp, ebp
pop ebp
ret
global main
main:
push ebp
mov ebp, esp
sub esp, 4
lea eax, [ebp-4]
push eax
mov eax, 3
push eax
mov eax, 2
push eax
call add
add esp, 8
pop ecx
mov [ecx], eax
lea eax, [ebp-4]
mov eax, [eax]
mov esp, ebp
pop ebp
ret
mov eax, 0
mov esp, ebp
pop ebp
ret
global _start
_start:
call main
mov ebx, eax
mov eax, 1
int 0x80$ /tmp/myas -f elf32 -o /tmp/t_simple.o /tmp/t_simple.S echo "as exit=$?" ld -m elf_i386 -o /tmp/t_simple /tmp/t_simple.o 2>&1 echo "ld exit=$?" /tmp/t_simple echo "run exit=$?"
as exit=0 ld exit=0 run exit=5
int add(int a, int b) {
return a + b;
}
int main(void) {
return add(2, 3); /* expect 5 */
}
The file /home/martin/workspace/prototypes/boot-to-claude/tests/toolchain/t01_basic.c has been updated successfully. (file state is current in your context — no need to Read it back)
int main(void) {
int sum = 0;
int i = 0;
while (i < 5) {
sum = sum + i;
i = i + 1;
}
/* sum = 0+1+2+3+4 = 10 */
for (int j = 0; j < 3; j++) {
sum = sum + 1;
}
/* sum = 13 */
int k = 0;
do {
k++;
} while (k < 4);
sum = sum + k; /* sum = 17 */
if (sum > 15) {
sum = sum + 100;
} else {
sum = 0;
}
/* sum = 117 */
int total = 0;
for (int n = 0; n < 10; n++) {
if (n == 5) continue;
if (n == 8) break;
total = total + 1;
}
/* n=0..4 (5), skip 5, n=6,7 (2), break at 8: total = 7 */
return sum - 117 + total; /* expect 7 */
}
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/tests/toolchain/t02_control.c (file state is current in your context — no need to Read it back)
int fib(int n) {
if (n < 2) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
int main(void) {
return fib(10); /* expect 55 */
}
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/tests/toolchain/t03_recursion.c (file state is current in your context — no need to Read it back)
int arr_sum(int *a, int n) {
int total = 0;
for (int i = 0; i < n; i++) {
total = total + a[i];
}
return total;
}
void inc_via_ptr(int *p) {
*p = *p + 1;
}
int main(void) {
int nums[5];
for (int i = 0; i < 5; i++) {
nums[i] = i * 2;
}
/* nums = 0,2,4,6,8 -> sum = 20 */
int x = 10;
int *px = &x;
inc_via_ptr(px);
/* x = 11 */
char buf[4];
buf[0] = 'a';
buf[1] = 'b';
buf[2] = 'c';
buf[3] = 0;
int charsum = buf[0] + buf[1] + buf[2]; /* 'a'+'b'+'c' = 97+98+99=294 */
return arr_sum(nums, 5) + x - charsum + 294; /* 20 + 11 - 294 + 294 = 31 */
}
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/tests/toolchain/t04_pointers.c (file state is current in your context — no need to Read it back)
typedef struct {
int x;
int y;
} point_t;
typedef struct {
char used;
int size;
} __attribute__((packed)) packed_t;
int dist_manhattan(point_t *a, point_t *b) {
int dx = a->x - b->x;
int dy = a->y - b->y;
if (dx < 0) dx = -dx;
if (dy < 0) dy = -dy;
return dx + dy;
}
static point_t g_origin;
int main(void) {
point_t p1;
p1.x = 3;
p1.y = 4;
point_t p2;
p2.x = 0;
p2.y = 0;
g_origin.x = 1;
g_origin.y = 1;
int d1 = dist_manhattan(&p1, &p2); /* 3+4=7 */
int d2 = dist_manhattan(&p1, &g_origin); /* 2+3=5 */
packed_t pk;
pk.used = 1;
pk.size = 100;
int psize = sizeof(packed_t); /* packed: 1 + 4 = 5 bytes, no padding */
return d1 + d2 + psize - pk.size + 100; /* 7+5+5-100+100 = 17 */
}
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/tests/toolchain/t05_structs.c (file state is current in your context — no need to Read it back)
#!/usr/bin/env bash
# Compiles each tests/toolchain/*.c fixture through CC (--target=host) +
# AS, links with GNU ld (interim - LD doesn't exist until Phase T5), runs
# it directly, and checks the exit code. Fast host-side iteration for the
# compiler; kernel-target builtins (port I/O etc) are verified separately
# via a QEMU boot, since they can't run on the host.
set -euo pipefail
cd "$(dirname "$0")/.."
CC=${CC_BIN:-build/toolchain/cc}
AS=${AS_BIN:-build/toolchain/as}
BUILD=build/toolchain_tests
mkdir -p "$BUILD"
declare -A EXPECTED=(
[t01_basic]=5
[t02_control]=7
[t03_recursion]=55
[t04_pointers]=31
[t05_structs]=17
)
fail=0
for src in tests/toolchain/*.c; do
name=$(basename "$src" .c)
exp=${EXPECTED[$name]:-}
if [ -z "$exp" ]; then
echo "SKIP $name (no expected value registered)"
continue
fi
"$CC" -I toolchain/include --target=host -o "$BUILD/$name.S" "$src"
"$AS" -f elf32 -o "$BUILD/$name.o" "$BUILD/$name.S"
ld -m elf_i386 -o "$BUILD/$name" "$BUILD/$name.o"
set +e
"$BUILD/$name"
got=$?
set -e
if [ "$got" -eq "$exp" ]; then
echo "PASS $name ($got)"
else
echo "FAIL $name: expected $exp, got $got"
fail=1
fi
done
if [ "$fail" -eq 0 ]; then
echo "run_toolchain_tests: PASS"
else
echo "run_toolchain_tests: FAIL"
exit 1
fi
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/tools/run_toolchain_tests.sh (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude mkdir -p build/toolchain cp /tmp/mycc build/toolchain/cc cp /tmp/myas build/toolchain/as chmod +x tools/run_toolchain_tests.sh ./tools/run_toolchain_tests.sh
Exit code 1 PASS t01_basic (5) PASS t02_control (7) SKIP t02_types (no expected value registered) PASS t03_recursion (55) PASS t04_pointers (31) ./tools/run_toolchain_tests.sh: line 25: 2891134 Segmentation fault (core dumped) "$BUILD/$name" FAIL t05_structs: expected 17, got 139 run_toolchain_tests: FAIL
$ cd /home/martin/workspace/prototypes/boot-to-claude build/toolchain/cc -I toolchain/include --target=host -o /tmp/t05.S tests/toolchain/t05_structs.c cat /tmp/t05.S
BITS 32
section .text
global dist_manhattan
dist_manhattan:
push ebp
mov ebp, esp
sub esp, 8
lea eax, [ebp-4]
push eax
lea eax, [ebp+12]
mov eax, [eax]
mov eax, [eax]
push eax
lea eax, [ebp+8]
mov eax, [eax]
mov eax, [eax]
pop ecx
sub eax, ecx
pop ecx
mov [ecx], eax
lea eax, [ebp-8]
push eax
lea eax, [ebp+12]
mov eax, [eax]
mov eax, [eax]
push eax
lea eax, [ebp+8]
mov eax, [eax]
mov eax, [eax]
pop ecx
sub eax, ecx
pop ecx
mov [ecx], eax
mov eax, 0
push eax
lea eax, [ebp-4]
mov eax, [eax]
pop ecx
cmp eax, ecx
setl al
movzx eax, al
cmp eax, 0
je .L0
lea eax, [ebp-4]
push eax
lea eax, [ebp-4]
mov eax, [eax]
neg eax
pop ecx
mov [ecx], eax
jmp .L1
.L0:
.L1:
mov eax, 0
push eax
lea eax, [ebp-8]
mov eax, [eax]
pop ecx
cmp eax, ecx
setl al
movzx eax, al
cmp eax, 0
je .L2
lea eax, [ebp-8]
push eax
lea eax, [ebp-8]
mov eax, [eax]
neg eax
pop ecx
mov [ecx], eax
jmp .L3
.L2:
.L3:
lea eax, [ebp-8]
mov eax, [eax]
push eax
lea eax, [ebp-4]
mov eax, [eax]
pop ecx
add eax, ecx
mov esp, ebp
pop ebp
ret
mov eax, 0
mov esp, ebp
pop ebp
ret
global main
main:
push ebp
mov ebp, esp
sub esp, 36
lea eax, [ebp-8]
push eax
mov eax, 3
pop ecx
mov [ecx], eax
lea eax, [ebp-8]
add eax, 4
push eax
mov eax, 4
pop ecx
mov [ecx], eax
lea eax, [ebp-16]
push eax
mov eax, 0
pop ecx
mov [ecx], eax
lea eax, [ebp-16]
add eax, 4
push eax
mov eax, 0
pop ecx
mov [ecx], eax
lea eax, [g_origin]
push eax
mov eax, 1
pop ecx
mov [ecx], eax
lea eax, [g_origin]
add eax, 4
push eax
mov eax, 1
pop ecx
mov [ecx], eax
lea eax, [ebp-20]
push eax
lea eax, [ebp-16]
push eax
lea eax, [ebp-8]
push eax
call dist_manhattan
add esp, 8
pop ecx
mov [ecx], eax
lea eax, [ebp-24]
push eax
lea eax, [g_origin]
push eax
lea eax, [ebp-8]
push eax
call dist_manhattan
add esp, 8
pop ecx
mov [ecx], eax
lea eax, [ebp-32]
push eax
mov eax, 1
pop ecx
mov [ecx], al
lea eax, [ebp-32]
add eax, 1
push eax
mov eax, 100
pop ecx
mov [ecx], eax
lea eax, [ebp-36]
push eax
mov eax, 5
pop ecx
mov [ecx], eax
mov eax, 100
push eax
lea eax, [ebp-32]
add eax, 1
mov eax, [eax]
push eax
lea eax, [ebp-36]
mov eax, [eax]
push eax
lea eax, [ebp-24]
mov eax, [eax]
push eax
lea eax, [ebp-20]
mov eax, [eax]
pop ecx
add eax, ecx
pop ecx
add eax, ecx
pop ecx
sub eax, ecx
pop ecx
add eax, ecx
mov esp, ebp
pop ebp
ret
mov eax, 0
mov esp, ebp
pop ebp
ret
global _start
_start:
call main
mov ebx, eax
mov eax, 1
int 0x80
section .bss
global g_origin
g_origin:
resb 8$ cd /home/martin/workspace/prototypes/boot-to-claude build/toolchain/as -f elf32 -o /tmp/t05.o /tmp/t05.S ld -m elf_i386 -o /tmp/t05 /tmp/t05.o which gdb && gdb -batch -ex run -ex bt -ex "info registers" /tmp/t05 2>&1 | tail -40
/bin/gdb Program received signal SIGSEGV, Segmentation fault. 0x08049144 in main () #0 0x08049144 in main () eax 0x1 1 ecx 0x0 0 edx 0x0 0 ebx 0x0 0 esp 0xffffcab4 0xffffcab4 ebp 0xffffcad8 0xffffcad8 esi 0x0 0 edi 0x0 0 eip 0x8049144 0x8049144 <main+94> eflags 0x10286 [ PF SF IF RF ] cs 0x23 35 ss 0x2b 43 ds 0x2b 43 es 0x2b 43 fs 0x0 0 gs 0x0 0
$ objdump -d /tmp/t05 2>&1 | grep -B5 -A2 "8049144:"
8049135: 89 01 mov %eax,(%ecx) 8049137: 8d 05 00 00 00 00 lea 0x0,%eax 804913d: 50 push %eax 804913e: b8 01 00 00 00 mov $0x1,%eax 8049143: 59 pop %ecx 8049144: 89 01 mov %eax,(%ecx) 8049146: 8d 05 00 00 00 00 lea 0x0,%eax 804914c: 81 c0 04 00 00 00 add $0x4,%eax
$ readelf -r /tmp/t05.o 2>&1 | head -30 echo "---symtab---" readelf -s /tmp/t05.o 2>&1 | grep -i origin
Relocation section '.rel.text' at offset 0x25e contains 3 entries:
Offset Info Type Sym.Value Sym. Name
00000139 00000801 R_386_32 00000000 g_origin
00000148 00000801 R_386_32 00000000 g_origin
00000187 00000801 R_386_32 00000000 g_origin
---symtab---
8: 00000000 0 NOTYPE GLOBAL DEFAULT 3 g_origin$ readelf -S /tmp/t05.o 2>&1 echo "---" readelf -s /tmp/t05.o 2>&1 | head -20
There are 7 section headers, starting at offset 0x3a4:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 000034 00022a 00 AX 0 0 4
[ 2] .bss NOBITS 00000000 00025e 000008 00 WA 0 0 4
[ 3] .rel.text REL 00000000 00025e 000018 08 4 1 4
[ 4] .symtab SYMTAB 00000000 000276 000090 10 5 5 4
[ 5] .strtab STRTAB 00000000 000306 00006d 00 0 0 1
[ 6] .shstrtab STRTAB 00000000 000373 000030 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), p (processor specific)
---
Symbol table '.symtab' contains 9 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000089 0 NOTYPE LOCAL DEFAULT 1 dist_manhattan.L0
2: 00000089 0 NOTYPE LOCAL DEFAULT 1 dist_manhattan.L1
3: 000000c5 0 NOTYPE LOCAL DEFAULT 1 dist_manhattan.L2
4: 000000c5 0 NOTYPE LOCAL DEFAULT 1 dist_manhattan.L3
5: 00000000 0 NOTYPE GLOBAL DEFAULT 1 dist_manhattan
6: 000000e6 0 NOTYPE GLOBAL DEFAULT 1 main
7: 0000021c 0 NOTYPE GLOBAL DEFAULT 1 _start
8: 00000000 0 NOTYPE GLOBAL DEFAULT 3 g_origin$ grep -n "st_shndx\|shidx_content\[s\] = nsh" /home/martin/workspace/prototypes/boot-to-claude/toolchain/as/elf_writer.c
59: sym.st_shndx = (Elf32_Half)(1 + s->section); /* +1: section 0 is the null section */ 62: sym.st_shndx = SHN_UNDEF; 87: shidx_content[s] = nsh++;
<tool_use_error>InputValidationError: Read was called with input that could not be parsed as JSON.
You sent (first 110 of 110 bytes): {"file_path": "/home/martin/workspace/prototypes/boot-to-claude/toolchain/as/elf_writer.c", "offset": 27, 100}
Common causes: unescaped backslashes in file paths (use / or \\), unescaped control characters, or truncated output. Retry with valid JSON.</tool_use_error>27 /* --- decide which content sections exist --- */
28 int content_secs[MAX_SECTIONS];
29 int n_content = 0;
30 for (int s = 0; s < MAX_SECTIONS; s++) {
31 if (ctx->sec_used[s]) content_secs[n_content++] = s;
32 }
33
34 int reloc_for[MAX_SECTIONS]; /* does section s have any relocations? */
35 for (int s = 0; s < MAX_SECTIONS; s++) reloc_for[s] = 0;
36 for (int i = 0; i < ctx->nrelocs; i++) reloc_for[ctx->relocs[i].section] = 1;
37
38 /* --- symbol table: locals first (ELF requirement), then globals --- */
39 static Elf32_Sym symtab[MAX_SYMS + 1];
40 int nsym = 0;
41 static strtab_builder_t strtab;
42 memset(&strtab, 0, sizeof(strtab));
43 static int sym_elf_index[MAX_SYMS];
44
45 symtab[nsym++] = (Elf32_Sym){0}; /* index 0: null symbol */
46
47 for (int pass = 0; pass < 2; pass++) { /* pass 0 = locals, pass 1 = globals */
48 for (int i = 0; i < ctx->nsyms; i++) {
49 symbol_t *s = &ctx->syms[i];
50 if (s->is_equ) continue;
51 if (!s->defined && !s->is_extern) continue;
52 int is_glob = s->is_global || s->is_extern;
53 if ((pass == 0 && is_glob) || (pass == 1 && !is_glob)) continue;
54
55 Elf32_Sym sym = {0};
56 sym.st_name = strtab_add(&strtab, s->name);
57 sym.st_info = (unsigned char)ELF32_ST_INFO(is_glob ? STB_GLOBAL : STB_LOCAL, STT_NOTYPE);
58 if (s->defined) {
59 sym.st_shndx = (Elf32_Half)(1 + s->section); /* +1: section 0 is the null section */
60 sym.st_value = s->offset;
61 } else {
62 sym.st_shndx = SHN_UNDEF;
63 }
64 sym_elf_index[i] = nsym;
65 symtab[nsym++] = sym;
66 }
67 }
68 int first_global = 1;
69 for (int i = 1; i < nsym; i++) {
70 if ((symtab[i].st_info >> 4) == STB_LOCAL) first_global = i + 1;
71 }
72
73 /* --- section header string table --- */
74 static strtab_builder_t shstrtab;
75 memset(&shstrtab, 0, sizeof(shstrtab));
76
77 /* section index plan: 0=NULL, 1..n_content=content, then one
78 * .rel.X per content section with relocs, then .symtab, .strtab,
79 * .shstrtab */
80 int shidx_content[MAX_SECTIONS];
81 for (int i = 0; i < MAX_SECTIONS; i++) { shidx_content[i] = -1; }
82
83 int nsh = 1;
84 uint32_t name_content[MAX_SECTIONS], name_rel[MAX_SECTIONS];
85 for (int i = 0; i < n_content; i++) {
86 int s = content_secs[i];
87 shidx_content[s] = nsh++;
88 name_content[s] = strtab_add(&shstrtab, SEC_NAMES[s]);
89 }
90 for (int i = 0; i < n_content; i++) {
91 int s = content_secs[i];
92 if (reloc_for[s]) {
93 char nm[MAX_NAME];
94 snprintf(nm, sizeof(nm), ".rel%s", SEC_NAMES[s]);
95 nsh++;
96 name_rel[s] = strtab_add(&shstrtab, nm);
97 }
98 }
99 int shidx_symtab = nsh++;
100 uint32_t name_symtab = strtab_add(&shstrtab, ".symtab");
101 int shidx_strtab = nsh++;
102 uint32_t name_strtab = strtab_add(&shstrtab, ".strtab");
103 int shidx_shstrtab = nsh++;
104 uint32_t name_shstrtab = strtab_add(&shstrtab, ".shstrtab");
105
106 /* --- relocation table entries, grouped by target section --- */
107 static Elf32_Rel rels[MAX_SECTIONS][MAX_RELOCS];
108 int nrels[MAX_SECTIONS] = {0};
109 for (int i = 0; i < ctx->nrelocs; i++) {
110 reloc_t *r = &ctx->relocs[i];
111 symbol_t *s = sym_find(ctx, r->sym);
112 if (!s) { fprintf(stderr, "as: relocation against unknown symbol '%s'\n", r->sym); return -1; }
113 int symidx = sym_elf_index[s - ctx->syms];
114 int type = r->kind == RELOC_PC32 ? R_386_PC32 : R_386_32;
115 int sec = r->section;
116 rels[sec][nrels[sec]].r_offset = r->offset;
117 rels[sec][nrels[sec]].r_info = ELF32_R_INFO(symidx, type);
118 nrels[sec]++;
119 }
120
121 /* --- lay out the file: Ehdr, content sections' bytes, rel tables,
122 * symtab, strtab, shstrtab, then the section header table --- */
123 uint32_t off = sizeof(Elf32_Ehdr);
124 uint32_t off_content[MAX_SECTIONS];
125 for (int i = 0; i < n_content; i++) {
126 int s = content_secs[i];- int reloc_for[MAX_SECTIONS]; /* does section s have any relocations? */
- for (int s = 0; s < MAX_SECTIONS; s++) reloc_for[s] = 0;
- for (int i = 0; i < ctx->nrelocs; i++) reloc_for[ctx->relocs[i].section] = 1;
-
- /* --- symbol table: locals first (ELF requirement), then globals --- */
- static Elf32_Sym symtab[MAX_SYMS + 1];
- int nsym = 0;
- static strtab_builder_t strtab;
- memset(&strtab, 0, sizeof(strtab));
- static int sym_elf_index[MAX_SYMS];
-
- symtab[nsym++] = (Elf32_Sym){0}; /* index 0: null symbol */
-
- for (int pass = 0; pass < 2; pass++) { /* pass 0 = locals, pass 1 = globals */
- for (int i = 0; i < ctx->nsyms; i++) {
- symbol_t *s = &ctx->syms[i];
- if (s->is_equ) continue;
- if (!s->defined && !s->is_extern) continue;
- int is_glob = s->is_global || s->is_extern;
- if ((pass == 0 && is_glob) || (pass == 1 && !is_glob)) continue;
-
- Elf32_Sym sym = {0};
- sym.st_name = strtab_add(&strtab, s->name);
- sym.st_info = (unsigned char)ELF32_ST_INFO(is_glob ? STB_GLOBAL : STB_LOCAL, STT_NOTYPE);
- if (s->defined) {
- sym.st_shndx = (Elf32_Half)(1 + s->section); /* +1: section 0 is the null section */
- sym.st_value = s->offset;
- } else {
- sym.st_shndx = SHN_UNDEF;
- }
- sym_elf_index[i] = nsym;
- symtab[nsym++] = sym;
- }
- }
- int first_global = 1;
- for (int i = 1; i < nsym; i++) {
- if ((symtab[i].st_info >> 4) == STB_LOCAL) first_global = i + 1;
- }
-
- /* --- section header string table --- */
- static strtab_builder_t shstrtab;
- memset(&shstrtab, 0, sizeof(shstrtab));
-
- /* section index plan: 0=NULL, 1..n_content=content, then one
- * .rel.X per content section with relocs, then .symtab, .strtab,
- * .shstrtab */
- int shidx_content[MAX_SECTIONS];
- for (int i = 0; i < MAX_SECTIONS; i++) { shidx_content[i] = -1; }
-
- int nsh = 1;
- uint32_t name_content[MAX_SECTIONS], name_rel[MAX_SECTIONS];
- for (int i = 0; i < n_content; i++) {
- int s = content_secs[i];
- shidx_content[s] = nsh++;
- name_content[s] = strtab_add(&shstrtab, SEC_NAMES[s]);
- }
- for (int i = 0; i < n_content; i++) {
- int s = content_secs[i];
- if (reloc_for[s]) {
- char nm[MAX_NAME];
- snprintf(nm, sizeof(nm), ".rel%s", SEC_NAMES[s]);
- nsh++;
- name_rel[s] = strtab_add(&shstrtab, nm);
- }
- }
- int shidx_symtab = nsh++;
- uint32_t name_symtab = strtab_add(&shstrtab, ".symtab");
- int shidx_strtab = nsh++;
- uint32_t name_strtab = strtab_add(&shstrtab, ".strtab");
- int shidx_shstrtab = nsh++;
- uint32_t name_shstrtab = strtab_add(&shstrtab, ".shstrtab");
+ int reloc_for[MAX_SECTIONS]; /* does section s have any relocations? */
+ for (int s = 0; s < MAX_SECTIONS; s++) reloc_for[s] = 0;
+ for (int i = 0; i < ctx->nrelocs; i++) reloc_for[ctx->relocs[i].section] = 1;
+
+ /* --- section header string table + index plan ---
+ * 0=NULL, 1..n_content=content, then one .rel.X per content section
+ * with relocs, then .symtab, .strtab, .shstrtab. Computed before the
+ * symbol table below, which needs shidx_content[] to translate our
+ * internal section_kind_t into the real ELF section header index -
+ * they're NOT the same number, since only sections actually used
+ * appear in the file (e.g. a .bss-only object has no .data, so .bss
+ * isn't at its "usual" index). */
+ static strtab_builder_t shstrtab;
+ memset(&shstrtab, 0, sizeof(shstrtab));
+
+ int shidx_content[MAX_SECTIONS];
+ for (int i = 0; i < MAX_SECTIONS; i++) { shidx_content[i] = -1; }
+
+ int nsh = 1;
+ uint32_t name_content[MAX_SECTIONS], name_rel[MAX_SECTIONS];
+ for (int i = 0; i < n_content; i++) {
+ int s = content_secs[i];
+ shidx_content[s] = nsh++;
+ name_content[s] = strtab_add(&shstrtab, SEC_NAMES[s]);
+ }
+ for (int i = 0; i < n_content; i++) {
+ int s = content_secs[i];
+ if (reloc_for[s]) {
+ char nm[MAX_NAME];
+ snprintf(nm, sizeof(nm), ".rel%s", SEC_NAMES[s]);
+ nsh++;
+ name_rel[s] = strtab_add(&shstrtab, nm);
+ }
+ }
+ int shidx_symtab = nsh++;
+ uint32_t name_symtab = strtab_add(&shstrtab, ".symtab");
+ int shidx_strtab = nsh++;
+ uint32_t name_strtab = strtab_add(&shstrtab, ".strtab");
+ int shidx_shstrtab = nsh++;
+ uint32_t name_shstrtab = strtab_add(&shstrtab, ".shstrtab");
+
+ /* --- symbol table: locals first (ELF requirement), then globals --- */
+ static Elf32_Sym symtab[MAX_SYMS + 1];
+ int nsym = 0;
+ static strtab_builder_t strtab;
+ memset(&strtab, 0, sizeof(strtab));
+ static int sym_elf_index[MAX_SYMS];
+
+ symtab[nsym++] = (Elf32_Sym){0}; /* index 0: null symbol */
+
+ for (int pass = 0; pass < 2; pass++) { /* pass 0 = locals, pass 1 = globals */
+ for (int i = 0; i < ctx->nsyms; i++) {
+ symbol_t *s = &ctx->syms[i];
+ if (s->is_equ) continue;
+ if (!s->defined && !s->is_extern) continue;
+ int is_glob = s->is_global || s->is_extern;
+ if ((pass == 0 && is_glob) || (pass == 1 && !is_glob)) continue;
+
+ Elf32_Sym sym = {0};
+ sym.st_name = strtab_add(&strtab, s->name);
+ sym.st_info = (unsigned char)ELF32_ST_INFO(is_glob ? STB_GLOBAL : STB_LOCAL, STT_NOTYPE);
+ if (s->defined) {
+ sym.st_shndx = (Elf32_Half)shidx_content[s->section];
+ sym.st_value = s->offset;
+ } else {
+ sym.st_shndx = SHN_UNDEF;
+ }
+ sym_elf_index[i] = nsym;
+ symtab[nsym++] = sym;
+ }
+ }
+ int first_global = 1;
+ for (int i = 1; i < nsym; i++) {
+ if ((symtab[i].st_info >> 4) == STB_LOCAL) first_global = i + 1;
+ }The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/as/elf_writer.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude/toolchain/as gcc -Wall -Wextra -O0 -g -o /tmp/myas as.h lexer.c parser.c encode.c bin_writer.c elf_writer.c main.c 2>&1 echo "compile exit=$?"
compile exit=0
$ cd /home/martin/workspace/prototypes/boot-to-claude cp /tmp/myas build/toolchain/as ./tools/run_toolchain_tests.sh
Exit code 1 PASS t01_basic (5) PASS t02_control (7) SKIP t02_types (no expected value registered) PASS t03_recursion (55) PASS t04_pointers (31) FAIL t05_structs: expected 17, got 15 run_toolchain_tests: FAIL
$ grep -n "__attribute__" /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/parser.c
206: if (is_kw(p, "__attribute__")) {198 type_t *st;
199 tag_t *existing = tagname[0] ? tag_find(p, tagname) : NULL;
200 if (check(p, TK_LBRACE)) {
201 advance(p);
202 st = type_new(is_union ? TY_UNION : TY_STRUCT);
203 safe_strcpy(st->tag, tagname, MAX_NAME);
204 st->members = parse_struct_members(p);
205 expect(p, TK_RBRACE);
206 if (is_kw(p, "__attribute__")) {
207 advance(p); expect(p, TK_LPAREN); expect(p, TK_LPAREN);
208 if (is_kw(p, "packed")) { advance(p); st->is_packed = 1; }
209 expect(p, TK_RPAREN); expect(p, TK_RPAREN);
210 }
211 if (tagname[0]) {
212 tag_t *tg = calloc(1, sizeof(tag_t));
213 safe_strcpy(tg->name, tagname, MAX_NAME);
214 tg->type = st;
215 tg->next = p->tags;
216 p->tags = tg;
217 }
218 } else if (existing) {
219 st = existing->type;
220 } else {
221 perr(p, "unknown struct/union tag");
222 return NULL;- if (is_kw(p, "__attribute__")) {
- advance(p); expect(p, TK_LPAREN); expect(p, TK_LPAREN);
- if (is_kw(p, "packed")) { advance(p); st->is_packed = 1; }
- expect(p, TK_RPAREN); expect(p, TK_RPAREN);
- }
- if (tagname[0]) {
+ if (is_kw(p, "__attribute__")) {
+ advance(p); expect(p, TK_LPAREN); expect(p, TK_LPAREN);
+ if (is_kw(p, "packed")) { advance(p); st->is_packed = 1; }
+ expect(p, TK_RPAREN); expect(p, TK_RPAREN);
+ }
+ /* Compute member offsets now, not lazily on first
+ * type_size() call - find_member() (used for `.`/`->`
+ * anywhere later in this file) needs real offsets
+ * immediately, and nothing else is guaranteed to trigger
+ * layout before then. */
+ type_size(st);
+ if (tagname[0]) {The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/parser.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc gcc -Wall -Wextra -O0 -g -o /tmp/mycc lexer.c preprocessor.c types.c parser.c codegen.c builtins.c main.c 2>&1 echo "compile exit=$?" cd /home/martin/workspace/prototypes/boot-to-claude cp /tmp/mycc build/toolchain/cc ./tools/run_toolchain_tests.sh
compile exit=0 PASS t01_basic (5) PASS t02_control (7) SKIP t02_types (no expected value registered) PASS t03_recursion (55) PASS t04_pointers (31) PASS t05_structs (17) run_toolchain_tests: PASS
- declare -A EXPECTED=( - [t01_basic]=5 - [t02_control]=7 - [t03_recursion]=55 - [t04_pointers]=31 - [t05_structs]=17 - ) + declare -A EXPECTED=( + [t01_basic]=5 + [t02_control]=7 + [t02_types]=42 + [t03_recursion]=55 + [t04_pointers]=31 + [t05_structs]=17 + )
The file /home/martin/workspace/prototypes/boot-to-claude/tools/run_toolchain_tests.sh has been updated successfully. (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude ./tools/run_toolchain_tests.sh
PASS t01_basic (5) PASS t02_control (7) PASS t02_types (42) PASS t03_recursion (55) PASS t04_pointers (31) PASS t05_structs (17) run_toolchain_tests: PASS
void kmain(void) {
/* Write "OK\n" directly to COM1 via the port-I/O builtins, proving
* __builtin_outb/__builtin_inb round-trip correctly through real
* hardware (well, QEMU's UART emulation) - not just host syscalls. */
const char *msg = "BUILTINS-OK\n";
while (*msg) {
int status = 0;
while (!(status & 0x20)) {
status = __builtin_inb(0x3f8 + 5);
}
__builtin_outb(0x3f8, *msg);
msg = msg + 1;
}
for (;;) {
__builtin_hlt();
}
}
File created successfully at: /tmp/kernel_builtin_test.c (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude build/toolchain/cc --target=kernel -o /tmp/kbt.S /tmp/kernel_builtin_test.c echo "compile exit=$?" cat /tmp/kbt.S
compile exit=0
BITS 32
section .text
global kmain
kmain:
push ebp
mov ebp, esp
sub esp, 8
lea eax, [ebp-4]
push eax
lea eax, [__str_0]
pop ecx
mov [ecx], eax
.L0:
lea eax, [ebp-4]
mov eax, [eax]
movzx eax, byte [eax]
cmp eax, 0
je .L1
lea eax, [ebp-8]
push eax
mov eax, 0
pop ecx
mov [ecx], eax
.L2:
mov eax, 32
push eax
lea eax, [ebp-8]
mov eax, [eax]
pop ecx
and eax, ecx
cmp eax, 0
sete al
movzx eax, al
cmp eax, 0
je .L3
lea eax, [ebp-8]
push eax
mov eax, 5
push eax
mov eax, 1016
pop ecx
add eax, ecx
mov edx, eax
in al, dx
movzx eax, al
pop ecx
mov [ecx], eax
jmp .L2
.L3:
mov eax, 1016
push eax
lea eax, [ebp-4]
mov eax, [eax]
movzx eax, byte [eax]
mov ecx, eax
pop eax
mov edx, eax
mov eax, ecx
out dx, al
lea eax, [ebp-4]
push eax
mov eax, 1
push eax
lea eax, [ebp-4]
mov eax, [eax]
pop ecx
add eax, ecx
pop ecx
mov [ecx], eax
jmp .L0
.L1:
.L4:
hlt
.L6:
jmp .L4
.L5:
mov eax, 0
mov esp, ebp
pop ebp
ret
section .rodata
global __str_0
__str_0:
db 66
db 85
db 73
db 76
db 84
db 73
db 78
db 83
db 45
db 79
db 75
db 10
db 0$ cd /home/martin/workspace/prototypes/boot-to-claude build/toolchain/as -f elf32 -o /tmp/kbt.o /tmp/kbt.S echo "as exit=$?" nasm -f elf32 -o /tmp/kbt_entry.o kernel/entry.S ld -m elf_i386 -T kernel/link.ld -nostdlib -o /tmp/kbt_kernel.elf /tmp/kbt_entry.o /tmp/kbt.o echo "ld exit=$?" objcopy -O binary /tmp/kbt_kernel.elf /tmp/kbt_kernel.bin ls -la /tmp/kbt_kernel.bin nasm -f bin -o /tmp/kbt_boot.bin boot/boot.S nasm -f bin -o /tmp/kbt_stage2.bin boot/stage2.S dd if=/dev/zero of=/tmp/kbt_disk.img bs=512 count=4096 status=none dd if=/tmp/kbt_boot.bin of=/tmp/kbt_disk.img conv=notrunc status=none dd if=/tmp/kbt_stage2.bin of=/tmp/kbt_disk.img bs=512 seek=1 conv=notrunc status=none dd if=/tmp/kbt_kernel.bin of=/tmp/kbt_disk.img bs=512 seek=5 conv=notrunc status=none timeout 5 qemu-system-i386 -drive file=/tmp/kbt_disk.img,format=raw -display none -serial stdio -no-reboot echo "boot exit=$?"
as exit=0 ld: warning: /tmp/kbt_kernel.elf has a LOAD segment with RWX permissions ld exit=0 -rwxr-x--- 1 martin martin 257 Jul 18 08:32 /tmp/kbt_kernel.bin BUILTINS-OK qemu-system-i386: terminating on signal 15 from pid 2897842 (timeout) boot exit=124
$ grep -n "is_static" /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/parser.c | grep -i func
908: decl = parse_func(&p, name, t, param_names, n_params, is_static);
$ grep -n "static node_t \*parse_func" -A 5 /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/parser.c
826:static node_t *parse_func(parser_t *p, const char *name, type_t *functy,
827- char param_names[][MAX_NAME], int n_params, int is_static) {
828- sym_t *fs = add_global(p, name, functy);
829- (void)is_static;
830-
831- if (match(p, TK_SEMI)) return NULL; /* prototype only */826 static node_t *parse_func(parser_t *p, const char *name, type_t *functy,
827 char param_names[][MAX_NAME], int n_params, int is_static) {
828 sym_t *fs = add_global(p, name, functy);
829 (void)is_static;
830
831 if (match(p, TK_SEMI)) return NULL; /* prototype only */
832
833 p->locals = NULL;
834 p->frame_size = 0;
835 p->cur_func_ret = functy->base;
836
837 node_t *n = node_new(ND_FUNC_DEF);
838 safe_strcpy(n->name, name, MAX_NAME);
839 n->type = functy->base;
840 safe_strcpy(n->asm_label, name, MAX_NAME);
841
842 /* cdecl: EBP+8, +12, ... for params, in order */
843 type_t *pt = functy->param;
844 int off = 8;
845 node_t *ptail = NULL;
846 for (int i = 0; i < n_params && pt; i++, pt = pt->next) {
847 sym_t *s = calloc(1, sizeof(sym_t));
848 safe_strcpy(s->name, param_names[i], MAX_NAME);
849 s->type = pt;
850 s->frame_offset = off;
851 s->next = p->locals;
852 p->locals = s;
853 off += 4;
854
855 node_t *pn = node_new(ND_VAR_DECL);
856 safe_strcpy(pn->name, param_names[i], MAX_NAME);
857 pn->type = pt;
858 pn->frame_offset = s->frame_offset;
859 if (!n->list) n->list = pn; else ptail->next = pn;
860 ptail = pn;
861 }
862
863 n->b = parse_block(p);
864 n->num = p->frame_size;
865 fs->type = functy;- static node_t *parse_func(parser_t *p, const char *name, type_t *functy,
- char param_names[][MAX_NAME], int n_params, int is_static) {
- sym_t *fs = add_global(p, name, functy);
- (void)is_static;
-
- if (match(p, TK_SEMI)) return NULL; /* prototype only */
-
- p->locals = NULL;
- p->frame_size = 0;
- p->cur_func_ret = functy->base;
-
- node_t *n = node_new(ND_FUNC_DEF);
- safe_strcpy(n->name, name, MAX_NAME);
- n->type = functy->base;
- safe_strcpy(n->asm_label, name, MAX_NAME);
+ static node_t *parse_func(parser_t *p, const char *name, type_t *functy,
+ char param_names[][MAX_NAME], int n_params, int is_static) {
+ sym_t *fs = add_global(p, name, functy);
+
+ if (match(p, TK_SEMI)) return NULL; /* prototype only */
+
+ p->locals = NULL;
+ p->frame_size = 0;
+ p->cur_func_ret = functy->base;
+
+ node_t *n = node_new(ND_FUNC_DEF);
+ safe_strcpy(n->name, name, MAX_NAME);
+ n->type = functy->base;
+ n->is_global = !is_static; /* controls whether codegen emits `global` - C's static limits linkage */
+ safe_strcpy(n->asm_label, name, MAX_NAME);The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/parser.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ grep -n "static void codegen_func" -A 8 /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/codegen.c
436:static void codegen_func(node_t *fn) {
437- safe_strcpy(g_cur_func, fn->name, MAX_NAME);
438- out("global %s\n%s:\n", fn->name, fn->name);
439- out(" push ebp\n mov ebp, esp\n");
440- if (fn->num) out(" sub esp, %d\n", (int)fn->num);
441- codegen_stmt(fn->b);
442- out(" mov eax, 0\n mov esp, ebp\n pop ebp\n ret\n");
443-}
444-- static void codegen_func(node_t *fn) {
- safe_strcpy(g_cur_func, fn->name, MAX_NAME);
- out("global %s\n%s:\n", fn->name, fn->name);
+ static void codegen_func(node_t *fn) {
+ safe_strcpy(g_cur_func, fn->name, MAX_NAME);
+ if (fn->is_global) out("global %s\n", fn->name); /* static functions keep internal linkage - no `global` */
+ out("%s:\n", fn->name);The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/codegen.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc gcc -Wall -Wextra -O0 -g -o /tmp/mycc lexer.c preprocessor.c types.c parser.c codegen.c builtins.c main.c 2>&1 echo "compile exit=$?" cd /home/martin/workspace/prototypes/boot-to-claude cp /tmp/mycc build/toolchain/cc ./tools/run_toolchain_tests.sh
compile exit=0 PASS t01_basic (5) PASS t02_control (7) PASS t02_types (42) PASS t03_recursion (55) PASS t04_pointers (31) PASS t05_structs (17) run_toolchain_tests: PASS
$ cd /home/martin/workspace/prototypes/boot-to-claude make clean >/dev/null ./tools/run_tests.sh 2>&1 | tail -5
ld: warning: build/kernel.elf has a LOAD segment with RWX permissions run_tests: PASS qemu-system-i386: terminating on signal 15 from pid 2898455 (bash)
1 # Self-hosted toolchain 2 3 `toolchain/` replaces `nasm`/`gcc`/`ld`/`objcopy` with our own tools, so 4 the OS build depends on external infrastructure only once - to bootstrap 5 the toolchain itself from source (see [Bootstrap](#bootstrap) below). 6 `qemu-system-i386` stays external; it's the machine we run on, not part 7 of what builds for it. 8 9 ## AS (assembler) 10 11 NASM-syntax-compatible, but deliberately scoped to exactly the 12 instruction/directive/operand forms the four `.S` files in this repo use 13 - not general x86 or general NASM compatibility. Extending this list is 14 expected if a future `.S` file needs more; it is not meant to be 15 exhaustive of x86. 16 17 No macro preprocessor (`%macro`/`%rep`/`%assign`/`%+`) - the one place 18 that used one (`kernel/idt_stubs.S`) was hand-expanded instead. 19 20 ### Directives 21 22 `BITS 16` / `BITS 32` (a file may switch modes partway through, as 23 `stage2.S` does for the real-mode -> protected-mode transition), `ORG`, 24 `EQU` (constant expressions, including label arithmetic like 25 `gdt_end - gdt_start`), `section NAME`, `global NAME`, `extern NAME`, 26 `align N`, `times EXPR db|dw|dd|dq ...`, `db`/`dw`/`dd`/`dq` (each 27 accepting a comma-separated list of immediate/string/expression values), 28 `resb N`. 29 30 ### Expressions 31 32 Decimal and `0x`-hex integer literals, binary literals with a `b` suffix 33 (`10011010b`), the current-position pseudo-symbols `$` (this address) and 34 `$$` (start of the current section/file), label references, and `+ - * / 35 >> <<` with parentheses. Must constant-fold at assemble time for `EQU`, 36 `times`, and data-directive values; may instead resolve to a relocation 37 when used as an instruction operand referencing a symbol whose final 38 address isn't known until link time (see [relocations](#relocations)). 39 40 ### Labels 41 42 Global labels (`name:`) and NASM-style local labels (`.name:`, scoped to 43 the most recent preceding global label - `.hang`, `.loop`, etc. appear 44 identically named in multiple places across these files and must not 45 collide). 46 47 ### Instructions 48 49 Every mnemonic/operand-form pair actually used, grouped by kind: 50 51 - **Data movement**: `mov` between 8/16/32-bit registers, segment 52 registers (`ds`/`es`/`fs`/`gs`/`ss`), memory (`[label]`, `[label + 53 disp]`, `[reg]`, `[reg+disp]`), and immediates/constant-expressions, 54 with explicit `byte`/`word`/`dword` size overrides where the operand 55 size isn't otherwise inferable; `movzx` (16->32 zero-extend); 56 `push`/`pop` (16/32-bit register); `pusha`/`popa`; `lodsb`. 57 - **Arithmetic/logic**: `add`, `sub`, `cmp`, `or`, `xor`, `shl` - register 58 or memory destination, register/immediate source. 59 - **Control flow**: `jmp` (near, to a label or a bare absolute address 60 constant; far, `SEG:offset` with an immediate/EQU segment and a label 61 offset), `call` (near, to a label), `ret`, `je`/`jz`/`jc`/`jbe` 62 (near, to a label). 63 - **I/O and privileged**: `in`/`out` (`al`/`ax` with an 8-bit immediate 64 port or `dx`), `int` (immediate vector), `cli`, `hlt`, `lgdt [label]`, 65 `mov` to/from `cr0`. 66 - **String/prefix**: `cld`, `rep stosb`. 67 68 ### Output formats 69 70 - `-f bin`: one flat byte stream at the file's `ORG` address (our `.S` 71 files needing this mode never declare `section`, so there's no 72 section-merging logic to speak of - just linear emission). 73 - `-f elf32`: a relocatable ELF32 object with real sections 74 (`.text`/`.text.entry`/`.data`/`.bss`), a symbol table (globals from 75 `global`, undefined externs from `extern`), and relocations for 76 anything not resolvable within the same section of the same file at 77 assemble time. 78 79 ### Relocations 80 81 Same-section, same-file references (e.g. `jmp .hang`, `jmp isr_common`) 82 are resolved directly by `AS` as fixed relative offsets - no relocation 83 entry needed, same as any assembler does internally. A relocation is 84 emitted only when the target's final address isn't known until link 85 time: `R_386_PC32` for relative `call`/`jmp` to an `extern` symbol 86 (`call kmain`, `call isr_handler`), `R_386_32` for an absolute address 87 used as data or an immediate where the symbol lives in a different 88 section or is `extern` (`kernel/idt_stubs.S`'s `isr_stub_table`, which is 89 in `.data` and holds the absolute addresses of `.text` labels; 90 `kernel/entry.S`'s `mov edi, _bss_start`, where `_bss_start`/`_bss_end` 91 are defined by `LD`'s linker-script-equivalent layout logic, not by any 92 `.S`/`.c` file - see `LD`'s docs section once written). 93 94 ### Verification 95 96 `AS` deliberately never chooses a shorter jump encoding the way NASM's 97 optimizer does (`jmp`/`jcc` always use the fixed-size near form, never 98 `EB`/short-`rel8`) - this keeps the encoder simple at the cost of 99 slightly larger code, so its output for `boot.S`/`stage2.S` is *not* 100 byte-identical to NASM's. Verification is therefore functional rather 101 than a byte diff: all four `.S` files (`boot.S`, `stage2.S` via `-f bin`; 102 `entry.S`, `idt_stubs.S` via `-f elf32`, linked with GNU `ld` as an 103 interim step until `LD` exists in Phase T5) were assembled with `AS` and 104 the resulting kernel boots to a working shell with `SELFTEST PASS` in 105 QEMU - the same bar `tools/run_tests.sh` holds the rest of the OS to. 106 The `-f elf32` relocations were additionally spot-checked directly: 107 `isr_stub_table`'s `R_386_32`-relocated entries resolve to exactly the 108 linked `isr_stub_N` symbols' addresses (verified via `nm`/`objdump`). 109 110 ## CC, LD, OBJCOPY 111 112 Documented as each phase lands - see the plan 113 (`docs/toolchain.md` grows alongside `toolchain/cc`, `toolchain/ld`, 114 `toolchain/objcopy`). 115 116 ## Bootstrap 117 118 _To be filled in once Phase T8 lands: the Stage0/1/2/3 procedure and how 119 to re-verify Stage2 == Stage3._ 120
- ### Instructions - - Every mnemonic/operand-form pair actually used, grouped by kind: - - - **Data movement**: `mov` between 8/16/32-bit registers, segment - registers (`ds`/`es`/`fs`/`gs`/`ss`), memory (`[label]`, `[label + - disp]`, `[reg]`, `[reg+disp]`), and immediates/constant-expressions, - with explicit `byte`/`word`/`dword` size overrides where the operand - size isn't otherwise inferable; `movzx` (16->32 zero-extend); - `push`/`pop` (16/32-bit register); `pusha`/`popa`; `lodsb`. - - **Arithmetic/logic**: `add`, `sub`, `cmp`, `or`, `xor`, `shl` - register - or memory destination, register/immediate source. - - **Control flow**: `jmp` (near, to a label or a bare absolute address - constant; far, `SEG:offset` with an immediate/EQU segment and a label - offset), `call` (near, to a label), `ret`, `je`/`jz`/`jc`/`jbe` - (near, to a label). - - **I/O and privileged**: `in`/`out` (`al`/`ax` with an 8-bit immediate - port or `dx`), `int` (immediate vector), `cli`, `hlt`, `lgdt [label]`, - `mov` to/from `cr0`. - - **String/prefix**: `cld`, `rep stosb`. + ### Instructions + + Every mnemonic/operand-form pair actually used, grouped by kind. The + original set (needed by the four hand-written `.S` files, all 16-bit + real-mode or the small fixed 32-bit sequences in `entry.S`/`idt_stubs.S`) + and the set CC's codegen added in Phase T4 (general 32-bit reg/mem forms) + are both listed together here since they share one encoder + (`toolchain/as/encode.c`), gated by addressing mode (`[esp+disp]` via + SIB, `[ebp+disp]` always as the disp32 form since `mod=00,rm=101` means + "disp32-direct, no base" rather than "EBP with no displacement", `[reg]` + for any other general register, `[label(+disp)]` as 32-bit disp32-direct + - see [relocations](#relocations)) rather than by which file originally + needed them: + + - **Data movement**: `mov` between 8/16/32-bit registers, segment + registers (`ds`/`es`/`fs`/`gs`/`ss`), memory, and immediates/constant- + expressions, with explicit `byte`/`word`/`dword` size overrides where + the operand size isn't otherwise inferable; `movzx` (8/16 -> 32 + zero-extend, register or memory source); `lea`; `push`/`pop` (16/32-bit + register, or an imm32); `pusha`/`popa`; `lodsb`. + - **Arithmetic/logic**: `add`, `sub`, `and`, `or`, `xor`, `cmp` (reg/mem + destination, reg/mem/imm source), `imul` (two-operand, `r32, r/m32`), + `idiv`/`cdq`, `not`/`neg`, `shl`/`shr` (by an imm8 or by `cl`). + - **Comparison results**: `sete`/`setne`/`setl`/`setle`/`setg`/`setge` + (r/m8 destination - CC always follows one with `movzx` back to 32-bit). + - **Control flow**: `jmp` (near, to a label or a bare absolute address + constant; far, `SEG:offset` with an immediate/EQU segment and a label + offset), `call` (near, to a label), `ret`, the full signed/unsigned- + agnostic conditional jump set CC needs (`je`/`jz`/`jne`/`jc`/`jbe`/ + `jl`/`jle`/`jg`/`jge`), all near-to-a-label. + - **I/O and privileged**: `in`/`out` (`al`/`ax` with an 8-bit immediate + port or `dx`), `int` (immediate vector), `cli`, `hlt`, `lgdt`/`lidt` + (`[label]` or `[reg]`), `mov` to/from `cr0`. + - **String/prefix**: `cld`, `rep stosb`. + + No jump-size optimization anywhere (near/far forms are always the fixed- + size encoding - see [Verification](#verification)), and no `xchg` - + codegen never needs it (see `docs/toolchain.md`'s note in `codegen.c` + about evaluation order making register swaps unnecessary).
The file /home/martin/workspace/prototypes/boot-to-claude/docs/toolchain.md has been updated successfully. (file state is current in your context — no need to Read it back)
- No jump-size optimization anywhere (near/far forms are always the fixed- - size encoding - see [Verification](#verification)), and no `xchg` - - codegen never needs it (see `docs/toolchain.md`'s note in `codegen.c` - about evaluation order making register swaps unnecessary). + No jump-size optimization anywhere (near/far forms are always the fixed- + size encoding - see [Verification](#verification)), and no `xchg` - CC's + codegen always evaluates operands in an order that already puts values in + the right registers, so a register swap is never needed.
The file /home/martin/workspace/prototypes/boot-to-claude/docs/toolchain.md has been updated successfully. (file state is current in your context — no need to Read it back)
- ## CC, LD, OBJCOPY
-
- Documented as each phase lands - see the plan
- (`docs/toolchain.md` grows alongside `toolchain/cc`, `toolchain/ld`,
- `toolchain/objcopy`).
+ ## CC (compiler)
+
+ `toolchain/cc`: preprocessor + lexer + parser + naive x86-32 codegen for
+ "tinyC" - a scoped C dialect, not general C89/C99. Emits `AS`-syntax
+ assembly text (mirrors `gcc -S | as`); two targets selected by
+ `--target=kernel` (freestanding, no runtime) or `--target=host` (adds a
+ `_start` that calls `main` then exits via a raw syscall). Native host
+ tool, not OS code.
+
+ ### Preprocessor
+
+ `#include` (quote form searches the including file's directory first,
+ then falls back to `-I` search paths, exactly reproducing the trick
+ `kernel/fs/tinyfs.c` relies on to pick kernel vs. host `string.h`; angle
+ form searches only `-I` paths, using dedicated header-name lexing since
+ `<foo.h>` isn't a normal token), `#pragma once` (dedup by resolved path),
+ and simple **object-like** `#define` with recursive constant
+ substitution (one level of nested macro expansion, e.g.
+ `TINYFS_INODE_BLOCK` expanding to `(TINYFS_BITMAP_BLOCK + 1)` whose own
+ pieces also expand) - no function-like macros, no `#ifdef`/`#if`, since
+ nothing in this codebase uses them.
+
+ ### Types
+
+ `void`, `char`, `short`, `int` (no `long`/`long long` - nothing in this
+ codebase needs a type wider than 32 bits, which keeps the type-specifier
+ grammar smaller), `signed`/`unsigned`, pointers, arrays, `struct`/
+ `union`/`enum`, function types (including function-pointer typedefs -
+ `typedef void (*fn_t)(int);` - the one parenthesized declarator form
+ supported; arbitrary nested declarators like pointer-to-array aren't,
+ since nothing here needs them), `typedef`. `const`/`volatile`/`inline`
+ are parsed and ignored; `static`/`extern` affect linkage
+ (`global`/no-`global` in the emitted asm) and, for locals, storage
+ (stack frame vs. a dedicated `.data`/`.bss` label). `char` always behaves
+ as unsigned (zero-extending loads) - simpler than tracking C's
+ implementation-defined signedness, and nothing here relies on a
+ negative `char`. `__attribute__((packed))` on a struct/union removes all
+ inter-member padding.
+
+ Struct/union layout is computed *eagerly*, the moment a definition's `}`
+ (and any trailing `__attribute__`) is parsed, not lazily on first
+ `sizeof`/use - `.`/`->` member lookups elsewhere in the file need real
+ offsets immediately, and nothing else was guaranteed to trigger layout
+ first (a real bug caught by `tests/toolchain/t05_structs.c`: struct
+ member access silently used offset 0 for every member until whatever
+ first called `sizeof` on that type - see the fix in `parser.c`).
+
+ ### Expressions/statements
+
+ The full standard precedence chain (assignment/compound-assignment,
+ ternary, `||`/`&&` short-circuit, bitwise, equality, relational, shift,
+ additive, multiplicative, unary, postfix), `sizeof` (type or expression),
+ casts, pointer/array indexing and address-of/dereference (array indexing
+ is lowered directly to `*(arr + idx)` in the parser, so codegen never
+ needs a separate indexing case), struct member access (`.`/`->`),
+ function calls. `if`/`else`, `while`, `do`/`while`, `for`, `return`,
+ `break`/`continue`, blocks. No `switch`, no `goto`/labels, no comma
+ operator, no variadic functions, no K&R-style declarations - unused
+ anywhere in this codebase.
+
+ Brace initializer lists (`{1, 2, 3}`) are only supported for globals/
+ `static` locals (compile-time-constant `.data` bytes) - this codebase
+ never brace-initializes a plain stack local, only scalars, so codegen
+ never needs to lower a brace list into runtime store instructions.
+ Compound literals (`(type){...}`) and designated initializers
+ (`[i] = x`, `.field = x`) aren't supported at all - the two call sites
+ that used them (`kernel/lang/script/eval.c`, `kernel/lang/compiler/
+ codegen.c`) get refactored to avoid them in Phase T3, a deliberate
+ scope-narrowing decision (see the plan) rather than an oversight.
+
+ ### Codegen
+
+ Naive and non-optimizing on purpose (see the plan's Phase T4 entry):
+ every expression's result lives in `EAX`; a binary operator evaluates
+ its right operand first and `push`es it, then its left operand into
+ `EAX`, then `pop`s the right operand into `ECX` and combines - no
+ register allocation, values spill to the real machine stack between
+ every sub-expression. cdecl calling convention (caller pushes args
+ right-to-left, callee's `EBP+8`/`+12`/... for params, return value in
+ `EAX`, caller cleans up the pushed args). Locals live at fixed `EBP`-
+ relative offsets assigned as their declarations are parsed; every slot
+ is a 4-byte-aligned minimum of 4 bytes regardless of the C type's actual
+ size (simplicity over density). `static` locals get their own unique
+ `.data`/`.bss` label instead of a stack slot, exactly like globals.
+
+ ### Builtins (`builtins.c`)
+
+ Fixed instruction sequences in place of a general GCC-extended-inline-
+ asm parser - the kernel's actual inline-asm usage reduces to exactly
+ these operations (see the plan's research notes). Recognized by name at
+ a call site, in place of an ordinary `call`; arguments are evaluated
+ through the normal expression codegen first, then moved into whatever
+ fixed registers the underlying instruction needs:
+
+ | builtin | x86 |
+ |---|---|
+ | `__builtin_inb(port)` / `__builtin_outb(port, val)` | `in al, dx` / `out dx, al` |
+ | `__builtin_inw(port)` / `__builtin_outw(port, val)` | `in ax, dx` / `out dx, ax` |
+ | `__builtin_hlt()` | `hlt` |
+ | `__builtin_lidt(ptr)` | `lidt [ptr]` |
+ | `__builtin_gdt_flush(ptr)` | `lgdt` + reload every data/stack segment to selector `0x10` + far-jump to selector `0x08` to reload `CS` - the one sequence that inherently can't be a single instruction, needs a fresh label per call site (own counter, `.Lbi` prefix so it never collides with codegen's own `.L` control-flow labels) |
+ | `__sys_open/read/write/lseek/close/exit` | raw `int 0x80` Linux x86-32 syscalls, host target only - see [Bootstrap](#bootstrap) for why host tools are libc-free |
+
+ ### Verification
+
+ `tests/toolchain/*.c` + `tools/run_toolchain_tests.sh`: each fixture
+ compiles through `CC --target=host` + `AS`, links with GNU `ld` (interim,
+ until `LD` exists in Phase T5), runs directly, and checks its exit code
+ - fast host-side iteration covering arithmetic, control flow, recursion,
+ pointers/arrays, structs/unions/packed layout, and function pointers.
+ Three real bugs were caught this way (not just compile-clean, actually
+ wrong-answer or crashing): the eager-struct-layout bug above; a missing
+ `[reg]`/`[reg+disp]` addressing case in `AS`'s generic 32-bit encoder
+ (fell through to the "bare label" case, corrupting the address - caught
+ by a standalone `AS` extension smoke test before CC even ran through
+ it); and an ELF symbol table bug where a defined symbol's section index
+ was computed as a fixed offset from its internal `section_kind_t`
+ instead of the section's *actual* assigned ELF header index - wrong the
+ moment a file didn't use all four section kinds in order (e.g. `.bss`
+ with no `.data`), silently pointing every reference at the wrong
+ section and segfaulting at run time.
+
+ Kernel-target builtins (port I/O, `hlt`) can't run on the host, so
+ they're verified separately: a small kernel-target program that polls
+ COM1's line-status register and writes bytes via `__builtin_inb`/
+ `__builtin_outb`, linked with the *real* `kernel/entry.S` and booted
+ through the *real* `boot.S`/`stage2.S` in QEMU, printed its expected
+ output over serial.
+
+ ## LD, OBJCOPY
+
+ Not yet built - Phases T5/T6. Everything so far links with GNU `ld` and
+ extracts with GNU `objcopy` as an interim step.The file /home/martin/workspace/prototypes/boot-to-claude/docs/toolchain.md has been updated successfully. (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude make clean >/dev/null rm -rf build/toolchain build/toolchain_tests git status --short
M docs/toolchain.md M toolchain/as/elf_writer.c M toolchain/as/encode.c M toolchain/as/parser.c ?? tests/toolchain/ ?? toolchain/cc/ ?? toolchain/include/ ?? tools/run_toolchain_tests.sh
1 #!/usr/bin/env bash
2 # Compiles each tests/toolchain/*.c fixture through CC (--target=host) +
3 # AS, links with GNU ld (interim - LD doesn't exist until Phase T5), runs
4 # it directly, and checks the exit code. Fast host-side iteration for the
5 # compiler; kernel-target builtins (port I/O etc) are verified separately
6 # via a QEMU boot, since they can't run on the host.
7 set -euo pipefail
8
9 cd "$(dirname "$0")/.."
10
11 CC=${CC_BIN:-build/toolchain/cc}
12 AS=${AS_BIN:-build/toolchain/as}
13 BUILD=build/toolchain_tests
14 mkdir -p "$BUILD"
15
16 declare -A EXPECTED=(
17 [t01_basic]=5
18 [t02_control]=7
19 [t02_types]=42
20 [t03_recursion]=55
21 [t04_pointers]=31
22 [t05_structs]=17
23 )
24
25 fail=0
26 for src in tests/toolchain/*.c; do
27 name=$(basename "$src" .c)
28 exp=${EXPECTED[$name]:-}
29 if [ -z "$exp" ]; then
30 echo "SKIP $name (no expected value registered)"
31 continue
32 fi
33
34 "$CC" -I toolchain/include --target=host -o "$BUILD/$name.S" "$src"
35 "$AS" -f elf32 -o "$BUILD/$name.o" "$BUILD/$name.S"
36 ld -m elf_i386 -o "$BUILD/$name" "$BUILD/$name.o"
37
38 set +e
39 "$BUILD/$name"
40 got=$?
41 set -e
42
43 if [ "$got" -eq "$exp" ]; then
44 echo "PASS $name ($got)"
45 else
46 echo "FAIL $name: expected $exp, got $got"
47 fail=1
48 fi
49 done
50
51 if [ "$fail" -eq 0 ]; then
52 echo "run_toolchain_tests: PASS"
53 else
54 echo "run_toolchain_tests: FAIL"
55 exit 1
56 fi
57 - CC=${CC_BIN:-build/toolchain/cc}
- AS=${AS_BIN:-build/toolchain/as}
- BUILD=build/toolchain_tests
- mkdir -p "$BUILD"
+ CC=${CC_BIN:-build/toolchain/cc}
+ AS=${AS_BIN:-build/toolchain/as}
+ BUILD=build/toolchain_tests
+ mkdir -p "$BUILD" build/toolchain
+
+ # Not wired into the main Makefile yet (that's Phase T7) - bootstrap with
+ # the host compiler if the tools haven't been built already.
+ if [ ! -x "$CC" ]; then
+ gcc -Wall -Wextra -O2 -o "$CC" \
+ toolchain/cc/lexer.c toolchain/cc/preprocessor.c toolchain/cc/types.c \
+ toolchain/cc/parser.c toolchain/cc/codegen.c toolchain/cc/builtins.c toolchain/cc/main.c
+ fi
+ if [ ! -x "$AS" ]; then
+ gcc -Wall -Wextra -O2 -o "$AS" \
+ toolchain/as/lexer.c toolchain/as/parser.c toolchain/as/encode.c \
+ toolchain/as/bin_writer.c toolchain/as/elf_writer.c toolchain/as/main.c
+ fiThe file /home/martin/workspace/prototypes/boot-to-claude/tools/run_toolchain_tests.sh has been updated successfully. (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude rm -rf build/toolchain build/toolchain_tests ./tools/run_toolchain_tests.sh
toolchain/cc/preprocessor.c: In function ‘process_lexer’:
toolchain/cc/preprocessor.c:71:42: warning: ‘%s’ directive output may be truncated writing up to 255 bytes into a region of size between 88 and 599 [-Wformat-truncation=]
71 | snprintf(cand, sizeof(cand), "%s/%s", cur_dir, name);
| ^~
......
107 | if (!resolve_include(cur_dir, path_tok.text, is_quote, resolved, sizeof(resolved))) {
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In file included from /usr/include/stdio.h:970,
from toolchain/cc/preprocessor.c:3:
In function ‘snprintf’,
inlined from ‘resolve_include’ at toolchain/cc/preprocessor.c:71:9,
inlined from ‘process_lexer’ at toolchain/cc/preprocessor.c:107:22:
/usr/include/x86_64-linux-gnu/bits/stdio2.h:68:10: note: ‘__builtin___snprintf_chk’ output between 2 and 768 bytes into a destination of size 600
68 | return __builtin___snprintf_chk (__s, __n, __USE_FORTIFY_LEVEL - 1,
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
69 | __glibc_objsize (__s), __fmt,
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
70 | __va_arg_pack ());
| ~~~~~~~~~~~~~~~~~
PASS t01_basic (5)
PASS t02_control (7)
PASS t02_types (42)
PASS t03_recursion (55)
PASS t04_pointers (31)
PASS t05_structs (17)
run_toolchain_tests: PASS$ grep -n "resolve_include" /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/preprocessor.c | head -3
68:static int resolve_include(const char *cur_dir, const char *name, int is_quote, char *out, size_t outsz) {
107: if (!resolve_include(cur_dir, path_tok.text, is_quote, resolved, sizeof(resolved))) {68 static int resolve_include(const char *cur_dir, const char *name, int is_quote, char *out, size_t outsz) {
69 char cand[600];
70 if (is_quote) {
71 snprintf(cand, sizeof(cand), "%s/%s", cur_dir, name);
72 FILE *f = fopen(cand, "rb");
73 if (f) { fclose(f); safe_strcpy(out, cand, outsz); return 1; }
74 }
75 for (int i = 0; i < g_n_include_dirs; i++) {
76 snprintf(cand, sizeof(cand), "%s/%s", g_include_dirs[i], name);
77 FILE *f = fopen(cand, "rb");
78 if (f) { fclose(f); safe_strcpy(out, cand, outsz); return 1; }
79 }
80 return 0;
81 }- static int resolve_include(const char *cur_dir, const char *name, int is_quote, char *out, size_t outsz) {
- char cand[600];
- if (is_quote) {
- snprintf(cand, sizeof(cand), "%s/%s", cur_dir, name);
- FILE *f = fopen(cand, "rb");
- if (f) { fclose(f); safe_strcpy(out, cand, outsz); return 1; }
- }
- for (int i = 0; i < g_n_include_dirs; i++) {
- snprintf(cand, sizeof(cand), "%s/%s", g_include_dirs[i], name);
- FILE *f = fopen(cand, "rb");
- if (f) { fclose(f); safe_strcpy(out, cand, outsz); return 1; }
- }
- return 0;
- }
+ static void join_path(char *dst, size_t dstsz, const char *dir, const char *name) {
+ size_t i = 0;
+ for (; i + 1 < dstsz && dir[i]; i++) dst[i] = dir[i];
+ if (i + 1 < dstsz) dst[i++] = '/';
+ for (size_t j = 0; i + 1 < dstsz && name[j]; i++, j++) dst[i] = name[j];
+ dst[i] = '\0';
+ }
+
+ static int resolve_include(const char *cur_dir, const char *name, int is_quote, char *out, size_t outsz) {
+ char cand[600];
+ if (is_quote) {
+ join_path(cand, sizeof(cand), cur_dir, name);
+ FILE *f = fopen(cand, "rb");
+ if (f) { fclose(f); safe_strcpy(out, cand, outsz); return 1; }
+ }
+ for (int i = 0; i < g_n_include_dirs; i++) {
+ join_path(cand, sizeof(cand), g_include_dirs[i], name);
+ FILE *f = fopen(cand, "rb");
+ if (f) { fclose(f); safe_strcpy(out, cand, outsz); return 1; }
+ }
+ return 0;
+ }The file /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc/preprocessor.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude rm -rf build/toolchain build/toolchain_tests ./tools/run_toolchain_tests.sh 2>&1
PASS t01_basic (5) PASS t02_control (7) PASS t02_types (42) PASS t03_recursion (55) PASS t04_pointers (31) PASS t05_structs (17) run_toolchain_tests: PASS
$ cd /home/martin/workspace/prototypes/boot-to-claude/toolchain/cc gcc -Wall -Wextra -O2 -c lexer.c preprocessor.c types.c parser.c codegen.c builtins.c main.c -o /dev/null 2>&1 echo "---" for f in lexer.c preprocessor.c types.c parser.c codegen.c builtins.c main.c; do gcc -Wall -Wextra -O2 -c "$f" -o /tmp/discard.o 2>&1 done echo "check complete"
gcc: fatal error: cannot specify ‘-o’ with ‘-c’, ‘-S’ or ‘-E’ with multiple files compilation terminated. --- check complete
$ cd /home/martin/workspace/prototypes/boot-to-claude rm -rf build/toolchain build/toolchain_tests git add toolchain/ tests/toolchain/ tools/run_toolchain_tests.sh docs/toolchain.md git status --short
M docs/toolchain.md A tests/toolchain/t01_basic.c A tests/toolchain/t02_control.c A tests/toolchain/t02_types.c A tests/toolchain/t03_recursion.c A tests/toolchain/t04_pointers.c A tests/toolchain/t05_structs.c M toolchain/as/elf_writer.c M toolchain/as/encode.c M toolchain/as/parser.c A toolchain/cc/builtins.c A toolchain/cc/cc.h A toolchain/cc/codegen.c A toolchain/cc/lexer.c A toolchain/cc/main.c A toolchain/cc/parser.c A toolchain/cc/preprocessor.c A toolchain/cc/types.c A toolchain/include/stddef.h A toolchain/include/stdint.h A tools/run_toolchain_tests.sh