New mechanism for embedded DSLs

This commit is contained in:
2020-12-08 21:24:54 -07:00
parent 1e928e495d
commit f0bbb644b8
6 changed files with 156 additions and 4 deletions

131
src/kiss/EmbeddedScript.hx Normal file
View File

@@ -0,0 +1,131 @@
package kiss;
#if macro
import haxe.macro.Expr;
import haxe.macro.Context;
import haxe.macro.PositionTools;
import sys.io.File;
#end
import kiss.Kiss;
typedef Command = () -> Void;
/**
Utility class for making embedded Kiss-based DSLs.
Basic examples:
kiss/src/test/cases/DSLTestCase.hx
projects/aoc/year2020/BootCode.*
**/
class EmbeddedScript {
var instructionPointer = 0;
var running = false;
public function new() {}
#if macro
public static function build(dslFile:String, scriptFile:String):Array<Field> {
var k = Kiss.defaultKissState();
var classFields = Context.getBuildFields();
var commandList:Array<Expr> = [];
// This brings in the DSL's functions and global variables.
// As a side-effect, it also fills the KissState with the macros and reader macros that make the DSL syntax
for (field in Kiss.build(dslFile, k)) {
classFields.push(field);
}
Reader.readAndProcess(new Stream(scriptFile), k.readTable, (nextExp) -> {
var field = Kiss.readerExpToField(nextExp, k, false);
if (field != null) {
classFields.push(field);
} else {
// In a DSL script, anything that's not a field definition is a command line
commandList.push(macro function() {
${Kiss.readerExpToHaxeExpr(nextExp, k)};
});
}
// TODO also allow label setting and multiple commands coming from the same expr?
// i.e. knot declarations need to end the previous knot, and BELOW that set a label for the new one, then increment the read count
});
classFields.push({
pos: PositionTools.make({
min: 0,
max: File.getContent(scriptFile).length,
file: scriptFile
}),
name: "instructions",
access: [APrivate],
kind: FVar(null, macro [$a{commandList}])
});
classFields.push({
pos: PositionTools.make({
min: 0,
max: File.getContent(scriptFile).length,
file: scriptFile
}),
name: "step",
access: [APublic],
kind: FFun({
ret: null,
args: [],
expr: macro {
instructions[instructionPointer]();
++instructionPointer;
if (instructionPointer >= instructions.length)
running = false;
}
})
});
classFields.push({
pos: PositionTools.make({
min: 0,
max: File.getContent(scriptFile).length,
file: scriptFile
}),
name: "run",
access: [APublic],
kind: FFun({
ret: null,
args: [],
expr: macro {
running = true;
while (running) {
step();
}
}
})
});
// Start a process that needs to take control of the main thread, and will call back to resume the script
classFields.push({
pos: PositionTools.make({
min: 0,
max: File.getContent(scriptFile).length,
file: scriptFile
}),
name: "await",
access: [APublic],
kind: FFun({
ret: null,
args: [
{
type: Helpers.parseComplexType("(()->Void)->Void", null),
name: "c"
}
],
expr: macro {
running = false;
c(run);
}
})
});
return classFields;
}
#end
}

View File

@@ -54,7 +54,7 @@ class Kiss {
/**
Build a Haxe class from a corresponding .kiss file
**/
macro static public function build(kissFile:String, ?k:KissState):Array<Field> {
static public function build(kissFile:String, ?k:KissState):Array<Field> {
try {
var classFields = Context.getBuildFields();
var stream = new Stream(kissFile);
@@ -94,7 +94,7 @@ class Kiss {
}
}
static function readerExpToField(exp:ReaderExp, k:KissState):Null<Field> {
public static function readerExpToField(exp:ReaderExp, k:KissState, errorIfNot = true):Null<Field> {
var fieldForms = k.fieldForms;
// Macros at top-level are allowed if they expand into a fieldform, or null like defreadermacro
@@ -107,11 +107,11 @@ class Kiss {
case CallExp({pos: _, def: Symbol(formName)}, args) if (fieldForms.exists(formName)):
fieldForms[formName](exp, args, k);
default:
throw CompileError.fromExp(exp, 'invalid valid field form');
if (errorIfNot) throw CompileError.fromExp(exp, 'invalid valid field form'); else return null;
};
}
static function readerExpToHaxeExpr(exp:ReaderExp, k:KissState):Expr {
public static function readerExpToHaxeExpr(exp:ReaderExp, k:KissState):Expr {
var macros = k.macros;
var specialForms = k.specialForms;
// Bind the table arguments of this function for easy recursive calling/passing

View File

@@ -241,6 +241,7 @@ class Macros {
}
var parser = new Parser();
var interp = new Interp();
// TODO reader macros also need to access the readtable
interp.variables.set("ReaderExp", ReaderExpDef);
interp.variables.set(streamArgName, stream);
interp.execute(parser.parseString(code));

3
src/test/cases/DSL.kiss Normal file
View File

@@ -0,0 +1,3 @@
// TODO make a better position reification scheme here
(defreadermacro "goop" [stream] #|ReaderExp.CallExp({pos: {file: "bleh", line: 1, column: 1, absoluteChar: 1}, def: ReaderExp.Symbol("Assert.isTrue")}, [{pos: {file: "bleh", line: 1, column: 1, absoluteChar: 1}, def: ReaderExp.Symbol("true")}])|#)
(defreadermacro "gloop" [stream] #|ReaderExp.CallExp({pos: {file: "bleh", line: 1, column: 1, absoluteChar: 1}, def: ReaderExp.Symbol("Assert.isFalse")}, [{pos: {file: "bleh", line: 1, column: 1, absoluteChar: 1}, def: ReaderExp.Symbol("false")}])|#)

View File

@@ -0,0 +1,2 @@
goop
gloop

View File

@@ -0,0 +1,15 @@
package test.cases;
import utest.Test;
import utest.Assert;
import kiss.EmbeddedScript;
import kiss.Prelude;
class DSLTestCase extends Test {
function testScript() {
new DSLScript().run();
}
}
@:build(kiss.EmbeddedScript.build("kiss/src/test/cases/DSL.kiss", "kiss/src/test/cases/DSLScript.dsl"))
class DSLScript extends EmbeddedScript {}