This commit is contained in:
2020-11-09 16:45:14 -07:00
parent 0a9b9a3c88
commit b6e30b24e1
6 changed files with 251 additions and 0 deletions

4
build.hxml Normal file
View File

@@ -0,0 +1,4 @@
-cp src
--macro nullSafety("hiss", Strict)
--main hiss.Main
--interp

65
src/hiss/Hiss.hx Normal file
View File

@@ -0,0 +1,65 @@
package hiss;
import haxe.macro.Context;
import haxe.macro.Expr;
import hiss.Stream;
import hiss.Reader;
class Hiss {
/**
Build a Haxe class from a corresponding .hiss file
**/
macro static public function build(hissFile:String):Array<Field> {
var classFields = Context.getBuildFields();
var stream = new Stream(hissFile);
var reader = new Reader();
while (!stream.isEmpty()) {
var position = stream.position();
var nextExp = reader.read(stream);
trace(nextExp);
// The last expression might be a comment, in which case None will be returned
switch (nextExp) {
case Some(nextExp):
classFields.push(readerExpToField(nextExp, position));
case None:
}
}
return classFields;
}
static function readerExpToField(exp:ReaderExp, position:String):Field {
switch (exp) {
case Call(Symbol("defvar"), args) if (args.length == 2):
return {
name: switch (args[0]) {
case Symbol(name):
name;
default:
throw 'The first argument to defvar at $position should be a variable name';
},
access: [APublic, AStatic],
kind: FVar(null, // TODO allow type anotations
readerExpToHaxeExpr(args[1])),
pos: Context.currentPos()
};
default:
throw '$exp at $position is not a valid defvar or defun expression';
}
}
static function readerExpToHaxeExpr(exp:ReaderExp):Expr {
return switch (exp) {
case Symbol(name):
Context.parse(name, Context.currentPos());
case Str(s):
return {
pos: Context.currentPos(),
expr: EConst(CString(s))
};
default:
throw 'cannot convert $exp yet';
};
}
}

1
src/hiss/Main.hiss Normal file
View File

@@ -0,0 +1 @@
(defvar message "Hello, world!")

8
src/hiss/Main.hx Normal file
View File

@@ -0,0 +1,8 @@
package hiss;
@:build(hiss.Hiss.build("src/hiss/Main.hiss"))
class Main {
public static function main() {
trace(message);
}
}

68
src/hiss/Reader.hx Normal file
View File

@@ -0,0 +1,68 @@
package hiss;
import haxe.ds.Option;
import hiss.Stream;
enum ReaderExp {
Call(func:ReaderExp, args:Array<ReaderExp>); // (f a1 a2...)
List(exps:Array<ReaderExp>); // [v1 v2 v3]
Str(s:String);
Symbol(name:String); // s
}
typedef ReadFunction = (Stream) -> Null<ReaderExp>;
class Reader {
var readTable:Map<String, ReadFunction> = new Map();
public function new() {
readTable["("] = (stream) -> Call(assertRead(stream), readExpArray(stream, ")"));
readTable["["] = (stream) -> List(readExpArray(stream, "]"));
readTable["\""] = (stream) -> Str(stream.expect("closing \"", () -> stream.takeUntilAndDrop("\"")));
readTable["/*"] = (stream) -> {
stream.dropUntil("*/");
null;
};
readTable["//"] = (stream) -> {
stream.dropUntil("\n");
null;
};
}
public function assertRead(stream:Stream):ReaderExp {
var position = stream.position();
return switch (read(stream)) {
case Some(exp):
exp;
case None:
throw "There were no expressions left in the stream at $position";
};
}
public function read(stream:Stream):Option<ReaderExp> {
var readTableKeys = [for (key in readTable.keys()) key];
readTableKeys.sort((a, b) -> b.length - a.length);
for (key in readTableKeys) {
switch (stream.peekChars(key.length)) {
case Some(k) if (k == key):
stream.dropString(key);
var expOrNull = readTable[key](stream);
return if (expOrNull != null) Some(expOrNull) else None;
default:
}
}
return Some(Symbol(stream.expect("a symbol name", () -> stream.takeUntilOneOf([")", "]", "/*", "\n", " "]))));
}
public function readExpArray(stream:Stream, end:String):Array<ReaderExp> {
var array = [];
while (stream.expect('$end to terminate list', () -> stream.peekChars(end.length)) != end) {
stream.dropWhitespace();
array.push(assertRead(stream));
}
stream.dropString(end);
return array;
}
}

105
src/hiss/Stream.hx Normal file
View File

@@ -0,0 +1,105 @@
package hiss;
import sys.io.File;
import haxe.ds.Option;
using StringTools;
using Lambda;
class Stream {
var content:String;
var file:String;
var line:Int;
var column:Int;
public function new(file:String) {
// Banish ye Windows line-endings
content = File.getContent(file).replace('\r', '');
this.file = file;
line = 1;
column = 1;
}
public function peekChars(chars:Int):Option<String> {
if (content.length < chars)
return None;
return Some(content.substr(0, chars));
}
public function isEmpty() {
return content.length == 0;
}
public function position() {
return '$file:$line:$column';
}
/** Every drop call should end up calling dropChars() or the position tracker will be wrong. **/
private function dropChars(count:Int) {
for (idx in 0...count) {
switch (content.charAt(idx)) {
case "\n":
line += 1;
column = 1;
default:
column += 1;
}
}
content = content.substr(count);
}
public function takeChars(count:Int):Option<String> {
if (count > content.length)
return None;
var toReturn = content.substr(0, count);
dropChars(count);
return Some(toReturn);
}
public function dropString(s:String) {
var toDrop = content.substr(0, s.length);
if (toDrop != s) {
throw 'Expected $s at ${position()}';
}
dropChars(s.length);
}
public function dropUntil(s:String) {
dropChars(content.indexOf(s));
}
public function dropWhitespace() {
var trimmed = content.ltrim();
dropChars(content.length - trimmed.length);
}
public function takeUntilOneOf(terminators:Array<String>):Option<String> {
var indices = [for (term in terminators) content.indexOf(term)].filter((idx) -> idx >= 0);
if (indices.length == 0)
return None;
var firstIndex = Math.floor(indices.fold(Math.min, indices[0]));
return takeChars(firstIndex);
}
public function takeUntilAndDrop(s:String):Option<String> {
var idx = content.indexOf(s);
if (idx < 0)
return None;
var toReturn = content.substr(0, idx);
dropChars(toReturn.length + s.length);
return Some(toReturn);
}
public function expect(whatToExpect:String, f:Void->Option<String>):String {
var position = position();
switch (f()) {
case Some(s):
return s;
case None:
throw 'Expected $whatToExpect at $position';
}
}
}