New mechanism for embedded DSLs
This commit is contained in:
131
kiss/src/kiss/EmbeddedScript.hx
Normal file
131
kiss/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
|
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 {
|
try {
|
||||||
var classFields = Context.getBuildFields();
|
var classFields = Context.getBuildFields();
|
||||||
var stream = new Stream(kissFile);
|
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;
|
var fieldForms = k.fieldForms;
|
||||||
|
|
||||||
// Macros at top-level are allowed if they expand into a fieldform, or null like defreadermacro
|
// 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)):
|
case CallExp({pos: _, def: Symbol(formName)}, args) if (fieldForms.exists(formName)):
|
||||||
fieldForms[formName](exp, args, k);
|
fieldForms[formName](exp, args, k);
|
||||||
default:
|
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 macros = k.macros;
|
||||||
var specialForms = k.specialForms;
|
var specialForms = k.specialForms;
|
||||||
// Bind the table arguments of this function for easy recursive calling/passing
|
// Bind the table arguments of this function for easy recursive calling/passing
|
||||||
|
@@ -241,6 +241,7 @@ class Macros {
|
|||||||
}
|
}
|
||||||
var parser = new Parser();
|
var parser = new Parser();
|
||||||
var interp = new Interp();
|
var interp = new Interp();
|
||||||
|
// TODO reader macros also need to access the readtable
|
||||||
interp.variables.set("ReaderExp", ReaderExpDef);
|
interp.variables.set("ReaderExp", ReaderExpDef);
|
||||||
interp.variables.set(streamArgName, stream);
|
interp.variables.set(streamArgName, stream);
|
||||||
interp.execute(parser.parseString(code));
|
interp.execute(parser.parseString(code));
|
||||||
|
3
kiss/src/test/cases/DSL.kiss
Normal file
3
kiss/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
kiss/src/test/cases/DSLScript.dsl
Normal file
2
kiss/src/test/cases/DSLScript.dsl
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
goop
|
||||||
|
gloop
|
15
kiss/src/test/cases/DSLTestCase.hx
Normal file
15
kiss/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