xxxxxxxxxx
275
// https://jborza.com/interpreters/2020/05/03/beginning-forth.html
// https://jborza.com/post/2021-02-06-forth-interpreter-continued/
// interpreter + "compiler"
// roughly made to match a low level target implementation (eg. the why word tokens are split and not a string)
//
// * word definition tokens are defined like other Forth words with ';' being immediate, simplifying the implementation
// * added handling of compilation error such as ": a : b ;" as it should cancel the compilation
// * moved error handling to the levaluate function
// * find words in reverse so that latest defined word override previous definitions
//
// perhaps a "reserved" (or hidden ?) field should also be added so ':' and ';' can't be redefined
//
let fmode = 0 // forth mode; 0 for interpreter; 1 for compiler
let stack = [] // forth stack
let curin = [] // currently processed input
let words = [ // the words list with pre defined primitives words
// t: token, f: code, i: immediate
{ t: [':'], // definition mode (compiler mode)
f: [(s) => {
fmode = 1
// consume next token and add to word list
let nextToken = ntoken(curin)
if (nextToken.length) {
words.push({
t: nextToken,
f: [],
i: 0
})
}
}],
i: 0
},
{ t: [';'], // end of definition mode
f: [(s) => {
fmode = 0
}],
i: 1
},
{ t: ['+'], // plus
f: [(s) => {
if (s.length < 2) {
return 1
}
let a = s.pop()
let b = s.pop()
s.push(a + b)
}],
i: 0
},
{ t: ['-'], // minus
f: [(s) => {
if (s.length < 2) {
return 1
}
let b = s.pop()
let a = s.pop()
s.push(b - a)
}],
i: 0
},
{ t: ['.'], // print last entry of the stack
f: [(s) => {
if (!s.length) {
return 1
}
addToLineBuffer(s[s.length - 1] + '')
}],
i: 0
},
{ // print defined words
t: ['w', 'o', 'r', 'd', 's'],
f: [(s) => {
let lwords = []
for (let i = 0; i < words.length; i += 1) {
lwords.push(words[i].t.join(''))
}
addToLineBuffer(lwords.join(' ') + '')
}],
i: 0
}
]
// find token (word) in dictionary
function ftoken(token) {
for (let w = words.length - 1; w >= 0; w -= 1) {
let word = words[w]
if (token.length === word.t.length) {
for (let t = 0; t < token.length; t += 1) {
if (token[t] !== word.t[t]) {
break
} else if (t === token.length - 1) {
return word
}
}
}
}
return null
}
// execute word code
function wexecute(word) {
for (let i = 0; i < word.f.length; i += 1) {
let r = word.f[i](stack)
if (r === 1) {
addToLineBuffer('Stack underflow')
return 0
}
}
return 1
}
// token ('word') evaluator
function tevaluate(token) {
let word = ftoken(token)
if (word) { // process word
if (!fmode || (fmode && word.i)) { // execute word code (when forth mode is either interpreter or compiler / immediate mode)
return wexecute(word)
} else { // compile mode (new word def. body)
let tail = words.length - 1
words[tail].f = words[tail].f.concat(word.f)
return 1
}
} else { // process number (everything which is not considered a valid word)
let i = parseInt(token.join(''), 10)
if (isNaN(i)) {
if (fmode) {
fmode = 0
words.pop()
}
addToLineBuffer(' Undefined word ' + token.join(''))
return 0
} else {
if (fmode) {
// append function which wrap stack push
let tail = words.length - 1
words[tail].f = words[tail].f.concat([() => { stack.push(i) }])
} else {
// push regular number
stack.push(i)
}
return 1
}
}
}
// get next token (consume)
function ntoken(t) {
let token = []
let c = t.shift()
while (c && c !== ' ') {
token.push(c)
c = t.shift()
}
return token
}
// line evaluator
function levaluate(s) {
curin = s
let token
while ((token = ntoken(curin)).length) {
if (!tevaluate(token)) {
return
}
}
buffer[buffer.length - 1] += ' ok'
}
//
// CLI emu.
let buffer = []
let buflen = 42
let linput = ''
let xcursor = 0
function addToLineBuffer(s) {
buffer.push(s)
if (buffer.length > buflen) {
buffer.shift()
}
}
function setup() {
createCanvas(800, 600)
textAlign(LEFT)
textFont('monospace')
textSize(16)
noStroke()
fill(255)
}
function draw() {
background(0)
text('PROTO FORTH', width - 7 * textSize(), 18)
// input / CLI history display
let bottomOffset = 10
for (let i = buffer.length - 1; i >= 0; i -= 1) {
let y = height - bottomOffset - textSize() * (buffer.length - i)
if (y > 0) {
text(buffer[i].toUpperCase(), 4, y)
}
}
let yprompt = height - bottomOffset
text('> ' + linput.toUpperCase(), 4, yprompt)
if ((frameCount >> 4 & 1) === 0) {
let ghost = ' '
for (let i = 0; i <= linput.length; i += 1) {
if (i == xcursor) {
ghost += '_'
} else {
ghost += ' '
}
}
text(ghost, 4, yprompt)
}
}
// input handler (CLI)
function keyTyped() {
if (keyCode !== 13 && keyCode !== 46) {
linput = linput.slice(0, xcursor) + key + linput.substring(xcursor, linput.length)
xcursor += 1
} else if (keyCode === 13) {
addToLineBuffer(linput)
levaluate((linput + ' ').split(''))
linput = ''
xcursor = 0
}
}
function keyPressed() {
if (keyCode === 8) {
linput = linput.slice(0, -1)
if (xcursor > 0) {
xcursor -= 1
}
} else if (keyCode === 37) {
if (xcursor > 0) {
xcursor -= 1
}
} else if (keyCode === 39) {
if (xcursor < linput.length) {
xcursor += 1
}
} else if (keyCode === 46) {
linput = linput.slice(0, xcursor) + linput.substring(xcursor + 1, linput.length)
}
}