testing system api revision
[watForth.git] / forth.js
1 /* This program is free software: you can redistribute it and/or modify
2 it under the terms of the GNU General Public License as published by
3 the Free Software Foundation, either version 3 of the License, or
4 (at your option) any later version.
5
6 This program is distributed in the hope that it will be useful,
7 but WITHOUT ANY WARRANTY; without even the implied warranty of
8 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 GNU General Public License for more details.
10
11 You should have received a copy of the GNU General Public License
12 along with this program. If not, see <http://www.gnu.org/licenses/>. */
13 'use strict'
14
15 /* State */
16 const output = {
17 print: (string) => {},
18 updateViews: () => {}
19 }
20 const channels = []
21 let wasmMem, forth
22
23 /* Init */
24 Promise.all([
25 fetch('forth.forth', {credentials: 'include', headers:{'content-type':'text/plain'}}).then((re) => re.text()),
26 fetch('forth.wasm', {credentials: 'include', headers:{'content-type':'application/wasm'}}).then(re => re.arrayBuffer())
27 ]).then((results) => {
28 WebAssembly.instantiate(results[1], wasmImport).then((module) => {
29 wasmMem = module.instance.exports.memory.buffer
30 forth = (chno, data) => {
31 if (typeof chno !== "number") {
32 data = chno
33 chno = 0
34 }
35 channels[chno].writeEnv(data)
36 if (chno === 0)
37 output.print(data)
38 module.instance.exports.main(chno)
39 output.updateViews()
40 }
41 console.log('wasm loaded')
42 forth(results[0])
43 })
44 })
45
46 /* Environment functions */
47 const wasmString = (addr, u) =>
48 String.fromCharCode.apply(
49 null,
50 new Uint16Array(wasmMem, addr, u >> 1)
51 )
52 const strToArrayBuffer = (str) => {
53 const buf = new ArrayBuffer(str.length << 1);
54 const bufView = new Uint16Array(buf);
55 for (let i = 0, strLen = str.length; i < strLen; i++)
56 bufView[i] = str.charCodeAt(i);
57 return buf;
58 }
59 const bufJoin = (buf1, buf2) => {
60 const newBuf = new Uint8Array(buf1.byteLength + buf2.byteLength)
61 newBuf.set(new Uint8Array(buf1), 0)
62 newBuf.set(new Uint8Array(buf2), buf1.byteLength)
63 return newBuf.buffer
64 }
65
66 /* I/O Channel Drivers */
67 class Channel {
68 constructor(opt) {
69 opt.chno = channels.indexOf(undefined)
70 if (opt.chno === -1)
71 opt.chno = channels.length
72 Object.assign(this, opt)
73 if (this.buffer === undefined)
74 this.buffer = new ArrayBuffer(0)
75 channels[this.chno] = this
76 return this
77 }
78 read(writeAddr, maxBytes) {
79 const wasmView = new Uint8Array(wasmMem, writeAddr, maxBytes)
80 const bufBytes = Math.min(maxBytes, this.buffer.byteLength)
81 const bufView = new Uint8Array(this.buffer, 0, bufBytes)
82 wasmView.set(bufView, 0)
83 this.buffer = this.buffer.slice(bufBytes)
84 return bufBytes
85 }
86 write(readAddr, maxBytes) {
87 const newBuf = new Uint8Array(this.buffer.byteLength + maxBytes)
88 newBuf.set(new Uint8Array(this.buffer), 0)
89 newBuf.set(new Uint8Array(wasmMem, readAddr, maxBytes), this.buffer.byteLength)
90 this.buffer = newBuf
91 }
92 writeEnv(data) {
93 switch (typeof data) {
94 case "string":
95 this.buffer = bufJoin(this.buffer, strToArrayBuffer(data))
96 break
97 case "object" :
98 if (data instanceof ArrayBuffer)
99 this.buffer = bufJoin(this.buffer, data)
100 else
101 this.buffer = bufJoin(this.buffer, strToArrayBuffer(JSON.stringify(data)))
102 break
103 case "number" :
104 const buf = new ArrayBuffer(4)
105 new DataView(buf, 0, 4).setInt32(data)
106 this.buffer = bufJoin(this.buffer, buf)
107 break
108 default :
109 console.error(`environment wrote unhandled object: ${data}`)
110 return
111 }
112 }
113 }
114 /* 0 STDIN, 1 STDOUT, 2 STDERR */
115 new Channel({
116 write(readAddr, maxBytes) {
117 super.write(readAddr, maxBytes)
118 output.print(wasmString(readAddr, maxBytes))
119 }
120 })
121 new Channel({
122 read(writeAddr, maxBytes) { return 0 },
123 write(readAddr, maxBytes) { output.print(`\\\ => ${wasmString(readAddr, maxBytes)}\n`) }
124 })
125 new Channel({
126 read(writeAddr, maxBytes) { return 0 },
127 write(readAddr, maxBytes) { console.error(wasmString(readAddr, maxBytes)) }
128 })
129
130 /* System runtime */
131 const simstack = []
132 const rstack = []
133 const dictionary = { EXECUTE: 12 }
134 const doesDictionary = {}
135 const wasmImport = {
136 env: {
137 pop: () => simstack.pop(),
138 push: (val) => simstack.push(val),
139 rinit: () => rstack.length = 0,
140 rpop: () => rstack.length ? rstack.pop() : 16388,
141 rpush: (val) => rstack.push(val),
142 sys_write: (chno, addr, u) => channels[chno] === undefined ? 0 : channels[chno].write(addr, u),
143 sys_read: (chno, addr, u) => channels[chno] === undefined ? 0 : channels[chno].read(addr, u),
144 sys_listen: (chno, cbAddr, addr, u) => { //sys_connect?
145 //TODO: call into the module to wasm fn "event" to push an event
146 //to forth, which pushes esi to the return stack and queues the
147 //callback function provided from listen. reqaddr could be
148 //"fetch", in which case no channel is used. otherwise very
149 //similar to sys_fetch.
150
151 //callbacks are events, like listen events
152 //totally event-driven, one listener per event
153 //other listen types: mouse, keyboard, touch, main_loop (10ms interval), wrtc, etc
154 //fetch is different because it offers no "write" interface, doesn't repeat
155 },
156 sys_fetch: (cbAddr, addr, u) => {
157 const str = wasmString(addr, u)
158 console.log(`fetching: ${str}`)
159 const args = JSON.parse(wasmString(addr, u))
160 console.log(args)
161 const url = args.url
162 delete args.url
163 const channel = new Channel()
164 fetch(url, args).then((re) => {
165 if (args.headers === undefined ||
166 args.headers['content-type'] === undefined ||
167 args.headers['content-type'].toLowerCase().indexOf('text/plain') === 0 )
168 re.text().then((txt) => forth(channel.chno, txt))
169 else {
170 const reader = new FileReader()
171 reader.onload = (evt) => forth(channel.chno, evt.target.result)
172 re.blob().then((blob) => reader.readAsArrayBuffer(blob))
173 }
174 }).catch(console.error)
175 //TODO: map to fetch promise, write to channel buffer,
176 //javascript "fetch" as fallback, explicit handles for
177 //"textEntry" or any third party protocols like activitypub
178 return channel.chno
179 },
180 sys_close: (chno) => delete channels[chno],
181 sys_echo: (val, base) => output.print(`\\\ => ${val.toString(base)} `),
182 sys_echochar: (val) => output.print(String.fromCharCode(val)),
183 sys_reflect: (addr) => {
184 console.log(`reflect: ${addr}: ${
185 new DataView(wasmMem, addr, 4)
186 .getUint32(0,true)
187 }`)
188 },
189 vocab_get: (addr, u) => dictionary[wasmString(addr, u).toUpperCase()] || 0,
190 vocab_set: (addr, u, v) => dictionary[wasmString(addr, u).toUpperCase()] = v,
191 does_get: (addr, u) => doesDictionary[wasmString(addr, u).toUpperCase()] || 0,
192 does_set: (addr, u, v) => doesDictionary[wasmString(addr, u).toUpperCase()] = v,
193 is_whitespace: (key) => /\s/.test(String.fromCharCode(key)),
194 sys_stack: () => console.log(`[${simstack}][${rstack}]`),
195 sys_parsenum: (addr, u, base) => {
196 const answer = Number.parseInt(wasmString(addr, u), base)
197 if (Number.isNaN(answer))
198 return -1
199 new DataView(wasmMem, addr, 4).setUint32(0,answer,true)
200 return 0
201 },
202 sys_words: () => {
203 output.print(Object.getOwnPropertyNames(dictionary).toString().split(',').join(' '))
204 }
205 }
206 }
207
208 /* View Logic */
209 window.onload = () => {
210 let forthdiv = document.getElementById("forth")
211 if (forthdiv === null) {
212 forthdiv = document.createElement("div")
213 forthdiv.setAttribute("style","height:100%;margin:auto;width:100%;max-width:640px;overflow-x:hidden;")
214 document.body.appendChild(forthdiv)
215 }
216 const outframe = document.createElement("div")
217 outframe.setAttribute("style", "background-color:black;padding-left:6px;padding-right:6px;color:chartreuse;height:268px;resize:vertical;display:flex;align-items:flex-end;flex-flow:row;")
218 const stackview = document.createElement("pre")
219 stackview.setAttribute("style", "white-space:pre-wrap;flex:0 0 6%;")
220 outframe.appendChild(stackview)
221 const txtoutput = document.createElement("pre")
222 txtoutput.setAttribute("style", "white-space:pre-wrap;max-height:256px;overflow-y:scroll;flex:1 0 342px;")
223 outframe.appendChild(txtoutput)
224 /* const rstackview = document.createElement("pre")
225 rstackview.setAttribute("style", "white-space:pre-wrap;flex:0 1 8%;")
226 outframe.appendChild(rstackview) */
227 const memview = document.createElement("pre")
228 memview.setAttribute("style", "padding-left: 8px; white-space:pre-wrap;flex:0 0 88px;")
229 outframe.appendChild(memview)
230 const txtinput = document.createElement("textarea")
231 txtinput.setAttribute("autofocus", "true")
232 txtinput.setAttribute("wrap", "hard")
233 txtinput.setAttribute("style", "resize:none;white-space:pre;margin-left:6%;width:77%;")
234 txtinput.oninput = () => txtinput.setAttribute("rows", ((txtinput.value.match(/[\n]/g) || [1]).length + 1).toString());
235 txtinput.oninput()
236 txtinput.onkeydown = (event) => {
237 switch (event.key) {
238 case "Enter":
239 if (event.ctrlKey) {
240 const endPos = txtinput.selectionStart + 1
241 txtinput.value =
242 txtinput.value.substring(0, txtinput.selectionStart) +
243 '\n' +
244 txtinput.value.substring(txtinput.selectionEnd, txtinput.value.length)
245 txtinput.setSelectionRange(endPos, endPos)
246 txtinput.oninput()
247 }
248 else {
249 if (!/\s/.test(txtinput.value.slice(-1)))
250 txtinput.value += " "
251 event.preventDefault()
252 event.stopPropagation()
253 forth(txtinput.value)
254 txtinput.value = ""
255 }
256 break
257 case "Backspace":
258 break
259 default:
260 break
261 }
262 }
263 forthdiv.appendChild(outframe)
264 forthdiv.appendChild(txtinput)
265 /* Set up output functions */
266 output.print = (string) => txtoutput.textContent += string,
267 output.updateViews = () => {
268 const base = new DataView(wasmMem, 14348 /* base */, 4).getUint32(0,true)
269 stackview.textContent = simstack.map((v) => v.toString(base)).join('\n')
270 // rstackview.textContent = rstack.join('\n')
271 let cnt = 0;
272 const maxBytes = 64
273 let here = new DataView(wasmMem, 14340 /* here */, 4).getUint32(0,true)
274 memview.textContent = Array.from(new Uint8Array(wasmMem, here - maxBytes, maxBytes), (v) => {
275 cnt++;
276 v = ('0' + (v & 0xFF).toString(16)).slice(-2)
277 if (cnt === maxBytes)
278 return v
279 if ((cnt % 16) === 0)
280 return `${v}\n=> ${(here -maxBytes + cnt).toString(base)}\n`
281 if ((cnt % 4) === 0)
282 return `${v}\n`
283 return `${v} `
284 }).join('')
285 outframe.scrollTop = outframe.scrollHeight
286 txtoutput.scrollTop = txtoutput.scrollHeight
287 }
288 }