Compare commits
108 Commits
9fb6faca8e
...
profiler
| Author | SHA1 | Date | |
|---|---|---|---|
|
956d42d008
|
|||
|
87d8345017
|
|||
|
d39fe580fc
|
|||
|
ba8c99a123
|
|||
|
cc8ae742f0
|
|||
|
812443d6ee
|
|||
|
67fc1d8d7b
|
|||
|
4000522b3a
|
|||
|
19ef69958d
|
|||
|
0a52388030
|
|||
|
6e8439eeaf
|
|||
|
eaa3242cc0
|
|||
|
f77adffbef
|
|||
|
c91f46be88
|
|||
|
6ee4adfea5
|
|||
|
5dc0a7a601
|
|||
|
896a1ca563
|
|||
|
37d56988ef
|
|||
|
6c643f8402
|
|||
|
7828b0f112
|
|||
|
e7affbf8b7
|
|||
|
02ee4c3c88
|
|||
|
c21b3c79c7
|
|||
|
1318c3cc4e
|
|||
|
74a8f21379
|
|||
|
6784cd02b4
|
|||
|
3a103c46d1
|
|||
|
8d4c53ca92
|
|||
|
5e39024f6d
|
|||
|
b85a4e8bc9
|
|||
|
401e8e1fad
|
|||
|
d4c837216a
|
|||
|
c93e9009da
|
|||
|
0056610238
|
|||
|
9b4ff3e8f6
|
|||
|
e9beacba3a
|
|||
|
acf5b6e284
|
|||
|
72c5f64312
|
|||
|
7135eeba74
|
|||
|
7099ca34a3
|
|||
|
3ebb74c73c
|
|||
|
0dd2a925d8
|
|||
|
2155d17731
|
|||
|
1452ffe615
|
|||
|
46a571be93
|
|||
|
d35b13fed0
|
|||
|
a3cfd405a9
|
|||
|
671e7f60d2
|
|||
|
580d5d2a4a
|
|||
|
1105daaad0
|
|||
|
347dd8f534
|
|||
|
f4433ce3a3
|
|||
|
714973f052
|
|||
|
4f878fdbab
|
|||
|
9fb3910a16
|
|||
|
22dc1fc0ca
|
|||
|
cc51b2d7be
|
|||
|
902404cb10
|
|||
|
d4718f1106
|
|||
|
33f5a4be06
|
|||
|
e2429b2b03
|
|||
|
2972030d0a
|
|||
|
2c3e5f46da
|
|||
|
93f3dd1f41
|
|||
|
cfa4fa7d4f
|
|||
|
94cee7d258
|
|||
|
092d870a9c
|
|||
|
6db71ee382
|
|||
|
5369a0969e
|
|||
|
118e6af896
|
|||
|
1c4b9f850a
|
|||
|
672a453f6c
|
|||
|
5a3084dd16
|
|||
|
77f6d57e1b
|
|||
|
510a74aa04
|
|||
|
75600d0568
|
|||
|
6a4877d52c
|
|||
|
554d918640
|
|||
|
ef0c395d57
|
|||
| f72d79dc19 | |||
| e5f9d2d828 | |||
| fb70a2585f | |||
| fb52e5a701 | |||
| a1b003a1cd | |||
| 9576769e09 | |||
| e13452db15 | |||
| 529aabd213 | |||
| d18ff1d2bb | |||
| e2ed73055f | |||
| 721e0f285b | |||
| c1c1db7b51 | |||
| b5c4070daf | |||
| 59e6027739 | |||
| 32c3c6e356 | |||
| 2a3949e09f | |||
| 22e477adf7 | |||
| c99430c75e | |||
| a77e7f4051 | |||
| 24459a8ae5 | |||
| a0e3706db7 | |||
| 6c30d96e96 | |||
| 2ff0c2ada1 | |||
| e5ee3fe076 | |||
| 8ed8bbad3e | |||
| 7f4d900688 | |||
| 54be0a8c0e | |||
| 832ce55108 | |||
| 259aa730f7 |
@@ -1,39 +1,85 @@
|
||||
# Wipforth
|
||||
|
||||
## Building and Running
|
||||
Wipforth is a Forth implementation that runs in the WebAssembly
|
||||
virtual machine. The system is bootstrapped from source on page load:
|
||||
the only non-text file is the favicon :)
|
||||
|
||||
To run, first compile the WebAssembly module:
|
||||
I/O is done via memory-mapped peripherals, which are emulated in
|
||||
JavaScript.
|
||||
|
||||
- For the Forth kernel, see [wipforth.ws](./wipforth.ws)
|
||||
- For the emulator, see [emu.js](./emu.js)
|
||||
- For the assembler, see [asm.js](./asm.js)
|
||||
- For the prelude (Forth code loaded right after the kernel boots),
|
||||
see [prelude.f](./prelude.f)
|
||||
- For a description of the peripherals, see the
|
||||
[Peripherals](#peripherals) section below.
|
||||
|
||||
## Building and Running Locally
|
||||
|
||||
There's a [Guile](https://www.gnu.org/software/guile/) script in the
|
||||
repo you can use for this:
|
||||
|
||||
```
|
||||
wat2wasm --enable-threads wipforth.wat
|
||||
```
|
||||
|
||||
Then run the server:
|
||||
|
||||
```
|
||||
python3 server.py
|
||||
guile server.scm
|
||||
```
|
||||
|
||||
You should then be able to open <http://localhost:8080> in a browser
|
||||
and use the system from there.
|
||||
|
||||
**NOTE**: The server is just a very simple instantiation of Python's
|
||||
built-in `http.server.HTTPServer`, configured to set the cross-origin
|
||||
headers required for `SharedMemoryBuffer` use. You could use any HTTP
|
||||
server that sets these headers.
|
||||
However, since everything is bootstrapped on the client, basically any
|
||||
HTTP server will do as long as it sets the appropriate response
|
||||
headers for `SharedMemoryBuffer` use:
|
||||
|
||||
- `Cross-Origin-Opener-Policy: same-origin`
|
||||
- `Cross-Origin-Embedder-Policy: require-corp`
|
||||
|
||||
So, if you don't have Guile on your system you can use something else
|
||||
like Python's `http.server`.
|
||||
|
||||
**NOTE**: You should **definitely not** use `server.scm` to serve the
|
||||
application on the open internet or anything like that; I just hacked
|
||||
it together for testing on localhost during development and it's
|
||||
probably hilariously insecure.
|
||||
|
||||
## End-to-End Tests
|
||||
|
||||
There's a (fairly minimal at the moment) end-to-end test suite defined
|
||||
in [tests.scm](./tests.scm). To run it you'll need:
|
||||
|
||||
- [Guile](https://www.gnu.org/software/guile/) again (no substitute
|
||||
this time, sorry)
|
||||
- [guile-json](https://github.com/aconchillo/guile-json)
|
||||
- Firefox
|
||||
|
||||
I'm also pretty sure it won't work on a non-POSIX system, though I
|
||||
haven't tried it.
|
||||
|
||||
Given that's all sorted, you should be able to run:
|
||||
|
||||
```
|
||||
guile tests.scm
|
||||
```
|
||||
|
||||
It will print a JUnit XML report to standard out. You can
|
||||
pretty-print it with `xmllint` if you have it installed:
|
||||
|
||||
```
|
||||
guile tests.scm | xmllint --format -
|
||||
```
|
||||
|
||||
## Peripherals
|
||||
|
||||
# Terminal
|
||||
### Terminal
|
||||
|
||||
| Name | Offset | Size / B | Access |
|
||||
|--------|--------|----------|--------------|
|
||||
| TXBUF | 0 | 32 | write |
|
||||
| RXBUF | 32 | 32 | read |
|
||||
| TXHEAD | 64 | 1 | atomic read |
|
||||
| TXTAIL | 65 | 1 | atomic write |
|
||||
| RXHEAD | 66 | 1 | atomic write |
|
||||
| RXTAIL | 67 | 1 | atomic read |
|
||||
| Name | Address | Size / B | Access |
|
||||
|--------|---------|----------|--------------|
|
||||
| TXBUF | 00h | 32 | write |
|
||||
| RXBUF | 20h | 32 | read |
|
||||
| TXHEAD | 40h | 4 | atomic read |
|
||||
| TXTAIL | 44h | 4 | atomic write |
|
||||
| RXHEAD | 48h | 4 | atomic write |
|
||||
| RXTAIL | 4Ch | 4 | atomic read |
|
||||
|
||||
For both sending (`TX`) and receiving (`RX`), there are three
|
||||
registers: `xBUF`, `xHEAD` and `xTAIL`:
|
||||
@@ -47,11 +93,11 @@ In order to be distinguishable from the empty state, the ring buffers
|
||||
must never be completely full -- there must always be *at least one*
|
||||
unoccupied byte between the tail and the head.
|
||||
|
||||
# System status
|
||||
### System status
|
||||
|
||||
| Name | Offset | Size / B | Access |
|
||||
|----------|--------|----------|--------------|
|
||||
| SYSREADY | 68 | 1 | atomic write |
|
||||
| Name | Address | Size / B | Access |
|
||||
|----------|---------|----------|--------------|
|
||||
| SYSREADY | 50h | 1 | atomic write |
|
||||
|
||||
The `SYSREADY` register is used to indicate when the system has booted
|
||||
up and is ready for user input.
|
||||
|
||||
@@ -1,7 +1,26 @@
|
||||
import { Assembler } from './asm.js';
|
||||
|
||||
const assemble = (async () => {
|
||||
const asm = new Assembler();
|
||||
const resp = await fetch('wipforth.ws');
|
||||
for await (const chunk of resp.body) {
|
||||
asm.push(chunk);
|
||||
}
|
||||
return asm.wasm();
|
||||
})();
|
||||
|
||||
self.onmessage = async (e) => {
|
||||
const exports = { emu: { mem: e.data } };
|
||||
const mod = await WebAssembly.instantiateStreaming(
|
||||
fetch('wipforth.wasm'), exports)
|
||||
mod.instance.exports.reset();
|
||||
switch (e.data.type) {
|
||||
case "load":
|
||||
const exports = { emu: { mem: e.data.mem } };
|
||||
const wasm = await assemble;
|
||||
self.mod = await WebAssembly.instantiate(wasm, exports);
|
||||
await self.postMessage('ready');
|
||||
break;
|
||||
|
||||
case "boot":
|
||||
self.mod.instance.exports.reset();
|
||||
console.log('System halt');
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
boot.js
|
||||
emu.js
|
||||
index.html
|
||||
prelude.f
|
||||
styles.css
|
||||
wipforth.ws
|
||||
@@ -0,0 +1,139 @@
|
||||
(define-module (e2e marionette)
|
||||
#:export (with-marionette
|
||||
start-firefox
|
||||
close-firefox
|
||||
client-setup
|
||||
client-teardown
|
||||
navigate
|
||||
input-line
|
||||
get-display))
|
||||
|
||||
(use-modules
|
||||
(json)
|
||||
(scheme base)
|
||||
(srfi srfi-9))
|
||||
|
||||
(define-syntax-rule (with-marionette client body ...)
|
||||
(let ((client #f) (pid #f))
|
||||
(dynamic-wind
|
||||
(lambda ()
|
||||
(set! pid (start-firefox))
|
||||
(set! client (client-setup)))
|
||||
(lambda () body ...)
|
||||
(lambda ()
|
||||
(when client (client-teardown client))
|
||||
(when pid (close-firefox pid))))))
|
||||
|
||||
(define (start-firefox)
|
||||
(let* ((profile-dir (mkdtemp "/tmp/marionette-XXXXXX"))
|
||||
(i/o (pipe))
|
||||
(pid (spawn "firefox"
|
||||
`("firefox" "--marionette" "--headless"
|
||||
"--profile" ,profile-dir "about:blank")
|
||||
#:output (cdr i/o)
|
||||
#:error (cdr i/o))))
|
||||
(let loop ((line (read-line (car i/o))))
|
||||
(cond
|
||||
((string-contains line "Listening on port") #nil)
|
||||
((eof-object? line) (format #t "EOF when starting firefox~%"))
|
||||
(#t (loop (read-line (car i/o))))))
|
||||
(close-port (car i/o))
|
||||
(close-port (cdr i/o))
|
||||
pid))
|
||||
|
||||
(define (close-firefox pid)
|
||||
(kill pid SIGINT)
|
||||
(waitpid pid)
|
||||
*unspecified*)
|
||||
|
||||
(define-record-type client
|
||||
(make-client socket msg-num)
|
||||
client?
|
||||
(socket client-socket)
|
||||
(msg-num client-msg-num client-msg-num-set!))
|
||||
|
||||
(define (client-msg-num-1+! client)
|
||||
(client-msg-num-set! client (1+ (client-msg-num client))))
|
||||
|
||||
(define (open-socket host port)
|
||||
(let ((sock (socket PF_INET SOCK_STREAM 0))
|
||||
(addr (inet-pton AF_INET host)))
|
||||
(connect sock AF_INET addr port)
|
||||
sock))
|
||||
|
||||
(define (handle-start-msg client)
|
||||
(let* ((msg (read-msg (client-socket client)))
|
||||
(protocol (assoc-ref msg "marionetteProtocol")))
|
||||
(unless (= 3 protocol)
|
||||
(format #t "Unsupported protocol: ~a~%" protocol))))
|
||||
|
||||
(define* (client-setup #:key (host "127.0.0.1") (port 2828))
|
||||
(let ((client (make-client (open-socket host port) 0)))
|
||||
(handle-start-msg client)
|
||||
(create-session client)
|
||||
client))
|
||||
|
||||
(define (client-teardown client)
|
||||
(close (client-socket client)))
|
||||
|
||||
(define (add-digit val digit)
|
||||
(+ (* val 10) (- digit 48)))
|
||||
|
||||
(define (read-msg-len sock)
|
||||
(let loop ((len 0) (byte (read-u8 sock)))
|
||||
(if (eq? byte 58)
|
||||
len
|
||||
(loop (add-digit len byte)
|
||||
(read-u8 sock)))))
|
||||
|
||||
(define (read-msg sock)
|
||||
(let ((len (read-msg-len sock)))
|
||||
(json-string->scm (utf8->string (read-bytevector len sock)))))
|
||||
|
||||
(define (send-msg sock msg)
|
||||
(let* ((payload (string->utf8 (scm->json-string msg)))
|
||||
(len (bytevector-length payload)))
|
||||
(write-bytevector (string->utf8 (number->string len)) sock)
|
||||
(write-u8 58 sock)
|
||||
(write-bytevector payload sock)))
|
||||
|
||||
(define (send-command client command params)
|
||||
(send-msg (client-socket client)
|
||||
(vector 0 (client-msg-num client) command params))
|
||||
(client-msg-num-1+! client)
|
||||
(let* ((response (read-msg (client-socket client)))
|
||||
(error (vector-ref response 2))
|
||||
(result (vector-ref response 3)))
|
||||
(unless (eq? 'null error)
|
||||
(format #t "Error: ~a~%" (assoc-ref error "message")))
|
||||
(assoc-ref result "value")))
|
||||
|
||||
(define (create-session client)
|
||||
(send-command client "WebDriver:NewSession" '())
|
||||
*unspecified*)
|
||||
|
||||
(define (navigate client url)
|
||||
(send-command client "WebDriver:Navigate" `(("url" . ,url)))
|
||||
*unspecified*)
|
||||
|
||||
(define (find-element client selector)
|
||||
(assoc-ref
|
||||
(send-command client "WebDriver:FindElement"
|
||||
`(("using" . "css selector")
|
||||
("value" . ,selector)))
|
||||
"element-6066-11e4-a52e-4f735466cecf"))
|
||||
|
||||
(define (send-keys client text)
|
||||
(let ((id (find-element client "body")))
|
||||
(send-command client "WebDriver:ElementSendKeys"
|
||||
`(("id" . ,id) ("text" . ,text)))))
|
||||
|
||||
(define (get-element-text client selector)
|
||||
(let ((id (find-element client selector)))
|
||||
(send-command client "WebDriver:GetElementText" `(("id" . ,id)))))
|
||||
|
||||
(define (input-line client text)
|
||||
(send-keys client (string-append text "\ue007")))
|
||||
|
||||
(define (get-display client)
|
||||
(get-element-text client "#output"))
|
||||
@@ -0,0 +1,71 @@
|
||||
(define-module (e2e testing)
|
||||
#:export (assert define-test run-tests run-tests-with-cleanup))
|
||||
|
||||
(use-modules
|
||||
(sxml simple)
|
||||
(srfi srfi-1))
|
||||
|
||||
(define *tests* (make-fluid '()))
|
||||
|
||||
(define (assert condition . args)
|
||||
(unless condition
|
||||
(let ((msg (if (null? args) "Assertion failed" (car args))))
|
||||
(raise-exception `(assertion-failed . ,msg)))))
|
||||
|
||||
(define-syntax-rule (define-test name body ...)
|
||||
(fluid-set!
|
||||
*tests*
|
||||
(append (fluid-ref *tests*)
|
||||
(list (cons 'name (lambda () body ...))))))
|
||||
|
||||
(define (fail-handler ex)
|
||||
(if (and (pair? ex) (eq? 'assertion-failed (car ex)))
|
||||
(cons 'fail (cdr ex))
|
||||
(cons 'error (format #f "~a" ex))))
|
||||
|
||||
(define (run-test test)
|
||||
(cons (car test)
|
||||
(with-exception-handler fail-handler
|
||||
(lambda ()
|
||||
((cdr test))
|
||||
'pass)
|
||||
#:unwind? #t)))
|
||||
|
||||
(define (fail? result)
|
||||
(and (pair? (cdr result))
|
||||
(eq? 'fail (cadr result))))
|
||||
|
||||
(define (error? result)
|
||||
(and (pair? (cdr result))
|
||||
(eq? 'error (cadr result))))
|
||||
|
||||
(define (test-junit-report result)
|
||||
`(testcase
|
||||
(@ (name ,(symbol->string (car result))))
|
||||
,@(cond
|
||||
((fail? result) `((failure (@ (message ,(cddr result))))))
|
||||
((error? result) `((error (@ (message ,(cddr result))))))
|
||||
(#t '()))))
|
||||
|
||||
(define (junit-report results fails errors)
|
||||
(let ((count (length results)))
|
||||
`(testsuites
|
||||
(testsuite
|
||||
(@ (name "wipforth e2e")
|
||||
(tests ,count)
|
||||
(failures ,fails)
|
||||
(errors ,errors))
|
||||
,@(map test-junit-report results)))))
|
||||
|
||||
(define (run-tests)
|
||||
(let* ((results (map run-test (fluid-ref *tests*)))
|
||||
(fails (length (filter fail? results)))
|
||||
(errors (length (filter error? results))))
|
||||
(sxml->xml (junit-report results fails errors))
|
||||
(exit (if (= (+ fails errors) 0) 0 1))))
|
||||
|
||||
(define-syntax-rule (run-tests-with-cleanup body ...)
|
||||
(dynamic-wind
|
||||
(lambda () #f)
|
||||
run-tests
|
||||
(lambda () body ...)))
|
||||
@@ -1,19 +1,19 @@
|
||||
const TXBUF = 0;
|
||||
const RXBUF = 32;
|
||||
const TXHEAD = 64;
|
||||
const TXTAIL = 65;
|
||||
const RXHEAD = 66;
|
||||
const RXTAIL = 67;
|
||||
const SYSREADY = 68;
|
||||
const TXBUF = 0x00;
|
||||
const RXBUF = 0x20;
|
||||
const TXHEAD = 0x40;
|
||||
const TXTAIL = 0x44;
|
||||
const RXHEAD = 0x48;
|
||||
const RXTAIL = 0x4c;
|
||||
const SYSREADY = 0x50;
|
||||
|
||||
const TXBUF_SIZE = 32;
|
||||
const RXBUF_SIZE = 32;
|
||||
const PERIPHS_SIZE = 69; // Nice
|
||||
const PERIPHS_SIZE = 81;
|
||||
|
||||
const POLL_INTERVAL_MS = 20;
|
||||
const DOT_INTERVAL_MS = 120;
|
||||
|
||||
const COLS = 80;
|
||||
const ROWS = 36;
|
||||
const TAB_WIDTH = 8;
|
||||
|
||||
const CURSOR_IDLE_TIME_MS = 1000;
|
||||
@@ -28,6 +28,7 @@ class Emulator {
|
||||
this.mem_u8 = new Uint8Array(this.mem.buffer);
|
||||
for (let i = 0; i < PERIPHS_SIZE; ++i)
|
||||
this.mem_u8[i] = 0;
|
||||
this.mem_i32 = new Int32Array(this.mem.buffer);
|
||||
|
||||
this.decoder = new TextDecoder('utf-8');
|
||||
this.encoder = new TextEncoder('utf-8');
|
||||
@@ -36,8 +37,9 @@ class Emulator {
|
||||
this.rx_queue = [];
|
||||
this.timer = setInterval(() => this.poll(), POLL_INTERVAL_MS);
|
||||
|
||||
this.rows = this.max_rows();
|
||||
this.grid = Array.from(
|
||||
{ length: ROWS },
|
||||
{ length: this.rows },
|
||||
() => new Array(COLS).fill(' '));
|
||||
this.cursor = { x: 0, y: 0 };
|
||||
this.range = {
|
||||
@@ -47,9 +49,31 @@ class Emulator {
|
||||
this.idle_timer = null;
|
||||
this.input_enable = false;
|
||||
document.addEventListener('keydown', (e) => this.handle_keydown(e));
|
||||
window.addEventListener('resize', () => this.handle_resize());
|
||||
|
||||
this.worker = new Worker('boot.js');
|
||||
this.worker.postMessage(this.mem);
|
||||
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 ");
|
||||
const dots = setInterval(() => this.print("."), DOT_INTERVAL_MS);
|
||||
this.worker = new Worker('boot.js', { type: 'module' });
|
||||
this.worker.postMessage({ type: "load", mem: this.mem });
|
||||
this.worker.onmessage = (e) => {
|
||||
clearInterval(dots);
|
||||
this.print(" done\n");
|
||||
this.worker.postMessage({ type: "boot" });
|
||||
this.prof.postMessage({ type: "start", mem: this.mem });
|
||||
};
|
||||
|
||||
fetch('prelude.f')
|
||||
.then(res => res.text())
|
||||
@@ -75,6 +99,7 @@ class Emulator {
|
||||
if (!this.input_enable) {
|
||||
const sysready = Atomics.load(this.mem_u8, SYSREADY);
|
||||
if (sysready != 0) {
|
||||
this.prof.postMessage({ type: "stop" });
|
||||
this.input_enable = true;
|
||||
this.flush_output();
|
||||
document.getElementById('cursor').classList.add('blinking');
|
||||
@@ -102,6 +127,7 @@ class Emulator {
|
||||
tail = this.fifo_next(tail);
|
||||
} while (this.fifo_next(tail) != head && this.rx_queue.length != 0);
|
||||
Atomics.store(this.mem_u8, RXTAIL, tail);
|
||||
Atomics.notify(this.mem_i32, RXTAIL / 4);
|
||||
}
|
||||
|
||||
print(str) {
|
||||
@@ -142,6 +168,8 @@ class Emulator {
|
||||
this.cursor_move(1, 0);
|
||||
} else if (e.key == 'Enter') {
|
||||
this.cursor.y = this.range.end.y + 1;
|
||||
while (this.cursor.y >= this.rows)
|
||||
this.scroll();
|
||||
this.cursor.x = 0;
|
||||
this.submit_line();
|
||||
Object.assign(this.range.start, this.cursor);
|
||||
@@ -226,7 +254,7 @@ class Emulator {
|
||||
next_cell(cell) {
|
||||
if (cell.x < COLS - 1)
|
||||
return { x: cell.x + 1, y: cell.y };
|
||||
else if (cell.y < ROWS - 1)
|
||||
else if (cell.y < this.rows - 1)
|
||||
return { x: 0, y: cell.y + 1 };
|
||||
else
|
||||
return null;
|
||||
@@ -239,6 +267,8 @@ class Emulator {
|
||||
this.grid[cell.y][cell.x] = this.grid[prev.y][prev.x];
|
||||
cell = prev;
|
||||
}
|
||||
if (this.next_cell(this.range.end) == null)
|
||||
this.scroll();
|
||||
this.range.end = this.next_cell(this.range.end);
|
||||
}
|
||||
|
||||
@@ -262,7 +292,7 @@ class Emulator {
|
||||
return '<span id="cursor">' + ec + '</span>';
|
||||
else
|
||||
return ec;
|
||||
}).join('');
|
||||
}).join('').trimEnd();
|
||||
}).join('\n');
|
||||
this.output.innerHTML = html;
|
||||
}
|
||||
@@ -283,6 +313,38 @@ class Emulator {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
scroll() {
|
||||
this.grid.shift()
|
||||
this.grid.push(new Array(COLS).fill(' '));
|
||||
this.cursor.y -= 1;
|
||||
this.range.start.y -= 1;
|
||||
this.range.end.y -= 1;
|
||||
}
|
||||
|
||||
max_rows() {
|
||||
const style = getComputedStyle(this.output);
|
||||
const line_height = parseFloat(style.lineHeight);
|
||||
const margin_top = parseFloat(style.marginTop);
|
||||
const margin_bottom = parseFloat(style.marginBottom);
|
||||
|
||||
const viewport_height = window.innerHeight;
|
||||
const output_height = viewport_height - margin_top - margin_bottom;
|
||||
return Math.floor(output_height / line_height) - 1;
|
||||
}
|
||||
|
||||
handle_resize() {
|
||||
this.rows = this.max_rows();
|
||||
while (this.grid.length < this.rows)
|
||||
this.grid.push(new Array(COLS).fill(' '));
|
||||
while (this.grid.length > this.rows) {
|
||||
this.grid.shift()
|
||||
this.cursor.y -= 1;
|
||||
this.range.start.y -= 1;
|
||||
this.range.end.y -= 1;
|
||||
}
|
||||
this.flush_output();
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 516 B |
@@ -3,6 +3,7 @@
|
||||
<head>
|
||||
<title>Wipforth</title>
|
||||
<link rel="stylesheet" type="text/css" href="styles.css"/>
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
</head>
|
||||
<body>
|
||||
<script type="text/javascript" src="emu.js"></script>
|
||||
|
||||
@@ -63,18 +63,6 @@
|
||||
|
||||
46 EMIT
|
||||
|
||||
\ Peripheral register addresses
|
||||
|
||||
: TXBUF 0 ;
|
||||
: RXBUF 32 ;
|
||||
: TXHEAD 64 ;
|
||||
: TXTAIL 65 ;
|
||||
: RXHEAD 66 ;
|
||||
: RXTAIL 67 ;
|
||||
: SYSREADY 68 ;
|
||||
|
||||
46 EMIT
|
||||
|
||||
\ Printing utilities
|
||||
|
||||
: CR 10 EMIT ;
|
||||
@@ -88,7 +76,7 @@
|
||||
; IMMEDIATE
|
||||
|
||||
: EMIT-DIGIT
|
||||
DUP 10 < IF CHAR 0 ELSE CHAR A THEN
|
||||
DUP 10 < IF CHAR 0 ELSE 10 - CHAR A THEN
|
||||
+ EMIT
|
||||
;
|
||||
|
||||
@@ -112,7 +100,11 @@ CHAR . EMIT
|
||||
\ Print any trailing zeros
|
||||
BASE @ /
|
||||
DUP 0<> IF
|
||||
BEGIN CHAR 0 EMIT BASE @ / 0= UNTIL
|
||||
BEGIN
|
||||
CHAR 0 EMIT
|
||||
BASE @ /
|
||||
DUP 0=
|
||||
UNTIL
|
||||
THEN
|
||||
|
||||
2DROP
|
||||
@@ -165,6 +157,10 @@ CHAR . EMIT
|
||||
|
||||
: NIP SWAP DROP ;
|
||||
|
||||
: HEX 16 BASE ! ;
|
||||
|
||||
: DECIMAL 10 BASE ! ;
|
||||
|
||||
: '
|
||||
WORD FIND DUP 0<> IF
|
||||
>CFA
|
||||
@@ -210,6 +206,22 @@ CHAR . EMIT
|
||||
|
||||
CHAR . EMIT
|
||||
|
||||
\ Peripheral register addresses
|
||||
|
||||
HEX
|
||||
|
||||
00 CONSTANT TXBUF
|
||||
20 CONSTANT RXBUF
|
||||
40 CONSTANT TXHEAD
|
||||
44 CONSTANT TXTAIL
|
||||
48 CONSTANT RXHEAD
|
||||
4C CONSTANT RXTAIL
|
||||
50 CONSTANT SYSREADY
|
||||
|
||||
DECIMAL
|
||||
|
||||
46 EMIT
|
||||
|
||||
\ A better word-not-found handler
|
||||
|
||||
: ANY-RX? RXHEAD AC@ RXTAIL AC@ <> ;
|
||||
@@ -236,14 +248,14 @@ CHAR . EMIT
|
||||
|
||||
\ Version number
|
||||
|
||||
: VERSION-MAJOR 0 ;
|
||||
: VERSION-MINOR 1 ;
|
||||
: VERSION-PATCH 0 ;
|
||||
0 CONSTANT VERSION-MAJOR
|
||||
2 CONSTANT VERSION-MINOR
|
||||
1 CONSTANT VERSION-PATCH
|
||||
|
||||
: VERSION-PRINT
|
||||
." v" VERSION-MAJOR .
|
||||
." ." VERSION-MINOR .
|
||||
." ." VERSION-PATCH .
|
||||
: PRINT-VERSION
|
||||
CHAR v EMIT VERSION-MAJOR .
|
||||
CHAR . EMIT VERSION-MINOR .
|
||||
CHAR . EMIT VERSION-PATCH .
|
||||
;
|
||||
|
||||
CHAR . EMIT
|
||||
@@ -257,7 +269,11 @@ CHAR . EMIT
|
||||
." |__,__/_/ .__(_)___/_//_/" CR
|
||||
." /_/ " CR
|
||||
CR
|
||||
." Wipforth " VERSION-PRINT CR
|
||||
." Wipforth " PRINT-VERSION CR
|
||||
." Copyright (c) Camden Dixie O'Brien" CR
|
||||
CR
|
||||
." Wipforth is freely available to use, modify and distribute for personal use" CR
|
||||
." under the Komorebi license, version 2.0.0." CR
|
||||
CR
|
||||
;
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||
import sys
|
||||
|
||||
class CORSRequestHandler(SimpleHTTPRequestHandler):
|
||||
def end_headers(self):
|
||||
self.send_header(
|
||||
'Cross-Origin-Opener-Policy', 'same-origin')
|
||||
self.send_header(
|
||||
'Cross-Origin-Embedder-Policy', 'require-corp')
|
||||
self.send_header(
|
||||
'Cache-Control', 'no-store, no-cache, must-revalidate')
|
||||
super().end_headers()
|
||||
|
||||
if __name__ == '__main__':
|
||||
server = HTTPServer(('localhost', 8080), CORSRequestHandler)
|
||||
server.serve_forever()
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
(define-module (server)
|
||||
#:export (run start stop main))
|
||||
|
||||
(use-modules
|
||||
(ice-9 atomic)
|
||||
(ice-9 binary-ports)
|
||||
(ice-9 threads)
|
||||
(srfi srfi-9)
|
||||
(web client)
|
||||
(web server)
|
||||
(web request)
|
||||
(web response)
|
||||
(web uri))
|
||||
|
||||
(define mime-types
|
||||
'(("html" . (text/html))
|
||||
("css" . (text/css))
|
||||
("js" . (application/javascript))
|
||||
("f" . (text/plain))
|
||||
("ws" . (text/plain))
|
||||
("png" . (image/png))))
|
||||
|
||||
(define (mime-type path)
|
||||
(let* ((dot (string-rindex path #\.))
|
||||
(ext (when dot (substring path (1+ dot)))))
|
||||
(or (assoc-ref mime-types ext) "application/octet-stream")))
|
||||
|
||||
(define headers
|
||||
'((cross-origin-opener-policy . "same-origin")
|
||||
(cross-origin-embedder-policy . "require-corp")
|
||||
(cache-control . (no-store no-cache must-revalidate))))
|
||||
|
||||
(define (is-dir? path)
|
||||
(eq? 'directory (stat:type (stat path))))
|
||||
|
||||
(define (request-path request)
|
||||
(let ((path (string-append "." (uri-path (request-uri request)))))
|
||||
(cond ((not (file-exists? path)) #nil)
|
||||
((is-dir? path) (string-append path "index.html"))
|
||||
(#t path))))
|
||||
|
||||
(define (file-handler request body)
|
||||
(let ((path (request-path request)))
|
||||
(if path
|
||||
(values (cons `(content-type . ,(mime-type path)) headers)
|
||||
(get-bytevector-all (open-input-file path)))
|
||||
(values (build-response #:code 404) #f))))
|
||||
|
||||
(define* (run #:key (port 8080))
|
||||
(run-server file-handler 'http `(#:port ,port)))
|
||||
|
||||
(define-record-type <handle>
|
||||
(make-handle close-flag thread port)
|
||||
handle?
|
||||
(close-flag handle-close-flag)
|
||||
(thread handle-thread)
|
||||
(port handle-port))
|
||||
|
||||
(define (serve impl server should-close)
|
||||
(let loop ()
|
||||
(unless (atomic-box-ref should-close)
|
||||
(serve-one-client file-handler impl server '())
|
||||
(loop)))
|
||||
(close-server impl server))
|
||||
|
||||
(define* (start #:key (port 8080))
|
||||
(let* ((impl (lookup-server-impl 'http))
|
||||
(server (open-server impl `(#:port ,port)))
|
||||
(should-close (make-atomic-box #f))
|
||||
(thread
|
||||
(call-with-new-thread
|
||||
(lambda () (serve impl server should-close)))))
|
||||
(make-handle should-close thread port)))
|
||||
|
||||
(define (stop handle)
|
||||
(atomic-box-set! (handle-close-flag handle) #t)
|
||||
(http-request (format #f "http://localhost:~a/" (handle-port handle)))
|
||||
(join-thread (handle-thread handle)))
|
||||
|
||||
(when (string=? (basename (current-filename)) (car (command-line)))
|
||||
(run))
|
||||
@@ -14,6 +14,7 @@ body {
|
||||
}
|
||||
|
||||
#output {
|
||||
width: 80ch;
|
||||
margin: 1.5em auto;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
(add-to-load-path (dirname (current-filename)))
|
||||
|
||||
(use-modules
|
||||
(e2e marionette)
|
||||
(e2e testing)
|
||||
(ice-9 regex)
|
||||
((server) #:prefix server-)
|
||||
(srfi srfi-1))
|
||||
|
||||
(define (lines s)
|
||||
(string-split s #\newline))
|
||||
|
||||
(define (get-result-line client)
|
||||
(let ((display (get-display client)))
|
||||
(last (lines (string-trim-right display)))))
|
||||
|
||||
(define server (server-start))
|
||||
(define firefox (start-firefox))
|
||||
(define client (client-setup))
|
||||
|
||||
(navigate client "http://localhost:8080")
|
||||
(sleep 5)
|
||||
|
||||
(define-test kernel-assembles-successfully
|
||||
(let* ((display (get-display client))
|
||||
(line (first (lines display))))
|
||||
(assert (string-match "Assembling kernel \\.+ done" line)
|
||||
(format #f "Kernel assemble line: ~s" line))))
|
||||
|
||||
(define-test prelude-loads-successfully
|
||||
(let* ((display (get-display client))
|
||||
(line (second (lines display))))
|
||||
(assert (string-match "Loading prelude \\.+ done" line)
|
||||
(format #f "Prelude load line: ~s" line))))
|
||||
|
||||
(define-test six-seven-times-dot-cr-yields-42
|
||||
(input-line client "6 7 * . CR")
|
||||
(let* ((result-line (get-result-line client)))
|
||||
(assert (string=? "42" result-line)
|
||||
(format #f "Result line: ~s" result-line))))
|
||||
|
||||
(define-test define-hello-then-run-hello-prints-message
|
||||
(input-line client ": HELLO .\" Hello, world!\" CR ;")
|
||||
(input-line client "HELLO")
|
||||
(let* ((result-line (get-result-line client)))
|
||||
(assert (string=? "Hello, world!" result-line)
|
||||
(format #f "Result line: ~s" result-line))))
|
||||
|
||||
(run-tests-with-cleanup
|
||||
(client-teardown client)
|
||||
(close-firefox firefox)
|
||||
(server-stop server))
|
||||
-2009
File diff suppressed because it is too large
Load Diff
+2098
File diff suppressed because it is too large
Load Diff
@@ -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));
|
||||
Reference in New Issue
Block a user