Compare commits

..

132 Commits

Author SHA1 Message Date
aa9c18346d Add message for when chromium hard reload bug breaks things 2026-03-21 23:16:37 +00:00
a2f06c977e Wrap no JS message in noscript tag 2026-03-21 23:15:57 +00:00
58e2cf3e1f Fix SharedArrayBuffer name in README 2026-03-20 13:36:54 +00:00
efdae93d90 Add favicon to deploy manifest 2026-03-20 13:35:59 +00:00
804626ddad Add asm.js to deploy manifest 2026-03-20 13:15:49 +00:00
cbe5733fcd Bump version patch number 2026-03-20 13:04:36 +00:00
7961c68639 Reduce wait in e2e tests 2026-03-20 13:04:28 +00:00
d202157a58 Reduce dot interval to 25 ms 2026-03-20 13:04:17 +00:00
97fc43bf93 Add "Welcome to" to welcome message 2026-03-20 13:04:10 +00:00
2c13ad4e1f Handle prelude loading message in JS rather than Forth 2026-03-20 13:03:57 +00:00
36429bf8bc Rework tokenizer to avoid unecessary allocations 2026-03-20 13:02:40 +00:00
c20e7e181b Reduce poll interval to 5 ms 2026-03-20 13:02:29 +00:00
fe5c55cabf Increase TX and RX buffer size to 128 bytes 2026-03-20 13:02:07 +00:00
67fc1d8d7b Remove race condition between assemble and prelude load prints 2026-03-18 15:17:08 +00:00
4000522b3a Remove obsolete assembly driver script 2026-03-18 15:08:57 +00:00
19ef69958d Update README 2026-03-18 15:04:32 +00:00
0a52388030 Update deploy manifest 2026-03-18 14:25:11 +00:00
6e8439eeaf Bump version number 2026-03-18 14:24:41 +00:00
eaa3242cc0 Update e2e tests 2026-03-18 14:24:25 +00:00
f77adffbef Update MIME types in server.scm 2026-03-18 14:24:05 +00:00
c91f46be88 Assemble kernel on client 2026-03-18 14:23:37 +00:00
6ee4adfea5 Translate kernel to Wasmasm 2026-03-18 10:41:09 +00:00
5dc0a7a601 Add temporary driver script 2026-03-18 10:35:14 +00:00
896a1ca563 Implement (limited) forward reference handling 2026-03-18 10:32:49 +00:00
37d56988ef Make a couple of tweaks to the kernel in preparation for porting 2026-03-18 10:30:26 +00:00
6c643f8402 Don't silently ignore trailing characters in numbers 2026-03-18 10:29:46 +00:00
7828b0f112 Yield newline token at end of comment 2026-03-15 21:39:14 +00:00
e7affbf8b7 Add .zero directive 2026-03-15 21:28:25 +00:00
02ee4c3c88 Support symbols in .at address field 2026-03-15 21:27:48 +00:00
c21b3c79c7 Fix names of atomic load and store opcodes 2026-03-15 20:04:49 +00:00
1318c3cc4e Add i64.const, i32.div_s and i32.rem_s opcodes 2026-03-15 20:04:27 +00:00
74a8f21379 Encode indices as unsigned LEB128 instead of signed 2026-03-15 20:04:06 +00:00
6784cd02b4 Encode section lengths with unsigned LEB128 2026-03-15 20:03:47 +00:00
3a103c46d1 Don't require ; to have space after in comments 2026-03-15 17:32:14 +00:00
8d4c53ca92 Allow implicit zero-init for globals 2026-03-15 14:26:22 +00:00
5e39024f6d Use unsigned shift in uleb128() 2026-03-15 14:15:40 +00:00
b85a4e8bc9 Encode data values in assembler, not parser 2026-03-15 14:15:34 +00:00
401e8e1fad Use unsigned right shift in Assembler.le() 2026-03-15 14:07:26 +00:00
d4c837216a Add f32 type 2026-03-15 13:58:33 +00:00
c93e9009da LEB128-encode index in action_symbol 2026-03-15 13:58:25 +00:00
0056610238 Add missing semicolon 2026-03-15 13:58:24 +00:00
9b4ff3e8f6 Use array flattening instead of spread operator in a few places 2026-03-15 13:58:23 +00:00
e9beacba3a De-duplicate consecutive locals of same type in wasm_section_code() 2026-03-15 13:58:23 +00:00
acf5b6e284 Handle failed def lookup in action_data() 2026-03-15 13:58:22 +00:00
72c5f64312 Handle global init value encoding in Assembler 2026-03-15 13:58:20 +00:00
7135eeba74 Restructure uleb128 2026-03-15 13:41:39 +00:00
7099ca34a3 Fix .word size 2026-03-15 13:41:05 +00:00
3ebb74c73c Check for null explicitly in token_top() 2026-03-15 13:40:43 +00:00
0dd2a925d8 Allow table elems to be labelled 2026-03-15 12:34:41 +00:00
2155d17731 Implement type, table and func symbol resolution 2026-03-15 12:28:29 +00:00
1452ffe615 Implement .table and .elem 2026-03-15 12:14:35 +00:00
46a571be93 Add error message for unhandled states and actions 2026-03-15 11:09:16 +00:00
d35b13fed0 Add .type directive 2026-03-15 11:05:37 +00:00
a3cfd405a9 Add some threads opcodes 2026-03-14 19:30:44 +00:00
671e7f60d2 Add a bunch of opcodes 2026-03-14 19:30:43 +00:00
580d5d2a4a Implement function type de-duplication 2026-03-14 19:23:02 +00:00
1105daaad0 Add support for extended opcodes 2026-03-14 19:23:00 +00:00
347dd8f534 Make all sections optional 2026-03-14 18:36:21 +00:00
f4433ce3a3 LEB128-encode addresses in data section 2026-03-14 18:35:33 +00:00
714973f052 LEB128-encode values from defs 2026-03-14 18:29:55 +00:00
4f878fdbab Add suport for block/loop/if/else 2026-03-14 18:26:12 +00:00
9fb3910a16 Allow defs to reference other defs 2026-03-14 15:04:25 +00:00
22dc1fc0ca Add support for labels 2026-03-14 14:52:44 +00:00
cc51b2d7be Fix data word size 2026-03-14 14:50:40 +00:00
902404cb10 Fix string interpolation in error messages 2026-03-14 14:01:09 +00:00
d4718f1106 Allow using defs in code 2026-03-14 13:59:42 +00:00
33f5a4be06 Fix bug in local lookup 2026-03-14 13:59:41 +00:00
e2429b2b03 Enable using defs in .byte and .word directives 2026-03-14 13:52:00 +00:00
2972030d0a Add .def support 2026-03-14 13:48:37 +00:00
2c3e5f46da Implement .align directive 2026-03-14 13:48:36 +00:00
93f3dd1f41 Implement .utf directive 2026-03-14 13:48:35 +00:00
cfa4fa7d4f Add .word directive 2026-03-14 13:48:34 +00:00
94cee7d258 Fix string interpolation in error messages 2026-03-14 13:48:33 +00:00
092d870a9c Implement .byte directive 2026-03-14 13:48:33 +00:00
6db71ee382 Add .at directive 2026-03-14 11:56:30 +00:00
5369a0969e Restructure copy implementation to avoid type-indexed block 2026-03-10 21:25:37 +00:00
118e6af896 Add support for globals (mutable only)
I'm planning on adding symbolic constants to the assembler, so I won't
really have much use for immutable globals.
2026-03-10 19:56:49 +00:00
1c4b9f850a Add support for imports (memory only) 2026-03-10 19:54:16 +00:00
672a453f6c Add string support to tokenizer 2026-03-10 17:53:58 +00:00
5a3084dd16 Implement .mem directive 2026-03-10 15:33:41 +00:00
77f6d57e1b Add support for locals 2026-03-10 00:37:05 +00:00
510a74aa04 Add base suffix for integers 2026-03-10 00:22:07 +00:00
75600d0568 Add symbol resolution (params only) 2026-03-10 00:22:06 +00:00
6a4877d52c Implement .param directive 2026-03-09 23:52:18 +00:00
554d918640 Create initial scaffolding for JS WASM assembler 2026-03-09 23:36:14 +00:00
ef0c395d57 Add manifest for wipforth-deploy 2026-03-08 21:06:35 +00:00
f72d79dc19 Fix typo in README 2026-03-04 22:14:12 +00:00
e5f9d2d828 Make run-tests exit 1 when there are failures 2026-03-04 22:08:34 +00:00
fb70a2585f Fix typo in test name 2026-03-04 21:31:22 +00:00
fb52e5a701 Add README section for end-to-end tests 2026-03-04 20:52:23 +00:00
a1b003a1cd Implement some end-to-end tests 2026-03-04 20:45:05 +00:00
9576769e09 Implement simple testing module for e2e tests 2026-03-04 20:22:24 +00:00
e13452db15 Run firefox in blank profile when e2e testing 2026-03-04 17:55:16 +00:00
529aabd213 Rename e2e-tests to e2e 2026-03-04 17:52:26 +00:00
d18ff1d2bb Modify server.scm so it can be loaded as a module 2026-03-04 17:26:46 +00:00
e2ed73055f Write Guile module for Firefox interaction with Marionette 2026-03-04 01:08:54 +00:00
721e0f285b Fix handling of trailing zeros in . 2026-03-03 13:17:37 +00:00
c1c1db7b51 Add note about server.scm probably being insecure 2026-03-02 21:52:05 +00:00
b5c4070daf Use EMIT when printing version number instead of ." 2026-03-02 19:00:57 +00:00
59e6027739 Use CONSTANT to define version numbers 2026-03-02 18:56:17 +00:00
32c3c6e356 Use atomic wait/notify on RXTAIL in KEY 2026-03-02 18:53:48 +00:00
2a3949e09f Make terminal xHEAD and xTAIL registers 32 bits
This enables waiting on them with memory.atomic.wait32 (there is no
wait8) which is needed to avoid spinning when waiting for a key.
2026-03-02 18:51:42 +00:00
22e477adf7 Fix hex number input 2026-03-02 18:50:34 +00:00
c99430c75e Fix headers in peripherals description 2026-03-02 17:59:32 +00:00
a77e7f4051 Rename "Offset" column to "Address" in peripherals description 2026-03-02 17:58:42 +00:00
24459a8ae5 List tools in Building and Running README section 2026-03-02 17:54:31 +00:00
a0e3706db7 Replace Python server with Guile 2026-03-02 17:53:50 +00:00
6c30d96e96 Add HEX and DECIMAL to prelude 2026-03-02 17:42:45 +00:00
2ff0c2ada1 Add more information to the README 2026-03-02 17:42:45 +00:00
e5ee3fe076 Add favicon 2026-03-02 17:42:45 +00:00
8ed8bbad3e Set output rows based on window size 2026-03-02 16:10:54 +00:00
7f4d900688 Fix EMIT-DIGIT 2026-03-02 16:10:54 +00:00
54be0a8c0e Fix output width at 80 chars and remove trailing spaces 2026-03-02 16:10:54 +00:00
832ce55108 Implement basic scrolling in emu.js 2026-03-02 16:10:54 +00:00
259aa730f7 Add copyright and license message to the banner 2026-03-02 16:10:54 +00:00
50f2f240fa Shorten SYSREADY comment in prelude 2026-03-01 20:26:28 +00:00
ee4c0d1d8c Add a better word-not-found handler 2026-03-01 19:52:52 +00:00
acaf198fae Implement CONSTANT, VARIABLE, VALUE and TO 2026-03-01 19:52:43 +00:00
1a9702ea5c Implement some utility words 2026-03-01 19:51:59 +00:00
5d704d3bde Print loading indicators during prelude 2026-03-01 19:51:47 +00:00
047d09260c Add wip.sh ASCII art banner 2026-03-01 19:51:47 +00:00
fac6c4d3b8 Implement TYPE and ." 2026-03-01 19:51:47 +00:00
a9fd395a94 Implement . 2026-03-01 19:51:47 +00:00
922a0e6e31 Implement block comments 2026-03-01 19:51:47 +00:00
d60708645d Implement recursive calls 2026-03-01 19:51:47 +00:00
8785c01f9e Implement loops 2026-03-01 19:51:47 +00:00
53d3a308cd Implement conditionals 2026-03-01 19:51:47 +00:00
fde6429d4f Add line comments and peripheral reg constants to prelude 2026-03-01 19:51:47 +00:00
c06ef248b7 Implement prelude-loading mechanism 2026-03-01 19:51:47 +00:00
f6a24ee3cf Fix >CFA returning incorrect value on hidden words 2026-03-01 19:51:47 +00:00
b372ae3d5f Rename DOCONST to DOVAL 2026-03-01 19:51:47 +00:00
9f1be9ad2f Add hook mechanism for handling word not found 2026-03-01 13:55:19 +00:00
16 changed files with 4387 additions and 2040 deletions

