A self-descriptive stack-based PC platform
git clone git://git.luxferre.top/equi.git
Log | Files | Refs | README | LICENSE

commit 1a95042757e140e16945da3166c38ac90c04724e
parent d48e6df014c778250cf2dbee84775783f0291586
Author: Luxferre <lux@ferre>
Date:   Fri, 12 Aug 2022 11:26:34 +0300

Persistent operations implemented

MREADME.md | 20++++++++++++++++----
Mequi.c | 34+++++++++++++++++++++++++++++-----
Aplatform-build-tools/PERS.DAT | 0
3 files changed, 45 insertions(+), 9 deletions(-)

diff --git a/README.md b/README.md @@ -137,7 +137,7 @@ Being a purely PC-oriented low-level runtime/programming environment, Equi has t - only implements three ports for `P` instruction: 0 as an echo port (returns passed parameters as corresponding result values), 1 as a random port (returns two random values in the results in the range between the two parameter values) and 2 as a CRC16 calculation port for a given memory location and its length, for any other port value it outputs its parameters to the standard error stream and puts three 0x0000 values back onto the stack, - doesn't implement non-blocking key input (the `,` instruction is identical to blocking key input instruction `?`), -- sandboxes the `{` and `}` operations using the file you supply in the command-line parameter. If you don't supply the file, these operations will effectively do nothing except putting 0x0000 (success status) onto the stack. +- sandboxes the `{` and `}` operations using the file with the name you supply on the compile time to the `PERSIST_FILE` constant. The file must already be created and accessible. If it doesn't exist, these operations will effectively do nothing except putting 0x0000 (success status) onto the stack. Additionally, this emulator implements `m` command line parameter that means that, instead of execution, the VM shall output the current command buffer contents upon reaching the `Q` instruction. This is particularly useful to save minified versions of `.equi` files to further reuse them in more space-restricted environments. Note that minified and non-minified files load and run fully identically, but the size difference can be significant. I.e. for the current FizzBuzz example version, the source is 1544 bytes long but its actual application snapshot in the command buffer (which can be dumped with the `m` parameter as a minified variant) is just [180 bytes long](examples/fizzbuz.min.equi). The rest is comments and whitespace characters that are skipped while loading the program into the command buffer. @@ -149,7 +149,8 @@ The following constants can be adjusted at compile time: - `LIT_STACK_SIZE` - literal stack size in bytes (255 max); - `GPD_AREA_SIZE` - GPD area size in bytes; - `CMD_BUF_SIZE` - command buffer size in bytes (65535 max); -- `CLT_ENTRIES_MAX` - size (in entries) of the compilation lookup table (CLT), each entry taking exactly 4 bytes. +- `CLT_ENTRIES_MAX` - size (in entries) of the compilation lookup table (CLT), each entry taking exactly 4 bytes; +- `PERSIST_FILE` - the name of persistent storage sandbox file (`PERS.DAT` by default). Please keep in mind that the reference implementation code primarily serves as a, well, reference on how the specification should be implemented, so it emphasizes on code portability and readability over performance whenever such a choice arises. @@ -198,6 +199,18 @@ The Makefile also provides a usual `apple2` target and the disk image will run o Because it aims for a different set of goals than typical Forth systems, mainly to explore the realms of blurring the borders between source and machine code, and to create a VM that can be easily programmed with printable text on the lowest level with no assembly required. Equi is to a typical Forth what VTL-2 was to BASIC, except in this case it is much more capable and extensible at its core. +### Too few core instructions! There still are lots of unused uppercase Latin letters, why not utilise them? + +Yes, Equi was designed to be useable from a standard keyboard but this doesn't mean every possible letter should be covered by an instruction. Implementation complexity should be kept low. Besides, new core features not present in every target system are much more convenient to implement via port I/O mechanism. + +### Too many core instructions! E.g. `-` can be easily replaced with `N+`, and all bitwise operations can be done using NAND or NOR alone! + +While Equi definitely is a minimalist runtime, it's not limited to a 16- or 32-instruction set and tries to keep the balance between simplicity of implementation and simplicity of usage (as far as it can go for a machine-level language). Omitting too many primitive operations would require programmers to paste more instructions instead of one or define them as custom words where it would be totally unnecessary. That being said, Equi's instruction set still might be optimised a little in future versions. + +### Why is there a distinction between instructions and custom-defined words? Forth doesn't have one! + +This distinction only exists to simplify program interpretation flow. Forth uses whitespace as an essential syntactic feature to delimit words and literals, Equi does not. Therefore, the only way to distinguish between a string literal and compiled word definition is my the means of a special instruction. And using for the compiled words the same approach as for the hexadecimal short literals (automatically try to detect one before an instruction) would be too resource-heavy for the oldest systems as it would involve computing CRC16 on the literal stack contents every single instruction. A dedicated instruction that denotes what to do with the literal stack is much more convenient and straightforward to implement. + ### Is Equi self-hosted, i.e. can it compile and run a new version of itself? Depends on what exactly you mean by this. If you mean something like [Uxntal assembler written in Uxntal](https://wiki.xxiivv.com/site/drifblim.html), the beauty of Equi is that it doesn't need such a tool, because what you type is what gets directly executed. A single-pass Equi code minifier, similar to what `equi m` does in the reference implementation, surely can be implemented in Equi itself, and a proof of this concept is under development now. With Equi being Turing-complete, a full Equi VM running inside an Equi VM is theoretically also possible, although it would be rather slow, complex to implement and bearing little to no practical use. If, however, you mean a compiler of Equi to the target's machine language, implemented in Equi itself, the amount of work required to do that would be comparable to implementing compilers on a Forth system, and would most likely hit the 64K RAM limit. But for the simplest targets this also it possible if you throw enough time and effort into this. @@ -220,8 +233,7 @@ There is no **required** variant of CRC-16. Different implementations using diff For now, no, but it may come true in the future versions. Now, more essential features are being focused upon. - -# Credits +## Credits Created by Luxferre in 2022, released into public domain. diff --git a/equi.c b/equi.c @@ -72,6 +72,11 @@ #define CLT_ENTRIES_MAX 512u #endif +/* Persistent storage sandbox file name */ +#ifndef PERSIST_FILE +#define PERSIST_FILE "PERS.DAT" +#endif + /* Some necessary constants and offsets derived from the above values */ #define STACK_SIZE_WORDS (STACK_SIZE / WS) /* Main and return stack size in words */ @@ -122,7 +127,8 @@ enum EquiErrors { CMD_OVERFLOW, INVALID_INSTRUCTION, INVALID_WORD, - PORT_IO_ERROR + PORT_IO_ERROR, + PERSIST_IO_ERROR }; /* Error reporting method */ @@ -154,6 +160,9 @@ void trapout(errcode) { case PORT_IO_ERROR: cerr("Port I/O error\n"); break; + case PERSIST_IO_ERROR: + cerr("Persistent storage I/O error\n"); + break; } exit(errcode); } @@ -296,13 +305,24 @@ ushort crc16(const uchar* data_p, uchar length) { /* Persistent operation handler */ -ushort persistOp(maddr, dataLen, adl, adh, isWrite) { +ushort persistOp(FILE *pfd, ushort maddr, ushort dataLen, ushort adl, ushort adh, uchar isWrite) { + ushort status = 0, proc; + if(pfd) { + fseek(pfd, (adh << 16) | adl, SEEK_SET); + if(isWrite) /* writing to the persistent area from the memory */ + proc = (ushort) fwrite(&flatram[maddr], 1, dataLen, pfd); + else /* reading from the persistent area into the memory */ + proc = (ushort) fread(&flatram[maddr], 1, dataLen, pfd); + if(proc != dataLen) + status = PERSIST_IO_ERROR; + } + pushMain(status); return 0; } /* Port I/O handler */ -void portIO(port, p2, p1) { +void portIO(ushort port, ushort p2, ushort p1) { ushort r1 = 0, r2 = 0, status = 0; switch(port) { case PORT_ECHO: @@ -329,6 +349,8 @@ void portIO(port, p2, p1) { void equi_main_loop() { uchar instr; ushort lhash, pbuf, pbuf2; + /* try to open the persistent sandbox file */ + FILE *pfd = fopen(PERSIST_FILE, "r+b"); /* reset all stacks before running and reinit CLT */ ram.msp = ram.rsp = ram.lsp = ram.cltp = 0; /* reset pc */ @@ -529,10 +551,10 @@ void equi_main_loop() { portIO(popMain(), popMain(), popMain()); break; case INS_PERSIST_READ: /* ( adh adl len maddr -- status) */ - pushMain(persistOp(popMain(), popMain(), popMain(), popMain(), 0)); + pushMain(persistOp(pfd, popMain(), popMain(), popMain(), popMain(), 0)); break; case INS_PERSIST_WRITE: /* ( adh adl len maddr -- status) */ - pushMain(persistOp(popMain(), popMain(), popMain(), popMain(), 1)); + pushMain(persistOp(pfd, popMain(), popMain(), popMain(), popMain(), 1)); break; default: /* all characters not processed before are invalid instructions */ trapout(INVALID_INSTRUCTION); @@ -543,6 +565,8 @@ void equi_main_loop() { continue; brx: break; } + /* close the persistent sandbox file if open */ + if(pfd) fclose(pfd); /* unset interpretation mode flag and exit */ ram.IM = 0; ram.pc = ram.ibp = 65535U; diff --git a/platform-build-tools/PERS.DAT b/platform-build-tools/PERS.DAT Binary files differ.