Compare commits

..

3 Commits

Author SHA1 Message Date
277c06b999 Add HEX and DECIMAL to prelude 2026-03-02 16:10:54 +00:00
33047a49e0 Add more information to the README 2026-03-02 16:10:54 +00:00
f9447d22d1 Add favicon 2026-03-02 16:10:54 +00:00
15 changed files with 2142 additions and 4094 deletions

111
README.md
View File

@@ -1,107 +1,68 @@
# Wipforth
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 :)
Wipforth is a simple Forth implementation that runs in the WebAssembly
virtual machine. It does I/O via memory-mapped peripherals, which are
emulated in JavaScript.
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 the Forth kernel, see [wipforth.wat](./wipforth.wat)
- For the JavaScript emulator, see [emu.js](./emu.js)
- For the Forth prelude, which is loaded at start-up, see
[prelude.f](./prelude.f)
- For a description of the peripherals, see the
[Peripherals](#peripherals) section below.
## Building and Running Locally
## Building and Running
There's a [Guile](https://www.gnu.org/software/guile/) script in the
repo you can use for this:
To run, first compile the WebAssembly module:
```
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
and use the system from there.
However, since everything is bootstrapped on the client, basically any
HTTP server will do as long as it sets the appropriate response
headers for `SharedArrayBuffer` 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 -
```
**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.
## Peripherals
### Terminal
# Terminal
| Name | Address | Size / B | Access |
|--------|---------|----------|--------------|
| TXBUF | 000h | 32 | write |
| RXBUF | 080h | 32 | read |
| TXHEAD | 100h | 4 | atomic read |
| TXTAIL | 104h | 4 | atomic write |
| RXHEAD | 108h | 4 | atomic write |
| RXTAIL | 10Ch | 4 | atomic read |
| 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 |
For both sending (`TX`) and receiving (`RX`), there are three
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
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.
data, and `xTAIL` being the offset of the first byte *after* the data.
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 | Address | Size / B | Access |
|----------|---------|----------|--------------|
| SYSREADY | 110h | 4 | atomic write |
| SYSINTER | 114h | 4 | atomic read |
| Name | Offset | Size / B | Access |
|----------|--------|----------|--------------|
| SYSREADY | 68 | 1 | atomic write |
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)
once the emulator has enabled user input and the system is
interactive.
up and is ready for user input.

1451
asm.js

File diff suppressed because it is too large Load Diff

27
boot.js
View File

@@ -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) => {
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();
const exports = { emu: { mem: e.data } };
const mod = await WebAssembly.instantiateStreaming(
fetch('wipforth.wasm'), exports)
mod.instance.exports.reset();
console.log('System halt');
break;
}
};

View File

@@ -1,8 +0,0 @@
asm.js
boot.js
emu.js
favicon.png
index.html
prelude.f
styles.css
wipforth.ws

View File

@@ -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"))

View File

@@ -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 ...)))

72
emu.js
View File

