From 0f82d8c22a0f9512ff17854f299c40553fd03bdc Mon Sep 17 00:00:00 2001 From: rajames Date: Thu, 15 Oct 2026 11:49:05 -0400 Subject: [PATCH] feat(v4): mkrom -- the ROM built off the node; it reads tokens The ROM's source is F18 code in capsules/v4/rom, assembled off the node by v4/tools/mkrom.c with the memory map's constants from v4/tools/mkmap.c (MACHINE.md 2). main.f18 is the entry table payloads call through (2.1); in.f18 reads the console and splits it into tokens exactly as text.c does, the token held packed as a counted string. The ROM's state is a reserved area of the map (host_map.h ROM_*). text.c gains the constant directive, so the off-node assembler and the ROM speak one language. The map also names (NUMBER)'s state, for the next task. Co-Authored-By: Claude Opus 5.5 --- capsules/v4/rom/in.f18 | 105 ++++++++++++++++++++++++++++++++ capsules/v4/rom/main.f18 | 11 ++++ v4/Makefile | 45 ++++++++++++-- v4/include/v4/text.h | 3 + v4/src/text.c | 18 +++++- v4/tests/host_map.h | 17 ++++++ v4/tests/rom_harness.h | 102 +++++++++++++++++++++++++++++++ v4/tests/test_host_rom_in.c | 117 ++++++++++++++++++++++++++++++++++++ v4/tests/test_text.c | 10 +++ v4/tools/mkmap.c | 47 +++++++++++++++ v4/tools/mkrom.c | 92 ++++++++++++++++++++++++++++ 11 files changed, 560 insertions(+), 7 deletions(-) create mode 100644 capsules/v4/rom/in.f18 create mode 100644 capsules/v4/rom/main.f18 create mode 100644 v4/tests/rom_harness.h create mode 100644 v4/tests/test_host_rom_in.c create mode 100644 v4/tools/mkmap.c create mode 100644 v4/tools/mkrom.c diff --git a/capsules/v4/rom/in.f18 b/capsules/v4/rom/in.f18 new file mode 100644 index 00000000..2cccec5c --- /dev/null +++ b/capsules/v4/rom/in.f18 @@ -0,0 +1,105 @@ +\ in.f18 -- the ROM's input: characters from the console, and tokens. +\ +\ The text the ROM assembles arrives at the console's receive registers, +\ a character at a time. The end of it is EOT (4), which the loader sends +\ after the last blob. A token is what v4/src/text.c's next_token makes +\ of the same text: blanks are space, tab, CR and LF; a token is at most 63 +\ characters; `\` as a whole token skips to the end of the line, and `(` as a +\ whole token to the next `)`. +\ +\ State, in the ROM's area of the memory map (MACHINE.md 2.1): +\ (TOK) the token, as a counted string: its count byte, then its +\ characters, four to a cell, low byte first, zero-padded -- +\ the form `",` lays down and (FIND) compares; 16 cells +\ (TOKN) its length +\ (PEEK) a character put back, plus one; 0: none +\ (LINE) how many new lines have been read +\ (EOF) non-zero once EOT has been read +\ (FAILED) 0, or the code of the error that stopped the ROM +\ (ERRLINE) (LINE) when it failed +\ Every routine here uses A. + +\ ( -- c ) wait for a character, then take it +: (KEY) + W: CONSOLE-STATUS a! @ if Z drop CONSOLE-RX a! @ ; + Z: drop jump W + +\ ( -- c ) the character put back, if there is one; else the next +: (GETC) + (PEEK) a! @ if NONE 0 ! -1 + ; + NONE: drop jump (KEY) + +\ ( c -- ) put a character back, to be the next one read +: (BACK) 1 + (PEEK) a! ! ; + +\ ( -- ) one more new line +: (LF+) (LINE) a! @ 1 + ! ; + +\ ( code -- ) stop with an error, noting the line +: (FAIL) (FAILED) a! ! (LINE) a! @ (ERRLINE) a! ! ; + +\ ( c -- ) one more character of the token: byte (TOKN)+1 of (TOK), into +\ a cell whose byte there is still zero +: (PUT) + (TOKN) a! @ 1 + + dup 2/ 2/ (TOK) + push + 3 and + S: if SD push 2* 2* 2* 2* 2* 2* 2* 2* pop -1 + jump S + SD: drop pop a! @ xor ! + (TOKN) a! @ 1 + ! ; + +\ ( i -- c ) character i of the token, from 0 +: (CHAR@) + 1 + dup 2/ 2/ (TOK) + a! @ + push 3 and + S: if SD -1 + pop 2/ 2/ 2/ 2/ 2/ 2/ 2/ 2/ push jump S + SD: drop pop 255 and ; + +\ ( -- len ) the next token into (TOK), its length into (TOKN). 0: the end, +\ or an error, which is in (FAILED). +: (TOKEN) + (EOF) a! @ if GO drop 0 ; + GO: drop + SKIP: (GETC) + dup 4 xor if END + drop dup 10 xor if NL + drop dup 32 xor if SP + drop dup 9 xor if SP + drop dup 13 xor if SP + drop 0 (TOKN) a! ! + (TOK) a! 15 FOR 0 !+ NEXT + READ: (TOKN) a! @ 63 xor if LONG + drop (PUT) (GETC) + dup 4 xor if TEND + drop dup 10 xor if TEND + drop dup 32 xor if TEND + drop dup 9 xor if TEND + drop dup 13 xor if TEND + drop jump READ + TEND: drop (BACK) + (TOKN) a! @ (TOK) a! @ xor ! + (TOKN) a! @ 1 xor if ONE + drop (TOKN) a! @ ; + ONE: drop 0 (CHAR@) + dup 92 xor if BSL + drop dup 40 xor if PAREN + drop drop 1 ; + BSL: drop drop + B1: (GETC) + dup 10 xor if BEND + drop dup 4 xor if BEND + drop drop jump B1 + BEND: drop (BACK) jump SKIP + PAREN: drop drop + P1: (GETC) + dup 41 xor if PCLOSE + drop dup 10 xor if PNL + drop dup 4 xor if PEOF + drop drop jump P1 + PNL: drop drop (LF+) jump P1 + PCLOSE: drop drop jump SKIP + PEOF: drop drop ERR-COMMENT (FAIL) 0 ; + END: drop drop 1 (EOF) a! ! 0 ; + NL: drop drop (LF+) jump SKIP + SP: drop drop jump SKIP + LONG: drop drop ERR-TOO-LONG (FAIL) 0 ; diff --git a/capsules/v4/rom/main.f18 b/capsules/v4/rom/main.f18 new file mode 100644 index 00000000..9ca7040e --- /dev/null +++ b/capsules/v4/rom/main.f18 @@ -0,0 +1,11 @@ +\ main.f18 -- the ROM's entry table. +\ +\ docs/v4.0.0/MACHINE.md 2.1. The ROM is shared code: a payload calls its +\ routines through this table and never to an address inside the ROM, so a +\ rebuilt ROM breaks no payload. It is at the ROM's base, one jump a word, +\ in a fixed order that only ever grows at its end. Entry i is at the ROM's +\ base plus i. DATASHEET-F18.md lists each routine's stack effect. + +: (ENTRIES) + jump (KEY) \ 0 ( -- c ) + jump (TOKEN) \ 1 ( -- len ) diff --git a/v4/Makefile b/v4/Makefile index 0b9cbf82..d3b4a662 100644 --- a/v4/Makefile +++ b/v4/Makefile @@ -104,6 +104,39 @@ NUCLEUS_32 := $(BINDIR)/nucleus-32.f18 HOST_DEFS := -DV4_NODE_WORDS=$(HOST_WORDS) -DV4_DATA_RING=$(HOST_DATA_RING) -DV4_RET_RING=$(HOST_RET_RING) -DV4_ROM_WORDS=$(HOST_ROM_WORDS) ENGINE_SRCS := $(addprefix $(HERE)/src/,node.c exec.c stack.c iword.c heat.c guard.c image.c fabric.c capsule.c message.c wire.c) +# THE ROM (docs/v4.0.0/MACHINE.md section 2). The machine's one binary: F18 +# code in capsules/v4/rom, assembled off the node by tools/mkrom.c, as a +# bitstream is built. tools/mkmap.c writes the memory map's constants it is +# assembled with, from tests/host_map.h. main.f18 comes first: the ROM's +# entry table is at its base. Tests named test_host_rom_*.c link the ROM of +# their width and read its symbols. +ROM_DIR := $(ROOT)/capsules/v4/rom +ROM_FILES := $(wildcard $(addprefix $(ROM_DIR)/,main.f18 in.f18 num.f18 sym.f18 pack.f18 word.f18 dir.f18)) +MKROM_WORDS := $(shell expr $(HOST_WORDS) + $(HOST_ROM_WORDS)) +rom_src = $(if $(findstring /test_host_rom_,$(1)),$(BINDIR)/rom-$(2).c) +rom_sym = $(if $(findstring /test_host_rom_,$(1)),-DV4_ROM_SYM='"$(BINDIR)/rom-$(2).sym"') + +define ROM_RULE +$(BINDIR)/mkmap-$(1): $(HERE)/tools/mkmap.c $$(SRCS) $$(wildcard $(HERE)/include/v4/*.h) $(HERE)/tests/host_map.h $(HERE)/Makefile + @mkdir -p $(BINDIR) + $$(CC) $$(CFLAGS) -I$(HERE)/include -DV4_CELL_BITS=$(1) $(HOST_DEFS) $(capsule_dir) $(HERE)/tools/mkmap.c $$(SRCS) -o $$@ + +$(BINDIR)/rom-map-$(1).f18: $(BINDIR)/mkmap-$(1) + $(BINDIR)/mkmap-$(1) > $$@ + +$(BINDIR)/mkrom-$(1): $(HERE)/tools/mkrom.c $$(SRCS) $$(wildcard $(HERE)/include/v4/*.h) $(HERE)/Makefile + @mkdir -p $(BINDIR) + $$(CC) $$(CFLAGS) -I$(HERE)/include -DV4_CELL_BITS=$(1) -DV4_NODE_WORDS=$(MKROM_WORDS) -DV4_DATA_RING=$(HOST_DATA_RING) -DV4_RET_RING=$(HOST_RET_RING) \ + -DROM_ORIGIN=$(HOST_WORDS) -DROM_RANGE=$(HOST_ROM_WORDS) $(HERE)/tools/mkrom.c $$(SRCS) -o $$@ + +$(BINDIR)/rom-$(1).c $(BINDIR)/rom-$(1).sym: $(BINDIR)/mkrom-$(1) $(BINDIR)/rom-map-$(1).f18 $$(ROM_FILES) + $(BINDIR)/mkrom-$(1) $(BINDIR)/rom-$(1).c $(BINDIR)/rom-$(1).sym $(BINDIR)/rom-map-$(1).f18 $$(ROM_FILES) +endef +$(foreach w,$(WIDTHS),$(eval $(call ROM_RULE,$(w)))) + +.PHONY: rom +rom: $(foreach w,$(WIDTHS),$(BINDIR)/rom-$(w).c) + define IMAGE_RULE $(BINDIR)/mkimage-$(1): $(HERE)/tools/mkimage.c $$(SRCS) $$(wildcard $(HERE)/include/v4/*.h) $(HERE)/tests/host_map.h $(HERE)/Makefile @mkdir -p $(BINDIR) @@ -257,16 +290,16 @@ endif # # Both binaries depend on this Makefile, so a flag change rebuilds them. define TEST_RULE -$(BINDIR)/$(1)-$(notdir $(2)): $(2) $$(SRCS) $(call blocks_src,$(2)) $(call blocks_objs,$(2)) $$(wildcard $(HERE)/include/v4/*.h) $$(wildcard $(HERE)/tests/*.h) $(HERE)/Makefile +$(BINDIR)/$(1)-$(notdir $(2)): $(2) $$(SRCS) $(call blocks_src,$(2)) $(call blocks_objs,$(2)) $(call rom_src,$(2),$(1)) $$(wildcard $(HERE)/include/v4/*.h) $$(wildcard $(HERE)/tests/*.h) $(HERE)/Makefile @mkdir -p $(BINDIR) - $$(CC) $$(CFLAGS) -I$(HERE)/include $(call blocks_inc,$(2)) -DV4_CELL_BITS=$(1) $(call node_size,$(2)) $(capsule_dir) \ - $(2) $$(SRCS) $(call blocks_src,$(2)) $(call blocks_objs,$(2)) -o $$@ + $$(CC) $$(CFLAGS) -I$(HERE)/include $(call blocks_inc,$(2)) -DV4_CELL_BITS=$(1) $(call node_size,$(2)) $(capsule_dir) $(call rom_sym,$(2),$(1)) \ + $(2) $$(SRCS) $(call blocks_src,$(2)) $(call blocks_objs,$(2)) $(call rom_src,$(2),$(1)) -o $$@ -$(BINDIR)/san-$(1)-$(notdir $(2)): $(2) $$(SRCS) $(call blocks_src,$(2)) $(call san_blocks_objs,$(2)) $$(wildcard $(HERE)/include/v4/*.h) $$(wildcard $(HERE)/tests/*.h) $(HERE)/Makefile +$(BINDIR)/san-$(1)-$(notdir $(2)): $(2) $$(SRCS) $(call blocks_src,$(2)) $(call san_blocks_objs,$(2)) $(call rom_src,$(2),$(1)) $$(wildcard $(HERE)/include/v4/*.h) $$(wildcard $(HERE)/tests/*.h) $(HERE)/Makefile @mkdir -p $(BINDIR) $$(CC) $$(CSTD) $$(WARN) -O1 -g -fno-omit-frame-pointer \ - -fsanitize=address,undefined -fno-sanitize-recover=all -I$(HERE)/include $(call blocks_inc,$(2)) -DV4_CELL_BITS=$(1) $(call node_size,$(2)) $(capsule_dir) \ - $(2) $$(SRCS) $(call blocks_src,$(2)) $(call san_blocks_objs,$(2)) -o $$@ + -fsanitize=address,undefined -fno-sanitize-recover=all -I$(HERE)/include $(call blocks_inc,$(2)) -DV4_CELL_BITS=$(1) $(call node_size,$(2)) $(capsule_dir) $(call rom_sym,$(2),$(1)) \ + $(2) $$(SRCS) $(call blocks_src,$(2)) $(call san_blocks_objs,$(2)) $(call rom_src,$(2),$(1)) -o $$@ endef # One run target per (width, test) and one prerequisite line per width, so diff --git a/v4/include/v4/text.h b/v4/include/v4/text.h index 02213392..66830306 100644 --- a/v4/include/v4/text.h +++ b/v4/include/v4/text.h @@ -46,6 +46,9 @@ * may use other macros; it may not define a word, a label * or a macro. * + * constant NAME VALUE + * NAME is the number VALUE, as v4_text_constant gives it. + * * header NAME a dictionary entry for NAME (capsule/dict.v4), laid down * in front of the word that the next `:` starts, and * linked to the header before it. NAME is taken as it diff --git a/v4/src/text.c b/v4/src/text.c index 73c15cdb..5269379b 100644 --- a/v4/src/text.c +++ b/v4/src/text.c @@ -65,7 +65,7 @@ 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 - || strcmp(s, "header") == 0 || flag_of(s) != 0; + || strcmp(s, "header") == 0 || strcmp(s, "constant") == 0 || flag_of(s) != 0; } static int is_label_def(const char *s) @@ -342,6 +342,21 @@ static void define_macro(v4_text *t, reader *r) define(t, name, KIND_MACRO, (v4_cell)start); } +/* `constant NAME VALUE`: as v4_text_constant, written in the text. */ +static void constant_directive(v4_text *t, reader *r) +{ + char name[V4_TEXT_MAX_TOKEN + 1u], value[V4_TEXT_MAX_TOKEN + 1u]; + v4_cell v; + int big; + + if (r->depth != 1) { fail(t, "not allowed in a macro", "constant"); return; } + if (!next_token(t, r, name)) { if (!t->failed) fail(t, "constant without a name", NULL); return; } + if (!next_token(t, r, value)) { if (!t->failed) fail(t, "constant without a value", name); return; } + if (!number_of(value, &v, &big)) { fail(t, "not a number", value); return; } + if (big) { fail(t, "number does not fit a cell", value); return; } + define(t, name, KIND_CONST, v); +} + static void one_token(v4_text *t, reader *r, const char *tok) { const v4_text_symbol *s; @@ -365,6 +380,7 @@ static void one_token(v4_text *t, reader *r, const char *tok) return; } if (strcmp(tok, "macro") == 0) { define_macro(t, r); return; } + if (strcmp(tok, "constant") == 0) { constant_directive(t, r); return; } if (strcmp(tok, "endmacro") == 0) { fail(t, "endmacro without macro", NULL); return; } if (is_label_def(tok)) { diff --git a/v4/tests/host_map.h b/v4/tests/host_map.h index 5893ba93..fdcfd472 100644 --- a/v4/tests/host_map.h +++ b/v4/tests/host_map.h @@ -119,6 +119,23 @@ #define MSG (BUF0_W - 48) /* the header of the message being served, 7 cells */ #define OUT_W (BUF0_W - 400) /* the output buffer: 256 cells, a character to a cell */ #define OUT_CELLS 256 +/* THE ROM'S STATE (docs/v4.0.0/MACHINE.md 2.1): a reserved area that belongs to the ROM's routines, which payloads + * call at run time; it is never cleared after the boot. OUT_W - 144 .. OUT_W - 65, which was free. */ +#define ROM_TOK (OUT_W - 144) /* the token being read, as a counted string: 16 cells */ +#define ROM_TOKN (OUT_W - 80) /* how many characters it has */ +#define ROM_PEEK (OUT_W - 79) /* a character put back, plus one; 0: none */ +#define ROM_LINE (OUT_W - 78) /* how many new lines have been read */ +#define ROM_EOF (OUT_W - 77) /* non-zero once EOT has been read: the input has ended */ +#define ROM_FAILED (OUT_W - 76) /* 0, or the code of the error that stopped the ROM */ +#define ROM_ERRLINE (OUT_W - 75) /* ROM_LINE when it failed */ +#define ROM_NBASE (OUT_W - 74) /* (NUMBER): the base, 10 or 16 */ +#define ROM_NNEG (OUT_W - 73) /* 1 after a leading - */ +#define ROM_NI (OUT_W - 72) /* the next character of the token */ +#define ROM_NU (OUT_W - 71) /* the value so far, unsigned */ +#define ROM_NBIG (OUT_W - 70) /* 1 once it has not fitted a cell */ +/* the ROM's error codes */ +#define ROM_ERR_TOO_LONG 1 /* a token longer than 63 characters: "name too long" */ +#define ROM_ERR_COMMENT 2 /* a ( with no ) before the end: "comment not closed" */ #define WORD_DEFINED (BUF0_W - 11) /* the xt of the word that is told of each new entry, or 0 */ #define WORD_FORGOTTEN (BUF0_W - 12) /* the xt of the word that is told when entries go, or 0 */ #define LOG_LEVEL (BUF0_W - 5) /* the level at or below which a message is printed */ diff --git a/v4/tests/rom_harness.h b/v4/tests/rom_harness.h new file mode 100644 index 00000000..8416f499 --- /dev/null +++ b/v4/tests/rom_harness.h @@ -0,0 +1,102 @@ +/* rom_harness.h -- run the ROM's routines on a host node, for tests. + * + * The ROM (capsules/v4/rom, built by tools/mkrom.c into $(BINDIR)/rom-.c) + * is attached to a host node as the machine's ROM. A routine is called + * through the ROM's entry table, as a payload calls it (MACHINE.md 2.1): the + * table's order is fixed, and is rom_entries[] below. Where a test needs to + * know whether P is inside a routine, the ROM's symbols are read from + * $(BINDIR)/rom-.sym (V4_ROM_SYM). + */ +#ifndef V4_TESTS_ROM_HARNESS_H +#define V4_TESTS_ROM_HARNESS_H + +#include "v4/testcode.h" +#include "host_map.h" +#include +#include + +#if V4_CELL_BITS == 64 +extern const v4_cell v4_rom_64[]; +extern const unsigned v4_rom_64_words; +#define ROM_IMAGE v4_rom_64 +#define ROM_IMAGE_WORDS v4_rom_64_words +#else +extern const v4_cell v4_rom_32[]; +extern const unsigned v4_rom_32_words; +#define ROM_IMAGE v4_rom_32 +#define ROM_IMAGE_WORDS v4_rom_32_words +#endif + +/* The entry table, in its fixed order: entry i is at V4_ROM_BASE + i. */ +static const char *const rom_entries[] = { + "(KEY)", "(TOKEN)", "(NUMBER)", "(FIND)", "(DEFINE)", + "(OP)", "(LIT)", "(BRANCH)", "(BRANCH>)", "(RESOLVE)", "(LABEL)" +}; +#define ROM_ENTRIES (sizeof rom_entries / sizeof rom_entries[0]) + +static v4_exec_state rom_es; +static v4_heat rom_h; + +/* The address a payload calls for the routine `name`: its entry. */ +static v4_cell rom_entry(const char *name) +{ + unsigned i; + for (i = 0; i < ROM_ENTRIES; i++) if (strcmp(rom_entries[i], name) == 0) return V4_ROM_BASE + (v4_cell)i; + printf("rom_harness: %s is not in the entry table\n", name); + return -1; +} + +/* Call the routine through its entry. 1 if it returned within `max_words` + * instruction words; 0 if not, and the node is left where it got to, to be + * fed and resumed. */ +static int rom_call(v4_node *n, const char *name, long max_words) +{ + return v4_test_call(n, &rom_es, &rom_h, rom_entry(name), max_words) >= 0; +} + +/* Go on with a call that had not returned. 1 if it now has. */ +static int rom_resume(v4_node *n, long max_words) +{ + long words = 0; + while (n->p != V4_TEST_HALT) { + if (words++ >= max_words || n->stopped) return 0; + (void)v4_exec_step_word(n, &rom_es, &rom_h); + } + return 1; +} + +/* 1 if P is inside the ROM's word `name`: at or after it, and before the + * next word the ROM has. */ +static int rom_in(const v4_node *n, const char *name) +{ + FILE *f = fopen(V4_ROM_SYM, "r"); + char sym[80]; + long addr, start = -1, next = -1; + if (!f) { printf("rom_harness: cannot read %s\n", V4_ROM_SYM); return 0; } + while (fscanf(f, "%79s %ld", sym, &addr) == 2) + if (strcmp(sym, name) == 0) start = addr; + rewind(f); + while (fscanf(f, "%79s %ld", sym, &addr) == 2) + if (start >= 0 && addr > start && (next < 0 || addr < next)) next = addr; + fclose(f); + if (start < 0) return 0; + if (next < 0) next = (long)V4_ROM_BASE + (long)ROM_IMAGE_WORDS; + return (long)n->p >= start && (long)n->p < next; +} + +/* Reset `n`, attach the ROM and the console's receive registers. */ +static void rom_boot(v4_node *n) +{ + (void)host_load; /* host_map.h's loader is not used here */ + (void)host_branch_slots; + (void)rom_call; /* not every test uses every helper */ + (void)rom_resume; + (void)rom_in; + v4_node_reset(n); + v4_exec_reset(&rom_es); + v4_heat_reset(&rom_h); + v4_node_rom_attach(n, ROM_IMAGE, ROM_IMAGE_WORDS); + v4_node_console_input_attach(n, CONSOLE_RX, CONSOLE_ST); +} + +#endif /* V4_TESTS_ROM_HARNESS_H */ diff --git a/v4/tests/test_host_rom_in.c b/v4/tests/test_host_rom_in.c new file mode 100644 index 00000000..3573cee1 --- /dev/null +++ b/v4/tests/test_host_rom_in.c @@ -0,0 +1,117 @@ +/* test_host_rom_in.c -- the ROM's input: characters from the console, and tokens. + * + * docs/v4.0.0/plans/2026-10-15-machine-step1-rom.md Task 2. The ROM reads + * the text it assembles from CONSOLE-RX, waiting on CONSOLE-STATUS, and + * splits it into tokens exactly as v4/src/text.c's next_token does: blanks + * are space, tab, CR and LF; `\` skips to the end of the line and `(` to the + * next `)`; a token is at most 63 characters. The end of the input is EOT + * (4), which the loader sends after the last blob. + * + * Routines are called through the ROM's entry table, as a payload calls them + * (MACHINE.md 2.1). + */ +#include "rom_harness.h" +#include + +static int failures = 0, checks = 0; +#define CHECK(c,...) do{checks++; if(!(c)){failures++; printf("FAIL %s:%d: ",__FILE__,__LINE__); printf(__VA_ARGS__); printf("\n");}}while(0) + +static v4_node n; + +/* the token the ROM has read, as a C string. (TOK) holds it as a counted + * string: the count byte, then the characters, four to a cell, low byte + * first, zero-padded -- the form `",` lays down and (FIND) compares. */ +static unsigned tok_byte(unsigned b) +{ + return (unsigned)(((v4_ucell)n.mem[ROM_TOK + (v4_cell)(b / 4u)] >> (8u * (b % 4u))) & 0xFFu); +} +static const char *tok(void) +{ + static char s[80]; + v4_cell len = n.mem[ROM_TOKN], i; + if (len < 0 || len > 63) len = 0; + for (i = 0; i < len; i++) s[i] = (char)tok_byte((unsigned)i + 1u); + s[len] = 0; + return s; +} +/* the token's padding is zero and its count byte is its length */ +static int tok_packed(void) +{ + unsigned b, len = (unsigned)n.mem[ROM_TOKN]; + if (tok_byte(0) != len) return 0; + for (b = len + 1u; b < 64u; b++) if (tok_byte(b) != 0) return 0; + return 1; +} + +/* call (TOKEN); its length, or -1 if it did not return */ +static v4_cell token(void) +{ + if (!rom_call(&n, "(TOKEN)", 100000)) return -1; + return v4_dstack_pop(&n.ds); +} + +static void feed(const char *s) { v4_node_console_feed(&n, s, (unsigned)strlen(s)); } + +int main(void) +{ + rom_boot(&n); + + /* (KEY) waits for a character, then takes it */ + feed("Q"); + CHECK(rom_call(&n, "(KEY)", 1000) && v4_dstack_pop(&n.ds) == 'Q', "(KEY) takes a character"); + CHECK(!rom_call(&n, "(KEY)", 1000), "with none pending it waits"); + CHECK(rom_in(&n, "(KEY)"), "inside (KEY): P %ld", (long)n.p); + + /* tokens, comments, lines */ + rom_boot(&n); + feed(" dup \\ c\n ( x\n y ) over\t; \x04"); + CHECK(token() == 3 && strcmp(tok(), "dup") == 0, "first token: dup, got '%s'", tok()); + CHECK(tok_packed(), "packed as a counted string, zero-padded"); + CHECK(n.mem[ROM_LINE] == 0, "no new line passed yet: %ld", (long)n.mem[ROM_LINE]); + CHECK(token() == 4 && strcmp(tok(), "over") == 0, "comments skipped: over, got '%s'", tok()); + CHECK(n.mem[ROM_LINE] == 2, "two new lines passed, one in each comment: %ld", (long)n.mem[ROM_LINE]); + CHECK(token() == 1 && strcmp(tok(), ";") == 0, "then ;, got '%s'", tok()); + CHECK(tok_packed(), "a shorter token after a longer one leaves no trace of it"); + CHECK(token() == 0, "then the end"); + CHECK(token() == 0, "and the end stays the end"); + CHECK(n.mem[ROM_FAILED] == 0, "nothing failed"); + + /* the input running dry is not the end: (TOKEN) waits (Review Focus 5) */ + rom_boot(&n); + CHECK(!rom_call(&n, "(TOKEN)", 10000), "no input: (TOKEN) waits"); + CHECK(rom_in(&n, "(KEY)"), "waiting in (KEY)"); + feed("ab"); + CHECK(!rom_resume(&n, 10000), "a token not yet ended: still waiting"); + feed("c\x04"); + CHECK(rom_resume(&n, 10000) && v4_dstack_pop(&n.ds) == 3 && strcmp(tok(), "abc") == 0, "then it ends: abc, got '%s'", tok()); + + /* a token ended by EOT, with no blank after it */ + rom_boot(&n); + feed("x\x04"); + CHECK(token() == 1 && strcmp(tok(), "x") == 0, "x before EOT"); + CHECK(token() == 0, "then the end"); + + /* "(" and "\" are comments only as whole tokens */ + rom_boot(&n); + feed("(x \\y )\x04"); + CHECK(token() == 2 && strcmp(tok(), "(x") == 0, "(x is a token, got '%s'", tok()); + CHECK(token() == 2 && strcmp(tok(), "\\y") == 0, "\\y is a token, got '%s'", tok()); + CHECK(token() == 1 && strcmp(tok(), ")") == 0, ") is a token, got '%s'", tok()); + + /* 63 characters is a token; 64 is "name too long" */ + rom_boot(&n); + feed("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \x04"); + CHECK(token() == 63 && n.mem[ROM_FAILED] == 0 && tok_packed(), "63 characters are a token"); + rom_boot(&n); + feed("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \x04"); + CHECK(token() == 0 && n.mem[ROM_FAILED] == ROM_ERR_TOO_LONG, "64 are name too long: failed %ld", (long)n.mem[ROM_FAILED]); + + /* a comment not closed */ + rom_boot(&n); + feed("\n( never closed\x04"); + CHECK(token() == 0 && n.mem[ROM_FAILED] == ROM_ERR_COMMENT, "( with no ): failed %ld", (long)n.mem[ROM_FAILED]); + CHECK(n.mem[ROM_ERRLINE] == 1, "on the second line (0-based 1): %ld", (long)n.mem[ROM_ERRLINE]); + + printf("test_host_rom_in: %d checks, %d failures\n", checks, failures); + return failures != 0; +} diff --git a/v4/tests/test_text.c b/v4/tests/test_text.c index b48fc548..d6dd7de0 100644 --- a/v4/tests/test_text.c +++ b/v4/tests/test_text.c @@ -489,6 +489,16 @@ int main(void) CHECK(v4_text_word(&tx, "FAR") > (v4_cell)(V4_NODE_WORDS - 200u), "FAR is far: %ld", (long)v4_text_word(&tx, "FAR")); } + /* ---- `constant NAME VALUE`, the directive form of v4_text_constant (MACHINE.md, the ROM's language) ---- */ + CHECK(ok("constant SEVEN 7 constant MINUS -2 constant BIG $7F : T SEVEN MINUS + BIG + ;") && result("T", 0, 0, 0) == 132, + "constant defines a literal: %s", v4_text_error(&tx)); + CHECK(ok("constant K 3 : T K+2 ;") && result("T", 0, 0, 0) == 5, "CONST+N works with a directive constant"); + CHECK(refused("constant K 3 constant K 4", "already defined", 1), "a constant twice is refused"); + CHECK(refused("constant dup 3", "cannot be used as a name", 1), "an opcode is not a constant's name"); + CHECK(refused("constant K", "constant without a value", 1), "constant needs a value"); + CHECK(refused("constant K x", "not a number", 1), "a constant's value is a number"); + CHECK(refused(": constant ;", "cannot be used as a name", 1), "constant is reserved"); + CHECK(v4_node_guards_intact(&na) && v4_node_guards_intact(&nb), "guards intact"); printf(" %d checks, %d failures\n", checks, failures); diff --git a/v4/tools/mkmap.c b/v4/tools/mkmap.c new file mode 100644 index 00000000..3de9d7fb --- /dev/null +++ b/v4/tools/mkmap.c @@ -0,0 +1,47 @@ +/* mkmap.c -- the memory map, as constants the ROM is assembled with. + * + * mkmap > rom-map-.f18 + * + * Runs on the build machine, built for the host node (HOST_WORDS and the + * host stack sizes) at one cell width. It writes, as `constant` lines in + * the text assembler's notation, the addresses and codes of + * v4/tests/host_map.h that the ROM's source (capsules/v4/rom) uses: the + * console's receive registers and the ROM's state area (MACHINE.md 2.1). + * The map has one source, host_map.h; this is how the ROM reads it. + */ +#include "../tests/host_map.h" +#include + +static void constant(const char *name, long value) +{ + printf("constant %s %ld\n", name, value); +} + +int main(void) +{ + (void)host_load; /* host_map.h's loader is not this tool's */ + (void)host_branch_slots; + printf("\\ Generated by v4/tools/mkmap.c from v4/tests/host_map.h -- do not edit.\n"); + constant("CONSOLE-RX", (long)CONSOLE_RX); + constant("CONSOLE-STATUS", (long)CONSOLE_ST); + constant("(TOK)", (long)ROM_TOK); + constant("(TOKN)", (long)ROM_TOKN); + constant("(PEEK)", (long)ROM_PEEK); + constant("(LINE)", (long)ROM_LINE); + constant("(EOF)", (long)ROM_EOF); + constant("(FAILED)", (long)ROM_FAILED); + constant("(ERRLINE)", (long)ROM_ERRLINE); + constant("(NBASE)", (long)ROM_NBASE); + constant("(NNEG)", (long)ROM_NNEG); + constant("(NI)", (long)ROM_NI); + constant("(NU)", (long)ROM_NU); + constant("(NBIG)", (long)ROM_NBIG); + /* the cell width's constants (NUMBER) needs, written as signed cells */ + constant("MSB", (long)(v4_cell)V4_MSB); + constant("DEC-LIMIT", (long)(v4_cell)(~(v4_ucell)0 / 10u)); + constant("DEC-NLAST", -(long)(~(v4_ucell)0 % 10u) - 1); + constant("HEX-HIGH", (long)(v4_cell)~(~(v4_ucell)0 >> 4)); + constant("ERR-TOO-LONG", (long)ROM_ERR_TOO_LONG); + constant("ERR-COMMENT", (long)ROM_ERR_COMMENT); + return 0; +} diff --git a/v4/tools/mkrom.c b/v4/tools/mkrom.c new file mode 100644 index 00000000..415bfa69 --- /dev/null +++ b/v4/tools/mkrom.c @@ -0,0 +1,92 @@ +/* mkrom.c -- build the ROM, off the node, as a bitstream is built. + * + * mkrom OUT.c OUT.sym MAP.f18 SOURCE.f18... + * + * docs/v4.0.0/MACHINE.md section 2. The ROM is F18 code, written in the + * text assembler's notation (include/v4/text.h) in capsules/v4/rom, and this + * is the only thing built off the node. It assembles MAP.f18 (the memory + * map's constants, from tools/mkmap.c) and then each SOURCE in the order + * given, at the ROM's base address, and writes: + * OUT.c the ROM: const v4_cell v4_rom_[] and its length + * OUT.sym every word of it, "NAME ADDRESS" a line, for the tests + * + * This tool is built with a node whose RAM reaches over the ROM's range + * (V4_NODE_WORDS = the host's RAM plus its ROM range, V4_ROM_WORDS = 0), so + * the assembler can lay the ROM down where it will run; ROM_ORIGIN is the + * host's RAM size, which is where the ROM starts on the machine. The + * assembler's branch placement depends only on the top of the address space, + * which is the same either way. + */ +#include "v4/text.h" +#include +#include + +#ifndef ROM_ORIGIN +#error "ROM_ORIGIN must be the host node's RAM size" +#endif +#ifndef ROM_RANGE +#error "ROM_RANGE must be the host node's ROM range, in words" +#endif + +static v4_node n; +static v4_text tx; + +static void die(const char *what, const char *detail) +{ + fprintf(stderr, "mkrom: %s%s%s\n", what, detail ? ": " : "", detail ? detail : ""); + exit(1); +} + +/* a cell as a C constant of the cell's type, with no implementation-defined conversion */ +static void put_cell(FILE *f, v4_cell v) +{ +#if V4_CELL_BITS == 64 + if (v == (v4_cell)((v4_ucell)1 << 63)) fprintf(f, "(-9223372036854775807LL - 1)"); + else fprintf(f, "%lldLL", (long long)v); +#else + if (v == (v4_cell)((v4_ucell)1 << 31)) fprintf(f, "(-2147483647L - 1)"); + else fprintf(f, "%ldL", (long)v); +#endif +} + +int main(int argc, char **argv) +{ + FILE *out; + v4_cell here, a; + int i; + unsigned s; + + if (argc < 5) die("usage: mkrom OUT.c OUT.sym MAP.f18 SOURCE.f18...", NULL); + if ((v4_ucell)V4_NODE_WORDS != (v4_ucell)ROM_ORIGIN + (v4_ucell)ROM_RANGE) + die("built with the wrong node size: RAM must reach over the ROM's range", NULL); + + v4_node_reset(&n); + v4_text_begin(&tx, &n, ROM_ORIGIN); + for (i = 3; i < argc; i++) + if (!v4_text_assemble_file(&tx, argv[i])) die("does not assemble", v4_text_error(&tx)); + if (!v4_text_finish(&tx)) die("not everything is defined", v4_text_error(&tx)); + here = v4_text_here(&tx); + if (here - (v4_cell)ROM_ORIGIN > (v4_cell)ROM_RANGE) die("the ROM is larger than its range", NULL); + + out = fopen(argv[1], "w"); + if (!out) die("cannot write", argv[1]); + fprintf(out, "/* Generated by v4/tools/mkrom.c from capsules/v4/rom -- do not edit. */\n#include \"v4/cell.h\"\n\n"); + fprintf(out, "const v4_cell v4_rom_%d[] = {\n", V4_CELL_BITS); + for (a = ROM_ORIGIN; a < here; a++) { + fprintf(out, " "); + put_cell(out, n.mem[a]); + fprintf(out, ",\n"); + } + if (here == ROM_ORIGIN) fprintf(out, " 0\n"); + fprintf(out, "};\nconst unsigned v4_rom_%d_words = %ldu;\n", V4_CELL_BITS, (long)(here - (v4_cell)ROM_ORIGIN)); + if (fclose(out) != 0) die("cannot write", argv[1]); + + out = fopen(argv[2], "w"); + if (!out) die("cannot write", argv[2]); + for (s = 0; s < tx.nsym; s++) + if (tx.sym[s].kind == 1) fprintf(out, "%s %ld\n", tx.pool + tx.sym[s].name, (long)tx.sym[s].value); + if (fclose(out) != 0) die("cannot write", argv[2]); + + fprintf(stderr, "mkrom: a ROM of %ld words, %d-bit cells -> %s\n", (long)(here - (v4_cell)ROM_ORIGIN), V4_CELL_BITS, argv[1]); + return 0; +}