webforth
Version:
Forth implemented in Javascript
232 lines (223 loc) • 10.9 kB
JavaScript
import { promises as fs, constants as fs_constants } from 'fs';
// eslint-disable-next-line import/extensions
import { Forth, ForthNodeExtensions, jsUsers, RP0offset, jsFunctionAttributes } from './index.js';
const extensions = ForthNodeExtensions;
/*
* This is a first cut at a dictionary dumper aka cross-compiler for Arduino
* Its written in Javascript - it could be written in Forth, but there is some significant string handling going on.
*
* To use this, see README.md and search for "Arduino support"
*/
const xcRevDefines = {}; // Extra defines to check for values in
const numTokens = 8; // Should match value of highest token - currently tokenValue
//TODO-DUMP should refuse to dump any code fields to non existent routines
// noinspection JSCheckFunctionSignatures,JSBitwiseOperatorUsage
class ForthXC extends Forth {
xcLine(s, fd) { // asynchronous if passed fd
if (fd) {
return fd.write(s); // Current position, utf8, return a promise { bytesWritten, buffer }
} else {
// noinspection JSUnresolvedFunction
this.TXstoreS(s);
}
}
xcNameEncode(s) { return escape(s) // C identifiers are alphanumeric and _
.replace(/\*/g, '%2A')
.replace(/\+/g, '%2B')
.replace(/-/g, '%2D')
.replace(/\./g, '%2E')
.replace(/\//g, '%2F')
.replace(/@/g, '%40')
.replace(/_/g, '%5F')
.replace(/%/g, '_');
}
xcFuncIdentifier(func, unusedToken) {
return `F_${this.xcNameEncode(func.name)}`;
}
xcXTIdentifier(name) {
return `XT_${this.xcNameEncode(name)}`;
}
xcNum(v) {
return v < 10 ? v.toString(10) : `0x${v.toString(16)}`;
}
async xcXTdef(name, xt, fd) {
await this.xcLine(`\n#define ${this.xcXTIdentifier(name)} ${this.xcNum(xt)} /* ${name.replace('*/', '* /')}*/`, fd);
}
async xcConstants(fd = undefined) {
console.assert(this.UZERO === this.ROM0, 'UZERO=ROM0 assumed by userAreaInit in .ino');
const keys = ['RAM0', 'ROM0', 'CELLL', 'TIB0', 'UPP', 'UZERO'];
for (let i = 0; i < keys.length; i++) {
const k = keys[i];
const val = this[k];
xcRevDefines[val] = k;
await this.xcLine(`\n#define ${k} ${this.xcNum(val)}`, fd);
}
await this.xcLine(`\n#define CELLTYPE ${this.CELLL === 2 ? 'uint16_t' : 'uint32_t'}`, fd);
await this.xcLine(`\n#define SIGNEDCELLTYPE ${this.CELLL === 2 ? 'int16_t' : 'int32_t'}`, fd);
await this.xcLine(`\n#define DOUBLECELLTYPE ${this.CELLL === 2 ? 'uint32_t' : 'uint64_t'}`, fd); // TODO-ARDUINO-32
await this.xcLine(`\n#define CELLSHIFT ${this.CELLL === 2 ? 1 : 2}`, fd);
await this.xcLine(`\n#define CELLOFFSETMASK ${this.CELLL === 2 ? 0x01 : 0x03}`, fd);
await this.xcLine('\n#define LITTLEENDIAN true', fd);
await this.xcLine(`\n#define ROMCELLS ${this.xcNum(this.ROMSIZE / this.CELLL)}`, fd);
await this.xcLine(`\n#define RAMCELLS ${this.xcNum(this.RAMSIZE / this.CELLL)}`, fd);
await this.xcLine(`\n#define SPP ${this.xcNum(this.SPP)}`, fd);
await this.xcLine(`\n#define RP0 ${this.xcNum(this.Ufetch(RP0offset))}`, fd);
await this.xcLine(`\n#define RAMNAMEE ${this.xcNum(this.RAMNAMEE)}`, fd);
await this.xcLine(`\n#define RAMCODEE ${this.xcNum(this.RAMCODEE)}`, fd);
await this.xcLine(`\n#define ROM(cellAddr) pgm_read_${this.CELLL === 4 ? 'dword' : 'word'}_near(&rom[cellAddr])`, fd);
await this.xcLine(`\n#define FUNCTIONSLENGTH ${this.jsFunctions.length + 1}`, fd);
await this.xcXTdef('COLD', this.JStoXT('COLD'), fd);
}
async xcNames(fd) {
await this.xcLine('\n/* === Dumping Arduino source from names === */', fd);
// Traverse dictionary to report each name
//TODO-XC handle multiple dictionaries TODO-VOCABULARY
let p = this.currentFetch(); // vocabulary
while (p = this.Mfetch(p)) {
const name = this.countedToJS(p);
const xt = this.na2xt(p);
const funcNumber = this.Mfetch(xt); // Typically 3 for colon words
if (name && !jsFunctionAttributes[funcNumber].defer) { await this.xcXTdef(name, xt, fd); }
p -= this.CELLL; // point at link address and loop for next word
}
}
async xcTokens(top = this.jsFunctions.length, fd) {
await this.xcLine('\n/* === Function tokens === */', fd);
for (let token = 0; token < top; token++) {
const func = this.jsFunctions[token];
if (func) {
await this.xcLine(`\n#define ${this.xcFuncIdentifier(func, token)} ${token}`, fd);
}
}
}
async xcFunctions({ processor }, fd) {
await this.xcLine('\n/* === Function table - maps tokens to functions === */', fd);
for (let token = 0; token < this.jsFunctions.length; token++) {
const func = this.jsFunctions[token];
if (func && !jsFunctionAttributes[token].defer) {
const arduinoFuncName = this.xcNameEncode(func.name);
await this.xcLine(`\nextern void ${arduinoFuncName}();`, fd);
}
}
// This is the actual array of functions - will have 0 for deferred ones
// On Arduino uno `const void (*f[62])() = {` worked, but ESP8266 required `void (* const f[])() PROGMEM = {`
await this.xcLine(`\nvoid (* const f[FUNCTIONSLENGTH])()${processor === 'esp8266' ? ' PROGMEM' : ''} = {`, fd);
const itemsPerLine = 4;
let itemsToGoOnLine = 0;
for (let token = 0; token < this.jsFunctions.length; token++) {
const func = this.jsFunctions[token];
if (!itemsToGoOnLine--) { itemsToGoOnLine = itemsPerLine - 1; await this.xcLine(`\n /* ${token} */`, fd); }
if (!func || jsFunctionAttributes[token].defer) {
await this.xcLine('0, ', fd);
} else {
const arduinoFuncName = this.xcNameEncode(func.name);
const forthName = jsFunctionAttributes[token].n;
if (forthName && (forthName !== arduinoFuncName)) {
await this.xcLine(`/* ${forthName.replace('*/', '* /')} */ `, fd);
}
await this.xcLine(`${arduinoFuncName}, `, fd);
}
}
await this.xcLine(' 0 };', fd);
}
async xcCode(fd) {
await this.xcLine('\nconst CELLTYPE rom[ROMCELLS] PROGMEM = {', fd);
const boundaries = {};
const itemsPerLine = 16;
let itemsAlreadyOnLine = 0;
boundaries[this.UZERO] = 'USER VARIABLE SAVE AREA';
boundaries[this.ROMCODEE] = 'CODE DICTIONARY';
boundaries[this.romCodeTop] = 'CURRENT TOP OF CODE DICTIONARY';
boundaries[this.romNameBottom] = 'CURRENT BOTTOM OF NAME DICTIONARY';
boundaries[this.ROMNAMEE] = 'TOP OF NAME DICTIONARY';
let CP = this.ROM0; // Start at bottom of m
const romTop = this.ROMNAMEE;
//const romTop = 100;
let definitionXT = this.ROMCODEE;
let definitionName = '';
let definitionNA = this.romNameBottom;
const longestDef = 256; // Arbitrary guess at how long a definition might be for forward jumps
// Link from name directory into Rom dictionary
while (CP < romTop) { // Only need to go to ROMNAMEE - above is in Ram
const val = this.Mfetch(CP);
const na = this._toName(val);
const value = (na && (this.Mfetch8(na) & 31))
? this.xcXTIdentifier(this.countedToJS(na))
: (definitionName && (definitionXT < val) && (val < (definitionXT + longestDef)))
? `${this.xcXTIdentifier(definitionName)} + ${this.xcNum(val - definitionXT)}`
: (xcRevDefines[val] && (val > 0x10)) // Arbitrary distinction that small numbers probably coincidental
? xcRevDefines[val]
: this.xcNum(val);
if (CP in boundaries) {
await this.xcLine(`\n/* ==== ${boundaries[CP]} ==== */`, fd);
}
if (CP < this.ROMCODEE) { // User save area
//TODO-XC maybe reverse User offset into name and comment listings
await this.xcLine(`\n/* ${this.xcNum(CP)} ${jsUsers[(CP - this.ROM0) / this.CELLL] ? jsUsers[(CP - this.ROM0) / this.CELLL][0] : 'unused'} */ ${value},`, fd);
// Code definitions area
} else if (CP < this.romCodeTop) {
const defNa = this._toName(CP);
if (defNa) {
definitionXT = CP;
definitionName = this.countedToJS(defNa);
await this.xcLine(`\n/* ${this.xcNum(CP)}: ${definitionName.replace('*/', '* /')} */ `, fd);
await this.xcLine(`${this.xcFuncIdentifier(this.jsFunctions[val], val)}, `, fd);
} else {
await this.xcLine(`${value}, `, fd);
}
// Space between Code and Names
} else if (CP < this.romNameBottom) {
if (itemsAlreadyOnLine++ === 0) {
await this.xcLine(`\n/* ${this.xcNum(CP)}: */ `, fd);
}
await this.xcLine(`${value}, `, fd);
if (itemsAlreadyOnLine >= itemsPerLine) itemsAlreadyOnLine = 0;
// Name area
} else if (CP < this.ROMNAMEE) {
if (CP === definitionNA) {
definitionNA = this.Mfetch(CP + this.CELLL) - 2 * this.CELLL;
definitionName = this.countedToJS(CP + 2 * this.CELLL);
await this.xcLine(`\n/* ${this.xcNum(CP)} ${definitionName.replace('*/', '* /')} */ ${value},`, fd);
} else {
await this.xcLine(` ${value},`, fd);
}
}
CP += this.CELLL;
}
await this.xcLine('\n};', fd);
}
async xcDictionary({ processor, folder }) {
await this.xcHeaderFile({ processor, folder });
await this.xcDictionaryFile({ processor, folder });
}
async xcDictionaryFile({ processor, folder }) {
const fd = await fs.open(`${folder}/webforth_dictionary.cpp`, fs_constants.O_WRONLY | fs_constants.O_CREAT | fs_constants.O_TRUNC);
await this.xcLine('#include "webforth_functions.h"', fd);
await this.xcLine('\n// === Arduino source built directly from dictionary - edit at your own risk ! === ', fd);
await this.xcNames(fd);
await this.xcTokens(undefined, fd);
await this.xcFunctions({ processor }, fd);
await this.xcCode(fd);
await this.xcLine('\n// === End of Arduino source from dictionary === \n', fd);
await fd.close();
}
async xcHeaderFile({ processor, folder }) {
const fd = await fs.open(`${folder}/webforth_functions.h`, fs_constants.O_WRONLY | fs_constants.O_CREAT | fs_constants.O_TRUNC);
await this.xcLine('#ifndef ARDUINO_WEBFORTH_H_\n#define ARDUINO_WEBFORTH_H_\n#include <Arduino.h>', fd);
await this.xcConstants(fd);
await this.xcLine(`
// Data defined currently in arduino_webforth.ino but used in webforth_functions.cpp
extern const CELLTYPE rom[ROMCELLS] PROGMEM;
extern void (* const f[FUNCTIONSLENGTH])()${processor === 'esp8266' ? ' PROGMEM' : ''};
// Needed by setup or loop defined in webforth_functions.cpp
extern CELLTYPE IP;
extern CELLTYPE IPnext();
extern void threadtoken(const CELLTYPE xt);
`, fd);
await this.xcLine('\n// Tokens are needed by webforth_functions.cpp which does not include webforth_dictionary.cpp', fd);
await this.xcTokens(numTokens + 1, fd);
await this.xcLine('\n#endif', fd);
await fd.close();
}
}
export { ForthXC, extensions };