@@ -1,17 +1,16 @@
const TXBUF = 0x000;
const RXBUF = 0x080;
const TXHEAD = 0x100;
const TXTAIL = 0x104;
const RXHEAD = 0x108;
const RXTAIL = 0x10c;
const TXBUF = 0;
const RXBUF = 32;
const TXHEAD = 64;
const TXTAIL = 65;
const RXHEAD = 66;
const RXTAIL = 67;
const SYSREADY = 68;
const SYSREADY = 0x110;
const SYSINTER = 0x114;
const TXBUF_SIZE = 32;
const RXBUF_SIZE = 32;
const PERIPHS_SIZE = 69; // Nice
const PERIPHS_SIZE = 0x200;
const POLL_INTERVAL_MS = 5;
const DOT_INTERVAL_MS = 25;
const POLL_INTERVAL_MS = 20;
const COLS = 80;
const TAB_WIDTH = 8;
@@ -28,7 +27,6 @@ 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');
@@ -51,19 +49,8 @@ class Emulator {
document.addEventListener('keydown', (e) => this.handle_keydown(e));
window.addEventListener('resize', () => this.handle_resize());
this.forth = new Worker('boot.js', { type: 'module' });
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);
};
this.worker = new Worker('boot.js');
this.worker.postMessage(this.mem);
fetch('prelude.f')
.then(res => res.text())
@@ -89,21 +76,15 @@ class Emulator {
if (!this.input_enable) {
const sysready = Atomics.load(this.mem_u8, SYSREADY);
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.blink = true;
this.flush_output();
document.getElementById('cursor').classList.add('blinking');
}
}
}
fifo_next(idx) {
return (idx + 1) & 0x7f;
return (idx + 1) & 0x1f;
}
handle_tx_data(head, tail) {
@@ -122,7 +103,6 @@ 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) {
@@ -191,14 +171,12 @@ class Emulator {
}
e.preventDefault();
this.blink = false;
this.flush_output();
document.getElementById('cursor').classList.remove('blinking');
if (this.idle_timer)
clearTimeout(this.idle_timer);
this.idle_timer = setTimeout(() => {
this.blink = true;
document.getElementById('cursor').classList.add('blinking');
}, CURSOR_IDLE_TIME_MS);
}
@@ -285,12 +263,10 @@ class Emulator {
return row.map((c, x) => {
const ec = this.html_escape(c);
if (this.input_enable
&& x == this.cursor.x && y == this.cursor.y) {
const cl = this.blink ? 'class="blinking"' : '';
return `<span id="cursor" ${cl}>` + ec + '</span>';
} else {
&& x == this.cursor.x && y == this.cursor.y)
return '<span id="cursor">' + ec + '</span>';
else
return ec;
}
}).join('').trimEnd();
}).join('\n');
this.output.innerHTML = html;
@@ -347,16 +323,6 @@ class Emulator {
}
window.addEventListener('DOMContentLoaded', () => {
const output = document.getElementById('output');
output.innerText = '';
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 {
document.getElementById('output').innerText = '';
window.emu = new Emulator();
}
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 516 B

After

Width:  |  Height:  |  Size: 499 B

View File

@@ -7,7 +7,8 @@
</head>
<body>
<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&#8482;</noscript></div></body>
cringe, I know... but I promise there are no frameworks&#8482;</div>
</body>
</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 :)
\ We'll periodically sprinkle these in so that it's clear to the user
\ that things are happening.
46 EMIT
\ Conditionals
: IF
@@ -20,6 +27,8 @@
SWAP !
; IMMEDIATE
46 EMIT
\ Loops
: BEGIN HERE @ ; IMMEDIATE
@@ -34,6 +43,8 @@
HERE @ - ,
; IMMEDIATE
46 EMIT
\ Recursive calls
: RECURSE LATEST @ >CFA , ; IMMEDIATE
@@ -50,6 +61,20 @@
( ( 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
: CR 10 EMIT ;
@@ -67,6 +92,8 @@
+ EMIT
;
CHAR . EMIT
: .
\ Handle negatives
DUP 0< IF CHAR - EMIT NEGATE THEN
@@ -85,16 +112,14 @@
\ Print any trailing zeros
BASE @ /
DUP 0<> IF
BEGIN
CHAR 0 EMIT
BASE @ /
DUP 0=
UNTIL
BEGIN CHAR 0 EMIT BASE @ / 0= UNTIL
THEN
2DROP
;
CHAR . EMIT
: TYPE ( addr len -- )
BEGIN
DUP 0= IF 2DROP EXIT THEN
@@ -103,6 +128,8 @@
AGAIN
;
CHAR . EMIT
: C, HERE @ C! 1 HERE +! ;
: ."
@@ -132,6 +159,8 @@
THEN
; IMMEDIATE
CHAR . EMIT
\ Misc utilities
: NIP SWAP DROP ;
@@ -150,6 +179,8 @@
: [COMPILE] ' , ; IMMEDIATE
CHAR . EMIT
\ Constants, variables and values
: CONSTANT
@@ -181,20 +212,7 @@
THEN
; IMMEDIATE
\ Peripheral register addresses
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
CHAR . EMIT
\ A better word-not-found handler
@@ -218,18 +236,22 @@ DECIMAL
' WNF-HANDLER TO WNFHOOK
CHAR . EMIT
\ Version number
0 CONSTANT VERSION-MAJOR
2 CONSTANT VERSION-MINOR
2 CONSTANT VERSION-PATCH
: VERSION-MAJOR 0 ;
: VERSION-MINOR 1 ;
: VERSION-PATCH 0 ;
: PRINT-VERSION
CHAR v EMIT VERSION-MAJOR .
CHAR . EMIT VERSION-MINOR .
CHAR . EMIT VERSION-PATCH .
: VERSION-PRINT
." v" VERSION-MAJOR .
." ." VERSION-MINOR .
." ." VERSION-PATCH .
;
CHAR . EMIT
\ Welcome banner
: BANNER
@@ -239,7 +261,7 @@ DECIMAL
." |__,__/_/ .__(_)___/_//_/" CR
." /_/ " CR
CR
." Welcome to Wipforth " PRINT-VERSION ." !" CR
." Wipforth " VERSION-PRINT CR
." Copyright (c) Camden Dixie O'Brien" CR
CR
." Wipforth is freely available to use, modify and distribute for personal use" CR
@@ -247,8 +269,8 @@ DECIMAL
CR
;
\ Set SYSREADY high and wait until interactive
1 SYSREADY AC!
SYSINTER WAIT DROP
." done" CR
BANNER
\ Set SYSREADY high to enable user input
1 SYSREADY AC!

16
server.py Normal file
View File

@@ -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()

View File

@@ -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))

View File

@@ -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
wipforth.wat Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff