Recently I read this series of articles (opens new window). This ticks interests on mine: programming language design and the bootstrap paradox (opens new window). It also warmed up my curiosity over the Forth language (opens new window), which I read about before, but never deeper than the basic "it is a language with everything in a stack".
Going over the articles I was having a hard time to understand the steps taken, so I ended up reading the Starting Forth Book (opens new window) to see if learning more of it would help, which increased even more my interest in the language and I started looking for a project to start playing with it. (Turns out that the author actually provided good explanations to what it was doing, it just happens that he was doing something that is hard/complex)
Obviously my first idea was to bootstrap Forth too. But I don't really have practical experience on assembly (except 1 hour debug.com
lesson on college) nor bootloaders, so instead I decided using the universal platform of our times: The Web. Yes, Javascript (and HTML and CSS).
The most straightforward way I come up was to use document.write()
to output and window.prompt()
to input. But this is very annoying and makes the browser hate you, so I quickly discarded this thought.
Then I came up with the idea of a mini terminal-like interface, where you type your input in a <input>
and hit enter to process it. Just above this input we should the previously entered commands and results and voilà, we have a minimal terminal. Like this:
After toying with it a bit and get to a design I found nice enough, I decided to use Vue for its logic, so I could integrate it on the blog more easily, maybe even create a Component around it and install. The prototype is here:
The prototype script starts with the Vue App creation:
const app = new Vue({
el: '#console',
data: {
inputLine: '',
history: [],
prompt: '? ',
inputHandler: handleInput,
},
// ...
}
The el
field just points the id of our main element. The data fields are:
- inputLine: stores the string shown on the text field on the bottom. It is kept in sync with its changes;
- history: It is the already typed commands and outputs. It could be a single string we keep appending, but the list makes a little easier to reason about (also it likely gets better performance, although it is not my focus).
- prompt: This text goes just before the input field, like a prompt. It is actually just the most recent output after the last new line.
- inputHandler: A function to be called when the user press Return on the input field. Here it is a data field, but when I turn this into a component it will become a prop, so we can have code like this:
<script> function handleInput(text) { /* ~~~~ Magic ~~~~ */ } </script> <TerminalV1 :handler="handleInput" />
We have some methods, the most important one being commitLine()
and outputLine()
const app = new Vue({
// ...
methods: {
commitLine() {
const input = this.inputLine
this.inputLine = ''
this.outputLine(input)
// here we call some handler method
this.inputHandler(input)
},
// ...
outputLine(text) {
text = this.prompt + (text || '')
this.history.push(text)
this.prompt = ''
}
},
}
The commitLine()
, is called when the user hits Return on the input field. It gets the field's contents, erase it, echoes it (through outputLine) and then forwards the content to the inputHandler()
.
The outputLine()
, just concatenate the current prompt with the given text and push it to history, the Vue's reactivity mechanism will do the actual dom update; It also clears the prompt, so the inputHandler must add one manually if needed (through the sister method output()).
Finally, to be able to test it, I wrote (somewhat poorly) a small Brainfuck (opens new window) interpreter, as it is the easier language to implement that I could think of. To those unfamiliar with it, Brainfuck operates with a indexed memory with typically 30000 cells, and all characters are considered comments, except those in the table bellow (from the cited wikipedia article):
Char | Meaning |
---|---|
> | Increment the data pointer (to point to the next cell to the right). |
< | Decrement the data pointer (to point to the next cell to the left). |
+ | Increment (increase by one) the byte at the data pointer. |
- | Decrement (decrease by one) the byte at the data pointer. |
. | Output the byte at the data pointer. |
, | Accept one byte of input, storing its value in the byte at the data pointer. |
[ | If the byte at the data pointer is zero, then instead of moving the instruction pointer forward to the next command, jump it forward to the command after the matching ] command. |
] | If the byte at the data pointer is nonzero, then instead of moving the instruction pointer forward to the next command, jump it back to the command after the matching [ command. |
This is a fairly standard interpreter, with the addition of !c
to clear the screen (I could add a button, but I decide to postpone it to TerminalV2), !r
to reset all memory cells and !s
, to present all non-zero memory cells. You can test it with the following hello world code, just be sure to paste it all at once, not line by line:
++++++++++[>+>+++>++++>+++++++>++++++++>+++++++++>++
++++++++>+++++++++++>++++++++++++<<<<<<<<<-]>>>>+.>>>
>+..<.<++++++++.>>>+.<<+.<<<<++++.<++.>>>+++++++.>>>.+++.
<+++++++.--------.<<<<<+.<+++.---.
As it was not the focus of this post, understanding inputHandler()
code is left as an exercise to the reader 😄
Oh, and the component is here bellow, with the same brainfuck interpreter working:
Now I have a good enough environment, I will start working on my Forth, probably. See you next time!