This commit is contained in:
Remko Tronçon 2018-05-12 21:09:19 +02:00
commit 73db2977f2
17 changed files with 6460 additions and 0 deletions

3
.babelrc Normal file
View file

@ -0,0 +1,3 @@
{
"presets": ["env"]
}

10
.eslintrc Normal file
View file

@ -0,0 +1,10 @@
env:
browser: true
es6: true
extends: eslint:recommended
parserOptions:
sourceType: module
globals:
WebAssembly: true
rules:
no-console: 0

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
.cache/
node_modules/
dist/
*.wasm
*.wasm.hex
*.tmp

36
Makefile Normal file
View file

@ -0,0 +1,36 @@
WAT2WASM=wat2wasm
WAT2WASM_FLAGS=
ifeq ($(DEBUG),1)
WAT2WASM_FLAGS=--debug-names
endif
PARCEL=./node_modules/.bin/parcel
WASM_FILES=dist/waforth.wasm
all: $(WASM_FILES)
$(PARCEL) build src/shell/index.html
dev-server: $(WASM_FILES)
$(PARCEL) src/shell/index.html
.PHONY: tests
tests: $(WASM_FILES)
$(PARCEL) --no-hmr -o dist/tests.html tests/index.html
wasm: $(WASM_FILES) src/tools/quadruple.wasm.hex
dist/waforth.wasm: src/waforth.wat dist
racket -f $< > src/waforth.wat.tmp
$(WAT2WASM) $(WAT2WASM_FLAGS) -o $@ src/waforth.wat.tmp
dist:
mkdir -p $@
src/tools/quadruple.wasm: src/tools/quadruple.wat
$(WAT2WASM) $(WAT2WASM_FLAGS) -o $@ $<
src/tools/quadruple.wasm.hex: src/tools/quadruple.wasm
hexdump -v -e '16/1 "_u%04X" "\n"' $< | sed 's/_/\\/g; s/\\u //g; s/.*/ "&"/' > $@
clean:
-rm -rf $(WASM_FILES) src/tools/quadruple.wasm src/tools/quadruple.wasm.hex src/waforth.wat.tmp dist

1
README.md Normal file
View file

@ -0,0 +1 @@
Nothing to see here (yet)

12
package.json Normal file
View file

@ -0,0 +1,12 @@
{
"private": true,
"dependencies": {},
"devDependencies": {
"babel-core": "^6.26.3",
"babel-preset-env": "^1.7.0",
"chai": "^4.1.2",
"eslint": "^4.19.1",
"mocha": "^5.2.0",
"parcel": "^1.8.1"
}
}

177
src/shell/WAForth.js Normal file
View file

