Compare commits
7 Commits
main
..
8929dc868f
| Author | SHA1 | Date | |
|---|---|---|---|
| 8929dc868f | |||
| 2480446ef5 | |||
| 53171df913 | |||
| 67b67c2505 | |||
| c1a3e86222 | |||
| e8138414c0 | |||
| 289443192f |
@@ -1,107 +1,68 @@
|
|||||||
# Wipforth
|
# Wipforth
|
||||||
|
|
||||||
Wipforth is a Forth implementation that runs in the WebAssembly
|
Wipforth is a simple Forth implementation that runs in the WebAssembly
|
||||||
virtual machine. The system is bootstrapped from source on page load:
|
virtual machine. It does I/O via memory-mapped peripherals, which are
|
||||||
the only non-text file is the favicon :)
|
emulated in JavaScript.
|
||||||
|
|
||||||
I/O is done via memory-mapped peripherals, which are emulated in
|
- For the Forth kernel, see [wipforth.wat](./wipforth.wat)
|
||||||
JavaScript.
|
- For the JavaScript emulator, see [emu.js](./emu.js)
|
||||||
|
- For the Forth prelude, which is loaded at start-up, see
|
||||||
- For the Forth kernel, see [wipforth.ws](./wipforth.ws)
|
[prelude.f](./prelude.f)
|
||||||
- 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
|
- For a description of the peripherals, see the
|
||||||
[Peripherals](#peripherals) section below.
|
[Peripherals](#peripherals) section below.
|
||||||
|
|
||||||
## Building and Running Locally
|
## Building and Running
|
||||||
|
|
||||||
There's a [Guile](https://www.gnu.org/software/guile/) script in the
|
To run, first compile the WebAssembly module:
|
||||||
repo you can use for this:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
guile server.scm
|
wat2wasm --enable-threads wipforth.wat
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run the server:
|
||||||
|
|
||||||
|
```
|
||||||
|
python3 server.py
|
||||||
```
|
```
|
||||||
|
|
||||||
You should then be able to open <http://localhost:8080> in a browser
|
You should then be able to open <http://localhost:8080> in a browser
|
||||||
and use the system from there.
|
and use the system from there.
|
||||||
|
|
||||||
However, since everything is bootstrapped on the client, basically any
|
**NOTE**: The server is just a very simple instantiation of Python's
|
||||||
HTTP server will do as long as it sets the appropriate response
|
built-in `http.server.HTTPServer`, configured to set the cross-origin
|
||||||
headers for `SharedArrayBuffer` use:
|
headers required for `SharedMemoryBuffer` use. You could use any HTTP
|
||||||
|
server that sets these headers.
|
||||||
- `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
|
## Peripherals
|
||||||
|
|
||||||
### Terminal
|
# Terminal
|
||||||
|
|
||||||
| Name | Address | Size / B | Access |
|
| Name | Offset | Size / B | Access |
|
||||||
|--------|---------|----------|--------------|
|
|--------|--------|----------|--------------|
|
||||||
| TXBUF | 000h | 32 | write |
|
| TXBUF | 0 | 32 | write |
|
||||||
| RXBUF | 080h | 32 | read |
|
| RXBUF | 32 | 32 | read |
|
||||||
| TXHEAD | 100h | 4 | atomic read |
|
| TXHEAD | 64 | 1 | atomic read |
|
||||||
| TXTAIL | 104h | 4 | atomic write |
|
| TXTAIL | 65 | 1 | atomic write |
|
||||||
| RXHEAD | 108h | 4 | atomic write |
|
| RXHEAD | 66 | 1 | atomic write |
|
||||||
| RXTAIL | 10Ch | 4 | atomic read |
|
| RXTAIL | 67 | 1 | 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*
|
||||||
unoccupied byte between the tail and the head.
|
unoccupied byte between the tail and the head.
|
||||||
|
|
||||||
### System status
|
# System status
|
||||||
|
|
||||||
| Name | Address | Size / B | Access |
|
| Name | Offset | Size / B | Access |
|
||||||
|----------|---------|----------|--------------|
|
|----------|--------|----------|--------------|
|
||||||
| SYSREADY | 110h | 4 | atomic write |
|
| SYSREADY | 68 | 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.
|
|
||||||
|
|||||||
@@ -1,26 +1,7 @@
|
|||||||
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) => {
|
self.onmessage = async (e) => {
|
||||||
switch (e.data.type) {
|
const exports = { emu: { mem: e.data } };
|
||||||
case "load":
|
const mod = await WebAssembly.instantiateStreaming(
|
||||||
const exports = { emu: { mem: e.data.mem } };
|
fetch('wipforth.wasm'), exports)
|
||||||
const wasm = await assemble;
|
mod.instance.exports.reset();
|
||||||
self.mod = await WebAssembly.instantiate(wasm, exports);
|
console.log('System halt');
|
||||||
await self.postMessage('ready');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "boot":
|
|
||||||
self.mod.instance.exports.reset();
|
|
||||||
console.log('System halt');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
asm.js
|
|
||||||
boot.js
|
|
||||||
emu.js
|
|
||||||
favicon.png
|
|
||||||
index.html
|
|
||||||
prelude.f
|
|
||||||
styles.css
|
|
||||||
wipforth.ws
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
(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"))
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
(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,17 +1,16 @@
|
|||||||
const TXBUF = 0x000;
|
const TXBUF = 0;
|
||||||
const RXBUF = 0x080;
|
const RXBUF = 32;
|
||||||
const TXHEAD = 0x100;
|
const TXHEAD = 64;
|
||||||
const TXTAIL = 0x104;
|
const TXTAIL = 65;
|
||||||
const RXHEAD = 0x108;
|
const RXHEAD = 66;
|
||||||
const RXTAIL = 0x10c;
|
const RXTAIL = 67;
|
||||||
|
const SYSREADY = 68;
|
||||||
|
|
||||||
const SYSREADY = 0x110;
|
const TXBUF_SIZE = 32;
|
||||||
const SYSINTER = 0x114;
|
const RXBUF_SIZE = 32;
|
||||||
|
const PERIPHS_SIZE = 69; // Nice
|
||||||
|
|
||||||
const PERIPHS_SIZE = 0x200;
|
const POLL_INTERVAL_MS = 20;
|
||||||
|
|
||||||
const POLL_INTERVAL_MS = 5;
|
|
||||||
const DOT_INTERVAL_MS = 25;
|
|
||||||
|
|
||||||
const COLS = 80;
|
const COLS = 80;
|
||||||
const TAB_WIDTH = 8;
|
const TAB_WIDTH = 8;
|
||||||
@@ -28,7 +27,6 @@ class Emulator {
|
|||||||
this.mem_u8 = new Uint8Array(this.mem.buffer);
|
this.mem_u8 = new Uint8Array(this.mem.buffer);
|
||||||
for (let i = 0; i < PERIPHS_SIZE; ++i)
|
for (let i = 0; i < PERIPHS_SIZE; ++i)
|
||||||
this.mem_u8[i] = 0;
|
this.mem_u8[i] = 0;
|
||||||
this.mem_i32 = new Int32Array(this.mem.buffer);
|
|
||||||
|
|
||||||
this.decoder = new TextDecoder('utf-8');
|
this.decoder = new TextDecoder('utf-8');
|
||||||
this.encoder = new TextEncoder('utf-8');
|
this.encoder = new TextEncoder('utf-8');
|
||||||
@@ -51,19 +49,8 @@ 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.worker = new Worker('boot.js');
|
||||||
|
this.worker.postMessage(this.mem);
|
||||||
this.print("Assembling kernel ");
|
|
||||||
this.dots = setInterval(() => this.print("."), DOT_INTERVAL_MS);
|
|
||||||
this.forth.postMessage({ type: "load", mem: this.mem });
|
|
||||||
this.forth.onmessage = (e) => {
|
|
||||||
clearInterval(this.dots);
|
|
||||||
this.print(" done\n");
|
|
||||||
|
|
||||||
this.print("Loading prelude ");
|
|
||||||
this.forth.postMessage({ type: "boot" });
|
|
||||||
this.dots = setInterval(() => this.print("."), DOT_INTERVAL_MS);
|
|
||||||
};
|
|
||||||
|
|
||||||
fetch('prelude.f')
|
fetch('prelude.f')
|
||||||
.then(res => res.text())
|
.then(res => res.text())
|
||||||
@@ -89,21 +76,15 @@ 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.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) {
|
||||||
@@ -122,7 +103,6 @@ class Emulator {
|
|||||||
tail = this.fifo_next(tail);
|
tail = this.fifo_next(tail);
|
||||||
} while (this.fifo_next(tail) != head && this.rx_queue.length != 0);
|
} while (this.fifo_next(tail) != head && this.rx_queue.length != 0);
|
||||||
Atomics.store(this.mem_u8, RXTAIL, tail);
|
Atomics.store(this.mem_u8, RXTAIL, tail);
|
||||||
Atomics.notify(this.mem_i32, RXTAIL / 4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
print(str) {
|
print(str) {
|
||||||
@@ -191,14 +171,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 +263,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;
|
||||||
@@ -347,16 +323,6 @@ class Emulator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
const output = document.getElementById('output');
|
document.getElementById('output').innerText = '';
|
||||||
output.innerText = '';
|
window.emu = new Emulator();
|
||||||
|
|
||||||
if (!self.crossOriginIsolated) {
|
|
||||||
output.innerText = "Yeah so there's this thing where Chromium ends "
|
|
||||||
+ "up ignoring COOP/COEP\nheaders after a hard reload "
|
|
||||||
+ "sometimes, and I haven't been able to\nfigure out how to "
|
|
||||||
+ "work around it yet. If you just wait a little while\nand "
|
|
||||||
+ "then reload normally then hopefully it should work haha";
|
|
||||||
} else {
|
|
||||||
window.emu = new Emulator();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 516 B After Width: | Height: | Size: 499 B |
+3
-2
@@ -7,7 +7,8 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script type="text/javascript" src="emu.js"></script>
|
<script type="text/javascript" src="emu.js"></script>
|
||||||
<div id="output"><noscript>I'm afraid you need javascript enabled for this to work :(
|
<div id="output">I'm afraid you need javascript enabled for this to work :(
|
||||||
|
|
||||||
cringe, I know... but I promise there are no frameworks™</noscript></div></body>
|
cringe, I know... but I promise there are no frameworks™</div>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -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,20 @@
|
|||||||
|
|
||||||
( ( Take that, C ) )
|
( ( Take that, C ) )
|
||||||
|
|
||||||
|
46 EMIT
|
||||||
|
|
||||||
|
\ Peripheral register addresses
|
||||||
|
|
||||||
|
: TXBUF 0 ;
|
||||||
|
: RXBUF 32 ;
|
||||||
|
: TXHEAD 64 ;
|
||||||
|
: TXTAIL 65 ;
|
||||||
|
: RXHEAD 66 ;
|
||||||
|
: RXTAIL 67 ;
|
||||||
|
: SYSREADY 68 ;
|
||||||
|
|
||||||
|
46 EMIT
|
||||||
|
|
||||||
\ Printing utilities
|
\ Printing utilities
|
||||||
|
|
||||||
: CR 10 EMIT ;
|
: CR 10 EMIT ;
|
||||||
@@ -67,6 +92,8 @@
|
|||||||
+ EMIT
|
+ EMIT
|
||||||
;
|
;
|
||||||
|
|
||||||
|
CHAR . EMIT
|
||||||
|
|
||||||
: .
|
: .
|
||||||
\ Handle negatives
|
\ Handle negatives
|
||||||
DUP 0< IF CHAR - EMIT NEGATE THEN
|
DUP 0< IF CHAR - EMIT NEGATE THEN
|
||||||
@@ -85,16 +112,14 @@
|
|||||||
\ Print any trailing zeros
|
\ Print any trailing zeros
|
||||||
BASE @ /
|
BASE @ /
|
||||||
DUP 0<> IF
|
DUP 0<> IF
|
||||||
BEGIN
|
BEGIN CHAR 0 EMIT BASE @ / 0= UNTIL
|
||||||
CHAR 0 EMIT
|
|
||||||
BASE @ /
|
|
||||||
DUP 0=
|
|
||||||
UNTIL
|
|
||||||
THEN
|
THEN
|
||||||
|
|
||||||
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 +128,8 @@
|
|||||||
AGAIN
|
AGAIN
|
||||||
;
|
;
|
||||||
|
|
||||||
|
CHAR . EMIT
|
||||||
|
|
||||||
: C, HERE @ C! 1 HERE +! ;
|
: C, HERE @ C! 1 HERE +! ;
|
||||||
|
|
||||||
: ."
|
: ."
|
||||||
@@ -132,14 +159,12 @@
|
|||||||
THEN
|
THEN
|
||||||
; IMMEDIATE
|
; IMMEDIATE
|
||||||
|
|
||||||
|
CHAR . EMIT
|
||||||
|
|
||||||
\ Misc utilities
|
\ Misc utilities
|
||||||
|
|
||||||
: NIP SWAP DROP ;
|
: NIP SWAP DROP ;
|
||||||
|
|
||||||
: HEX 16 BASE ! ;
|
|
||||||
|
|
||||||
: DECIMAL 10 BASE ! ;
|
|
||||||
|
|
||||||
: '
|
: '
|
||||||
WORD FIND DUP 0<> IF
|
WORD FIND DUP 0<> IF
|
||||||
>CFA
|
>CFA
|
||||||
@@ -150,6 +175,8 @@
|
|||||||
|
|
||||||
: [COMPILE] ' , ; IMMEDIATE
|
: [COMPILE] ' , ; IMMEDIATE
|
||||||
|
|
||||||
|
CHAR . EMIT
|
||||||
|
|
||||||
\ Constants, variables and values
|
\ Constants, variables and values
|
||||||
|
|
||||||
: CONSTANT
|
: CONSTANT
|
||||||
@@ -181,20 +208,7 @@
|
|||||||
THEN
|
THEN
|
||||||
; IMMEDIATE
|
; IMMEDIATE
|
||||||
|
|
||||||
\ Peripheral register addresses
|
CHAR . EMIT
|
||||||
|
|
||||||
HEX
|
|
||||||
|
|
||||||
000 CONSTANT TXBUF
|
|
||||||
080 CONSTANT RXBUF
|
|
||||||
100 CONSTANT TXHEAD
|
|
||||||
104 CONSTANT TXTAIL
|
|
||||||
108 CONSTANT RXHEAD
|
|
||||||
10C CONSTANT RXTAIL
|
|
||||||
110 CONSTANT SYSREADY
|
|
||||||
114 CONSTANT SYSINTER
|
|
||||||
|
|
||||||
DECIMAL
|
|
||||||
|
|
||||||
\ A better word-not-found handler
|
\ A better word-not-found handler
|
||||||
|
|
||||||
@@ -218,18 +232,22 @@ DECIMAL
|
|||||||
|
|
||||||
' WNF-HANDLER TO WNFHOOK
|
' WNF-HANDLER TO WNFHOOK
|
||||||
|
|
||||||
|
CHAR . EMIT
|
||||||
|
|
||||||
\ Version number
|
\ Version number
|
||||||
|
|
||||||
0 CONSTANT VERSION-MAJOR
|
: VERSION-MAJOR 0 ;
|
||||||
2 CONSTANT VERSION-MINOR
|
: VERSION-MINOR 1 ;
|
||||||
2 CONSTANT VERSION-PATCH
|
: VERSION-PATCH 0 ;
|
||||||
|
|
||||||
: PRINT-VERSION
|
: VERSION-PRINT
|
||||||
CHAR v EMIT VERSION-MAJOR .
|
." v" VERSION-MAJOR .
|
||||||
CHAR . EMIT VERSION-MINOR .
|
." ." VERSION-MINOR .
|
||||||
CHAR . EMIT VERSION-PATCH .
|
." ." VERSION-PATCH .
|
||||||
;
|
;
|
||||||
|
|
||||||
|
CHAR . EMIT
|
||||||
|
|
||||||
\ Welcome banner
|
\ Welcome banner
|
||||||
|
|
||||||
: BANNER
|
: BANNER
|
||||||
@@ -239,16 +257,16 @@ DECIMAL
|
|||||||
." |__,__/_/ .__(_)___/_//_/" CR
|
." |__,__/_/ .__(_)___/_//_/" CR
|
||||||
." /_/ " CR
|
." /_/ " CR
|
||||||
CR
|
CR
|
||||||
." Welcome to Wipforth " PRINT-VERSION ." !" CR
|
." Wipforth " VERSION-PRINT 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 available to use, modify and distribute for personal use under" CR
|
||||||
." under the Komorebi license, version 2.0.0." CR
|
." the Komorebi license, version 2.0.0." CR
|
||||||
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!
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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
@@ -1,81 +0,0 @@
|
|||||||
(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))
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
(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 1)
|
|
||||||
|
|
||||||
(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
-2106
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user