Compare commits

..

6 Commits

Author SHA1 Message Date
956d42d008 Create assembler driver script for recording word addresses 2026-03-20 12:40:09 +00:00
87d8345017 Run profiler while prelude is loading 2026-03-19 22:03:46 +00:00
d39fe580fc Implement sampling profiler 2026-03-19 22:03:36 +00:00
ba8c99a123 Mirror IP and RSP regs in memory
This will allow the profiler to sample the current return stack and
instruction pointer.  It can't just use the ip and rsp globals
directly, as I had initially planned, since these cannot be sent
in between threads.
2026-03-19 22:01:51 +00:00
cc8ae742f0 Add more opcodes to assembler 2026-03-19 22:01:33 +00:00
812443d6ee Support exports of globals in assembler 2026-03-19 19:53:50 +00:00
9 changed files with 260 additions and 164 deletions

View File

@@ -74,21 +74,20 @@ guile tests.scm | xmllint --format -
| Name | Address | Size / B | Access | | Name | Address | Size / B | Access |
|--------|---------|----------|--------------| |--------|---------|----------|--------------|
| TXBUF | 000h | 32 | write | | TXBUF | 00h | 32 | write |
| RXBUF | 080h | 32 | read | | RXBUF | 20h | 32 | read |
| TXHEAD | 100h | 4 | atomic read | | TXHEAD | 40h | 4 | atomic read |
| TXTAIL | 104h | 4 | atomic write | | TXTAIL | 44h | 4 | atomic write |
| RXHEAD | 108h | 4 | atomic write | | RXHEAD | 48h | 4 | atomic write |
| RXTAIL | 10Ch | 4 | atomic read | | RXTAIL | 4Ch | 4 | atomic read |
For both sending (`TX`) and receiving (`RX`), there are three For both sending (`TX`) and receiving (`RX`), there are three
registers: `xBUF`, `xHEAD` and `xTAIL`: registers: `xBUF`, `xHEAD` and `xTAIL`:
- `xBUF` registers are 128-byte FIFO ring buffers used for data - `xBUF` registers are 32-byte FIFO ring buffers used for data
- The `xHEAD` and `xTAIL` registers specify the start and end of data - The `xHEAD` and `xTAIL` registers specify the start and end of data
in the ring buffer, `xHEAD` being the offset of the first byte of in the ring buffer, `xHEAD` being the offset of the first byte of
data, and `xTAIL` being the offset of the first byte *after* the data, and `xTAIL` being the offset of the first byte *after* the data.
data.
In order to be distinguishable from the empty state, the ring buffers In order to be distinguishable from the empty state, the ring buffers
must never be completely full -- there must always be *at least one* must never be completely full -- there must always be *at least one*
@@ -98,10 +97,7 @@ unoccupied byte between the tail and the head.
| Name | Address | Size / B | Access | | Name | Address | Size / B | Access |
|----------|---------|----------|--------------| |----------|---------|----------|--------------|
| SYSREADY | 110h | 4 | atomic write | | SYSREADY | 50h | 1 | atomic write |
| SYSINTER | 114h | 4 | atomic read |
The `SYSREADY` register is used to indicate when the system has booted The `SYSREADY` register is used to indicate when the system has booted
up and is ready for user input. `SYSINTER` is set (and notified on) up and is ready for user input.
once the emulator has enabled user input and the system is
interactive.

68
asm.js
View File

