feat(v4.0.0): assemble definitions written as text
Test support, beside the slot packer: reads definitions in the notation DECOMPOSITION.md uses -- words, opcodes, literals, constants, labels, branches, FOR NEXT and FOR UNEXT, in-line macros, comments -- and lays them down through the slot packer, so they no longer have to be retyped opcode by opcode in C. Tested by assembling SWAP, UM/MOD, C@ and C! both ways on two nodes and comparing memory word for word, by running what is assembled, and on every error it reports, each with its line number. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
861f800b7f
commit
7b710aa322
+428
@@ -0,0 +1,428 @@
|
||||
/* text.c -- assemble definitions written as text. See text.h. */
|
||||
#include "v4/text.h"
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
enum { KIND_WORD = 1, KIND_CONST, KIND_MACRO };
|
||||
|
||||
static const char *const mnemonic[V4_OPCODE_COUNT] = {
|
||||
";", "ex", "jump", "call", "unext", "next", "if", "-if",
|
||||
"@p", "@+", "@b", "@", "!p", "!+", "!b", "!",
|
||||
"+*", "2*", "2/", "inv", "+", "and", "xor", "drop",
|
||||
"dup", "pop", "over", "a", "nop", "push", "b!", "a!"
|
||||
};
|
||||
|
||||
/* Where tokens come from: the source text, or a macro's text on top of it. */
|
||||
typedef struct {
|
||||
const char *p[V4_TEXT_MAX_DEPTH + 1u];
|
||||
unsigned depth; /* 0: nothing; 1: the source; more: in macros */
|
||||
} reader;
|
||||
|
||||
static void fail(v4_text *t, const char *what, const char *token)
|
||||
{
|
||||
if (t->failed) return;
|
||||
t->failed = 1;
|
||||
if (token) snprintf(t->message, sizeof t->message, "line %u: %s: %s", t->line, what, token);
|
||||
else snprintf(t->message, sizeof t->message, "line %u: %s", t->line, what);
|
||||
}
|
||||
|
||||
static unsigned intern(v4_text *t, const char *s)
|
||||
{
|
||||
size_t len = strlen(s) + 1u;
|
||||
unsigned at = t->pool_used;
|
||||
if (len > V4_TEXT_POOL - t->pool_used) { fail(t, "out of name space", s); return 0; }
|
||||
memcpy(t->pool + at, s, len);
|
||||
t->pool_used += (unsigned)len;
|
||||
return at;
|
||||
}
|
||||
|
||||
static int opcode_of(const char *s)
|
||||
{
|
||||
for (int i = 0; i < (int)V4_OPCODE_COUNT; i++) if (strcmp(s, mnemonic[i]) == 0) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static const v4_text_symbol *find(const v4_text *t, const char *s)
|
||||
{
|
||||
for (unsigned i = t->nsym; i-- > 0; ) if (strcmp(t->pool + t->sym[i].name, s) == 0) return &t->sym[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int is_reserved(const char *s)
|
||||
{
|
||||
return strcmp(s, ":") == 0 || strcmp(s, "macro") == 0 || strcmp(s, "endmacro") == 0
|
||||
|| strcmp(s, "\\") == 0 || strcmp(s, "(") == 0
|
||||
|| strcmp(s, "FOR") == 0 || strcmp(s, "NEXT") == 0 || strcmp(s, "UNEXT") == 0;
|
||||
}
|
||||
|
||||
static int is_label_def(const char *s)
|
||||
{
|
||||
size_t len = strlen(s);
|
||||
return len > 1 && s[len - 1] == ':';
|
||||
}
|
||||
|
||||
/* A whole token that is a number: decimal with an optional sign, or $hex.
|
||||
* It must fit a cell, read as signed or as unsigned. */
|
||||
static int number_of(const char *s, v4_cell *out, int *too_big)
|
||||
{
|
||||
const char *digits = s;
|
||||
char *end;
|
||||
int neg = 0, base = 10;
|
||||
unsigned long long u;
|
||||
|
||||
*too_big = 0;
|
||||
if (*digits == '$') { base = 16; digits++; }
|
||||
else if (*digits == '-') { neg = 1; digits++; }
|
||||
if (*digits == 0) return 0;
|
||||
for (const char *c = digits; *c; c++) {
|
||||
int ok = (*c >= '0' && *c <= '9')
|
||||
|| (base == 16 && ((*c >= 'a' && *c <= 'f') || (*c >= 'A' && *c <= 'F')));
|
||||
if (!ok) return 0;
|
||||
}
|
||||
errno = 0;
|
||||
u = strtoull(digits, &end, base);
|
||||
if (*end) return 0;
|
||||
if (errno == ERANGE
|
||||
|| (neg ? u > (unsigned long long)V4_MSB : u > (unsigned long long)(v4_ucell)~(v4_ucell)0)) {
|
||||
*too_big = 1;
|
||||
return 1;
|
||||
}
|
||||
*out = neg ? (v4_cell)((v4_ucell)0 - (v4_ucell)u) : (v4_cell)(v4_ucell)u;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* The next token, or 0 at the end. Comments are skipped here, and lines
|
||||
* counted, in the source only: a macro's text holds neither. */
|
||||
static int next_token(v4_text *t, reader *r, char *tok)
|
||||
{
|
||||
for (;;) {
|
||||
const char *p;
|
||||
unsigned len = 0;
|
||||
if (r->depth == 0) return 0;
|
||||
p = r->p[r->depth - 1u];
|
||||
while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') {
|
||||
if (*p == '\n' && r->depth == 1) t->line++;
|
||||
p++;
|
||||
}
|
||||
if (*p == 0) { r->depth--; continue; }
|
||||
while (*p && *p != ' ' && *p != '\t' && *p != '\r' && *p != '\n') {
|
||||
if (len < V4_TEXT_MAX_TOKEN) tok[len] = *p;
|
||||
len++; p++;
|
||||
}
|
||||
if (len > V4_TEXT_MAX_TOKEN) { tok[V4_TEXT_MAX_TOKEN] = 0; fail(t, "name too long", tok); return 0; }
|
||||
tok[len] = 0;
|
||||
if (strcmp(tok, "\\") == 0) {
|
||||
while (*p && *p != '\n') p++;
|
||||
r->p[r->depth - 1u] = p;
|
||||
continue;
|
||||
}
|
||||
if (strcmp(tok, "(") == 0) {
|
||||
while (*p && *p != ')') { if (*p == '\n' && r->depth == 1) t->line++; p++; }
|
||||
if (*p == 0) { fail(t, "comment not closed", NULL); return 0; }
|
||||
r->p[r->depth - 1u] = p + 1;
|
||||
continue;
|
||||
}
|
||||
r->p[r->depth - 1u] = p;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* A branch may go in the next slot only if that slot can reach every word of
|
||||
* the node; otherwise it starts a new word. */
|
||||
static void place_branch(v4_text *t)
|
||||
{
|
||||
v4_asm *as = &t->as;
|
||||
if (as->slot == 0) return;
|
||||
if (!v4_iword_branch_legal(as->slot)
|
||||
|| (v4_ucell)v4_iword_slot_mask(as->slot) < (v4_ucell)(V4_NODE_WORDS - 1u))
|
||||
(void)v4_asm_label(as);
|
||||
}
|
||||
|
||||
static void check_asm(v4_text *t, const char *tok)
|
||||
{
|
||||
if (t->as.error) fail(t, "does not fit: memory is full, or a branch cannot reach", tok);
|
||||
}
|
||||
|
||||
static void branch_to(v4_text *t, unsigned op, v4_cell target, const char *tok)
|
||||
{
|
||||
place_branch(t);
|
||||
v4_asm_branch(&t->as, op, target);
|
||||
check_asm(t, tok);
|
||||
}
|
||||
|
||||
static void branch_later(v4_text *t, unsigned op, const char *name, int local)
|
||||
{
|
||||
v4_text_ref *ref;
|
||||
if (t->nref == V4_TEXT_MAX_REFS) { fail(t, "too many unresolved branches", name); return; }
|
||||
place_branch(t);
|
||||
ref = &t->ref[t->nref];
|
||||
ref->name = intern(t, name);
|
||||
ref->ref = v4_asm_branch_fwd(&t->as, op);
|
||||
ref->line = t->line;
|
||||
ref->local = local;
|
||||
check_asm(t, name);
|
||||
if (!t->failed) t->nref++;
|
||||
}
|
||||
|
||||
/* A branch to `name`. Written as a bare name it is a call to a word. After
|
||||
* a branch mnemonic it is a label of this word if the word has one, before
|
||||
* or after the branch, and only otherwise a word -- so it cannot be settled
|
||||
* until the word ends, even when a word of that name already exists. */
|
||||
static void branch(v4_text *t, unsigned op, const char *name, int may_be_label)
|
||||
{
|
||||
const v4_text_symbol *s;
|
||||
if (may_be_label)
|
||||
for (unsigned i = 0; i < t->nlabel; i++)
|
||||
if (strcmp(t->pool + t->label[i].name, name) == 0) { branch_to(t, op, t->label[i].addr, name); return; }
|
||||
s = find(t, name);
|
||||
if (s && s->kind == KIND_WORD && !may_be_label) { branch_to(t, op, s->value, name); return; }
|
||||
if ((s && s->kind != KIND_WORD) || opcode_of(name) >= 0 || is_reserved(name) || is_label_def(name)) {
|
||||
fail(t, "not something to branch to", name);
|
||||
return;
|
||||
}
|
||||
{
|
||||
v4_cell v; int big;
|
||||
if (number_of(name, &v, &big)) { fail(t, "not something to branch to", name); return; }
|
||||
}
|
||||
branch_later(t, op, name, may_be_label);
|
||||
}
|
||||
|
||||
static void resolve(v4_text *t, unsigned i, v4_cell target)
|
||||
{
|
||||
v4_asm_resolve(&t->as, t->ref[i].ref, target);
|
||||
if (t->as.error) {
|
||||
unsigned line = t->line;
|
||||
t->line = t->ref[i].line;
|
||||
fail(t, "branch cannot reach", t->pool + t->ref[i].name);
|
||||
t->line = line;
|
||||
}
|
||||
t->ref[i] = t->ref[--t->nref];
|
||||
}
|
||||
|
||||
/* The word being assembled ends: its labels go, and what was not a label of
|
||||
* it must be a word. */
|
||||
static void end_word(v4_text *t)
|
||||
{
|
||||
if (t->nfor) { fail(t, "FOR without NEXT or UNEXT", NULL); t->nfor = 0; }
|
||||
for (unsigned i = 0; i < t->nref && !t->failed; ) {
|
||||
const v4_text_symbol *s;
|
||||
if (!t->ref[i].local) { i++; continue; }
|
||||
t->ref[i].local = 0;
|
||||
s = find(t, t->pool + t->ref[i].name);
|
||||
if (s && s->kind == KIND_WORD) resolve(t, i, s->value); /* moves another into place i */
|
||||
else i++;
|
||||
}
|
||||
t->nlabel = 0;
|
||||
}
|
||||
|
||||
static void define(v4_text *t, const char *name, unsigned char kind, v4_cell value)
|
||||
{
|
||||
v4_cell v; int big;
|
||||
if (opcode_of(name) >= 0 || is_reserved(name) || is_label_def(name) || number_of(name, &v, &big)) {
|
||||
fail(t, "cannot be used as a name", name);
|
||||
return;
|
||||
}
|
||||
if (find(t, name)) { fail(t, "already defined", name); return; }
|
||||
if (t->nsym == V4_TEXT_MAX_SYMBOLS) { fail(t, "too many names", name); return; }
|
||||
t->sym[t->nsym].name = intern(t, name);
|
||||
t->sym[t->nsym].kind = kind;
|
||||
t->sym[t->nsym].value = value;
|
||||
if (!t->failed) t->nsym++;
|
||||
}
|
||||
|
||||
static void define_macro(v4_text *t, reader *r)
|
||||
{
|
||||
char name[V4_TEXT_MAX_TOKEN + 1u], tok[V4_TEXT_MAX_TOKEN + 1u];
|
||||
unsigned start, line = t->line;
|
||||
|
||||
if (r->depth != 1) { fail(t, "macro inside a macro", NULL); return; }
|
||||
if (!next_token(t, r, name)) { fail(t, "macro without a name", NULL); return; }
|
||||
start = t->pool_used;
|
||||
for (;;) {
|
||||
size_t len;
|
||||
if (!next_token(t, r, tok)) {
|
||||
if (!t->failed) { t->line = line; fail(t, "macro without endmacro", name); }
|
||||
return;
|
||||
}
|
||||
if (strcmp(tok, "endmacro") == 0) break;
|
||||
if (strcmp(tok, ":") == 0 || strcmp(tok, "macro") == 0 || is_label_def(tok)) {
|
||||
fail(t, "not allowed in a macro", tok);
|
||||
return;
|
||||
}
|
||||
len = strlen(tok);
|
||||
if (len + 2u > V4_TEXT_POOL - t->pool_used) { fail(t, "out of name space", name); return; }
|
||||
memcpy(t->pool + t->pool_used, tok, len);
|
||||
t->pool_used += (unsigned)len;
|
||||
t->pool[t->pool_used++] = ' ';
|
||||
}
|
||||
if (t->pool_used == V4_TEXT_POOL) { fail(t, "out of name space", name); return; }
|
||||
t->pool[t->pool_used++] = 0;
|
||||
define(t, name, KIND_MACRO, (v4_cell)start);
|
||||
}
|
||||
|
||||
static void one_token(v4_text *t, reader *r, const char *tok)
|
||||
{
|
||||
const v4_text_symbol *s;
|
||||
v4_asm *as = &t->as;
|
||||
v4_cell v;
|
||||
int op, big;
|
||||
|
||||
if (strcmp(tok, ":") == 0) {
|
||||
char name[V4_TEXT_MAX_TOKEN + 1u];
|
||||
if (r->depth != 1) { fail(t, "not allowed in a macro", tok); return; }
|
||||
if (!next_token(t, r, name)) { fail(t, ": without a name", NULL); return; }
|
||||
end_word(t);
|
||||
define(t, name, KIND_WORD, v4_asm_label(as));
|
||||
return;
|
||||
}
|
||||
if (strcmp(tok, "macro") == 0) { define_macro(t, r); return; }
|
||||
if (strcmp(tok, "endmacro") == 0) { fail(t, "endmacro without macro", NULL); return; }
|
||||
|
||||
if (is_label_def(tok)) {
|
||||
char name[V4_TEXT_MAX_TOKEN + 1u];
|
||||
v4_cell addr;
|
||||
size_t len = strlen(tok) - 1u;
|
||||
memcpy(name, tok, len);
|
||||
name[len] = 0;
|
||||
for (unsigned i = 0; i < t->nlabel; i++)
|
||||
if (strcmp(t->pool + t->label[i].name, name) == 0) { fail(t, "label already defined in this word", name); return; }
|
||||
if (t->nlabel == V4_TEXT_MAX_LABELS) { fail(t, "too many labels in one word", name); return; }
|
||||
addr = v4_asm_label(as);
|
||||
t->label[t->nlabel].name = intern(t, name);
|
||||
t->label[t->nlabel].addr = addr;
|
||||
t->nlabel++;
|
||||
for (unsigned i = 0; i < t->nref && !t->failed; )
|
||||
if (t->ref[i].local && strcmp(t->pool + t->ref[i].name, name) == 0) resolve(t, i, addr);
|
||||
else i++;
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(tok, "FOR") == 0) {
|
||||
if (t->nfor == V4_TEXT_MAX_DEPTH) { fail(t, "FOR nested too deep", NULL); return; }
|
||||
v4_asm_op(as, V4_OP_PUSH);
|
||||
t->for_addr[t->nfor++] = v4_asm_label(as);
|
||||
check_asm(t, tok);
|
||||
return;
|
||||
}
|
||||
if (strcmp(tok, "NEXT") == 0) {
|
||||
if (t->nfor == 0) { fail(t, "NEXT without FOR", NULL); return; }
|
||||
branch_to(t, V4_OP_NEXT, t->for_addr[--t->nfor], tok);
|
||||
return;
|
||||
}
|
||||
if (strcmp(tok, "UNEXT") == 0) {
|
||||
v4_cell body;
|
||||
if (t->nfor == 0) { fail(t, "UNEXT without FOR", NULL); return; }
|
||||
body = t->for_addr[--t->nfor];
|
||||
/* unext repeats the word it is in, so it must still be the word the
|
||||
* body started, with a slot left, and that word must own no literal */
|
||||
if (as->here != body || as->slot == 0 || as->slot >= V4_SLOT_COUNT || as->nlit != 0) {
|
||||
fail(t, "FOR ... UNEXT must fit one instruction word and hold no literal", NULL);
|
||||
return;
|
||||
}
|
||||
v4_asm_op(as, V4_OP_UNEXT);
|
||||
check_asm(t, tok);
|
||||
return;
|
||||
}
|
||||
|
||||
op = opcode_of(tok);
|
||||
if (op >= 0) {
|
||||
if (op == V4_OP_FETCH_P) { fail(t, "write the number, not @p", NULL); return; }
|
||||
if (v4_op_is_branch((unsigned)op)) {
|
||||
char target[V4_TEXT_MAX_TOKEN + 1u];
|
||||
if (!next_token(t, r, target)) { fail(t, "branch without a target", tok); return; }
|
||||
branch(t, (unsigned)op, target, 1);
|
||||
return;
|
||||
}
|
||||
v4_asm_op(as, (unsigned)op);
|
||||
check_asm(t, tok);
|
||||
return;
|
||||
}
|
||||
|
||||
s = find(t, tok);
|
||||
if (s && s->kind == KIND_MACRO) {
|
||||
if (r->depth == V4_TEXT_MAX_DEPTH + 1u) { fail(t, "macros nested too deep", tok); return; }
|
||||
r->p[r->depth++] = t->pool + (unsigned)s->value;
|
||||
return;
|
||||
}
|
||||
if (number_of(tok, &v, &big)) {
|
||||
if (big) { fail(t, "number does not fit a cell", tok); return; }
|
||||
v4_asm_lit(as, v);
|
||||
check_asm(t, tok);
|
||||
return;
|
||||
}
|
||||
if (s && s->kind == KIND_CONST) {
|
||||
v4_asm_lit(as, s->value);
|
||||
check_asm(t, tok);
|
||||
return;
|
||||
}
|
||||
branch(t, V4_OP_CALL, tok, 0); /* a word, defined already or later */
|
||||
}
|
||||
|
||||
void v4_text_begin(v4_text *t, v4_node *n, v4_cell origin)
|
||||
{
|
||||
v4_asm_begin(&t->as, n, origin);
|
||||
t->nsym = 0;
|
||||
t->nlabel = 0;
|
||||
t->nref = 0;
|
||||
t->nfor = 0;
|
||||
t->pool_used = 0;
|
||||
t->line = 0;
|
||||
t->failed = 0;
|
||||
t->message[0] = 0;
|
||||
}
|
||||
|
||||
void v4_text_constant(v4_text *t, const char *name, v4_cell value)
|
||||
{
|
||||
if (t->failed) return;
|
||||
if (strlen(name) > V4_TEXT_MAX_TOKEN) { fail(t, "name too long", name); return; }
|
||||
define(t, name, KIND_CONST, value);
|
||||
}
|
||||
|
||||
int v4_text_assemble(v4_text *t, const char *source)
|
||||
{
|
||||
char tok[V4_TEXT_MAX_TOKEN + 1u];
|
||||
reader r;
|
||||
|
||||
if (t->failed) return 0;
|
||||
r.p[0] = source;
|
||||
r.depth = 1;
|
||||
t->line = 1;
|
||||
while (!t->failed && next_token(t, &r, tok)) one_token(t, &r, tok);
|
||||
if (!t->failed) end_word(t);
|
||||
return !t->failed;
|
||||
}
|
||||
|
||||
int v4_text_finish(v4_text *t)
|
||||
{
|
||||
if (t->failed) return 0;
|
||||
end_word(t);
|
||||
while (t->nref && !t->failed) {
|
||||
const v4_text_symbol *s = find(t, t->pool + t->ref[0].name);
|
||||
if (!s || s->kind != KIND_WORD) {
|
||||
t->line = t->ref[0].line;
|
||||
fail(t, "never defined", t->pool + t->ref[0].name);
|
||||
break;
|
||||
}
|
||||
resolve(t, 0, s->value);
|
||||
}
|
||||
if (!t->failed && !v4_asm_ok(&t->as)) fail(t, "does not fit: memory is full", NULL);
|
||||
return !t->failed;
|
||||
}
|
||||
|
||||
v4_cell v4_text_word(const v4_text *t, const char *name)
|
||||
{
|
||||
const v4_text_symbol *s = find(t, name);
|
||||
return s && s->kind == KIND_WORD ? s->value : (v4_cell)-1;
|
||||
}
|
||||
|
||||
v4_cell v4_text_here(v4_text *t)
|
||||
{
|
||||
return v4_asm_label(&t->as);
|
||||
}
|
||||
|
||||
const char *v4_text_error(const v4_text *t)
|
||||
{
|
||||
return t->failed ? t->message : "";
|
||||
}
|
||||
Reference in New Issue
Block a user