sandbox/doc/part-0x02.rst

280 lines
7.8 KiB
ReStructuredText

Write You a Forth, 0x02
-----------------------
:date: 2018-02-22 10:48
:tags: wyaf, forth
The basic framework will consist of two main parts:
1. A modular I/O subsystem: on Linux, it makes sense to use the operating
system's terminal I/O features. On the MSP430, there won't be the luxury
of any operating system and I'll have to build out the I/O facilities. The
I/O interface will be defined in ``io.h``; the build system will eventually
have to decide which interface implementation to bring in.
2. A toplevel function (the C++ ``main`` function, for example) that will
handle starting up the Forth system and bring us into an interpreter. We'll
put this in ``kforth.cc``.
The project will also need a build system. For simplicity, I'll at least start
with a basic Makefile::
# Makefile
CXXSTD := c++14
CXXFLAGS := -std=$(CXXSTD) -Werror -Wall -g -O0
OBJS := linux/io.o \
kforth.o
TARGET := kforth
all: $(TARGET)
$(TARGET): $(OBJS)
$(CXX) $(CFLAGS) -o $@ $(OBJS)
clean:
rm -f $(OBJS) $(TARGET)
A simple frontend
^^^^^^^^^^^^^^^^^
Starting out with the most basic front end; we'll first want to include our I/O
interface::
#include "io.h"
If kforth is running on Linux, and it will be for the first stage, the
frontend should pull in Linux specific pieces. ``linux.h`` is the place
to set up the Linux-specific pieces::
#ifdef __linux__
#include "linux.h"
#endif // __linux__
The interpreter function takes an I/O interface instance, and reads lines in
an infinite loop, printing "ok" after each line is read. I'll go over the
methods called on the ``interface`` instance when I get to the I/O subsystem.
Printing the line buffer right now helps to verify that the I/O subsystem is
working correctly::
static char ok[] = "ok.\n";
static void
interpreter(IO &interface)
{
static size_t buflen = 0;
static char linebuf[81];
while (true) {
buflen = interface.rdbuf(linebuf, 80, true, '\n');
interface.wrln(linebuf, buflen);
interface.wrbuf(ok, 4);
}
}
The main function, for right now, can just instantiate a new I/O interface and
then call the interpreter::
static char banner[] = "kforth interpreter\n";
const size_t bannerlen = 19;
int
main(void)
{
#ifdef __linux__
Console interface;
#endif
interface.wrbuf(banner, bannerlen);
interpreter(interface);
return 0;
}
That gives a good interactive test framework that I can use to start playing
with the system. I'm trying to avoid bringing in ``iostream`` directly in order
to force writing and building useful tooling built around the I/O interface.
This is, after all, the Forth ideal: start with a core system, then build your
world on top of that.
The I/O interface
^^^^^^^^^^^^^^^^^
In the truest of C++ fashions, the I/O interface is defined with the ``IO``
abstract base class::
#ifndef __KF_IO_H__
#define __KF_IO_H__
#include "defs.h"
class IO {
public:
// Virtual destructor is required in all ABCs.
virtual ~IO() {};
The two building block methods are the lowest-level. My original plan was to
include these in the interface, but there's one snag with that: line endings.
But, we'll get to that.
::
// Building block methods.
virtual char rdch(void) = 0;
virtual void wrch(char c) = 0;
I could have just made the buffer I/O methods functions inside the ``io.h``
header, but it seems easy to just include them here. I may move them outside
the class later, though.
::
// Buffer I/O.
virtual size_t rdbuf(char *buf, size_t len, bool stopat, char stopch) = 0;
virtual void wrbuf(char *buf, size_t len) = 0;
Line I/O presents some challenges. On a serial console, it's the sequence 0x0d
0x0a; on the Linux terminal, it's 0x0a. Therefore, reading a line is
platform-dependent, and I can't just make this a generic function unless I want
to handle all the cases. And, *surprise surprise*, right now I don't.
::
// Line I/O
virtual bool rdln(char *buf, size_t len, size_t *readlen) = 0;
virtual void wrln(char *buf, size_t len) = 0;
};
#endif // __KF_IO_H__
The Linux implementation is the ``Console`` (as seen in ``main``). The header
file isn't interesting; it's basically a copy of ``io.h`` in ``linux/io.h``.
::
#include <iostream>
#include "../io.h"
#include "io.h"
The building blocks flush I/O. ``getchar`` is used instead of ``cin`` because
the latter skips whitespace. Later, flushing may be removed but it's not a
performance concern yet.
::
char
Console::rdch()
{
std::cout.flush();
return getchar();
}
void
Console::wrch(char c)
{
std::cout.flush();
std::cout << c;
}
The buffer read and write functions are straightforward, and are just built on
top of the character read and write methods.
::
size_t
Console::rdbuf(char *buf, size_t len, bool stopat, char stopch)
{
size_t n = 0;
char ch;
while (n < len) {
ch = this->rdch();
if (stopat && stopch == ch) {
break;
}
buf[n++] = ch;
}
return n;
}
void
Console::wrbuf(char *buf, size_t len)
{
for (size_t n = 0; n < len; n++) {
this->wrch(buf[n]);
}
}
Line reading doesn't reuse the buffer I/O functions, because the latter
doesn't indicate whether the buffer ran out or the line has ended. I could add
length checks and whatnot, but this is straightforward and gives me something
to work with now. Again, the mantra is dumb and works rather than clever. For
now.
::
bool
Console::rdln(char *buf, size_t len, size_t *readlen) {
size_t n = 0;
char ch;
bool line = false;
while (n < len) {
ch = this->rdch();
if (ch == '\n') {
line = true;
break;
}
buf[n++] = ch;
}
if (nullptr != readlen) {
*readlen = n;
}
return line;
}
Line writing, however, can absolutely reuse the buffer and character I/O
methods.
::
void
Console::wrln(char *buf, size_t len)
{
this->wrbuf(buf, len);
this->wrch(0x0a);
}
``defs.h``
^^^^^^^^^^
The common definition file ``defs.h`` is just a front for the actual platform
definitions::
#ifndef __KF_DEFS_H__
#define __KF_DEFS_H__
#ifdef __linux__
#include "linux/defs.h"
#endif
#endif // __KF_DEFS_H__
The Linux definitions in ``linux/defs.h`` just bring in the standard
definitions from the standard library::
#ifndef __KF_LINUX_DEFS_H__
#define __KF_LINUX_DEFS_H__
#include <stddef.h>
#endif
Next steps
^^^^^^^^^^
I guess the next thing to do will be to start parsing.
Some housekeeping: I'll keep the state of the code at each part in
the tag ``part-$PART``; this part, for example is in the tag
`part-0x02`_.
.. _part-0x02: https://github.com/kisom/kforth/tree/part-0x02