@@ -14,29 +14,20 @@ class Tokenizer {
this.buffer = []; this.buffer = [];
this.comment = false; this.comment = false;
this.string = false; this.string = false;
this.cursor = 0;
}
find(pred) {
for (let i = this.cursor; i < this.buffer.length; ++i) {
if (pred(this.buffer[i]))
return i;
}
return null;
} }
skip() { skip() {
const idx = this.find((cp) => !this.skips.has(cp)); const idx = this.buffer.findIndex((cp) => !this.skips.has(cp));
this.cursor = idx == null ? this.buffer.length : idx; this.buffer = idx == -1 ? [] : this.buffer.slice(idx);
} }
next_string() { next_string() {
const idx = this.find((cp) => cp == this.string_quote); const idx = this.buffer.findIndex((cp) => cp == this.string_quote);
if (idx == null) { if (idx == -1) {
this.string = true; this.string = true;
} else { } else {
const string = this.buffer.slice(this.cursor, idx).join(""); const string = this.buffer.slice(0, idx).join("");
this.cursor = idx + 1; this.buffer = this.buffer.slice(idx + 1);
this.string = false; this.string = false;
return { string: string }; return { string: string };
} }
@@ -47,20 +38,18 @@ class Tokenizer {
return this.next_string(); return this.next_string();
this.skip(); this.skip();
if (this.buffer[this.cursor] == LINE_END) { if (this.buffer[0] == LINE_END)
++this.cursor; return this.buffer.shift();
return LINE_END;
}
if (this.buffer[this.cursor] == this.string_quote) { if (this.buffer[0] == this.string_quote) {
++this.cursor; this.buffer.shift();
return this.next_string(); return this.next_string();
} }
const idx = this.find((cp) => this.delims.has(cp)); const idx = this.buffer.findIndex((cp) => this.delims.has(cp));
if (idx != null) { if (idx != -1) {
const token = this.buffer.slice(this.cursor, idx).join(""); const token = this.buffer.slice(0, idx).join("");
this.cursor = idx; this.buffer = this.buffer.slice(idx);
return token; return token;
} }
} }
@@ -198,6 +187,9 @@ const opcodes = {
"i32.shl": 0x74, "i32.shl": 0x74,
"i32.shr_s": 0x75, "i32.shr_s": 0x75,
"i32.shr_u": 0x76, "i32.shr_u": 0x76,
"i64.or": 0x84,
"i64.shl": 0x86,
"i64.extend_i32_u": 0xad,
// Threads instructions // Threads instructions
"memory.atomic.notify": [ 0xfe, 0x00 ], "memory.atomic.notify": [ 0xfe, 0x00 ],
@@ -205,6 +197,7 @@ const opcodes = {
"i32.atomic.load": [ 0xfe, 0x10 ], "i32.atomic.load": [ 0xfe, 0x10 ],
"i32.atomic.load8_u": [ 0xfe, 0x12 ], "i32.atomic.load8_u": [ 0xfe, 0x12 ],
"i32.atomic.store": [ 0xfe, 0x17 ], "i32.atomic.store": [ 0xfe, 0x17 ],
"i64.atomic.store": [ 0xfe, 0x18 ],
"i32.atomic.store8": [ 0xfe, 0x19 ], "i32.atomic.store8": [ 0xfe, 0x19 ],
}; };
@@ -899,6 +892,7 @@ const Section = Object.freeze({
const Kind = Object.freeze({ const Kind = Object.freeze({
FUNC: 0x00, FUNC: 0x00,
MEM: 0x02, MEM: 0x02,
GLOBAL: 0x03,
}); });
export class Assembler { export class Assembler {
@@ -953,8 +947,28 @@ export class Assembler {
} }
action_export(action) { action_export(action) {
const index = Object.keys(this.funcs).indexOf(action.name); const func_index = Object.keys(this.funcs).indexOf(action.name);
this.exports[action.name] = { kind: Kind.FUNC, index }; if (func_index != -1) {
this.exports[action.name] = {
kind: Kind.FUNC,
index: func_index,
};
return;
}
const global_index = Object.keys(this.globals).indexOf(action.name);
if (global_index != -1) {
this.exports[action.name] = {
kind: Kind.GLOBAL,
index: global_index,
};
return;
}
console.error(
`ERROR: Unable to resolve export ${action.name} `
+ "(only functions and globals currently supported)"
);
} }
action_func(action) { action_func(action) {

View File

@@ -1,4 +1,3 @@
asm.js
boot.js boot.js
emu.js emu.js
index.html index.html

73
emu.js
View File

@@ -1,17 +1,17 @@
const TXBUF = 0x000; const TXBUF = 0x00;
const RXBUF = 0x080; const RXBUF = 0x20;
const TXHEAD = 0x100; const TXHEAD = 0x40;
const TXTAIL = 0x104; const TXTAIL = 0x44;
const RXHEAD = 0x108; const RXHEAD = 0x48;
const RXTAIL = 0x10c; const RXTAIL = 0x4c;
const SYSREADY = 0x50;
const SYSREADY = 0x110; const TXBUF_SIZE = 32;
const SYSINTER = 0x114; const RXBUF_SIZE = 32;
const PERIPHS_SIZE = 81;
const PERIPHS_SIZE = 0x200; const POLL_INTERVAL_MS = 20;
const DOT_INTERVAL_MS = 120;
const POLL_INTERVAL_MS = 5;
const DOT_INTERVAL_MS = 25;
const COLS = 80; const COLS = 80;
const TAB_WIDTH = 8; const TAB_WIDTH = 8;
@@ -51,18 +51,28 @@ class Emulator {
document.addEventListener('keydown', (e) => this.handle_keydown(e)); document.addEventListener('keydown', (e) => this.handle_keydown(e));
window.addEventListener('resize', () => this.handle_resize()); window.addEventListener('resize', () => this.handle_resize());
this.forth = new Worker('boot.js', { type: 'module' }); this.prof = new Worker("prof.js");
this.prof.onmessage = (e) => {
const blob = new Blob(
[JSON.stringify(e.data)],
{ type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "wipforth-profile.json";
a.click();
URL.revokeObjectURL(url);
};
this.print("Assembling kernel "); this.print("Assembling kernel ");
this.dots = setInterval(() => this.print("."), DOT_INTERVAL_MS); const dots = setInterval(() => this.print("."), DOT_INTERVAL_MS);
this.forth.postMessage({ type: "load", mem: this.mem }); this.worker = new Worker('boot.js', { type: 'module' });
this.forth.onmessage = (e) => { this.worker.postMessage({ type: "load", mem: this.mem });
clearInterval(this.dots); this.worker.onmessage = (e) => {
clearInterval(dots);
this.print(" done\n"); this.print(" done\n");
this.worker.postMessage({ type: "boot" });
this.print("Loading prelude "); this.prof.postMessage({ type: "start", mem: this.mem });
this.forth.postMessage({ type: "boot" });
this.dots = setInterval(() => this.print("."), DOT_INTERVAL_MS);
}; };
fetch('prelude.f') fetch('prelude.f')
@@ -89,21 +99,16 @@ class Emulator {
if (!this.input_enable) { if (!this.input_enable) {
const sysready = Atomics.load(this.mem_u8, SYSREADY); const sysready = Atomics.load(this.mem_u8, SYSREADY);
if (sysready != 0) { if (sysready != 0) {
clearInterval(this.dots); this.prof.postMessage({ type: "stop" });
this.print(" done\n");
Atomics.store(this.mem_u8, SYSINTER, 1);
Atomics.notify(this.mem_i32, SYSINTER / 4);
this.input_enable = true; this.input_enable = true;
this.blink = true;
this.flush_output(); this.flush_output();
document.getElementById('cursor').classList.add('blinking');
} }
} }
} }
fifo_next(idx) { fifo_next(idx) {
return (idx + 1) & 0x7f; return (idx + 1) & 0x1f;
} }
handle_tx_data(head, tail) { handle_tx_data(head, tail) {
@@ -191,14 +196,12 @@ class Emulator {
} }
e.preventDefault(); e.preventDefault();
this.blink = false;
this.flush_output(); this.flush_output();
document.getElementById('cursor').classList.remove('blinking'); document.getElementById('cursor').classList.remove('blinking');
if (this.idle_timer) if (this.idle_timer)
clearTimeout(this.idle_timer); clearTimeout(this.idle_timer);
this.idle_timer = setTimeout(() => { this.idle_timer = setTimeout(() => {
this.blink = true;
document.getElementById('cursor').classList.add('blinking'); document.getElementById('cursor').classList.add('blinking');
}, CURSOR_IDLE_TIME_MS); }, CURSOR_IDLE_TIME_MS);
} }
@@ -285,12 +288,10 @@ class Emulator {
return row.map((c, x) => { return row.map((c, x) => {
const ec = this.html_escape(c); const ec = this.html_escape(c);
if (this.input_enable if (this.input_enable
&& x == this.cursor.x && y == this.cursor.y) { && x == this.cursor.x && y == this.cursor.y)
const cl = this.blink ? 'class="blinking"' : ''; return '<span id="cursor">' + ec + '</span>';
return `<span id="cursor" ${cl}>` + ec + '</span>'; else
} else {
return ec; return ec;
}
}).join('').trimEnd(); }).join('').trimEnd();
}).join('\n'); }).join('\n');
this.output.innerHTML = html; this.output.innerHTML = html;

View File

@@ -1,5 +1,12 @@
76 EMIT 111 EMIT 97 EMIT 100 EMIT 105 EMIT 110 EMIT 103 EMIT 32 EMIT
112 EMIT 114 EMIT 101 EMIT 108 EMIT 117 EMIT 100 EMIT 101 EMIT 32 EMIT
: \ KEY 10 = 0BRANCH [ -20 , ] ; IMMEDIATE \ Now we have line comments :) : \ KEY 10 = 0BRANCH [ -20 , ] ; IMMEDIATE \ Now we have line comments :)
\ We'll periodically sprinkle these in so that it's clear to the user
\ that things are happening.
46 EMIT
\ Conditionals \ Conditionals
: IF : IF
@@ -20,6 +27,8 @@
SWAP ! SWAP !
; IMMEDIATE ; IMMEDIATE
46 EMIT
\ Loops \ Loops
: BEGIN HERE @ ; IMMEDIATE : BEGIN HERE @ ; IMMEDIATE
@@ -34,6 +43,8 @@
HERE @ - , HERE @ - ,
; IMMEDIATE ; IMMEDIATE
46 EMIT
\ Recursive calls \ Recursive calls
: RECURSE LATEST @ >CFA , ; IMMEDIATE : RECURSE LATEST @ >CFA , ; IMMEDIATE
@@ -50,6 +61,8 @@
( ( Take that, C ) ) ( ( Take that, C ) )
46 EMIT
\ Printing utilities \ Printing utilities
: CR 10 EMIT ; : CR 10 EMIT ;
@@ -67,6 +80,8 @@
+ EMIT + EMIT
; ;
CHAR . EMIT
: . : .
\ Handle negatives \ Handle negatives
DUP 0< IF CHAR - EMIT NEGATE THEN DUP 0< IF CHAR - EMIT NEGATE THEN
@@ -95,6 +110,8 @@
2DROP 2DROP
; ;
CHAR . EMIT
: TYPE ( addr len -- ) : TYPE ( addr len -- )
BEGIN BEGIN
DUP 0= IF 2DROP EXIT THEN DUP 0= IF 2DROP EXIT THEN
@@ -103,6 +120,8 @@
AGAIN AGAIN
; ;
CHAR . EMIT
: C, HERE @ C! 1 HERE +! ; : C, HERE @ C! 1 HERE +! ;
: ." : ."
@@ -132,6 +151,8 @@
THEN THEN
; IMMEDIATE ; IMMEDIATE
CHAR . EMIT
\ Misc utilities \ Misc utilities
: NIP SWAP DROP ; : NIP SWAP DROP ;
@@ -150,6 +171,8 @@
: [COMPILE] ' , ; IMMEDIATE : [COMPILE] ' , ; IMMEDIATE
CHAR . EMIT
\ Constants, variables and values \ Constants, variables and values
: CONSTANT : CONSTANT
@@ -181,21 +204,24 @@
THEN THEN
; IMMEDIATE ; IMMEDIATE
CHAR . EMIT
\ Peripheral register addresses \ Peripheral register addresses
HEX HEX
000 CONSTANT TXBUF 00 CONSTANT TXBUF
080 CONSTANT RXBUF 20 CONSTANT RXBUF
100 CONSTANT TXHEAD 40 CONSTANT TXHEAD
104 CONSTANT TXTAIL 44 CONSTANT TXTAIL
108 CONSTANT RXHEAD 48 CONSTANT RXHEAD
10C CONSTANT RXTAIL 4C CONSTANT RXTAIL
110 CONSTANT SYSREADY 50 CONSTANT SYSREADY
114 CONSTANT SYSINTER
DECIMAL DECIMAL
46 EMIT
\ A better word-not-found handler \ A better word-not-found handler
: ANY-RX? RXHEAD AC@ RXTAIL AC@ <> ; : ANY-RX? RXHEAD AC@ RXTAIL AC@ <> ;
@@ -218,11 +244,13 @@ DECIMAL
' WNF-HANDLER TO WNFHOOK ' WNF-HANDLER TO WNFHOOK
CHAR . EMIT
\ Version number \ Version number
0 CONSTANT VERSION-MAJOR 0 CONSTANT VERSION-MAJOR
2 CONSTANT VERSION-MINOR 2 CONSTANT VERSION-MINOR
2 CONSTANT VERSION-PATCH 1 CONSTANT VERSION-PATCH
: PRINT-VERSION : PRINT-VERSION
CHAR v EMIT VERSION-MAJOR . CHAR v EMIT VERSION-MAJOR .
@@ -230,6 +258,8 @@ DECIMAL
CHAR . EMIT VERSION-PATCH . CHAR . EMIT VERSION-PATCH .
; ;
CHAR . EMIT
\ Welcome banner \ Welcome banner
: BANNER : BANNER
@@ -239,7 +269,7 @@ DECIMAL
." |__,__/_/ .__(_)___/_//_/" CR ." |__,__/_/ .__(_)___/_//_/" CR
." /_/ " CR ." /_/ " CR
CR CR
." Welcome to Wipforth " PRINT-VERSION ." !" CR ." Wipforth " PRINT-VERSION CR
." Copyright (c) Camden Dixie O'Brien" CR ." Copyright (c) Camden Dixie O'Brien" CR
CR CR
." Wipforth is freely available to use, modify and distribute for personal use" CR ." Wipforth is freely available to use, modify and distribute for personal use" CR
@@ -247,8 +277,8 @@ DECIMAL
CR CR
; ;
\ Set SYSREADY high and wait until interactive ." done" CR
1 SYSREADY AC!
SYSINTER WAIT DROP
BANNER BANNER
\ Set SYSREADY high to enable user input
1 SYSREADY AC!

48
prof.js Normal file
View File

@@ -0,0 +1,48 @@
const INTERVAL_MS = 1;
const RS_TOP_ADDR = 0x10000;
const PROF_DATA_ADDR = 0x58;
const PROF_DATA_IDX = PROF_DATA_ADDR / 8;
let mem_8;
let mem_64;
let sampler;
const samples = [];
function sample() {
const data = Atomics.load(mem_64, PROF_DATA_IDX);
const ip = Number(data & 0xffffffffn);
const rsp = Number(data >> 32n);
samples.push({ ip, rs_bytes: mem_8.slice(rsp, RS_TOP_ADDR) });
}
function i32(bytes) {
return bytes[0]
| (bytes[1] << 8)
| (bytes[2] << 16)
| (bytes[3] << 24);
}
function postproc({ ip, rs_bytes }) {
const rs = [];
for (let i = 0; i < rs_bytes.length; i += 4)
rs.push(i32(rs_bytes.slice(i, i + 4)));
rs.reverse();
return { ip, rs };
}
self.onmessage = (e) => {
switch (e.data.type) {
case "start":
console.log("Starting profiler");
mem_8 = new Uint8Array(e.data.mem.buffer);
mem_64 = new BigUint64Array(e.data.mem.buffer);
ip = e.data.ip;
rsp = e.data.rsp;
sampler = setInterval(sample, INTERVAL_MS);
break;
case "stop":
clearInterval(sample);
console.log("Stopped profiler");
self.postMessage(samples.map(postproc));
}
};

View File

@@ -19,7 +19,7 @@
(define client (client-setup)) (define client (client-setup))
(navigate client "http://localhost:8080") (navigate client "http://localhost:8080")
(sleep 1) (sleep 5)
(define-test kernel-assembles-successfully (define-test kernel-assembles-successfully
(let* ((display (get-display client)) (let* ((display (get-display client))

View File

@@ -2,12 +2,15 @@
.import main "emu" "mem" .import main "emu" "mem"
;; Peripheral registers ;; Peripheral registers
.def TXBUF 000h .def TXBUF 00h
.def RXBUF 080h .def RXBUF 20h
.def TXHEAD 100h .def TXHEAD 40h
.def TXTAIL 104h .def TXTAIL 44h
.def RXHEAD 108h .def RXHEAD 48h
.def RXTAIL 10Ch .def RXTAIL 4Ch
;; Mirror of registers for profiler to sample
.def PROF_DATA 58h
.def DICT_START 0200h .def DICT_START 0200h
@@ -635,15 +638,6 @@
end end
call next call next
.func wait
.elem codewords wait WAIT_CODEWORD
call pop
i32.const 0
i64.const -1
memory.atomic.wait32 2 0
call push
call next
;; Core utility words ;; Core utility words
.func exit .func exit
@@ -761,7 +755,7 @@
local.get head local.get head
i32.const 1 i32.const 1
i32.add i32.add
i32.const 7Fh i32.const 1Fh
i32.and i32.and
i32.atomic.store8 0 0 i32.atomic.store8 0 0
@@ -777,7 +771,7 @@
local.tee tail local.tee tail
i32.const 1 i32.const 1
i32.add i32.add
i32.const 7Fh i32.const 1Fh
i32.and i32.and
local.tee n local.tee n
i32.const TXHEAD i32.const TXHEAD
@@ -1235,15 +1229,6 @@ COPY:
.word COPY_CODEWORD .word COPY_CODEWORD
.def PREV _COPY .def PREV _COPY
_WAIT:
.word PREV
.byte 4
.utf8 "WAIT"
.align
WAIT:
.word WAIT_CODEWORD
.def PREV _WAIT
_EXIT: _EXIT:
.word PREV .word PREV
.byte 4 .byte 4
@@ -2091,6 +2076,13 @@ KERNEL_DEFS_END:
.func trampoline .func trampoline
loop iter loop iter
global.get fn call_indirect codeword codewords global.get fn call_indirect codeword codewords
i32.const PROF_DATA
global.get ip i64.extend_i32_u
global.get rsp i64.extend_i32_u
i64.const 32 i64.shl i64.or
i64.atomic.store 3 0
global.get run br_if iter global.get run br_if iter
end end

16
words.js Normal file
View File

@@ -0,0 +1,16 @@
import { Assembler } from "./asm.js";
const asm = new Assembler();
for await (const chunk of Deno.stdin.readable) {
asm.push(chunk);
}
asm.wasm();
const defs = Object.entries(asm.defs);
while (defs[0][0] != '_DUP')
defs.shift();
while (defs.at(-1)[0] != 'WNF_HANDLER')
defs.pop();
const words = Object.fromEntries(defs.filter(([k,v]) => !k.startsWith("_")));
console.log(JSON.stringify(words));