@ -0,0 +1,177 @@
let wasmModule;
// eslint-disable-next-line no-unused-vars
function arrayToBase64(bytes) {
var binary = "";
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
class WAForth {
start() {
let nextTableBase = 0;
let table;
const buffer = (this.buffer = []);
// TODO: Try to bundle this. See https://github.com/parcel-bundler/parcel/issues/647
const initialize =
wasmModule != null
? Promise.resolve(wasmModule)
: fetch("waforth.wasm")
.then(resp => resp.arrayBuffer())
.then(module => {
wasmModule = module;
return wasmModule;
});
return initialize
.then(m =>
WebAssembly.instantiate(m, {
shell: {
emit: this.onEmit,
key: () => {
if (buffer.length === 0) {
// throw Error("Buffer underrun");
return -1;
}
return buffer.pop();
},
load: (offset, length) => {
// console.log(
// "LOAD",
// new Uint8Array(this.core.exports.memory.buffer, offset, length),
// arrayToBase64(
// new Uint8Array(
// this.core.exports.memory.buffer,
// offset,
// length
// )
// )
// );
var tableBase = table.length + nextTableBase;
if (tableBase >= table.length) {
table.grow(table.length); // Double size
}
var module = new WebAssembly.Module(
new Uint8Array(this.core.exports.memory.buffer, offset, length)
);
new WebAssembly.Instance(module, {
env: { table, tableBase }
});
nextTableBase = nextTableBase + 1;
return tableBase;
},
debug: d => {
console.log("DEBUG: ", d);
}
},
tmp: {
number: outOffset => {
const length = new Uint32Array(
this.core.exports.memory.buffer,
0x200,
4
)[0];
const s = new Uint8Array(
this.core.exports.memory.buffer,
0x204,
length
);
let sign = 1;
let val = 0;
sign = 1;
if (s[0] === 45) {
sign = -1;
} else if (s[0] < 48 || s[0] > 57) {
return -1;
} else {
val = val + s[0] - 48;
}
for (let i = 1; i < s.length; ++i) {
if (s[i] < 48 || s[i] > 57) {
return -1;
}
val = val * 10 + (s[i] - 48);
}
new Int32Array(this.core.exports.memory.buffer, outOffset, 4)[0] =
sign * val;
return 0;
},
find: (latest, outOffset) => {
const wordAddr = new Int32Array(
this.core.exports.memory.buffer,
outOffset - 4,
4
)[0];
const length = new Uint32Array(
this.core.exports.memory.buffer,
wordAddr,
4
)[0];
const word = new Uint8Array(
this.core.exports.memory.buffer,
wordAddr + 4,
length
);
const out = new Int32Array(
this.core.exports.memory.buffer,
outOffset - 4,
8
);
// console.log("FIND", wordAddr, length, word);
const u8 = new Uint8Array(
this.core.exports.memory.buffer,
0x0000,
0x4000
);
const s4 = new Int32Array(
this.core.exports.memory.buffer,
0x0000,
0x4000
);
let p = latest;
while (p != 0) {
// console.log("P", p);
const wordLength = u8[p + 4] & 0x1f;
const immediate = (u8[p + 4] & 0x80) != 0;
if (wordLength === length) {
let ok = true;
for (let i = 0; i < length; ++i) {
if (word[i] !== u8[p + 5 + i]) {
ok = false;
break;
}
}
if (ok) {
// console.log("Found!");
out[0] = p;
out[1] = immediate ? 1 : -1;
return;
}
}
p = s4[p / 4];
}
out[1] = 0;
// console.log("Not found");
}
}
})
)
.then(instance => {
this.core = instance.instance;
table = this.core.exports.table;
this._internal = this.core.exports; // For testing
});
}
read(s) {
const data = new TextEncoder().encode(s);
for (let i = data.length - 1; i >= 0; --i) {
this.buffer.push(data[i]);
}
}
}
export default WAForth;

16
src/shell/index.css Normal file
View file

@ -0,0 +1,16 @@
body {
height: 100vh;
padding: 0;
margin: 0;
}
.terminal {
box-sizing: border-box;
border: none;
padding: 1em;
height: 100%;
width: 100%;
font-family: monospace;
background-color: black;
color: white;
}

12
src/shell/index.html Normal file
View file

@ -0,0 +1,12 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>WAForth</title>
<link rel="stylesheet" type="text/css" href="./index.css">
</head>
<body>
<textarea autofocus id="terminal" class="terminal"></textarea>
<script src="./index.js"></script>
</body>
</html>

28
src/shell/index.js Normal file
View file

@ -0,0 +1,28 @@
import WAForth from "./WAForth";
const forth = new WAForth();
const terminal = document.getElementById("terminal");
forth.onEmit = c => {
terminal.value = terminal.value + String.fromCharCode(c);
};
forth.start().then(() => {
const command = [];
terminal.addEventListener("keydown", function(ev) {
var isPrintable =
!ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey;
if (ev.keyCode == 13) {
ev.preventDefault();
console.log("SUBMIT", command.join(""));
window.setTimeout(() => {
ev.target.value = ev.target.value + " ok\n";
}, 500);
} else if (ev.keyCode == 8) {
if (command.length > 0) {
command.pop();
}
} else if (isPrintable) {
command.push(ev.key);
}
});
});

66
src/tools/assembler.rkt Normal file
View file

@ -0,0 +1,66 @@
#lang racket
(define (asm-symbol? x)
(and (symbol? x) (eq? (string-ref (symbol->string x) 0) #\!)))
(define (preprocess x)
(cond ((list? x)
(cond ((null? x) '())
((and (list? (car x)) (asm-symbol? (caar x)))
(append (eval (car x)) (preprocess (cdr x))))
(else
(cons (preprocess (car x)) (preprocess (cdr x))))))
((asm-symbol? x)
(eval x))
(else x)))
(define (byte->hex byte)
(define digits "0123456789ABCDEF")
(string (string-ref digits (quotient byte 16))
(string-ref digits (remainder byte 16))))
(define (byte->string byte)
(cond ((> byte 255) (raise "Illegal byte"))
((and (> byte 32) (< byte 127) (not (or (eqv? byte 34) (eqv? byte 92))))
(string (integer->char byte)))
(else (string-append "\\" (byte->hex byte)))))
(define (string-escape s)
(string-join (map byte->string (map char->integer (string->list s))) ""))
(define (serialize x)
(cond ((list? x)
(string-append "(" (string-join (map serialize x) " ") ")"))
((string? x)
(string-append "\"" (string-escape x) "\""))
((symbol? x)
(symbol->string x))
((number? x)
(number->string x))
((bytes? x)
(string-append
"\""
(string-join (map byte->string (bytes->list x)) "")
"\""))
(else (raise (list "Unexpected type" x)))))
(define (priority x)
(cond ((eq? x 'module) 0)
((and (list? x) (eq? (car x) 'import)) 1)
((and (list? x) (eq? (car x) 'table)) 2)
((and (list? x) (eq? (car x) 'memory)) 2)
((and (list? x) (eq? (car x) 'global)) 3)
(else 100)))
(define (wasm-assemble module)
(display
(serialize
(sort (preprocess module)
(lambda (x y) (< (priority x) (priority y)))))))
(define-syntax module
(syntax-rules ()
((_ arg ...)
(wasm-assemble '(module arg ...)))))
(provide module)

47
src/tools/quadruple.wat Normal file
View file

@ -0,0 +1,47 @@
;; Template for defining 'word' modules
;; Used to 'reverse engineer' the binary code to emit from the compiler
(module
(import "env" "table" (table 4 anyfunc))
(import "env" "tableBase" (global $tableBase i32))
(type $void (func))
(type $push (func (param i32)))
(type $pop (func (result i32)))
(type $endLoop (func (param i32) (result i32)))
(func $word
;; Push
(call_indirect (type $push) (i32.const 43) (i32.const 1))
;; Word call
(call_indirect (type $void) (i32.const 10))
;; Conditional
(if (i32.ne (call_indirect (type $pop) (i32.const 2)) (i32.const 0))
(then
(nop)
(nop))
(else
(nop)
(nop)
(nop)))
;; do loop
(call_indirect (type $void) (i32.const 4))
(block $endDoLoop
(loop $doLoop
(nop)
(br_if $endDoLoop (call_indirect (type $endLoop) (i32.const 1) (i32.const 3)))
(br $doLoop)))
;; repeat loop
(block $endRepeatLoop
(loop $repeatLoop
(nop)
(br_if $endRepeatLoop (i32.eqz (call_indirect (type $pop) (i32.const 2))))
(nop)
(br $repeatLoop)))
(call $word))
(elem (get_global $tableBase) $word))

1047
src/waforth.wat Normal file

File diff suppressed because it is too large Load diff

4
tests/.eslintrc Normal file
View file

@ -0,0 +1,4 @@
env:
mocha: true
globals:
expect: false

12
tests/index.html Normal file
View file

@ -0,0 +1,12 @@
<html>
<head>
<meta charset="utf-8">
<title>Mocha Tests</title>
<link href="https://cdn.rawgit.com/mochajs/mocha/2.2.5/mocha.css" rel="stylesheet" />
</head>
<body>
<h1 style="font-family: sans-serif; margin: 1rem; ">WAForth Test Suite</h1>
<div style="margin-top: 30px;" id="mocha"></div>
<script src="./index.js"></script>
</body>
</html>

948
tests/index.js Normal file
View file

@ -0,0 +1,948 @@
import WAForth from "../src/shell/WAForth";
import { mocha } from "mocha";
import { expect } from "chai";
mocha.setup("bdd");
const WORD_BASE = 0x200;
describe("WAForth", () => {
let forth, stack, word, output, core, memory, memory8;
beforeEach(() => {
forth = new WAForth();
forth.onEmit = c => {
output.push(c);
};
const x = forth.start().then(() => {
core = forth._internal;
output = [];
memory = new Int32Array(core.memory.buffer, 0, 0x4000);
memory8 = new Uint8Array(core.memory.buffer, 0, 0x4000);
// dictionary = new Uint8Array(core.memory.buffer, 0x1000, 0x1000);
word = new Uint8Array(core.memory.buffer, WORD_BASE + 4, 0x20);
stack = new Int32Array(core.memory.buffer, core.tos(), 0x100);
});
return x;
});
// eslint-disable-next-line no-unused-vars
function dumpTable() {
for (let i = 0; i < core.table.length; ++i) {
console.log("table", i, core.table.get(i));
}
}
function getWordLength() {
return new Int32Array(core.memory.buffer, WORD_BASE, 4)[0];
}
function run(s) {
forth.read(s);
const r = core.interpret();
expect(r).to.not.be.below(0);
return r;
}
function here() {
run("HERE");
const result = stack[0];
run("DROP");
return result;
}
describe("leb128", () => {
it("should convert 0x0", () => {
const r = core.leb128(0x0, 0x0);
expect(r).to.eql(0x1);
expect(memory8[0]).to.eql(0x0);
});
it("should convert 0x17", () => {
const r = core.leb128(0x0, 0x17);
expect(r).to.eql(0x1);
expect(memory8[0]).to.eql(0x17);
});
it("should convert 0x80", () => {
const r = core.leb128(0x0, 0x80);
expect(r).to.eql(0x2);
expect(memory8[0]).to.eql(0x80);
expect(memory8[1]).to.eql(0x01);
});
it("should convert 0x12345", () => {
const r = core.leb128(0x0, 0x12345);
expect(r).to.eql(0x3);
expect(memory8[0]).to.eql(0xc5);
expect(memory8[1]).to.eql(0xc6);
expect(memory8[2]).to.eql(0x04);
});
it("should convert -1", () => {
const r = core.leb128(0x0, -1);
expect(r).to.eql(0x1);
expect(memory8[0]).to.eql(0x7f);
});
it("should convert -0x12345", () => {
const r = core.leb128(0x0, -0x12345);
expect(r).to.eql(0x3);
expect(memory8[0]).to.eql(0xbb);
expect(memory8[1]).to.eql(0xb9);
expect(memory8[2]).to.eql(0x7b);
});
});
describe("leb128-4p", () => {
it("should convert 0x0", () => {
expect(core.leb128_4p(0x0)).to.eql(0x808080);
});
it("should convert 0x17", () => {
expect(core.leb128_4p(0x17)).to.eql(0x808097);
});
it("should convert 0x80", () => {
expect(core.leb128_4p(0x80)).to.eql(0x808180);
});
it("should convert 0xBADF00D", () => {
expect(core.leb128_4p(0xbadf00d)).to.eql(0x5db7e08d);
});
it("should convert 0xFFFFFFF", () => {
expect(core.leb128_4p(0xfffffff)).to.eql(0x7fffffff);
});
});
describe("interpret", () => {
it("should return an error when word is not found", () => {
forth.read("BADWORD");
expect(core.interpret()).to.eql(-1);
});
it("should interpret a positive number", () => {
forth.read("123");
expect(core.interpret()).to.eql(0);
expect(stack[0]).to.eql(123);
});
it("should interpret a negative number", () => {
forth.read("-123");
expect(core.interpret()).to.eql(0);
expect(stack[0]).to.eql(-123);
});
it("should fail on half a word", () => {
forth.read("23FOO");
expect(core.interpret()).to.eql(-1);
});
});
describe("DUP", () => {
it("should work", () => {
run("121");
run("DUP");
expect(stack[0]).to.eql(121);
expect(stack[1]).to.eql(121);
});
});
describe("?DUP", () => {
it("should duplicate when not 0", () => {
run("121");
run("?DUP 5");
expect(stack[0]).to.eql(121);
expect(stack[1]).to.eql(121);
expect(stack[2]).to.eql(5);
});
it("should not duplicate when 0", () => {
run("0");
run("?DUP 5");
expect(stack[0]).to.eql(0);
expect(stack[1]).to.eql(5);
});
});
describe("2DUP", () => {
it("should work", () => {
run("222");
run("173");
run("2DUP");
run("5");
expect(stack[0]).to.eql(222);
expect(stack[1]).to.eql(173);
expect(stack[2]).to.eql(222);
expect(stack[3]).to.eql(173);
expect(stack[4]).to.eql(5);
});
});
describe("ROT", () => {
it("should work", () => {
run("1 2 3 ROT 5");
expect(stack[0]).to.eql(2);
expect(stack[1]).to.eql(3);
expect(stack[2]).to.eql(1);
expect(stack[3]).to.eql(5);
});
});
describe("*", () => {
it("should multiply", () => {
run("3");
run("4");
run("*");
run("5");
expect(stack[0]).to.eql(12);
expect(stack[1]).to.eql(5);
});
it("should multiply negative", () => {
run("-3");
run("4");
run("*");
run("5");
expect(stack[0]).to.eql(-12);
expect(stack[1]).to.eql(5);
});
});
describe("+", () => {
it("should add", () => {
run("3");
run("4");
run("+");
run("5");
expect(stack[0]).to.eql(7);
expect(stack[1]).to.eql(5);
});
});
describe("-", () => {
it("should subtract", () => {
run("8 5 - 5");
expect(stack[0]).to.eql(3);
expect(stack[1]).to.eql(5);
});
it("should subtract to negative", () => {
run("8 13 - 5");
expect(stack[0]).to.eql(-5);
expect(stack[1]).to.eql(5);
});
it("should subtract negative", () => {
run("8 -3 - 5");
expect(stack[0]).to.eql(11);
expect(stack[1]).to.eql(5);
});
});
describe("/", () => {
it("should divide", () => {
run("15 5 / 5");
expect(stack[0]).to.eql(3);
expect(stack[1]).to.eql(5);
});
it("should divide negative", () => {
run("15 -5 / 5");
expect(stack[0]).to.eql(-3);
expect(stack[1]).to.eql(5);
});
});
describe("1+", () => {
it("should work with positive numbers", () => {
run("3");
run("1+");
run("5");
expect(stack[0]).to.eql(4);
expect(stack[1]).to.eql(5);
});
it("should work with negative numbers", () => {
run("-3");
run("1+");
run("5");
expect(stack[0]).to.eql(-2);
expect(stack[1]).to.eql(5);
});
});
describe("1-", () => {
it("should work with positive numbers", () => {
run("3");
run("1-");
run("5");
expect(stack[0]).to.eql(2);
expect(stack[1]).to.eql(5);
});
it("should work with negative numbers", () => {
run("-3");
run("1-");
run("5");
expect(stack[0]).to.eql(-4);
expect(stack[1]).to.eql(5);
});
});
describe(">", () => {
it("should test true when greater", () => {
run("5");
run("3");
run(">");
run("5");
expect(stack[0]).to.eql(-1);
expect(stack[1]).to.eql(5);
});
it("should test false when smaller", () => {
run("3");
run("5");
run(">");
run("5");
expect(stack[0]).to.eql(0);
expect(stack[1]).to.eql(5);
});
it("should test false when equal", () => {
run("5");
run("5");
run(">");
run("5");
expect(stack[0]).to.eql(0);
expect(stack[1]).to.eql(5);
});
it("should work with negative numbers", () => {
run("5");
run("-3");
run(">");
run("5");
expect(stack[0]).to.eql(-1);
expect(stack[1]).to.eql(5);
});
});
describe("NEGATE", () => {
it("should negate positive number", () => {
run("7 NEGATE 5");
expect(stack[0]).to.eql(-7);
expect(stack[1]).to.eql(5);
});
it("should negate negative number", () => {
run("-7 NEGATE 5");
expect(stack[0]).to.eql(7);
expect(stack[1]).to.eql(5);
});
it("should negate negative zero", () => {
run("0 NEGATE 5");
expect(stack[0]).to.eql(0);
expect(stack[1]).to.eql(5);
});
});
describe("0=", () => {
it("should test true", () => {
run("0");
run("0=");
run("5");
expect(stack[0]).to.eql(-1);
expect(stack[1]).to.eql(5);
});
it("should test false", () => {
run("23");
run("0=");
run("5");
expect(stack[0]).to.eql(0);
expect(stack[1]).to.eql(5);
});
});
describe("0>", () => {
it("should test true", () => {
run("2");
run("0>");
run("5");
expect(stack[0]).to.eql(-1);
expect(stack[1]).to.eql(5);
});
it("should test false", () => {
run("-3");
run("0>");
run("5");
expect(stack[0]).to.eql(0);
expect(stack[1]).to.eql(5);
});
});
describe("OVER", () => {
it("should work", () => {
run("12");
run("34");
run("OVER");
run("5");
expect(stack[0]).to.eql(12);
expect(stack[1]).to.eql(34);
expect(stack[2]).to.eql(12);
expect(stack[3]).to.eql(5);
});
});
describe("SWAP", () => {
it("should work", () => {
run("12");
run("34");
run("SWAP");
run("5");
expect(stack[0]).to.eql(34);
expect(stack[1]).to.eql(12);
expect(stack[2]).to.eql(5);
});
});
describe("EMIT", () => {
it("should work once", () => {
run("87");
run("EMIT");
expect(output).to.eql([87]);
});
it("should work twice", () => {
run("97");
run("87");
run("EMIT");
run("EMIT");
expect(output).to.eql([87, 97]);
});
});
describe("DROP", () => {
it("should drop", () => {
run("222");
run("173");
run("DROP");
run("190");
expect(stack[0]).to.eql(222);
expect(stack[1]).to.eql(190);
});
});
describe("ERASE", () => {
it("should erase", () => {
const ptr = here();
memory8[ptr] = 222;
memory8[ptr + 1] = 173;
memory8[ptr + 2] = 190;
memory8[ptr + 3] = 239;
run((ptr + 1).toString(10));
run("2 ERASE 5");
expect(memory8[ptr + 0]).to.eql(222);
expect(memory8[ptr + 1]).to.eql(0x00);
expect(memory8[ptr + 2]).to.eql(0x00);
expect(memory8[ptr + 3]).to.eql(239);
expect(stack[0]).to.eql(5);
});
});
describe("IF/ELSE/THEN", () => {
it("should take the then branch without else", () => {
run(`: FOO IF 8 THEN ;`);
run("1 FOO 5");
expect(stack[0]).to.eql(8);
expect(stack[1]).to.eql(5);
});
it("should not take the then branch without else", () => {
run(": FOO");
run("IF");
run("8");
run("THEN");
run("0");
run(";");
run("FOO");
run("5");
expect(stack[0]).to.eql(5);
});
it("should take the then branch with else", () => {
run(": FOO");
run("IF");
run("8");
run("ELSE");
run("9");
run("THEN");
run(";");
run("1");
run("FOO");
run("5");
expect(stack[0]).to.eql(8);
expect(stack[1]).to.eql(5);
});
it("should take the else branch with else", () => {
run(": FOO");
run("IF");
run("8");
run("ELSE");
run("9");
run("THEN");
run(";");
run("0");
run("FOO");
run("5");
expect(stack[0]).to.eql(9);
expect(stack[1]).to.eql(5);
});
it("should support nested if", () => {
run(`: FOO
IF
IF 8 ELSE 9 THEN
10
ELSE
11
THEN
;`);
run("0 1 FOO 5");
expect(stack[0]).to.eql(9);
expect(stack[1]).to.eql(10);
expect(stack[2]).to.eql(5);
});
});
describe("DO/LOOP", () => {
it("should run a loop", () => {
run(`: FOO 4 0 DO 3 LOOP ;`);
run("FOO 5");
expect(stack[0]).to.eql(3);
expect(stack[1]).to.eql(3);
expect(stack[2]).to.eql(3);
expect(stack[3]).to.eql(3);
expect(stack[4]).to.eql(5);
});
it("should run a nested loop", () => {
run(`: FOO 3 0 DO 2 0 DO 3 LOOP LOOP ;`);
run("FOO 5");
expect(stack[0]).to.eql(3);
expect(stack[1]).to.eql(3);
expect(stack[2]).to.eql(3);
expect(stack[3]).to.eql(3);
expect(stack[4]).to.eql(3);
expect(stack[5]).to.eql(3);
expect(stack[6]).to.eql(5);
});
});
describe("+LOOP", () => {
it("should increment a loop", () => {
run(`: FOO 10 0 DO 3 2 +LOOP ;`);
run("FOO 5");
expect(stack[0]).to.eql(3);
expect(stack[1]).to.eql(3);
expect(stack[2]).to.eql(3);
expect(stack[3]).to.eql(3);
expect(stack[4]).to.eql(3);
expect(stack[5]).to.eql(5);
});
});
describe("I", () => {
it("should work", () => {
run(`: FOO 4 0 DO I LOOP ;`);
run("FOO 5");
expect(stack[0]).to.eql(0);
expect(stack[1]).to.eql(1);
expect(stack[2]).to.eql(2);
expect(stack[3]).to.eql(3);
expect(stack[4]).to.eql(5);
});
it("should work in a nested loop", () => {
run(`: FOO 3 0 DO 2 0 DO I LOOP LOOP ;`);
run("FOO 5");
expect(stack[0]).to.eql(0);
expect(stack[1]).to.eql(1);
expect(stack[2]).to.eql(0);
expect(stack[3]).to.eql(1);
expect(stack[4]).to.eql(0);
expect(stack[5]).to.eql(1);
expect(stack[6]).to.eql(5);
});
});
describe("J", () => {
it("should work", () => {
run(`: FOO 3 0 DO 2 0 DO J LOOP LOOP ;`);
run("FOO 5");
expect(stack[0]).to.eql(0);
expect(stack[1]).to.eql(0);
expect(stack[2]).to.eql(1);
expect(stack[3]).to.eql(1);
expect(stack[4]).to.eql(2);
expect(stack[5]).to.eql(2);
expect(stack[6]).to.eql(5);
});
it("should work in a nested loop", () => {
run(`: FOO 3 0 DO 2 0 DO J LOOP LOOP ;`);
run("FOO 5");
expect(stack[0]).to.eql(0);
expect(stack[1]).to.eql(0);
expect(stack[2]).to.eql(1);
expect(stack[3]).to.eql(1);
expect(stack[4]).to.eql(2);
expect(stack[5]).to.eql(2);
expect(stack[6]).to.eql(5);
});
});
describe("BEGIN / WHILE / REPEAT", () => {
it("should work", () => {
run(`: FOO BEGIN DUP 2 * DUP 16 < WHILE DUP REPEAT 7 ;`);
run("1 FOO 5");
expect(stack[0]).to.eql(1);
expect(stack[1]).to.eql(2);
expect(stack[2]).to.eql(2);
expect(stack[3]).to.eql(4);
expect(stack[4]).to.eql(4);
expect(stack[5]).to.eql(8);
expect(stack[6]).to.eql(8);
expect(stack[7]).to.eql(16);
expect(stack[8]).to.eql(7);
expect(stack[9]).to.eql(5);
});
});
describe("word", () => {
it("should read a word", () => {
forth.read(" FOO BAR BAZ ");
core.word();
expect(getWordLength()).to.eql(3);
expect([word[0], word[1], word[2]]).to.eql([70, 79, 79]);
});
it("should read two words", () => {
forth.read(" FOO BAR BAZ ");
core.word();
core.word();
expect(getWordLength()).to.eql(3);
expect([word[0], word[1], word[2]]).to.eql([66, 65, 82]);
});
it("should skip comments", () => {
forth.read(" \\ FOO BAZ\n BART BAZ");
core.word();
expect(getWordLength()).to.eql(4);
expect([word[0], word[1], word[2], word[3]]).to.eql([66, 65, 82, 84]);
});
it("should stop at end of buffer while parsing word", () => {
forth.read("FOO");
core.word();
expect(getWordLength()).to.eql(3);
expect([word[0], word[1], word[2]]).to.eql([70, 79, 79]);
});
it("should stop at end of buffer while parsing comments", () => {
forth.read(" \\FOO");
core.word();
expect(getWordLength()).to.eql(0);
expect([word[0]]).to.eql([0]);
});
it("should stop when parsing empty line", () => {
forth.read(" ");
core.word();
expect(getWordLength()).to.eql(0);
expect([word[0]]).to.eql([0]);
});
it("should stop when parsing nothing", () => {
forth.read("");
core.word();
expect(getWordLength()).to.eql(0);
expect([word[0]]).to.eql([0]);
});
});
describe("FIND", () => {
it("should find a word", () => {
forth.read("DUP");
core.word();
core.push(WORD_BASE);
core.FIND();
expect(stack[0]).to.eql(8520);
expect(stack[1]).to.eql(-1);
});
it("should find a short word", () => {
forth.read("!");
core.word();
core.push(WORD_BASE);
core.FIND();
expect(stack[0]).to.eql(8192);
expect(stack[1]).to.eql(-1);
});
it("should find an immediate word", () => {
forth.read("+LOOP");
core.word();
core.push(WORD_BASE);
core.FIND();
expect(stack[0]).to.eql(8228);
expect(stack[1]).to.eql(1);
});
it("should not find an unexisting word", () => {
forth.read("BADWORD");
core.word();
core.push(WORD_BASE);
core.FIND();
expect(stack[0]).to.eql(WORD_BASE);
expect(stack[1]).to.eql(0);
});
it("should not find a very long unexisting word", () => {
forth.read("VERYVERYVERYBADWORD");
core.word();
core.push(WORD_BASE);
core.FIND();
expect(stack[0]).to.eql(WORD_BASE);
expect(stack[1]).to.eql(0);
});
});
describe("BASE", () => {
it("should contain the base", () => {
run("BASE @ 5");
expect(stack[0]).to.eql(10);
expect(stack[1]).to.eql(5);
});
});
describe("KEY", () => {
it("should read a key", () => {
run("KEY F");
run("5");
expect(stack[0]).to.eql(70);
expect(stack[1]).to.eql(5);
});
});
describe("LITERAL", () => {
it("should put a literal on the stack", () => {
run("20 : FOO LITERAL ;");
run("5 FOO");
expect(stack[0]).to.eql(5);
expect(stack[1]).to.eql(20);
});
});
describe("[ / ]", () => {
it("should work", () => {
run(": FOO [ 20 5 * ] LITERAL ;");
run("5 FOO 6");
expect(stack[0]).to.eql(5);
expect(stack[1]).to.eql(100);
expect(stack[2]).to.eql(6);
});
});
describe("C@", () => {
it("should fetch an aligned character", () => {
const ptr = here();
memory8[ptr] = 222;
memory8[ptr + 1] = 173;
run(ptr.toString());
run("C@");
expect(stack[0]).to.eql(222);
});
it("should fetch an unaligned character", () => {
const ptr = here();
memory8[ptr] = 222;
memory8[ptr + 1] = 173;
run((ptr + 1).toString());
run("C@");
expect(stack[0]).to.eql(173);
});
});
describe("C!", () => {
it("should store an aligned character", () => {
const ptr = here();
memory8[ptr] = 222;
memory8[ptr + 1] = 173;
run("190");
run(ptr.toString());
run("C! 5");
expect(stack[0]).to.eql(5);
expect(memory8[ptr]).to.eql(190);
expect(memory8[ptr + 1]).to.eql(173);
});
it("should store an unaligned character", () => {
const ptr = here();
memory8[ptr] = 222;
memory8[ptr + 1] = 173;
run("190");
run((ptr + 1).toString());
run("C! 5");
expect(stack[0]).to.eql(5);
expect(memory8[ptr]).to.eql(222);
expect(memory8[ptr + 1]).to.eql(190);
});
});
describe("@", () => {
it("should fetch", () => {
const ptr = here();
memory[ptr / 4] = 123456;
run(ptr.toString());
run("@ 5");
expect(stack[0]).to.eql(123456);
expect(stack[1]).to.eql(5);
});
});
describe("!", () => {
it("should store", () => {
const ptr = here();
run("12345");
run(ptr.toString());
run("! 5");
expect(stack[0]).to.eql(5);
expect(memory[ptr / 4]).to.eql(12345);
});
});
describe(",", () => {
it("should add word", () => {
run("HERE");
run("1234");
run(",");
run("HERE");
expect(stack[1] - stack[0]).to.eql(4);
expect(memory[stack[0] / 4]).to.eql(1234);
});
});
describe("RECURSE", () => {
it("should recurse", () => {
run(": FOO DUP 4 < IF DUP 1+ RECURSE ELSE 12 THEN 13 ;");
run("1 FOO 5");
expect(stack[0]).to.eql(1);
expect(stack[1]).to.eql(2);
expect(stack[2]).to.eql(3);
expect(stack[3]).to.eql(4);
expect(stack[4]).to.eql(12);
expect(stack[5]).to.eql(13);
expect(stack[6]).to.eql(13);
expect(stack[7]).to.eql(13);
expect(stack[8]).to.eql(13);
expect(stack[9]).to.eql(5);
});
});
describe("CREATE", () => {
it("should create words", () => {
run("HERE");
run("LATEST");
run("CREATE DUP");
run("HERE");
run("LATEST");
expect(stack[2] - stack[0]).to.eql(4 + 4);
expect(stack[3]).to.eql(stack[0]);
expect(stack[3]).to.not.eql(stack[1]);
});
it("should create findable words", () => {
run("CREATE FOOBAR");
run("LATEST");
run("CREATE BAM");
forth.read("FOOBAR");
core.word();
core.push(WORD_BASE);
core.FIND();
expect(stack[1]).to.eql(stack[0]);
expect(stack[2]).to.eql(-1);
});
it("should align unaligned words", () => {
run("CREATE DUPE");
run("HERE");
expect(stack[0] % 4).to.eql(0);
});
it("should align aligned words", () => {
run("CREATE DUP");
run("HERE");
expect(stack[0] % 4).to.eql(0);
});
});
describe("IMMEDIATE", () => {
it("should make words immediate", () => {
run("CREATE FOOBAR IMMEDIATE");
forth.read("FOOBAR");
core.word();
core.push(WORD_BASE);
core.FIND();
expect(stack[1]).to.eql(1);
});
});
describe(":", () => {
it("should compile multiple instructions", () => {
run(": FOOBAR 4 * ;");
run("3 FOOBAR");
expect(stack[0]).to.eql(12);
});
it("should compile negative numbers", () => {
run(": FOOBAR -4 * ;");
run("3 FOOBAR");
expect(stack[0]).to.eql(-12);
});
it("should compile large numbers", () => {
run(": FOOBAR 111111 * ;");
run("3 FOOBAR");
expect(stack[0]).to.eql(333333);
});
});
describe("system", () => {
it.skip("should run sieve", () => {
run(`
: prime? ( n -- ? ) HERE + C@ 0= ;
: composite! ( n -- ) HERE + 1 SWAP C! ;
: sieve ( n -- )
HERE OVER ERASE
2
BEGIN
2DUP DUP * >
WHILE
DUP prime? IF
2DUP DUP * DO
I composite!
DUP +LOOP
THEN
1+
REPEAT
DROP
." Primes: " 2 DO I prime? IF I . THEN LOOP
;`);
});
});
});
// mocha.checkLeaks();
mocha.globals(["jQuery"]);
mocha.run();

4035
yarn.lock Normal file

File diff suppressed because it is too large Load diff