New mechanism for embedded DSLs
This commit is contained in:
131
src/kiss/EmbeddedScript.hx
Normal file
131
src/kiss/EmbeddedScript.hx
Normal 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
|
||||
}
|
@@ -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
|
||||
|
@@ -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
3
src/test/cases/DSL.kiss
Normal 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")}])|#)
|
2
src/test/cases/DSLScript.dsl
Normal file
2
src/test/cases/DSLScript.dsl
Normal file
@@ -0,0 +1,2 @@
|
||||
goop
|
||||
gloop
|
15
src/test/cases/DSLTestCase.hx
Normal file
15
src/test/cases/DSLTestCase.hx
Normal 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 {}
|
Reference in New Issue
Block a user