107
README.md
View File

@@ -1,48 +1,107 @@
# Wipforth # 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 guile server.scm
```
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.
**NOTE**: The server is just a very simple instantiation of Python's However, since everything is bootstrapped on the client, basically any
built-in `http.server.HTTPServer`, configured to set the cross-origin HTTP server will do as long as it sets the appropriate response
headers required for `SharedMemoryBuffer` use. You could use any HTTP headers for `SharedArrayBuffer` use:
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 | Offset | Size / B | Access | | Name | Address | Size / B | Access |
|--------|--------|----------|--------------| |--------|---------|----------|--------------|
| TXBUF | 0 | 32 | write | | TXBUF | 000h | 32 | write |
| RXBUF | 32 | 32 | read | | RXBUF | 080h | 32 | read |
| TXHEAD | 64 | 1 | atomic read | | TXHEAD | 100h | 4 | atomic read |
| TXTAIL | 65 | 1 | atomic write | | TXTAIL | 104h | 4 | atomic write |
| RXHEAD | 66 | 1 | atomic write | | RXHEAD | 108h | 4 | atomic write |
| RXTAIL | 67 | 1 | atomic read | | RXTAIL | 10Ch | 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 32-byte FIFO ring buffers used for data - `xBUF` registers are 128-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. 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 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
| Name | Address | Size / B | Access |
|----------|---------|----------|--------------|
| SYSREADY | 110h | 4 | atomic write |
| SYSINTER | 114h | 4 | atomic read |
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.

1451
asm.js Normal file

File diff suppressed because it is too large Load Diff

27
boot.js
View File

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

8
deploy-manifest.conf Normal file
View File

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

139
e2e/marionette.scm Normal file
View File

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

71
e2e/testing.scm Normal file
View File

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

136
emu.js
View File

@@ -1,18 +1,19 @@
const TXBUF = 0; const TXBUF = 0x000;
const RXBUF = 32; const RXBUF = 0x080;
const TXHEAD = 64; const TXHEAD = 0x100;
const TXTAIL = 65; const TXTAIL = 0x104;
const RXHEAD = 66; const RXHEAD = 0x108;
const RXTAIL = 67; const RXTAIL = 0x10c;
const TXBUF_SIZE = 32; const SYSREADY = 0x110;
const RXBUF_SIZE = 32; const SYSINTER = 0x114;
const PERIPHS_SIZE = 68;
const POLL_INTERVAL_MS = 20; const PERIPHS_SIZE = 0x200;
const POLL_INTERVAL_MS = 5;
const DOT_INTERVAL_MS = 25;
const COLS = 80; const COLS = 80;
const ROWS = 36;
const TAB_WIDTH = 8; const TAB_WIDTH = 8;
const CURSOR_IDLE_TIME_MS = 1000; const CURSOR_IDLE_TIME_MS = 1000;
@@ -27,6 +28,7 @@ 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');
@@ -35,8 +37,9 @@ class Emulator {
this.rx_queue = []; this.rx_queue = [];
this.timer = setInterval(() => this.poll(), POLL_INTERVAL_MS); this.timer = setInterval(() => this.poll(), POLL_INTERVAL_MS);
this.rows = this.max_rows();
this.grid = Array.from( this.grid = Array.from(
{ length: ROWS }, { length: this.rows },
() => new Array(COLS).fill(' ')); () => new Array(COLS).fill(' '));
this.cursor = { x: 0, y: 0 }; this.cursor = { x: 0, y: 0 };
this.range = { this.range = {
@@ -44,13 +47,30 @@ class Emulator {
end: { x: 0, y: 0 } end: { x: 0, y: 0 }
}; };
this.idle_timer = null; this.idle_timer = null;
this.input_enable = false;
document.addEventListener('keydown', (e) => this.handle_keydown(e)); document.addEventListener('keydown', (e) => this.handle_keydown(e));
window.addEventListener('resize', () => this.handle_resize());
this.flush_output(); this.forth = new Worker('boot.js', { type: 'module' });
document.getElementById('cursor').classList.add('blinking');
this.worker = new Worker('boot.js'); this.print("Assembling kernel ");
this.worker.postMessage(this.mem); 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')
.then(res => res.text())
.then(text => {
for (const cu of this.encoder.encode(text))
this.rx_queue.push(cu);
});
} }
poll() { poll() {
@@ -65,10 +85,25 @@ class Emulator {
if (this.fifo_next(rxtail) != rxhead) if (this.fifo_next(rxtail) != rxhead)
this.handle_rx_data(rxhead, rxtail); this.handle_rx_data(rxhead, rxtail);
} }
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();
}
}
} }
fifo_next(idx) { fifo_next(idx) {
return (idx + 1) & 0x1f; return (idx + 1) & 0x7f;
} }
handle_tx_data(head, tail) { handle_tx_data(head, tail) {
@@ -87,6 +122,7 @@ 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) {
@@ -118,12 +154,17 @@ class Emulator {
} }
handle_keydown(e) { handle_keydown(e) {
if (!this.input_enable)
return;
if (e.key.length == 1 && !e.ctrlKey && !e.altKey && !e.metaKey) { if (e.key.length == 1 && !e.ctrlKey && !e.altKey && !e.metaKey) {
this.shift_up(this.cursor); this.shift_up(this.cursor);
this.grid[this.cursor.y][this.cursor.x] = e.key; this.grid[this.cursor.y][this.cursor.x] = e.key;
this.cursor_move(1, 0); this.cursor_move(1, 0);
} else if (e.key == 'Enter') { } else if (e.key == 'Enter') {
this.cursor.y = this.range.end.y + 1; this.cursor.y = this.range.end.y + 1;
while (this.cursor.y >= this.rows)
this.scroll();
this.cursor.x = 0; this.cursor.x = 0;
this.submit_line(); this.submit_line();
Object.assign(this.range.start, this.cursor); Object.assign(this.range.start, this.cursor);
@@ -150,12 +191,14 @@ 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);
} }
@@ -208,7 +251,7 @@ class Emulator {
next_cell(cell) { next_cell(cell) {
if (cell.x < COLS - 1) if (cell.x < COLS - 1)
return { x: cell.x + 1, y: cell.y }; 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 }; return { x: 0, y: cell.y + 1 };
else else
return null; return null;
@@ -221,6 +264,8 @@ class Emulator {
this.grid[cell.y][cell.x] = this.grid[prev.y][prev.x]; this.grid[cell.y][cell.x] = this.grid[prev.y][prev.x];
cell = prev; cell = prev;
} }
if (this.next_cell(this.range.end) == null)
this.scroll();
this.range.end = this.next_cell(this.range.end); this.range.end = this.next_cell(this.range.end);
} }
@@ -239,11 +284,14 @@ class Emulator {
const html = this.grid.map((row, y) => { const html = this.grid.map((row, y) => {
return row.map((c, x) => { return row.map((c, x) => {
const ec = this.html_escape(c); const ec = this.html_escape(c);
if (x == this.cursor.x && y == this.cursor.y) if (this.input_enable
return '<span id="cursor">' + ec + '</span>'; && x == this.cursor.x && y == this.cursor.y) {
else const cl = this.blink ? 'class="blinking"' : '';
return `<span id="cursor" ${cl}>` + ec + '</span>';
} else {
return ec; return ec;
}).join(''); }
}).join('').trimEnd();
}).join('\n'); }).join('\n');
this.output.innerHTML = html; this.output.innerHTML = html;
} }
@@ -264,9 +312,51 @@ class Emulator {
return c; 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', () => { window.addEventListener('DOMContentLoaded', () => {
document.getElementById('output').innerText = ''; 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 {
window.emu = new Emulator(); window.emu = new Emulator();
}
}); });

BIN
favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B

View File

@@ -3,11 +3,11 @@
<head> <head>
<title>Wipforth</title> <title>Wipforth</title>
<link rel="stylesheet" type="text/css" href="styles.css"/> <link rel="stylesheet" type="text/css" href="styles.css"/>
<link rel="icon" type="image/png" href="favicon.png"/>
</head> </head>
<body> <body>
<script type="text/javascript" src="emu.js"></script> <script type="text/javascript" src="emu.js"></script>
<div id="output">I'm afraid you need javascript enabled for this to work :( <div id="output"><noscript>I'm afraid you need javascript enabled for this to work :(
cringe, I know... but I promise there are no frameworks&#8482;</div> cringe, I know... but I promise there are no frameworks&#8482;</noscript></div></body>
</body>
</html> </html>

254
prelude.f Normal file
View File

@@ -0,0 +1,254 @@
: \ KEY 10 = 0BRANCH [ -20 , ] ; IMMEDIATE \ Now we have line comments :)
\ Conditionals
: IF
LIT 0BRANCH ,
HERE @ 0 ,
; IMMEDIATE
: THEN
DUP HERE @ SWAP -
SWAP !
; IMMEDIATE
: ELSE
LIT BRANCH ,
HERE @ 0 ,
SWAP
DUP HERE @ SWAP -
SWAP !
; IMMEDIATE
\ Loops
: BEGIN HERE @ ; IMMEDIATE
: UNTIL
LIT 0BRANCH ,
HERE @ - ,
; IMMEDIATE
: AGAIN
LIT BRANCH ,
HERE @ - ,
; IMMEDIATE
\ Recursive calls
: RECURSE LATEST @ >CFA , ; IMMEDIATE
\ Block comments (with nesting)
: (
BEGIN
KEY DUP
40 = IF RECURSE THEN
41 =
UNTIL
; IMMEDIATE
( ( Take that, C ) )
\ Printing utilities
: CR 10 EMIT ;
: SPACE 32 EMIT ;
: CHAR
KEY
STATE @ 0<> IF
LIT LIT , , \ Append LIT <key> to definition if compiling
THEN
; IMMEDIATE
: EMIT-DIGIT
DUP 10 < IF CHAR 0 ELSE 10 - CHAR A THEN
+ EMIT
;
: .
\ Handle negatives
DUP 0< IF CHAR - EMIT NEGATE THEN
\ Determine starting place value
1 BEGIN BASE @ * 2DUP / 0= UNTIL
\ Print leading non-zero digits
BEGIN
BASE @ /
2DUP / EMIT-DIGIT
TUCK MOD SWAP
OVER 0=
UNTIL
\ Print any trailing zeros
BASE @ /
DUP 0<> IF
BEGIN
CHAR 0 EMIT
BASE @ /
DUP 0=
UNTIL
THEN
2DROP
;
: TYPE ( addr len -- )
BEGIN
DUP 0= IF 2DROP EXIT THEN
SWAP DUP C@ EMIT 1+
SWAP 1-
AGAIN
;
: C, HERE @ C! 1 HERE +! ;
: ."
STATE @ IF
LIT BRANCH , \ Compile BRANCH for jumping over string data
HERE @ 0 , \ Compile placeholder offset and push offset cell address
0 BEGIN
KEY
DUP CHAR " = IF
DROP
HERE @ ALIGN HERE ! \ Pad to alignment
SWAP DUP HERE @ SWAP - \ Compute offset
OVER ! \ Update branch offset to skip data
4+ LIT LIT , , \ Append LIT <addr>
LIT LIT , , \ Append LIT <len>
LIT TYPE , \ Append TYPE
EXIT
THEN
C, 1+ \ Write byte and increment length
AGAIN
ELSE
BEGIN
KEY
DUP CHAR " = IF DROP EXIT THEN
EMIT
AGAIN
THEN
; IMMEDIATE
\ Misc utilities
: NIP SWAP DROP ;
: HEX 16 BASE ! ;
: DECIMAL 10 BASE ! ;
: '
WORD FIND DUP 0<> IF
>CFA
ELSE
DROP WNFHOOK EXECUTE
THEN
;
: [COMPILE] ' , ; IMMEDIATE
\ Constants, variables and values
: CONSTANT
WORD CREATE
DOCOL ,
LIT LIT ,
,
LIT EXIT ,
;
: VARIABLE
WORD CREATE
DOVAR ,
0 ,
;
: VALUE
WORD CREATE
DOVAL , ,
;
: TO
' 4+
STATE @ IF
LIT LIT , ,
LIT ! ,
ELSE
!
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
\ A better word-not-found handler
: ANY-RX? RXHEAD AC@ RXTAIL AC@ <> ;
: WNF-HANDLER
." Unknown word: "
WORDBUF SWAP TYPE CR
STATE @ IF
\ Remove incomplete definition and switch back to interpret mode
LATEST @ HERE !
LATEST @ @ LATEST !
[COMPILE] [
THEN
\ We only consume to the next newline if there is RX data available,
\ because if the offending word was at the end of the line then the
\ newline would have already been consumed by WORD.
ANY-RX? IF BEGIN KEY 10 = UNTIL THEN
QUIT
;
' WNF-HANDLER TO WNFHOOK
\ Version number
0 CONSTANT VERSION-MAJOR
2 CONSTANT VERSION-MINOR
2 CONSTANT VERSION-PATCH
: PRINT-VERSION
CHAR v EMIT VERSION-MAJOR .
CHAR . EMIT VERSION-MINOR .
CHAR . EMIT VERSION-PATCH .
;
\ Welcome banner
: BANNER
." _ __ " CR
." _ __(_)__ ___ / / " CR
." | |/|/ / / _ \_ (_-./ _ \" CR
." |__,__/_/ .__(_)___/_//_/" CR
." /_/ " CR
CR
." Welcome to 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
;
\ Set SYSREADY high and wait until interactive
1 SYSREADY AC!
SYSINTER WAIT DROP
BANNER

View File

@@ -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
server.scm Normal file
View File

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

View File

@@ -14,6 +14,7 @@ body {
} }
#output { #output {
width: 80ch;
margin: 1.5em auto; margin: 1.5em auto;
white-space: pre; white-space: pre;
} }

52
tests.scm Normal file
View File

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

File diff suppressed because it is too large Load Diff

2106
wipforth.ws Normal file

File diff suppressed because it is too large Load Diff