reorganize kiss into its own directory
This commit is contained in:
5
kiss/build-scripts/common-args.hxml
Normal file
5
kiss/build-scripts/common-args.hxml
Normal file
@@ -0,0 +1,5 @@
|
||||
-lib hscript
|
||||
-lib uuid
|
||||
-lib tink_macro
|
||||
-cp kiss/src
|
||||
-D analyzer-optimize
|
||||
3
kiss/build-scripts/common-test-args.hxml
Normal file
3
kiss/build-scripts/common-test-args.hxml
Normal file
@@ -0,0 +1,3 @@
|
||||
-lib utest
|
||||
-D test
|
||||
--main test.TestMain
|
||||
2
kiss/build-scripts/cpp/test-cpp.sh
Normal file
2
kiss/build-scripts/cpp/test-cpp.sh
Normal file
@@ -0,0 +1,2 @@
|
||||
#! /bin/bash
|
||||
./bin/cpp/test/TestMain
|
||||
3
kiss/build-scripts/cpp/test.hxml
Normal file
3
kiss/build-scripts/cpp/test.hxml
Normal file
@@ -0,0 +1,3 @@
|
||||
-lib hxcpp
|
||||
-cpp bin/cpp/test
|
||||
-cmd bash kiss/build-scripts/cpp/test-cpp.sh
|
||||
1
kiss/build-scripts/interp/test.hxml
Normal file
1
kiss/build-scripts/interp/test.hxml
Normal file
@@ -0,0 +1 @@
|
||||
--interp
|
||||
2
kiss/build-scripts/js/test.hxml
Normal file
2
kiss/build-scripts/js/test.hxml
Normal file
@@ -0,0 +1,2 @@
|
||||
--js bin/js/test.js
|
||||
--cmd node bin/js/test.js
|
||||
3
kiss/build-scripts/nodejs/test.hxml
Normal file
3
kiss/build-scripts/nodejs/test.hxml
Normal file
@@ -0,0 +1,3 @@
|
||||
-lib hxnodejs
|
||||
--js bin/nodejs/test.js
|
||||
--cmd node bin/nodejs/test.js
|
||||
8
kiss/build-scripts/py/test-py.sh
Normal file
8
kiss/build-scripts/py/test-py.sh
Normal file
@@ -0,0 +1,8 @@
|
||||
#! /bin/bash
|
||||
|
||||
# "python" is supposed to mean Python3 everywhere now, but not in practice
|
||||
if [ ! -z "$(which python3)" ]; then
|
||||
python3 bin/py/test.py
|
||||
else
|
||||
python bin/py/test.py
|
||||
fi
|
||||
2
kiss/build-scripts/py/test.hxml
Normal file
2
kiss/build-scripts/py/test.hxml
Normal file
@@ -0,0 +1,2 @@
|
||||
--python bin/py/test.py
|
||||
--cmd bash kiss/build-scripts/py/test-py.sh
|
||||
7
kiss/build.hxml
Normal file
7
kiss/build.hxml
Normal file
@@ -0,0 +1,7 @@
|
||||
-cp kiss/src
|
||||
-lib hscript
|
||||
-lib uuid
|
||||
-lib tink_macro
|
||||
--macro nullSafety("kiss", Strict)
|
||||
--main kiss.Main
|
||||
--interp
|
||||
17
kiss/haxelib.json
Normal file
17
kiss/haxelib.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "kiss",
|
||||
"url": "https://github.com/hissvn/kisslang",
|
||||
"license": "LGPL",
|
||||
"tags": ["cross"],
|
||||
"description": "",
|
||||
"version": "0.0.0",
|
||||
"releasenote": "It isn't safe to use this library yet.",
|
||||
"contributors": ["NQNStudios"],
|
||||
"classPath": "src/",
|
||||
"main": "kiss.Main",
|
||||
"dependencies": {
|
||||
"hscript": "",
|
||||
"uuid": "",
|
||||
"tink_macro": ""
|
||||
}
|
||||
}
|
||||
44
kiss/src/kiss/CompileError.hx
Normal file
44
kiss/src/kiss/CompileError.hx
Normal file
@@ -0,0 +1,44 @@
|
||||
package kiss;
|
||||
|
||||
import kiss.Reader;
|
||||
import kiss.List;
|
||||
|
||||
using kiss.Stream;
|
||||
using kiss.Reader;
|
||||
|
||||
class CompileError {
|
||||
var exps:List<ReaderExp>;
|
||||
var message:String;
|
||||
|
||||
function new(exps:Array<ReaderExp>, message:String) {
|
||||
this.exps = exps;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public static function fromExp(exp:ReaderExp, message:String) {
|
||||
return new CompileError([exp], message);
|
||||
}
|
||||
|
||||
public static function fromArgs(exps:Array<ReaderExp>, message:String) {
|
||||
return new CompileError(exps, message);
|
||||
}
|
||||
|
||||
public function toString() {
|
||||
var posPrefix = switch (exps.length) {
|
||||
case 1:
|
||||
exps[0].pos.toPrint();
|
||||
default:
|
||||
var firstPos = exps[0].pos.toPrint();
|
||||
var lastPos = exps[-1].pos.toPrint();
|
||||
var justLineAndColumnIdx = lastPos.indexOf(":") + 1;
|
||||
firstPos + '-' + lastPos.substr(justLineAndColumnIdx);
|
||||
}
|
||||
|
||||
return '\nKiss compilation failed!\n'
|
||||
+ posPrefix
|
||||
+ ": "
|
||||
+ message
|
||||
+ "\nFrom:"
|
||||
+ [for (exp in exps) exp.def.toString()].toString();
|
||||
}
|
||||
}
|
||||
95
kiss/src/kiss/FieldForms.hx
Normal file
95
kiss/src/kiss/FieldForms.hx
Normal file
@@ -0,0 +1,95 @@
|
||||
package kiss;
|
||||
|
||||
import haxe.macro.Expr;
|
||||
import haxe.macro.Context;
|
||||
import kiss.Reader;
|
||||
import kiss.Helpers;
|
||||
import kiss.Stream;
|
||||
import kiss.CompileError;
|
||||
import kiss.Kiss;
|
||||
|
||||
using kiss.Helpers;
|
||||
using kiss.Reader;
|
||||
using StringTools;
|
||||
|
||||
// Field forms convert Kiss reader expressions into Haxe macro class fields
|
||||
typedef FieldFormFunction = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> Field;
|
||||
|
||||
class FieldForms {
|
||||
public static function builtins() {
|
||||
var map:Map<String, FieldFormFunction> = [];
|
||||
|
||||
map["defvar"] = varOrProperty.bind("defvar");
|
||||
map["defprop"] = varOrProperty.bind("defprop");
|
||||
|
||||
map["defun"] = funcOrMethod.bind("defun");
|
||||
map["defmethod"] = funcOrMethod.bind("defmethod");
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
static function fieldAccess(formName:String, fieldName:String, nameExp:ReaderExp, ?access:Array<Access>) {
|
||||
if (access == null) {
|
||||
access = if (formName == "defvar" || formName == "defprop") {
|
||||
[AFinal];
|
||||
} else {
|
||||
[];
|
||||
};
|
||||
}
|
||||
return switch (nameExp.def) {
|
||||
case MetaExp("mut", nameExp):
|
||||
access.remove(AFinal);
|
||||
trace('ACCESS $access');
|
||||
fieldAccess(formName, fieldName, nameExp, access);
|
||||
default:
|
||||
if (formName == "defvar" || formName == "defun") {
|
||||
access.push(AStatic);
|
||||
}
|
||||
access.push(if (fieldName.startsWith("_")) APrivate else APublic);
|
||||
access;
|
||||
};
|
||||
}
|
||||
|
||||
static function fieldName(formName:String, nameExp:ReaderExp) {
|
||||
return switch (nameExp.def) {
|
||||
case Symbol(name) | TypedExp(_, {pos: _, def: Symbol(name)}):
|
||||
name;
|
||||
case MetaExp(_, nameExp):
|
||||
fieldName(formName, nameExp);
|
||||
default:
|
||||
throw CompileError.fromExp(nameExp, 'The first argument to $formName should be a variable name or typed variable name.');
|
||||
};
|
||||
}
|
||||
|
||||
static function varOrProperty(formName:String, wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState):Field {
|
||||
wholeExp.checkNumArgs(2, 3, '($formName [optional :type] [variable] [optional: &mut] [value])');
|
||||
|
||||
var name = fieldName(formName, args[0]);
|
||||
var access = fieldAccess(formName, name, args[0]);
|
||||
|
||||
return {
|
||||
name: name,
|
||||
access: access,
|
||||
kind: FVar(switch (args[0].def) {
|
||||
case TypedExp(type, _):
|
||||
Helpers.parseComplexType(type, args[0]);
|
||||
default: null;
|
||||
}, k.convert(args[1])),
|
||||
pos: wholeExp.macroPos()
|
||||
};
|
||||
}
|
||||
|
||||
static function funcOrMethod(formName:String, wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState):Field {
|
||||
wholeExp.checkNumArgs(3, null, '($formName [optional :type] [name] [[argNames...]] [body...])');
|
||||
|
||||
var name = fieldName(formName, args[0]);
|
||||
var access = fieldAccess(formName, name, args[0]);
|
||||
|
||||
return {
|
||||
name: name,
|
||||
access: access,
|
||||
kind: FFun(Helpers.makeFunction(args[0], args[1], args.slice(2), k)),
|
||||
pos: wholeExp.macroPos()
|
||||
};
|
||||
}
|
||||
}
|
||||
205
kiss/src/kiss/Helpers.hx
Normal file
205
kiss/src/kiss/Helpers.hx
Normal file
@@ -0,0 +1,205 @@
|
||||
package kiss;
|
||||
|
||||
import haxe.macro.Expr;
|
||||
import haxe.macro.Context;
|
||||
import haxe.macro.PositionTools;
|
||||
import kiss.Reader;
|
||||
import kiss.CompileError;
|
||||
import kiss.Kiss;
|
||||
import kiss.SpecialForms;
|
||||
|
||||
using kiss.Reader;
|
||||
using kiss.Helpers;
|
||||
using StringTools;
|
||||
|
||||
class Helpers {
|
||||
public static function macroPos(exp:ReaderExp) {
|
||||
var kissPos = exp.pos;
|
||||
return PositionTools.make({
|
||||
min: kissPos.absoluteChar,
|
||||
max: kissPos.absoluteChar,
|
||||
file: kissPos.file
|
||||
});
|
||||
}
|
||||
|
||||
public static function withMacroPosOf(e:ExprDef, exp:ReaderExp):Expr {
|
||||
return {
|
||||
pos: macroPos(exp),
|
||||
expr: e
|
||||
};
|
||||
}
|
||||
|
||||
static function startsWithUpperCase(s:String) {
|
||||
return s.charAt(0) == s.charAt(0).toUpperCase();
|
||||
}
|
||||
|
||||
public static function parseTypePath(path:String, from:ReaderExp):TypePath {
|
||||
var genericParts = path.split("<");
|
||||
var typeParams:Array<TypeParam> = null;
|
||||
if (genericParts.length > 1) {
|
||||
typeParams = [
|
||||
for (typeParam in genericParts[1].substr(0, genericParts[1].length - 1).split(",")) {
|
||||
TPType(parseComplexType(typeParam, from));
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
var parts:List<String> = genericParts[0].trim().split(".");
|
||||
var uppercaseParts:List<Bool> = parts.map(startsWithUpperCase);
|
||||
for (isUpcase in uppercaseParts.slice(0, -2)) {
|
||||
if (isUpcase) {
|
||||
throw CompileError.fromExp(from, 'Type path $path should only have capitalized type and subtype');
|
||||
}
|
||||
}
|
||||
var lastIsCap = uppercaseParts[-1];
|
||||
var penultIsCap = uppercaseParts[-2];
|
||||
|
||||
return if (penultIsCap && lastIsCap) {
|
||||
{
|
||||
sub: parts[-1],
|
||||
name: parts[-2],
|
||||
pack: parts.slice(0, -2),
|
||||
params: typeParams
|
||||
};
|
||||
} else if (lastIsCap) {
|
||||
{
|
||||
name: parts[-1],
|
||||
pack: parts.slice(0, -1),
|
||||
params: typeParams
|
||||
};
|
||||
} else {
|
||||
throw CompileError.fromExp(from, 'Type path $path should end with a capitalized type');
|
||||
};
|
||||
}
|
||||
|
||||
public static function parseComplexType(path:String, from:ReaderExp):ComplexType {
|
||||
return TPath(parseTypePath(path, from));
|
||||
}
|
||||
|
||||
// TODO generic type parameter declarations
|
||||
public static function makeFunction(?name:ReaderExp, argList:ReaderExp, body:List<ReaderExp>, k:KissState):Function {
|
||||
var funcName = if (name != null) {
|
||||
switch (name.def) {
|
||||
case Symbol(name) | TypedExp(_, {pos: _, def: Symbol(name)}):
|
||||
name;
|
||||
default:
|
||||
throw CompileError.fromExp(name, 'function name should be a symbol or typed symbol');
|
||||
};
|
||||
} else {
|
||||
"";
|
||||
};
|
||||
|
||||
var numArgs = 0;
|
||||
// Once the &opt meta appears, all following arguments are optional until &rest
|
||||
var opt = false;
|
||||
// Once the &rest meta appears, no other arguments can be declared
|
||||
var rest = false;
|
||||
var restProcessed = false;
|
||||
|
||||
function makeFuncArg(funcArg:ReaderExp):FunctionArg {
|
||||
if (restProcessed) {
|
||||
throw CompileError.fromExp(funcArg, "cannot declare more arguments after a &rest argument");
|
||||
}
|
||||
return switch (funcArg.def) {
|
||||
case MetaExp("rest", innerFuncArg):
|
||||
if (funcName == "") {
|
||||
throw CompileError.fromExp(funcArg, "lambda does not support &rest arguments");
|
||||
}
|
||||
|
||||
// rest arguments define a Kiss special form with the function's name that wraps
|
||||
// the rest args in a list when calling it from Kiss
|
||||
k.specialForms[funcName] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> {
|
||||
var realCallArgs = args.slice(0, numArgs);
|
||||
var restArgs = args.slice(numArgs);
|
||||
realCallArgs.push(ListExp(restArgs).withPosOf(wholeExp));
|
||||
ECall(k.convert(Symbol(funcName).withPosOf(wholeExp)), realCallArgs.map(k.convert)).withMacroPosOf(wholeExp);
|
||||
};
|
||||
|
||||
opt = true;
|
||||
rest = true;
|
||||
makeFuncArg(innerFuncArg);
|
||||
case MetaExp("opt", innerFuncArg):
|
||||
opt = true;
|
||||
makeFuncArg(innerFuncArg);
|
||||
default:
|
||||
if (rest) {
|
||||
restProcessed = true;
|
||||
} else {
|
||||
++numArgs;
|
||||
}
|
||||
{
|
||||
name: switch (funcArg.def) {
|
||||
case Symbol(name) | TypedExp(_, {pos: _, def: Symbol(name)}):
|
||||
name;
|
||||
default:
|
||||
throw CompileError.fromExp(funcArg, 'function argument should be a symbol or typed symbol');
|
||||
},
|
||||
type: switch (funcArg.def) {
|
||||
case TypedExp(type, _):
|
||||
Helpers.parseComplexType(type, funcArg);
|
||||
default: null;
|
||||
},
|
||||
opt: opt
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// TODO use let instead of begin to make the args immutable by default
|
||||
return {
|
||||
ret: if (name != null) switch (name.def) {
|
||||
case TypedExp(type, _): Helpers.parseComplexType(type, name);
|
||||
default: null;
|
||||
} else null,
|
||||
args: switch (argList.def) {
|
||||
case ListExp(funcArgs):
|
||||
funcArgs.map(makeFuncArg);
|
||||
case CallExp(_, _):
|
||||
throw CompileError.fromExp(argList, 'expected an argument list. Change the parens () to brackets []');
|
||||
default:
|
||||
throw CompileError.fromExp(argList, 'expected an argument list');
|
||||
},
|
||||
expr: EReturn(k.convert(CallExp(Symbol("begin").withPos(body[0].pos), body).withPos(body[0].pos))).withMacroPosOf(body[-1])
|
||||
}
|
||||
}
|
||||
|
||||
// alias replacements are processed by the reader
|
||||
public static function defAlias(k:KissState, whenItsThis:String, makeItThisInstead:ReaderExpDef) {
|
||||
// The alias has to be followed by a terminator to count!
|
||||
for (terminator in Reader.terminators) {
|
||||
k.readTable[whenItsThis + terminator] = (s:Stream) -> {
|
||||
s.putBackString(terminator);
|
||||
makeItThisInstead;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Throw a CompileError if the given expression has the wrong number of arguments
|
||||
**/
|
||||
public static function checkNumArgs(wholeExp:ReaderExp, min:Null<Int>, max:Null<Int>, ?expectedForm:String) {
|
||||
if (expectedForm == null) {
|
||||
expectedForm = if (max == min) {
|
||||
'$min arguments';
|
||||
} else if (max == null) {
|
||||
'at least $min arguments';
|
||||
} else if (min == null) {
|
||||
'no more than $max arguments';
|
||||
} else if (min == null && max == null) {
|
||||
throw 'checkNumArgs() needs a min or a max';
|
||||
} else {
|
||||
'between $min and $max arguments';
|
||||
};
|
||||
}
|
||||
|
||||
var args = switch (wholeExp.def) {
|
||||
case CallExp(_, args): args;
|
||||
default: throw CompileError.fromExp(wholeExp, "Can only check number of args in a CallExp");
|
||||
};
|
||||
|
||||
if (min != null && args.length < min) {
|
||||
throw CompileError.fromExp(wholeExp, 'Not enough arguments. Expected $expectedForm');
|
||||
} else if (max != null && args.length > max) {
|
||||
throw CompileError.fromExp(wholeExp, 'Too many arguments. Expected $expectedForm');
|
||||
}
|
||||
}
|
||||
}
|
||||
147
kiss/src/kiss/Kiss.hx
Normal file
147
kiss/src/kiss/Kiss.hx
Normal file
@@ -0,0 +1,147 @@
|
||||
package kiss;
|
||||
|
||||
#if macro
|
||||
import haxe.Exception;
|
||||
import haxe.macro.Context;
|
||||
import haxe.macro.Expr;
|
||||
import kiss.Stream;
|
||||
import kiss.Reader;
|
||||
import kiss.FieldForms;
|
||||
import kiss.SpecialForms;
|
||||
import kiss.Macros;
|
||||
import kiss.CompileError;
|
||||
|
||||
using kiss.Helpers;
|
||||
using kiss.Reader;
|
||||
using tink.MacroApi;
|
||||
|
||||
typedef ExprConversion = (ReaderExp) -> Expr;
|
||||
|
||||
typedef KissState = {
|
||||
className:String,
|
||||
readTable:Map<String, ReadFunction>,
|
||||
fieldForms:Map<String, FieldFormFunction>,
|
||||
specialForms:Map<String, SpecialFormFunction>,
|
||||
macros:Map<String, MacroFunction>,
|
||||
convert:ExprConversion
|
||||
};
|
||||
|
||||
class Kiss {
|
||||
/**
|
||||
Build a Haxe class from a corresponding .kiss file
|
||||
**/
|
||||
macro static public function build(kissFile:String):Array<Field> {
|
||||
try {
|
||||
var classFields = Context.getBuildFields();
|
||||
var className = Context.getLocalClass().get().name;
|
||||
|
||||
var stream = new Stream(kissFile);
|
||||
|
||||
var k = {
|
||||
className: className,
|
||||
readTable: Reader.builtins(),
|
||||
fieldForms: FieldForms.builtins(),
|
||||
specialForms: SpecialForms.builtins(),
|
||||
macros: Macros.builtins(),
|
||||
convert: null
|
||||
}
|
||||
k.convert = readerExpToHaxeExpr.bind(_, k);
|
||||
|
||||
// Helpful aliases
|
||||
k.defAlias("print", Symbol("Prelude.print"));
|
||||
k.defAlias("map", Symbol("Lambda.map"));
|
||||
k.defAlias("filter", Symbol("Lambda.filter")); // TODO use truthy as the default filter function
|
||||
k.defAlias("has", Symbol("Lambda.has"));
|
||||
k.defAlias("count", Symbol("Lambda.count"));
|
||||
|
||||
while (true) {
|
||||
stream.dropWhitespace();
|
||||
if (stream.isEmpty())
|
||||
break;
|
||||
var position = stream.position();
|
||||
var nextExp = Reader.read(stream, k.readTable);
|
||||
|
||||
// The last expression might be a comment, in which case None will be returned
|
||||
switch (nextExp) {
|
||||
case Some(nextExp):
|
||||
#if test
|
||||
Sys.println(nextExp.def.toString());
|
||||
#end
|
||||
var field = readerExpToField(nextExp, k);
|
||||
if (field != null)
|
||||
classFields.push(field);
|
||||
case None:
|
||||
stream.dropWhitespace(); // If there was a comment, drop whitespace that comes after
|
||||
}
|
||||
}
|
||||
|
||||
return classFields;
|
||||
} catch (err:CompileError) {
|
||||
Sys.println(err);
|
||||
Sys.exit(1);
|
||||
return null; // Necessary for build() to compile
|
||||
} catch (err:Exception) {
|
||||
throw err; // Re-throw haxe exceptions for precise stacks
|
||||
}
|
||||
}
|
||||
|
||||
static function readerExpToField(exp:ReaderExp, k:KissState):Null<Field> {
|
||||
var fieldForms = k.fieldForms;
|
||||
|
||||
// Macros at top-level are allowed if they expand into a fieldform, or null like defreadermacro
|
||||
var macros = k.macros;
|
||||
|
||||
return switch (exp.def) {
|
||||
case CallExp({pos: _, def: Symbol(mac)}, args) if (macros.exists(mac)):
|
||||
var expandedExp = macros[mac](exp, args, k);
|
||||
if (expandedExp != null) readerExpToField(macros[mac](expandedExp, args, k), k) else null;
|
||||
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');
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
var convert = readerExpToHaxeExpr.bind(_, k);
|
||||
var expr = switch (exp.def) {
|
||||
case Symbol(name):
|
||||
Context.parse(name, exp.macroPos());
|
||||
case StrExp(s):
|
||||
EConst(CString(s)).withMacroPosOf(exp);
|
||||
case CallExp({pos: _, def: Symbol(mac)}, args) if (macros.exists(mac)):
|
||||
convert(macros[mac](exp, args, k));
|
||||
case CallExp({pos: _, def: Symbol(specialForm)}, args) if (specialForms.exists(specialForm)):
|
||||
specialForms[specialForm](exp, args, k);
|
||||
case CallExp(func, args):
|
||||
ECall(convert(func), [for (argExp in args) convert(argExp)]).withMacroPosOf(exp);
|
||||
|
||||
/*
|
||||
// Typed expressions in the wild become casts:
|
||||
case TypedExp(type, innerExp):
|
||||
ECast(convert(innerExp), if (type.length > 0) Helpers.parseComplexType(type, exp) else null).withMacroPosOf(wholeExp);
|
||||
*/
|
||||
case ListExp(elements):
|
||||
ENew({
|
||||
pack: ["kiss"],
|
||||
name: "List"
|
||||
}, [
|
||||
EArrayDecl([for (elementExp in elements) convert(elementExp)]).withMacroPosOf(exp)
|
||||
]).withMacroPosOf(exp);
|
||||
case RawHaxe(code):
|
||||
Context.parse(code, exp.macroPos());
|
||||
case FieldExp(field, innerExp):
|
||||
EField(convert(innerExp), field).withMacroPosOf(exp);
|
||||
default:
|
||||
throw CompileError.fromExp(exp, 'conversion not implemented');
|
||||
};
|
||||
#if test
|
||||
Sys.println(expr.toString());
|
||||
#end
|
||||
return expr;
|
||||
}
|
||||
}
|
||||
#end
|
||||
55
kiss/src/kiss/List.hx
Normal file
55
kiss/src/kiss/List.hx
Normal file
@@ -0,0 +1,55 @@
|
||||
package kiss;
|
||||
|
||||
/**
|
||||
Kiss enhances Haxe Arrays to support negative indices and return nullable-typed values.
|
||||
**/
|
||||
@:forward(length, concat, contains, copy, filter, indexOf, // insert is re-implemented with negative index support
|
||||
iterator, join, keyValueIterator,
|
||||
lastIndexOf, map, pop, push, remove, resize, reverse, shift, // slice is re-implemented with negative index support
|
||||
sort,
|
||||
// splice is re-implemented with negative index support,
|
||||
toString, unshift)
|
||||
abstract List<T>(Array<T>) from Array<T> to Array<T> {
|
||||
public inline function new(a:Array<T>) {
|
||||
this = a;
|
||||
}
|
||||
|
||||
@:from
|
||||
static public function fromArray<T>(a:Array<T>) {
|
||||
return new List<T>(a);
|
||||
}
|
||||
|
||||
@:to
|
||||
public function toArray<T>() {
|
||||
return this;
|
||||
}
|
||||
|
||||
inline function realIndex(idx:Int) {
|
||||
return if (idx < 0) this.length + idx else idx;
|
||||
}
|
||||
|
||||
@:arrayAccess
|
||||
public inline function get<T>(idx:Int):Null<T> {
|
||||
return this[realIndex(idx)];
|
||||
}
|
||||
|
||||
@:arrayAccess
|
||||
public inline function arrayWrite(idx:Int, v:T):T {
|
||||
this[realIndex(idx)] = v;
|
||||
return v;
|
||||
}
|
||||
|
||||
public function insert(idx:Int, v:T) {
|
||||
this.insert(realIndex(idx), v);
|
||||
}
|
||||
|
||||
public function slice(start:Int, ?end:Int) {
|
||||
if (end == null)
|
||||
end = this.length;
|
||||
return this.slice(realIndex(start), realIndex(end));
|
||||
}
|
||||
|
||||
public function splice(start:Int, len:Int) {
|
||||
return this.splice(realIndex(start), len);
|
||||
}
|
||||
}
|
||||
300
kiss/src/kiss/Macros.hx
Normal file
300
kiss/src/kiss/Macros.hx
Normal file
@@ -0,0 +1,300 @@
|
||||
package kiss;
|
||||
|
||||
import haxe.macro.Expr;
|
||||
import haxe.macro.Context;
|
||||
import hscript.Parser;
|
||||
import hscript.Interp;
|
||||
import uuid.Uuid;
|
||||
import kiss.Reader;
|
||||
import kiss.Kiss;
|
||||
import kiss.CompileError;
|
||||
|
||||
using uuid.Uuid;
|
||||
using kiss.Reader;
|
||||
using kiss.Helpers;
|
||||
|
||||
// Macros generate new Kiss reader expressions from the arguments of their call expression.
|
||||
typedef MacroFunction = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> Null<ReaderExp>;
|
||||
|
||||
class Macros {
|
||||
public static function builtins() {
|
||||
var macros:Map<String, MacroFunction> = [];
|
||||
|
||||
macros["%"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k) -> {
|
||||
wholeExp.checkNumArgs(2, 2, '(% [divisor] [dividend])');
|
||||
CallExp(Symbol("Prelude.mod").withPosOf(wholeExp), [exps[1], exps[0]]).withPosOf(wholeExp);
|
||||
};
|
||||
|
||||
macros["^"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k) -> {
|
||||
wholeExp.checkNumArgs(2, 2, '(^ [base] [exponent])');
|
||||
CallExp(Symbol("Prelude.pow").withPosOf(wholeExp), [exps[1], exps[0]]).withPosOf(wholeExp);
|
||||
};
|
||||
|
||||
macros["+"] = variadicMacro("Prelude.add");
|
||||
|
||||
macros["-"] = variadicMacro("Prelude.subtract");
|
||||
|
||||
macros["*"] = variadicMacro("Prelude.multiply");
|
||||
|
||||
macros["/"] = variadicMacro("Prelude.divide");
|
||||
|
||||
macros["min"] = variadicMacro("Prelude.min");
|
||||
macros["max"] = variadicMacro("Prelude.max");
|
||||
|
||||
macros[">"] = variadicMacro("Prelude.greaterThan");
|
||||
macros[">="] = variadicMacro("Prelude.greaterEqual");
|
||||
macros["<"] = variadicMacro("Prelude.lessThan");
|
||||
macros["<="] = variadicMacro("Prelude.lesserEqual");
|
||||
|
||||
macros["="] = variadicMacro("Prelude.areEqual");
|
||||
|
||||
// the (apply [func] [args]) macro keeps its own list of aliases for the math operators
|
||||
// that can't just be function aliases because they emulate &rest behavior
|
||||
var opAliases = [
|
||||
"+" => "Prelude.add",
|
||||
"-" => "Prelude.subtract",
|
||||
"*" => "Prelude.multiply",
|
||||
"/" => "Prelude.divide",
|
||||
">" => "Prelude.greaterThan",
|
||||
">=" => "Prelude.greaterEqual",
|
||||
"<" => "Prelude.lessThan",
|
||||
"<=" => "Prelude.lesserEqual",
|
||||
"=" => "Prelude.areEqual"
|
||||
];
|
||||
|
||||
macros["apply"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k) -> {
|
||||
wholeExp.checkNumArgs(2, 2, '(apply [func] [argList])');
|
||||
|
||||
var callOn = switch (exps[0].def) {
|
||||
case FieldExp(field, exp):
|
||||
exp;
|
||||
default:
|
||||
Symbol("null").withPosOf(wholeExp);
|
||||
};
|
||||
var func = switch (exps[0].def) {
|
||||
case Symbol(sym) if (opAliases.exists(sym)):
|
||||
Symbol(opAliases[sym]).withPosOf(wholeExp);
|
||||
default:
|
||||
exps[0];
|
||||
};
|
||||
var args = switch (exps[0].def) {
|
||||
case Symbol(sym) if (opAliases.exists(sym)):
|
||||
ListExp([
|
||||
CallExp(FieldExp("map", exps[1]).withPosOf(wholeExp), [Symbol("kiss.Operand.fromDynamic").withPosOf(wholeExp)]).withPosOf(wholeExp)
|
||||
]).withPosOf(wholeExp);
|
||||
default:
|
||||
exps[1];
|
||||
};
|
||||
CallExp(Symbol("Reflect.callMethod").withPosOf(wholeExp), [callOn, func, args]).withPosOf(wholeExp);
|
||||
};
|
||||
|
||||
function bodyIf(formName:String, negated:Bool, wholeExp:ReaderExp, args:Array<ReaderExp>, k) {
|
||||
wholeExp.checkNumArgs(2, null, '($formName [condition] [body...])');
|
||||
var condition = if (negated) {
|
||||
CallExp(Symbol("not").withPosOf(args[0]), [args[0]]).withPosOf(args[0]);
|
||||
} else {
|
||||
args[0];
|
||||
}
|
||||
return CallExp(Symbol("if").withPosOf(wholeExp), [
|
||||
condition,
|
||||
CallExp(Symbol("begin").withPosOf(wholeExp), args.slice(1)).withPosOf(wholeExp)
|
||||
]).withPosOf(wholeExp);
|
||||
}
|
||||
macros["when"] = bodyIf.bind("when", false);
|
||||
macros["unless"] = bodyIf.bind("unless", true);
|
||||
|
||||
macros["cond"] = cond;
|
||||
|
||||
// (or... ) uses (cond... ) under the hood
|
||||
macros["or"] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k) -> {
|
||||
var uniqueVarName = "_" + Uuid.v4().toShort();
|
||||
var uniqueVarSymbol = Symbol(uniqueVarName).withPos(args[0].pos);
|
||||
|
||||
CallExp(Symbol("begin").withPosOf(wholeExp), [
|
||||
CallExp(Symbol("deflocal").withPosOf(wholeExp), [
|
||||
MetaExp("mut", TypedExp("Dynamic", uniqueVarSymbol).withPosOf(wholeExp)).withPosOf(wholeExp),
|
||||
Symbol("null").withPosOf(wholeExp)
|
||||
]).withPos(args[0].pos),
|
||||
CallExp(Symbol("cond").withPosOf(wholeExp), [
|
||||
for (arg in args) {
|
||||
CallExp(CallExp(Symbol("set").withPosOf(wholeExp), [uniqueVarSymbol, arg]).withPosOf(wholeExp), [uniqueVarSymbol]).withPosOf(wholeExp);
|
||||
}
|
||||
]).withPosOf(wholeExp)
|
||||
]).withPosOf(wholeExp);
|
||||
};
|
||||
|
||||
// (and... uses (cond... ) and (not ...) under the hood)
|
||||
macros["and"] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k) -> {
|
||||
var uniqueVarName = "_" + Uuid.v4().toShort();
|
||||
var uniqueVarSymbol = Symbol(uniqueVarName).withPosOf(wholeExp);
|
||||
|
||||
var condCases = [
|
||||
for (arg in args) {
|
||||
CallExp(CallExp(Symbol("not").withPosOf(wholeExp),
|
||||
[
|
||||
CallExp(Symbol("set").withPosOf(wholeExp), [uniqueVarSymbol, arg]).withPosOf(wholeExp)
|
||||
]).withPosOf(wholeExp), [Symbol("null").withPosOf(wholeExp)]).withPosOf(wholeExp);
|
||||
}
|
||||
];
|
||||
condCases.push(CallExp(Symbol("true").withPosOf(wholeExp), [uniqueVarSymbol]).withPosOf(wholeExp));
|
||||
|
||||
CallExp(Symbol("begin").withPosOf(wholeExp), [
|
||||
CallExp(Symbol("deflocal").withPosOf(wholeExp), [
|
||||
MetaExp("mut", TypedExp("Dynamic", uniqueVarSymbol).withPosOf(wholeExp)).withPosOf(wholeExp),
|
||||
Symbol("null").withPosOf(wholeExp)
|
||||
]).withPosOf(wholeExp),
|
||||
CallExp(Symbol("cond").withPosOf(wholeExp), condCases).withPosOf(wholeExp)
|
||||
]).withPosOf(wholeExp);
|
||||
};
|
||||
|
||||
function arraySet(wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) {
|
||||
return CallExp(Symbol("set").withPosOf(wholeExp), [
|
||||
CallExp(Symbol("nth").withPosOf(wholeExp), [exps[0], exps[1]]).withPosOf(wholeExp),
|
||||
exps[2]
|
||||
]).withPosOf(wholeExp);
|
||||
}
|
||||
macros["set-nth"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) -> {
|
||||
wholeExp.checkNumArgs(3, 3, "(set-nth [list] [index] [value])");
|
||||
arraySet(wholeExp, exps, k);
|
||||
};
|
||||
macros["dict-set"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) -> {
|
||||
wholeExp.checkNumArgs(3, 3, "(dict-set [dict] [key] [value])");
|
||||
arraySet(wholeExp, exps, k);
|
||||
};
|
||||
|
||||
macros["assert"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) -> {
|
||||
wholeExp.checkNumArgs(1, 2, "(assert [expression] [message])");
|
||||
var expression = exps[0];
|
||||
var basicMessage = 'Assertion ${expression.def.toString()} failed';
|
||||
var messageExp = if (exps.length > 1) {
|
||||
CallExp(Symbol("+").withPosOf(wholeExp), [StrExp(basicMessage + ": ").withPosOf(wholeExp), exps[1]]);
|
||||
} else {
|
||||
StrExp(basicMessage);
|
||||
};
|
||||
CallExp(Symbol("unless").withPosOf(wholeExp), [
|
||||
expression,
|
||||
CallExp(Symbol("throw").withPosOf(wholeExp), [messageExp.withPosOf(wholeExp)]).withPosOf(wholeExp)
|
||||
]).withPosOf(wholeExp);
|
||||
};
|
||||
|
||||
// Under the hood, (defmacrofun ...) defines a runtime function that accepts Quote arguments and a special form that quotes the arguments to macrofun calls
|
||||
macros["defmacrofun"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) -> {
|
||||
wholeExp.checkNumArgs(3, null, "(defmacrofun [name] [args] [body...])");
|
||||
var macroName = switch (exps[0].def) {
|
||||
case Symbol(name): name;
|
||||
default: throw CompileError.fromExp(exps[0], 'first argument for defmacrofun should be a symbol for the macro name');
|
||||
};
|
||||
var macroNumArgs = switch (exps[1].def) {
|
||||
case ListExp(argNames): argNames.length;
|
||||
default: throw CompileError.fromExp(exps[1], 'second argument of defmacrofun should be a list of argument names');
|
||||
};
|
||||
k.specialForms[macroName] = (wholeExp:ReaderExp, callArgs:Array<ReaderExp>, convert) -> {
|
||||
// Macro functions don't need to check their argument numbers
|
||||
// because macro function calls expand to function calls that the Haxe compiler will check
|
||||
ECall(Context.parse('${k.className}.${macroName}', wholeExp.macroPos()), [
|
||||
for (callArg in callArgs)
|
||||
EFunction(FArrow, {
|
||||
args: [],
|
||||
ret: null,
|
||||
expr: EReturn(k.convert(callArg)).withMacroPosOf(wholeExp)
|
||||
}).withMacroPosOf(wholeExp)
|
||||
]).withMacroPosOf(wholeExp);
|
||||
};
|
||||
|
||||
CallExp(Symbol("defun").withPosOf(wholeExp), exps).withPosOf(wholeExp);
|
||||
}
|
||||
|
||||
macros["defreadermacro"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) -> {
|
||||
wholeExp.checkNumArgs(3, 3, '(defreadermacro ["[startingString]" or [startingStrings...]] [[streamArgName]] [RawHaxe])');
|
||||
|
||||
// reader macros can define a list of strings that will trigger the macro. When there are multiple,
|
||||
// the macro will put back the initiating string into the stream so you can check which one it was
|
||||
var stringsThatMatch = switch (exps[0].def) {
|
||||
case StrExp(s):
|
||||
[s];
|
||||
case ListExp(strings):
|
||||
[
|
||||
for (s in strings)
|
||||
switch (s.def) {
|
||||
case StrExp(s):
|
||||
s;
|
||||
default:
|
||||
throw CompileError.fromExp(s, 'initiator list of defreadermacro must only contain strings');
|
||||
}
|
||||
];
|
||||
default:
|
||||
throw CompileError.fromExp(exps[0], 'first argument to defreadermacro should be a String or list of strings');
|
||||
};
|
||||
|
||||
for (s in stringsThatMatch) {
|
||||
switch (exps[1].def) {
|
||||
case ListExp([{pos: _, def: Symbol(streamArgName)}]):
|
||||
// For now, reader macros only support a one-expression body implemented in #|raw haxe|# (which can contain a block).
|
||||
switch (exps[2].def) {
|
||||
case RawHaxe(code):
|
||||
k.readTable[s] = (stream) -> {
|
||||
if (stringsThatMatch.length > 1) {
|
||||
stream.putBackString(s);
|
||||
}
|
||||
var parser = new Parser();
|
||||
var interp = new Interp();
|
||||
interp.variables.set("ReaderExp", ReaderExpDef);
|
||||
interp.variables.set(streamArgName, stream);
|
||||
interp.execute(parser.parseString(code));
|
||||
};
|
||||
default:
|
||||
throw CompileError.fromExp(exps[2], 'third argument to defreadermacro should be #|raw haxe|#');
|
||||
}
|
||||
default:
|
||||
throw CompileError.fromExp(exps[1], 'second argument to defreadermacro should be [steamArgName]');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
macros["defalias"] = (wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) -> {
|
||||
wholeExp.checkNumArgs(2, 2, "(defalias [whenItsThis] [makeItThis])");
|
||||
k.defAlias(switch (exps[0].def) {
|
||||
case Symbol(whenItsThis):
|
||||
whenItsThis;
|
||||
default:
|
||||
throw CompileError.fromExp(exps[0], 'first argument to defalias should be a symbol for the alias');
|
||||
}, exps[1].def);
|
||||
return null;
|
||||
};
|
||||
|
||||
return macros;
|
||||
}
|
||||
|
||||
// cond expands telescopically into a nested if expression
|
||||
static function cond(wholeExp:ReaderExp, exps:Array<ReaderExp>, k:KissState) {
|
||||
wholeExp.checkNumArgs(1, null, "(cond [cases...])");
|
||||
return switch (exps[0].def) {
|
||||
case CallExp(condition, body):
|
||||
CallExp(Symbol("if").withPosOf(wholeExp), [
|
||||
condition,
|
||||
CallExp(Symbol("begin").withPosOf(wholeExp), body).withPosOf(wholeExp),
|
||||
if (exps.length > 1) {
|
||||
cond(CallExp(Symbol("cond").withPosOf(wholeExp), exps.slice(1)).withPosOf(wholeExp), exps.slice(1), k);
|
||||
} else {
|
||||
Symbol("null").withPosOf(wholeExp);
|
||||
}
|
||||
]).withPosOf(wholeExp);
|
||||
default:
|
||||
throw CompileError.fromExp(exps[0], 'top-level expression of (cond... ) must be a call list starting with a condition expression');
|
||||
};
|
||||
}
|
||||
|
||||
static function variadicMacro(func:String):MacroFunction {
|
||||
return (wholeExp:ReaderExp, exps:Array<ReaderExp>, k) -> {
|
||||
CallExp(Symbol(func).withPosOf(wholeExp), [
|
||||
ListExp([
|
||||
for (exp in exps) {
|
||||
CallExp(Symbol("kiss.Operand.fromDynamic").withPosOf(wholeExp), [exp]).withPosOf(wholeExp);
|
||||
}
|
||||
]).withPosOf(wholeExp)
|
||||
]).withPosOf(wholeExp);
|
||||
};
|
||||
}
|
||||
}
|
||||
4
kiss/src/kiss/Main.hx
Normal file
4
kiss/src/kiss/Main.hx
Normal file
@@ -0,0 +1,4 @@
|
||||
package kiss;
|
||||
|
||||
@:build(kiss.Kiss.build("kiss/src/kiss/Main.kiss"))
|
||||
class Main {}
|
||||
2
kiss/src/kiss/Main.kiss
Normal file
2
kiss/src/kiss/Main.kiss
Normal file
@@ -0,0 +1,2 @@
|
||||
(defun main []
|
||||
(print "Hello, world!"))
|
||||
92
kiss/src/kiss/Operand.hx
Normal file
92
kiss/src/kiss/Operand.hx
Normal file
@@ -0,0 +1,92 @@
|
||||
package kiss;
|
||||
|
||||
import haxe.ds.Either;
|
||||
|
||||
/**
|
||||
Arithmetic operands
|
||||
**/
|
||||
abstract Operand(Either<String, Float>) from Either<String, Float> to Either<String, Float> {
|
||||
@:from inline static function fromString(s:String):Operand {
|
||||
return Left(s);
|
||||
}
|
||||
|
||||
@:from inline static function fromInt(i:Int):Operand {
|
||||
return Right(0.0 + i);
|
||||
}
|
||||
|
||||
@:from inline static function fromFloat(f:Float):Operand {
|
||||
return Right(0.0 + f);
|
||||
}
|
||||
|
||||
@:from inline static function fromBool(b:Bool):Operand {
|
||||
return if (b == true) Right(Math.POSITIVE_INFINITY) else Right(Math.NEGATIVE_INFINITY);
|
||||
}
|
||||
|
||||
// Doing this one implicitly just wasn't working in conjunction with Lambda.fold
|
||||
|
||||
/* @:from */
|
||||
public inline static function fromDynamic(d:Dynamic):Operand {
|
||||
return switch (Type.typeof(d)) {
|
||||
case TInt | TFloat: Right(0.0 + d);
|
||||
// Treating true and false as operands can be useful for equality. In practice, no one should use them
|
||||
// as operands for any other reason
|
||||
case TBool:
|
||||
fromBool(d);
|
||||
default:
|
||||
if (Std.isOfType(d, String)) {
|
||||
Left(d);
|
||||
}
|
||||
// Taking a gamble here that no one will ever try to pass a different kind of Either as an operand,
|
||||
// because at runtime this is as specific as the check can get.
|
||||
else if (Std.isOfType(d, Either)) {
|
||||
d;
|
||||
} else {
|
||||
throw '$d cannot be converted to Operand';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@:to public inline function toString():Null<String> {
|
||||
return switch (this) {
|
||||
case Left(s): s;
|
||||
default: null;
|
||||
};
|
||||
}
|
||||
|
||||
@:to public inline function toFloat():Null<Float> {
|
||||
return switch (this) {
|
||||
case Right(f): f;
|
||||
default: null;
|
||||
};
|
||||
}
|
||||
|
||||
@:to public inline function toInt():Null<Int> {
|
||||
return switch (this) {
|
||||
case Right(f): Math.floor(f);
|
||||
default: null;
|
||||
};
|
||||
}
|
||||
|
||||
@:to public inline function toBool():Null<Bool> {
|
||||
return switch (this) {
|
||||
case Right(f) if (f == Math.POSITIVE_INFINITY): true;
|
||||
case Right(f) if (f == Math.NEGATIVE_INFINITY): false;
|
||||
default: null;
|
||||
}
|
||||
}
|
||||
|
||||
// This wasn't working as an implicit conversion in conjunction with Lambda.fold()
|
||||
|
||||
/* @:to */
|
||||
public static function toDynamic(o:Dynamic):Null<Dynamic> {
|
||||
return if (Std.isOfType(o, Either)) {
|
||||
var o:Operand = cast o;
|
||||
switch (o) {
|
||||
case Right(f): f;
|
||||
case Left(str): str;
|
||||
};
|
||||
} else {
|
||||
o;
|
||||
};
|
||||
}
|
||||
}
|
||||
176
kiss/src/kiss/Prelude.hx
Normal file
176
kiss/src/kiss/Prelude.hx
Normal file
@@ -0,0 +1,176 @@
|
||||
package kiss;
|
||||
|
||||
using Std;
|
||||
|
||||
import kiss.Operand;
|
||||
import haxe.ds.Either;
|
||||
|
||||
class Prelude {
|
||||
static function variadic(op:(Operand, Operand) -> Null<Operand>, comparison = false):(Array<Operand>) -> Dynamic {
|
||||
return (l:kiss.List<Operand>) -> switch (Lambda.fold(l.slice(1), op, l[0])) {
|
||||
case null:
|
||||
false;
|
||||
case somethingElse if (comparison):
|
||||
true;
|
||||
case somethingElse:
|
||||
Operand.toDynamic(somethingElse);
|
||||
};
|
||||
}
|
||||
|
||||
// Kiss arithmetic will incur overhead because of these switch statements, but the results will not be platform-dependent
|
||||
static function _add(a:Operand, b:Operand):Operand {
|
||||
return switch ([a, b]) {
|
||||
case [Left(str), Left(str2)]:
|
||||
Left(str2 + str);
|
||||
case [Right(f), Right(f2)]:
|
||||
Right(f + f2);
|
||||
default:
|
||||
throw 'cannot add mismatched types ${Operand.toDynamic(a)} and ${Operand.toDynamic(b)}';
|
||||
};
|
||||
}
|
||||
|
||||
public static var add = variadic(_add);
|
||||
|
||||
static function _subtract(val:Operand, from:Operand):Operand {
|
||||
return switch ([from, val]) {
|
||||
case [Right(from), Right(val)]:
|
||||
Right(from - val);
|
||||
default:
|
||||
throw 'cannot subtract ${Operand.toDynamic(val)} from ${Operand.toDynamic(from)}';
|
||||
}
|
||||
}
|
||||
|
||||
public static var subtract = variadic(_subtract);
|
||||
|
||||
static function _multiply(a:Operand, b:Operand):Operand {
|
||||
return switch ([a, b]) {
|
||||
case [Right(f), Right(f2)]:
|
||||
Right(f * f2);
|
||||
case [Left(a), Left(b)]:
|
||||
throw 'cannot multiply strings "$a" and "$b"';
|
||||
case [Right(i), Left(s)] | [Left(s), Right(i)] if (i % 1 == 0):
|
||||
var result = "";
|
||||
for (_ in 0...Math.floor(i)) {
|
||||
result += s;
|
||||
}
|
||||
Left(result);
|
||||
default:
|
||||
throw 'cannot multiply ${Operand.toDynamic(a)} and ${Operand.toDynamic(b)}';
|
||||
};
|
||||
}
|
||||
|
||||
public static var multiply = variadic(_multiply);
|
||||
|
||||
static function _divide(bottom:Operand, top:Operand):Operand {
|
||||
return switch ([top, bottom]) {
|
||||
case [Right(f), Right(f2)]:
|
||||
Right(f / f2);
|
||||
default:
|
||||
throw 'cannot divide $top by $bottom';
|
||||
};
|
||||
}
|
||||
|
||||
public static var divide = variadic(_divide);
|
||||
|
||||
public static function mod(bottom:Float, top:Float):Float {
|
||||
return top % bottom;
|
||||
}
|
||||
|
||||
public static function pow(exponent:Float, base:Float):Float {
|
||||
return Math.pow(base, exponent);
|
||||
}
|
||||
|
||||
static function _min(a:Operand, b:Operand):Operand {
|
||||
return Right(Math.min(a.toFloat(), b.toFloat()));
|
||||
}
|
||||
|
||||
public static var min = variadic(_min);
|
||||
|
||||
static function _max(a:Operand, b:Operand):Operand {
|
||||
return Right(Math.max(a.toFloat(), b.toFloat()));
|
||||
}
|
||||
|
||||
public static var max = variadic(_max);
|
||||
|
||||
static function _greaterThan(b:Operand, a:Operand):Null<Operand> {
|
||||
return if (a == null || b == null) null else if (a.toFloat() > b.toFloat()) b else null;
|
||||
}
|
||||
|
||||
public static var greaterThan = variadic(_greaterThan, true);
|
||||
|
||||
static function _greaterEqual(b:Operand, a:Operand):Null<Operand> {
|
||||
return if (a == null || b == null) null else if (a.toFloat() >= b.toFloat()) b else null;
|
||||
}
|
||||
|
||||
public static var greaterEqual = variadic(_greaterEqual, true);
|
||||
|
||||
static function _lessThan(b:Operand, a:Operand):Null<Operand> {
|
||||
return if (a == null || b == null) null else if (a.toFloat() < b.toFloat()) b else null;
|
||||
}
|
||||
|
||||
public static var lessThan = variadic(_lessThan, true);
|
||||
|
||||
static function _lesserEqual(b:Operand, a:Operand):Null<Operand> {
|
||||
return if (a == null || b == null) null else if (a.toFloat() <= b.toFloat()) b else null;
|
||||
}
|
||||
|
||||
public static var lesserEqual = variadic(_lesserEqual, true);
|
||||
|
||||
static function _areEqual(a:Operand, b:Operand):Null<Operand> {
|
||||
return if (a == null || b == null) null else switch ([a, b]) {
|
||||
case [Left(aStr), Left(bStr)] if (aStr == bStr):
|
||||
a;
|
||||
case [Right(aFloat), Right(bFloat)] if (aFloat == bFloat):
|
||||
a;
|
||||
default:
|
||||
null;
|
||||
};
|
||||
}
|
||||
|
||||
public static var areEqual = variadic(_areEqual, true);
|
||||
|
||||
public static function groups<T>(a:Array<T>, size, keepRemainder = false) {
|
||||
var numFullGroups = Math.floor(a.length / size);
|
||||
var fullGroups = [
|
||||
for (num in 0...numFullGroups) {
|
||||
var start = num * size;
|
||||
var end = (num + 1) * size;
|
||||
a.slice(start, end);
|
||||
}
|
||||
];
|
||||
if (a.length % size != 0 && keepRemainder) {
|
||||
fullGroups.push(a.slice(numFullGroups * size));
|
||||
}
|
||||
return fullGroups;
|
||||
}
|
||||
|
||||
public static dynamic function truthy(v:Any) {
|
||||
return switch (Type.typeof(v)) {
|
||||
case TNull: false;
|
||||
case TInt | TFloat: (v : Float) > 0;
|
||||
case TBool: (v : Bool);
|
||||
default:
|
||||
// Empty strings are falsy
|
||||
if (v.isOfType(String)) {
|
||||
var str:String = cast v;
|
||||
str.length > 0;
|
||||
} else if (v.isOfType(Array)) {
|
||||
// Empty lists are falsy
|
||||
var lst:Array<Dynamic> = cast v;
|
||||
lst.length > 0;
|
||||
} else {
|
||||
// Any other value is true by default
|
||||
true;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static function print<T>(v:T) {
|
||||
#if (sys || hxnodejs)
|
||||
Sys.println(v);
|
||||
#else
|
||||
trace(v);
|
||||
#end
|
||||
return v;
|
||||
}
|
||||
}
|
||||
178
kiss/src/kiss/Reader.hx
Normal file
178
kiss/src/kiss/Reader.hx
Normal file
@@ -0,0 +1,178 @@
|
||||
package kiss;
|
||||
|
||||
import haxe.ds.Option;
|
||||
import kiss.Stream;
|
||||
|
||||
using kiss.Reader;
|
||||
|
||||
typedef ReaderExp = {
|
||||
pos:Position,
|
||||
def:ReaderExpDef
|
||||
};
|
||||
|
||||
enum ReaderExpDef {
|
||||
CallExp(func:ReaderExp, args:Array<ReaderExp>); // (f a1 a2...)
|
||||
ListExp(exps:Array<ReaderExp>); // [v1 v2 v3]
|
||||
StrExp(s:String); // "literal"
|
||||
Symbol(name:String); // s
|
||||
RawHaxe(code:String); // #| haxeCode() |#
|
||||
TypedExp(path:String, exp:ReaderExp); // :type [exp]
|
||||
MetaExp(meta:String, exp:ReaderExp); // &meta [exp]
|
||||
FieldExp(field:String, exp:ReaderExp); // .field [exp]
|
||||
}
|
||||
|
||||
typedef ReadFunction = (Stream) -> Null<ReaderExpDef>;
|
||||
|
||||
class Reader {
|
||||
// The built-in readtable
|
||||
public static function builtins() {
|
||||
var readTable:Map<String, ReadFunction> = [];
|
||||
|
||||
readTable["("] = (stream) -> CallExp(assertRead(stream, readTable), readExpArray(stream, ")", readTable));
|
||||
readTable["["] = (stream) -> ListExp(readExpArray(stream, "]", readTable));
|
||||
readTable["\""] = (stream) -> StrExp(stream.expect("closing \"", () -> stream.takeUntilAndDrop("\"")));
|
||||
readTable["/*"] = (stream) -> {
|
||||
stream.dropUntil("*/");
|
||||
stream.dropString("*/");
|
||||
null;
|
||||
};
|
||||
readTable["//"] = (stream) -> {
|
||||
stream.dropUntil("\n");
|
||||
null;
|
||||
};
|
||||
readTable["#|"] = (stream) -> RawHaxe(stream.expect("closing |#", () -> stream.takeUntilAndDrop("|#")));
|
||||
// For defmacrofuns, unquoting with , is syntactic sugar for calling a Quote (Void->T)
|
||||
readTable[","] = (stream) -> CallExp(assertRead(stream, readTable), []);
|
||||
// If/when proper defmacro is added, reading every Unquote directly as a CallExp won't work anymore
|
||||
|
||||
readTable[":"] = (stream) -> TypedExp(nextToken(stream, "a type path"), assertRead(stream, readTable));
|
||||
|
||||
readTable["&"] = (stream) -> MetaExp(nextToken(stream, "a meta symbol like mut, optional, rest"), assertRead(stream, readTable));
|
||||
|
||||
readTable["!"] = (stream:Stream) -> CallExp(Symbol("not").withPos(stream.position()), [assertRead(stream, readTable)]);
|
||||
|
||||
// Helpful for defining predicates to pass to Haxe functions:
|
||||
readTable["?"] = (stream:Stream) -> CallExp(Symbol("Prelude.truthy").withPos(stream.position()), [assertRead(stream, readTable)]);
|
||||
|
||||
// Lets you dot-access a function result without binding it to a name
|
||||
readTable["."] = (stream:Stream) -> FieldExp(nextToken(stream, "a field name"), assertRead(stream, readTable));
|
||||
|
||||
// Because macro keys are sorted by length and peekChars(0) returns "", this will be used as the default reader macro:
|
||||
readTable[""] = (stream) -> Symbol(nextToken(stream, "a symbol name"));
|
||||
// TODO make - A macro for numerical negation
|
||||
|
||||
return readTable;
|
||||
}
|
||||
|
||||
public static final terminators = [")", "]", "/*", "\n", " "];
|
||||
|
||||
static function nextToken(stream:Stream, expect:String) {
|
||||
return stream.expect(expect, () -> stream.takeUntilOneOf(terminators));
|
||||
}
|
||||
|
||||
public static function assertRead(stream:Stream, readTable:Map<String, ReadFunction>):ReaderExp {
|
||||
var position = stream.position();
|
||||
return switch (read(stream, readTable)) {
|
||||
case Some(exp):
|
||||
exp;
|
||||
case None:
|
||||
throw 'There were no expressions left in the stream at $position';
|
||||
};
|
||||
}
|
||||
|
||||
public static function read(stream:Stream, readTable:Map<String, ReadFunction>):Option<ReaderExp> {
|
||||
stream.dropWhitespace();
|
||||
|
||||
if (stream.isEmpty())
|
||||
return None;
|
||||
|
||||
var position = stream.position();
|
||||
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.withPos(position));
|
||||
} else {
|
||||
read(stream, readTable);
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
throw 'No macro to read next expression';
|
||||
}
|
||||
|
||||
public static function readExpArray(stream:Stream, end:String, readTable:Map<String, ReadFunction>):Array<ReaderExp> {
|
||||
var array = [];
|
||||
while (!stream.startsWith(end)) {
|
||||
stream.dropWhitespace();
|
||||
if (!stream.startsWith(end))
|
||||
array.push(assertRead(stream, readTable));
|
||||
}
|
||||
stream.dropString(end);
|
||||
return array;
|
||||
}
|
||||
|
||||
public static function withPos(def:ReaderExpDef, pos:Position) {
|
||||
return {
|
||||
pos: pos,
|
||||
def: def
|
||||
};
|
||||
}
|
||||
|
||||
public static function withPosOf(def:ReaderExpDef, exp:ReaderExp) {
|
||||
return {
|
||||
pos: exp.pos,
|
||||
def: def
|
||||
};
|
||||
}
|
||||
|
||||
public static function toString(exp:ReaderExpDef) {
|
||||
return switch (exp) {
|
||||
case CallExp(func, args):
|
||||
// (f a1 a2...)
|
||||
var str = '(' + func.def.toString();
|
||||
if (args.length > 0)
|
||||
str += " ";
|
||||
str += [
|
||||
for (arg in args) {
|
||||
arg.def.toString();
|
||||
}
|
||||
].join(" ");
|
||||
str += ')';
|
||||
str;
|
||||
case ListExp(exps):
|
||||
// [v1 v2 v3]
|
||||
var str = '[';
|
||||
str += [
|
||||
for (exp in exps) {
|
||||
exp.def.toString();
|
||||
}
|
||||
].join(" ");
|
||||
str += ']';
|
||||
str;
|
||||
case StrExp(s):
|
||||
// "literal"
|
||||
'"$s"';
|
||||
case Symbol(name):
|
||||
// s
|
||||
name;
|
||||
case RawHaxe(code):
|
||||
// #| haxeCode() |#
|
||||
'#| $code |#';
|
||||
case TypedExp(path, exp):
|
||||
// :type [exp]
|
||||
':$path ${exp.def.toString()}';
|
||||
case MetaExp(meta, exp):
|
||||
// &meta
|
||||
'&$meta ${exp.def.toString()}';
|
||||
case FieldExp(field, exp):
|
||||
'.$field ${exp.def.toString()}';
|
||||
}
|
||||
}
|
||||
}
|
||||
297
kiss/src/kiss/SpecialForms.hx
Normal file
297
kiss/src/kiss/SpecialForms.hx
Normal file
@@ -0,0 +1,297 @@
|
||||
package kiss;
|
||||
|
||||
import haxe.macro.Expr;
|
||||
import haxe.macro.Context;
|
||||
import kiss.Reader;
|
||||
import uuid.Uuid;
|
||||
|
||||
using uuid.Uuid;
|
||||
using kiss.Reader;
|
||||
using kiss.Helpers;
|
||||
using kiss.Prelude;
|
||||
|
||||
import kiss.Kiss;
|
||||
|
||||
// Special forms convert Kiss reader expressions into Haxe macro expressions
|
||||
typedef SpecialFormFunction = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> Expr;
|
||||
|
||||
class SpecialForms {
|
||||
public static function builtins() {
|
||||
var map:Map<String, SpecialFormFunction> = [];
|
||||
|
||||
map["begin"] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> {
|
||||
// Sometimes empty blocks are useful, so a checkNumArgs() seems unnecessary here for now.
|
||||
|
||||
EBlock([for (bodyExp in args) k.convert(bodyExp)]).withMacroPosOf(wholeExp);
|
||||
};
|
||||
|
||||
function arrayAccess(wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) {
|
||||
return EArray(k.convert(args[0]), k.convert(args[1])).withMacroPosOf(wholeExp);
|
||||
};
|
||||
map["nth"] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> {
|
||||
wholeExp.checkNumArgs(2, 2, "(nth [list] [idx])");
|
||||
arrayAccess(wholeExp, args, k);
|
||||
};
|
||||
map["dict-get"] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> {
|
||||
wholeExp.checkNumArgs(2, 2, "(dict-get [dict] [key])");
|
||||
arrayAccess(wholeExp, args, k);
|
||||
};
|
||||
|
||||
function makeQuickNth(idx:Int, name:String) {
|
||||
map[name] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> {
|
||||
wholeExp.checkNumArgs(1, 1, '($name [list])');
|
||||
EArray(k.convert(args[0]), macro $v{idx}).withMacroPosOf(wholeExp);
|
||||
};
|
||||
}
|
||||
makeQuickNth(0, "first");
|
||||
makeQuickNth(1, "second");
|
||||
makeQuickNth(2, "third");
|
||||
makeQuickNth(3, "fourth");
|
||||
makeQuickNth(4, "fifth");
|
||||
makeQuickNth(5, "sixth");
|
||||
makeQuickNth(6, "seventh");
|
||||
makeQuickNth(7, "eighth");
|
||||
makeQuickNth(8, "ninth");
|
||||
makeQuickNth(9, "tenth");
|
||||
makeQuickNth(-1, "last");
|
||||
|
||||
// TODO rest
|
||||
|
||||
// Declare anonymous objects
|
||||
map["object"] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> {
|
||||
if (args.length % 2 != 0) {
|
||||
throw CompileError.fromExp(wholeExp, "(object [field bindings]...) must have an even number of arguments");
|
||||
}
|
||||
EObjectDecl([
|
||||
for (pair in args.groups(2))
|
||||
{
|
||||
quotes: Unquoted,
|
||||
field: switch (pair[0].def) {
|
||||
case Symbol(name): name;
|
||||
case TypedExp(_,
|
||||
{pos: _, def: Symbol(_)}): throw CompileError.fromExp(pair[0], "type specification on anonymous objects will be ignored");
|
||||
default: throw CompileError.fromExp(pair[0], "first expression in anonymous object field binding should be a plain symbol");
|
||||
},
|
||||
expr: k.convert(pair[1])
|
||||
}
|
||||
]).withMacroPosOf(wholeExp);
|
||||
};
|
||||
|
||||
map["new"] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> {
|
||||
wholeExp.checkNumArgs(1, null, '(new [type] [constructorArgs...])');
|
||||
var classType = switch (args[0].def) {
|
||||
case Symbol(name): name;
|
||||
default: throw CompileError.fromExp(args[0], 'first arg in (new [type] ...) should be a class to instantiate');
|
||||
};
|
||||
ENew(Helpers.parseTypePath(classType, args[0]), args.slice(1).map(k.convert)).withMacroPosOf(wholeExp);
|
||||
};
|
||||
|
||||
map["set"] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> {
|
||||
wholeExp.checkNumArgs(2, 2, "(set [variable] [value])");
|
||||
EBinop(OpAssign, k.convert(args[0]), k.convert(args[1])).withMacroPosOf(wholeExp);
|
||||
};
|
||||
|
||||
function toVar(nameExp:ReaderExp, valueExp:ReaderExp, k:KissState, ?isFinal:Bool):Var {
|
||||
// This check seems like unnecessary repetition but it's not. It allows is so that individual destructured bindings can specify mutability
|
||||
return if (isFinal == null) {
|
||||
switch (nameExp.def) {
|
||||
case MetaExp("mut", innerNameExp):
|
||||
toVar(innerNameExp, valueExp, k, false);
|
||||
default:
|
||||
toVar(nameExp, valueExp, k, true);
|
||||
};
|
||||
} else {
|
||||
name: switch (nameExp.def) {
|
||||
case Symbol(name) | TypedExp(_, {pos: _, def: Symbol(name)}):
|
||||
name;
|
||||
default:
|
||||
throw CompileError.fromExp(nameExp, 'expected a symbol or typed symbol for variable name in a var binding');
|
||||
},
|
||||
type: switch (nameExp.def) {
|
||||
case TypedExp(type, _):
|
||||
Helpers.parseComplexType(type, nameExp);
|
||||
default: null;
|
||||
},
|
||||
isFinal: isFinal,
|
||||
expr: k.convert(valueExp)
|
||||
};
|
||||
}
|
||||
|
||||
function toVars(namesExp:ReaderExp, valueExp:ReaderExp, k:KissState, ?isFinal:Bool):Array<Var> {
|
||||
return if (isFinal == null) {
|
||||
switch (namesExp.def) {
|
||||
case MetaExp("mut", innerNamesExp):
|
||||
toVars(innerNamesExp, valueExp, k, false);
|
||||
default:
|
||||
toVars(namesExp, valueExp, k, true);
|
||||
};
|
||||
} else {
|
||||
switch (namesExp.def) {
|
||||
case Symbol(_) | TypedExp(_, {pos: _, def: Symbol(_)}):
|
||||
[toVar(namesExp, valueExp, k, isFinal)];
|
||||
case ListExp(nameExps):
|
||||
var uniqueVarName = "_" + Uuid.v4().toShort();
|
||||
var uniqueVarSymbol = Symbol(uniqueVarName).withPosOf(valueExp);
|
||||
var idx = 0;
|
||||
// Only evaluate the list expression being destructured once:
|
||||
[toVar(uniqueVarSymbol, valueExp, k, true)].concat([
|
||||
for (nameExp in nameExps)
|
||||
toVar(nameExp,
|
||||
CallExp(Symbol("nth").withPosOf(valueExp),
|
||||
[uniqueVarSymbol, Symbol(Std.string(idx++)).withPosOf(valueExp)]).withPosOf(valueExp),
|
||||
k, if (isFinal == false) false else null)
|
||||
]);
|
||||
default:
|
||||
throw CompileError.fromExp(namesExp, "Can only bind variables to a symbol or list of symbols for destructuring");
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
map["deflocal"] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> {
|
||||
wholeExp.checkNumArgs(2, 3, "(deflocal [optional :type] [variable] [optional: &mut] [value])");
|
||||
EVars(toVars(args[0], args[1], k)).withMacroPosOf(wholeExp);
|
||||
};
|
||||
|
||||
map["let"] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> {
|
||||
wholeExp.checkNumArgs(2, null, "(let [[bindings...]] [body...])");
|
||||
var bindingList = switch (args[0].def) {
|
||||
case ListExp(bindingExps) if (bindingExps.length > 0 && bindingExps.length % 2 == 0):
|
||||
bindingExps;
|
||||
default:
|
||||
throw CompileError.fromExp(args[0], 'let bindings should be a list expression with an even number of sub expressions');
|
||||
};
|
||||
var bindingPairs = bindingList.groups(2);
|
||||
var varDefs = [];
|
||||
for (bindingPair in bindingPairs) {
|
||||
varDefs = varDefs.concat(toVars(bindingPair[0], bindingPair[1], k));
|
||||
}
|
||||
|
||||
var body = args.slice(1);
|
||||
if (body.length == 0) {
|
||||
throw CompileError.fromArgs(args, '(let....) expression needs a body');
|
||||
}
|
||||
|
||||
EBlock([
|
||||
EVars(varDefs).withMacroPosOf(wholeExp),
|
||||
EBlock(body.map(k.convert)).withMacroPosOf(wholeExp)
|
||||
]).withMacroPosOf(wholeExp);
|
||||
};
|
||||
|
||||
map["lambda"] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> {
|
||||
wholeExp.checkNumArgs(2, null, "(lambda [[argsNames...]] [body...])");
|
||||
EFunction(FArrow, Helpers.makeFunction(null, args[0], args.slice(1), k)).withMacroPosOf(wholeExp);
|
||||
};
|
||||
|
||||
function forExpr(formName:String, wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) {
|
||||
wholeExp.checkNumArgs(3, null, '($formName [varName] [list] [body...])');
|
||||
var uniqueVarName = "_" + Uuid.v4().toShort();
|
||||
var namesExp = args[0];
|
||||
var listExp = args[1];
|
||||
var bodyExps = args.slice(2);
|
||||
bodyExps.insert(0, CallExp(Symbol("deflocal").withPosOf(args[2]), [namesExp, Symbol(uniqueVarName).withPosOf(args[2])]).withPosOf(args[2]));
|
||||
var body = CallExp(Symbol("begin").withPosOf(args[2]), bodyExps).withPosOf(args[2]);
|
||||
return EFor(EBinop(OpIn, EConst(CIdent(uniqueVarName)).withMacroPosOf(wholeExp), k.convert(listExp)).withMacroPosOf(wholeExp),
|
||||
k.convert(body)).withMacroPosOf(wholeExp);
|
||||
}
|
||||
|
||||
map["doFor"] = forExpr.bind("doFor");
|
||||
map["for"] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> {
|
||||
EArrayDecl([forExpr("for", wholeExp, args, k)]).withMacroPosOf(wholeExp);
|
||||
};
|
||||
|
||||
map["return"] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> {
|
||||
wholeExp.checkNumArgs(1, 1, '(return [value])');
|
||||
EReturn(k.convert(args[0])).withMacroPosOf(wholeExp);
|
||||
};
|
||||
|
||||
map["break"] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> {
|
||||
wholeExp.checkNumArgs(0, 0, "(break)");
|
||||
EBreak.withMacroPosOf(wholeExp);
|
||||
};
|
||||
|
||||
map["continue"] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> {
|
||||
wholeExp.checkNumArgs(0, 0, "(continue)");
|
||||
EContinue.withMacroPosOf(wholeExp);
|
||||
};
|
||||
|
||||
// TODO (case... ) for switch
|
||||
|
||||
// Type check syntax:
|
||||
map["the"] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> {
|
||||
wholeExp.checkNumArgs(2, 2, '(the [type] [value])');
|
||||
ECheckType(k.convert(args[1]), switch (args[0].def) {
|
||||
case Symbol(type): Helpers.parseComplexType(type, args[0]);
|
||||
default: throw CompileError.fromExp(args[0], 'first argument to (the... ) should be a valid type');
|
||||
}).withMacroPosOf(wholeExp);
|
||||
};
|
||||
|
||||
map["try"] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> {
|
||||
wholeExp.checkNumArgs(1, null, "(try [thing] [catches...])");
|
||||
var tryKissExp = args[0];
|
||||
var catchKissExps = args.slice(1);
|
||||
ETry(k.convert(tryKissExp), [
|
||||
for (catchKissExp in catchKissExps) {
|
||||
switch (catchKissExp.def) {
|
||||
case CallExp({pos: _, def: Symbol("catch")}, catchArgs):
|
||||
{
|
||||
name: switch (catchArgs[0].def) {
|
||||
case ListExp([
|
||||
{
|
||||
pos: _,
|
||||
def: Symbol(name) | TypedExp(_, {pos: _, def: Symbol(name)})
|
||||
}
|
||||
]): name;
|
||||
default: throw CompileError.fromExp(catchArgs[0], 'first argument to (catch... ) should be a one-element argument list');
|
||||
},
|
||||
type: switch (catchArgs[0].def) {
|
||||
case ListExp([{pos: _, def: TypedExp(type, _)}]):
|
||||
Helpers.parseComplexType(type, catchArgs[0]);
|
||||
default: null;
|
||||
},
|
||||
expr: k.convert(CallExp(Symbol("begin").withPos(catchArgs[1].pos), catchArgs.slice(1)).withPos(catchArgs[1].pos))
|
||||
};
|
||||
default:
|
||||
throw CompileError.fromExp(catchKissExp,
|
||||
'expressions following the first expression in a (try... ) should all be (catch [[error]] [body...]) expressions');
|
||||
}
|
||||
}
|
||||
]).withMacroPosOf(wholeExp);
|
||||
};
|
||||
|
||||
map["throw"] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> {
|
||||
if (args.length != 1) {
|
||||
throw CompileError.fromExp(wholeExp, 'throw expression should only throw one value');
|
||||
}
|
||||
EThrow(k.convert(args[0])).withMacroPosOf(wholeExp);
|
||||
};
|
||||
|
||||
map["if"] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> {
|
||||
wholeExp.checkNumArgs(2, 3, '(if [cond] [then] [?else])');
|
||||
|
||||
var condition = macro Prelude.truthy(${k.convert(args[0])});
|
||||
var thenExp = k.convert(args[1]);
|
||||
var elseExp = if (args.length > 2) {
|
||||
k.convert(args[2]);
|
||||
} else {
|
||||
// Kiss (if... ) expressions all need to generate a Haxe else block
|
||||
// to make sure they always return something
|
||||
macro null;
|
||||
};
|
||||
|
||||
macro if ($condition)
|
||||
$thenExp
|
||||
else
|
||||
$elseExp;
|
||||
};
|
||||
|
||||
map["not"] = (wholeExp:ReaderExp, args:Array<ReaderExp>, k:KissState) -> {
|
||||
if (args.length != 1)
|
||||
throw CompileError.fromExp(wholeExp, '(not... ) only takes one argument, not $args');
|
||||
var condition = k.convert(args[0]);
|
||||
var truthyExp = macro Prelude.truthy($condition);
|
||||
macro !$truthyExp;
|
||||
};
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
157
kiss/src/kiss/Stream.hx
Normal file
157
kiss/src/kiss/Stream.hx
Normal file
@@ -0,0 +1,157 @@
|
||||
package kiss;
|
||||
|
||||
import sys.io.File;
|
||||
import haxe.ds.Option;
|
||||
|
||||
using StringTools;
|
||||
using Lambda;
|
||||
|
||||
typedef Position = {
|
||||
file:String,
|
||||
line:Int,
|
||||
column:Int,
|
||||
absoluteChar:Int
|
||||
};
|
||||
|
||||
class Stream {
|
||||
var content:String;
|
||||
var file:String;
|
||||
var line:Int;
|
||||
var column:Int;
|
||||
var absoluteChar:Int;
|
||||
|
||||
public function new(file:String) {
|
||||
// Banish ye Windows line-endings
|
||||
content = File.getContent(file).replace('\r', '');
|
||||
// Life is easier with a trailing newline
|
||||
if (content.charAt(content.length - 1) != "\n")
|
||||
content += "\n";
|
||||
|
||||
this.file = file;
|
||||
line = 1;
|
||||
column = 1;
|
||||
absoluteChar = 0;
|
||||
}
|
||||
|
||||
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():Position {
|
||||
return {
|
||||
file: file,
|
||||
line: line,
|
||||
column: column,
|
||||
absoluteChar: absoluteChar
|
||||
};
|
||||
}
|
||||
|
||||
public static function toPrint(p:Position) {
|
||||
return '${p.file}:${p.line}:${p.column}';
|
||||
}
|
||||
|
||||
public function startsWith(s:String) {
|
||||
return switch (peekChars(s.length)) {
|
||||
case Some(s1) if (s == s1): true;
|
||||
default: false;
|
||||
};
|
||||
}
|
||||
|
||||
var lineLengths = [];
|
||||
|
||||
/** 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) {
|
||||
absoluteChar += 1;
|
||||
switch (content.charAt(idx)) {
|
||||
case "\n":
|
||||
line += 1;
|
||||
lineLengths.push(column);
|
||||
column = 1;
|
||||
default:
|
||||
column += 1;
|
||||
}
|
||||
}
|
||||
content = content.substr(count);
|
||||
}
|
||||
|
||||
public function putBackString(s:String) {
|
||||
var idx = s.length - 1;
|
||||
while (idx >= 0) {
|
||||
absoluteChar -= 1;
|
||||
switch (s.charAt(idx)) {
|
||||
case "\n":
|
||||
line -= 1;
|
||||
column = lineLengths.pop();
|
||||
default:
|
||||
column -= 1;
|
||||
}
|
||||
--idx;
|
||||
}
|
||||
content = s + content;
|
||||
}
|
||||
|
||||
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 takeLine():Option<String> {
|
||||
return takeUntilAndDrop("\n");
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
}
|
||||
13
kiss/src/test/TestMain.hx
Normal file
13
kiss/src/test/TestMain.hx
Normal file
@@ -0,0 +1,13 @@
|
||||
package test;
|
||||
|
||||
import utest.Runner;
|
||||
import utest.ui.Report;
|
||||
|
||||
class TestMain {
|
||||
public static function main() {
|
||||
var runner = new Runner();
|
||||
runner.addCases(test.cases);
|
||||
Report.create(runner);
|
||||
runner.run();
|
||||
}
|
||||
}
|
||||
249
kiss/src/test/cases/BasicTestCase.hx
Normal file
249
kiss/src/test/cases/BasicTestCase.hx
Normal file
@@ -0,0 +1,249 @@
|
||||
package test.cases;
|
||||
|
||||
import utest.Test;
|
||||
import utest.Assert;
|
||||
import kiss.Prelude;
|
||||
import kiss.List;
|
||||
|
||||
using StringTools;
|
||||
|
||||
@:build(kiss.Kiss.build("kiss/src/test/cases/BasicTestCase.kiss"))
|
||||
class BasicTestCase extends Test {
|
||||
function testStaticVar() {
|
||||
Assert.equals("Howdy", BasicTestCase.message);
|
||||
}
|
||||
|
||||
function testHaxeInsertion() {
|
||||
Assert.equals(23, BasicTestCase.mathResult);
|
||||
}
|
||||
|
||||
function testStaticFunction() {
|
||||
Assert.equals(6, BasicTestCase.myFloor(6.5));
|
||||
}
|
||||
|
||||
function testFuncall() {
|
||||
Assert.equals(7, BasicTestCase.funResult);
|
||||
}
|
||||
|
||||
function testField() {
|
||||
Assert.equals(5, new BasicTestCase().myField);
|
||||
}
|
||||
|
||||
function testMethod() {
|
||||
Assert.equals(5, new BasicTestCase().myMethod());
|
||||
}
|
||||
|
||||
function testArray() {
|
||||
var arr = BasicTestCase.myArray;
|
||||
Assert.equals(3, arr.length);
|
||||
Assert.equals(1, arr[0]);
|
||||
Assert.equals(2, arr[1]);
|
||||
Assert.equals(3, arr[2]);
|
||||
|
||||
// Kiss arrays can be negatively indexed like Python
|
||||
Assert.equals(3, arr[-1]);
|
||||
Assert.equals(2, arr[-2]);
|
||||
Assert.equals(1, arr[-3]);
|
||||
}
|
||||
|
||||
function testArrayAccess() {
|
||||
Assert.equals(3, BasicTestCase.myArrayLast);
|
||||
}
|
||||
|
||||
function testVariadicAdd() {
|
||||
Assert.equals(6, BasicTestCase.mySum);
|
||||
}
|
||||
|
||||
function testVariadicSubtract() {
|
||||
Assert.equals(-2, BasicTestCase.myDifference);
|
||||
}
|
||||
|
||||
function testVariadicMultiply() {
|
||||
_testMultiplication();
|
||||
}
|
||||
|
||||
function testVariadicDivide() {
|
||||
Assert.equals(0.5, BasicTestCase.myQuotient);
|
||||
}
|
||||
|
||||
function testMod() {
|
||||
Assert.equals(4, BasicTestCase.myRemainder);
|
||||
}
|
||||
|
||||
function testPow() {
|
||||
Assert.equals(256, BasicTestCase.myPower);
|
||||
}
|
||||
|
||||
function testUnop() {
|
||||
Assert.equals(7, BasicTestCase.myInc);
|
||||
Assert.equals(7, BasicTestCase.myNum);
|
||||
}
|
||||
|
||||
function testMin() {
|
||||
Assert.equals(1, BasicTestCase.myMin);
|
||||
}
|
||||
|
||||
function testMax() {
|
||||
Assert.equals(9, BasicTestCase.myMax);
|
||||
}
|
||||
|
||||
function testLessThan() {
|
||||
_testLessThan();
|
||||
}
|
||||
|
||||
function testLesserEqual() {
|
||||
_testLesserEqual();
|
||||
}
|
||||
|
||||
function testGreaterThan() {
|
||||
_testGreaterThan();
|
||||
}
|
||||
|
||||
function testGreaterEqual() {
|
||||
_testGreaterEqual();
|
||||
}
|
||||
|
||||
function testEqual() {
|
||||
_testEqual();
|
||||
}
|
||||
|
||||
function testIf() {
|
||||
_testIf();
|
||||
}
|
||||
|
||||
function testMacros() {
|
||||
Assert.equals(7, BasicTestCase.incrementTwice(5));
|
||||
|
||||
var seasonsGreetings = "ho ";
|
||||
Assert.equals("ho ho ho ", BasicTestCase.doTwiceString(() -> {
|
||||
seasonsGreetings += "ho ";
|
||||
}));
|
||||
}
|
||||
|
||||
// TODO to really test typed variable definitions, check for compilation failure on a bad example
|
||||
function testTypedDefvar() {
|
||||
Assert.equals(8, BasicTestCase.myInt);
|
||||
}
|
||||
|
||||
function testTryCatch() {
|
||||
Assert.equals(5, BasicTestCase.myTryCatch("string error"));
|
||||
Assert.equals(6, BasicTestCase.myTryCatch(404));
|
||||
Assert.equals(7, BasicTestCase.myTryCatch(["list error"]));
|
||||
}
|
||||
|
||||
function testTypeCheck() {
|
||||
Assert.equals(5, BasicTestCase.myTypeCheck());
|
||||
}
|
||||
|
||||
function testGroups() {
|
||||
Assert.equals([[1, 2], [3, 4]].toString(), BasicTestCase.myGroups1().toString());
|
||||
Assert.equals([[1, 2, 3], [4]].toString(), BasicTestCase.myGroups2().toString());
|
||||
}
|
||||
|
||||
function testLet() {
|
||||
_testLet();
|
||||
}
|
||||
|
||||
function testConstructors() {
|
||||
Assert.equals("sup", BasicTestCase.myConstructedString);
|
||||
}
|
||||
|
||||
function testCond() {
|
||||
Assert.equals("this one", BasicTestCase.myCond1);
|
||||
Assert.equals("the default", BasicTestCase.myCond2);
|
||||
Assert.equals("this", BasicTestCase.myCond3);
|
||||
Assert.equals(null, BasicTestCase.myCondFallthrough);
|
||||
}
|
||||
|
||||
function testSetAndDeflocal() {
|
||||
Assert.equals("another thing", BasicTestCase.mySetLocal());
|
||||
}
|
||||
|
||||
function testOr() {
|
||||
Assert.equals(5, BasicTestCase.myOr1);
|
||||
}
|
||||
|
||||
function testAnd() {
|
||||
Assert.equals(6, BasicTestCase.myAnd1);
|
||||
Assert.equals(null, BasicTestCase.myAnd2);
|
||||
Assert.equals(null, BasicTestCase.myAnd3);
|
||||
}
|
||||
|
||||
function testNot() {
|
||||
Assert.equals(false, BasicTestCase.myNot1);
|
||||
Assert.equals(false, BasicTestCase.myNot2);
|
||||
}
|
||||
|
||||
function testLambda() {
|
||||
Assert.equals([5, 6].toString(), BasicTestCase.myFilteredList.toString());
|
||||
}
|
||||
|
||||
function testWhen() {
|
||||
Assert.equals(6, BasicTestCase.myWhen1);
|
||||
}
|
||||
|
||||
function testQuickNths() {
|
||||
_testQuickNths();
|
||||
}
|
||||
|
||||
function testListDestructuring() {
|
||||
_testListDestructuring();
|
||||
}
|
||||
|
||||
function testDoFor() {
|
||||
_testDoFor();
|
||||
}
|
||||
|
||||
function testOptionalArguments() {
|
||||
myOptionalFunc(5);
|
||||
}
|
||||
|
||||
function testRestArguments() {
|
||||
Assert.equals(5, myRest1);
|
||||
Assert.equals(5, myRest2);
|
||||
Assert.equals(5, myRest3);
|
||||
}
|
||||
|
||||
function testCombinedOptRest() {
|
||||
Assert.equals("abcd", myCombined1);
|
||||
Assert.equals("aboop", myCombined2);
|
||||
Assert.equals("ab", myCombined3);
|
||||
}
|
||||
|
||||
function testFieldExps() {
|
||||
_testFieldExps();
|
||||
}
|
||||
|
||||
function testBreakContinue() {
|
||||
_testBreakContinue();
|
||||
}
|
||||
|
||||
function testAssert() {
|
||||
_testAssert();
|
||||
}
|
||||
|
||||
function testApply() {
|
||||
_testApply();
|
||||
}
|
||||
|
||||
function testApplyWithMethod() {
|
||||
Assert.equals(30, applyWithMethod(new BasicObject(5)));
|
||||
Assert.equals(18, applyWithMethod(new BasicObject(3)));
|
||||
}
|
||||
|
||||
function testAnonymousObject() {
|
||||
_testAnonymousObject();
|
||||
}
|
||||
}
|
||||
|
||||
class BasicObject {
|
||||
var val:Int;
|
||||
|
||||
public function new(val:Int) {
|
||||
this.val = val;
|
||||
}
|
||||
|
||||
public function multiply(otherVal:Int) {
|
||||
return val * otherVal;
|
||||
}
|
||||
}
|
||||
319
kiss/src/test/cases/BasicTestCase.kiss
Normal file
319
kiss/src/test/cases/BasicTestCase.kiss
Normal file
@@ -0,0 +1,319 @@
|
||||
// (defvar) declares static variables
|
||||
(defvar message "Howdy")
|
||||
|
||||
// #| ... |# parses and injects raw Haxe code
|
||||
(defvar mathResult #|5 + 6 * 3|#) // Order of operations will apply
|
||||
|
||||
// (defun) declares static functions
|
||||
(defun myFloor [num]
|
||||
// funcalls can use dot access
|
||||
(Math.floor num))
|
||||
|
||||
// functions are resolved in the macro context
|
||||
(defvar funResult (myFloor 7.5))
|
||||
|
||||
// (defprop) declares instance variables
|
||||
(defprop myField 5)
|
||||
|
||||
// (defmethod) declares instance methods
|
||||
(defmethod myMethod [] this.myField)
|
||||
|
||||
// [...] returns a Kiss array (they have special features and convert implicitly)
|
||||
(defvar myArray [1 2 3])
|
||||
|
||||
// Array access is via nth
|
||||
(defvar myArrayLast (nth myArray -1))
|
||||
|
||||
// Variadic math uses haxe's Lambda.fold under the hood
|
||||
(defvar mySum (+ 1 2 3))
|
||||
|
||||
(defvar myDifference (- 5 4 3))
|
||||
|
||||
(defun _testMultiplication []
|
||||
(Assert.equals 60 (* 2 5 6))
|
||||
(Assert.equals 5522401584 (* 84 289 89 71 36))
|
||||
(Assert.equals "heyheyhey" (* "hey" 3)))
|
||||
|
||||
// All math operations return floats, none truncate by default
|
||||
(defvar myQuotient (/ 6 3 2 2))
|
||||
|
||||
(defvar myRemainder (% 10 6))
|
||||
|
||||
(defvar myPower (^ 2 8))
|
||||
|
||||
(defvar &mut myNum 6)
|
||||
(defvar myInc ++myNum)
|
||||
|
||||
(defvar myMin (min 9 3 7 1))
|
||||
(defvar myMax (max 9 3 7 1))
|
||||
|
||||
(defun _testLessThan []
|
||||
(Assert.isTrue (< 1 2 3 4))
|
||||
(Assert.isFalse (< 1 1 3 4))
|
||||
(Assert.isFalse (< 1 12 12)))
|
||||
|
||||
(defun _testLesserEqual []
|
||||
(Assert.isTrue (<= 1 2 3 4))
|
||||
(Assert.isTrue (<= 1 1 3 4))
|
||||
(Assert.isFalse (<= 1 12 11)))
|
||||
|
||||
(defun _testGreaterThan []
|
||||
(Assert.isTrue (> 4 3 2 1))
|
||||
(Assert.isFalse (> 4 4 2 1))
|
||||
(Assert.isFalse (> 9 3 3)))
|
||||
|
||||
(defun _testGreaterEqual []
|
||||
(Assert.isTrue (>= 4 3 2 1))
|
||||
(Assert.isTrue (>= 4 4 2 1))
|
||||
(Assert.isFalse (>= 9 4 5)))
|
||||
|
||||
(defun _testEqual []
|
||||
(Assert.isTrue (= 1 1 1 1))
|
||||
(Assert.isFalse (= 1 2 1 1))
|
||||
(Assert.isTrue (= "hey" "hey" "hey"))
|
||||
(Assert.isFalse (= "hey" "you" "hey"))
|
||||
(Assert.isTrue (= true true true))
|
||||
(Assert.isFalse (= true false true))
|
||||
(Assert.isTrue (= false false false)))
|
||||
|
||||
(defun _testIf []
|
||||
(Assert.equals true (if 1 true false))
|
||||
(Assert.equals false (if 0 true false))
|
||||
(Assert.equals false (if -1 true false))
|
||||
(Assert.equals false (if null true false))
|
||||
(Assert.equals true (if true true false))
|
||||
(Assert.equals false (if false true false))
|
||||
(Assert.equals true (if "string" true false))
|
||||
(Assert.equals false (if "" true false))
|
||||
(Assert.equals false (if [] true false))
|
||||
(Assert.equals true (if [1] true false))
|
||||
(Assert.equals 5 (if true 5))
|
||||
(Assert.equals null (if false 5)))
|
||||
|
||||
(defvar :Int myInt 8)
|
||||
|
||||
(defmacrofun doTwiceInt [intOp]
|
||||
,intOp
|
||||
,intOp)
|
||||
|
||||
// I think this causes doTwiceInt's runtime function to be typed as requiring Quote<Int> and returning Int
|
||||
(defun :Int incrementTwice [:Int val]
|
||||
(doTwiceInt ++val))
|
||||
|
||||
(defmacrofun doTwiceString [stringOp]
|
||||
,stringOp
|
||||
,stringOp)
|
||||
|
||||
(defun myTryCatch [:Any e]
|
||||
(try
|
||||
(throw e)
|
||||
(catch [:String error] 5)
|
||||
(catch [:Int error] 6)
|
||||
(catch [error] 7)))
|
||||
|
||||
(defun myTypeCheck []
|
||||
(the Int 5))
|
||||
|
||||
(defun myGroups1 []
|
||||
(Prelude.groups [1 2 3 4] 2))
|
||||
|
||||
(defun myGroups2 []
|
||||
(Prelude.groups [1 2 3 4] 3 true))
|
||||
|
||||
(defun _testLet []
|
||||
(let [a 5
|
||||
b 6
|
||||
:String c "stuff"]
|
||||
(Assert.equals 5 a)
|
||||
(Assert.equals 6 b)
|
||||
(Assert.equals "stuff" c))
|
||||
(let [&mut a "str1"]
|
||||
(Assert.equals "str1" a)
|
||||
(set a "str2")
|
||||
(Assert.equals "str2" a)))
|
||||
|
||||
(defvar myConstructedString (new String "sup"))
|
||||
|
||||
(defvar myCond1 (cond
|
||||
((= 5 6) "not this")
|
||||
((= 8 9) "not this either")
|
||||
((= 1 1) "this one")
|
||||
(true "not the default")))
|
||||
|
||||
(defvar myCond2 (cond
|
||||
((= 5 6) "not this")
|
||||
((= 8 9) "not this either")
|
||||
((= 2 1) "not the third one")
|
||||
(true "the default")))
|
||||
|
||||
(defvar myCond3 (cond
|
||||
((= 5 5) "this")
|
||||
(true "default")))
|
||||
|
||||
(defvar myCondFallthrough (cond
|
||||
(false "not this")))
|
||||
|
||||
(defvar myOr1 (or null 5))
|
||||
|
||||
(defvar myAnd1 (and 5 6))
|
||||
(defvar myAnd2 (and false 5 6))
|
||||
(defvar myAnd3 (and 5 false 6))
|
||||
|
||||
(defun mySetLocal []
|
||||
(deflocal &mut loc "one thing")
|
||||
(set loc "another thing")
|
||||
loc)
|
||||
|
||||
(defvar myNot1 (not 5))
|
||||
(defvar myNot2 !5)
|
||||
|
||||
(defvar myFilteredList (begin
|
||||
(deflocal l [-1 -2 5 -3 6])
|
||||
(l.filter (lambda [v] (< 0 v)))))
|
||||
|
||||
(defvar myWhen1 (when true 5 6))
|
||||
|
||||
(defvar myListOfTen [1 2 3 4 5 6 7 8 9 10])
|
||||
|
||||
(defun _testQuickNths []
|
||||
(Assert.equals 1 (first myListOfTen))
|
||||
(Assert.equals 2 (second myListOfTen))
|
||||
(Assert.equals 3 (third myListOfTen))
|
||||
(Assert.equals 4 (fourth myListOfTen))
|
||||
(Assert.equals 5 (fifth myListOfTen))
|
||||
(Assert.equals 6 (sixth myListOfTen))
|
||||
(Assert.equals 7 (seventh myListOfTen))
|
||||
(Assert.equals 8 (eighth myListOfTen))
|
||||
(Assert.equals 9 (ninth myListOfTen))
|
||||
(Assert.equals 10 (tenth myListOfTen))
|
||||
(Assert.equals 10 (last myListOfTen)))
|
||||
|
||||
(defun _testListDestructuring []
|
||||
(deflocal [a b c d &mut e f g h i j] myListOfTen)
|
||||
(Assert.equals 1 a)
|
||||
(Assert.equals 2 b)
|
||||
(Assert.equals 3 c)
|
||||
(Assert.equals 4 d)
|
||||
(Assert.equals 5 e)
|
||||
(set e 6)
|
||||
(Assert.equals 6 e)
|
||||
(Assert.equals 6 f)
|
||||
(Assert.equals 7 g)
|
||||
(Assert.equals 8 h)
|
||||
(Assert.equals 9 i)
|
||||
(Assert.equals 10 j)
|
||||
|
||||
(let [[a b c &mut d e f g h i j] myListOfTen]
|
||||
(Assert.equals 1 a)
|
||||
(Assert.equals 2 b)
|
||||
(Assert.equals 3 c)
|
||||
(Assert.equals 4 d)
|
||||
(set d 6)
|
||||
(Assert.equals 6 d)
|
||||
(Assert.equals 5 e)
|
||||
(Assert.equals 6 f)
|
||||
(Assert.equals 7 g)
|
||||
(Assert.equals 8 h)
|
||||
(Assert.equals 9 i)
|
||||
(Assert.equals 10 j)))
|
||||
|
||||
|
||||
(defvar myMetaList [myListOfTen myListOfTen myListOfTen])
|
||||
|
||||
(defun _testDoFor []
|
||||
(deflocal &mut c 0)
|
||||
(doFor v myListOfTen
|
||||
(Assert.equals (+ c 1) v)
|
||||
(set c v))
|
||||
(doFor [a b c d e f g h i j] myMetaList
|
||||
(Assert.equals 1 a)
|
||||
(Assert.equals 2 b)
|
||||
(Assert.equals 3 c)
|
||||
(Assert.equals 4 d)
|
||||
(Assert.equals 5 e)
|
||||
(Assert.equals 6 f)
|
||||
(Assert.equals 7 g)
|
||||
(Assert.equals 8 h)
|
||||
(Assert.equals 9 i)
|
||||
(Assert.equals 10 j)))
|
||||
|
||||
(defun _testFor []
|
||||
(deflocal incrementedList (for v myListOfTen (+ 1 v)))
|
||||
(let [[a b c d e f g h i j] incrementedList]
|
||||
(Assert.equals 2 a)
|
||||
(Assert.equals 3 b)
|
||||
(Assert.equals 4 c)
|
||||
(Assert.equals 5 d)
|
||||
(Assert.equals 6 e)
|
||||
(Assert.equals 7 f)
|
||||
(Assert.equals 8 g)
|
||||
(Assert.equals 9 h)
|
||||
(Assert.equals 10 i)
|
||||
(Assert.equals 11 j))
|
||||
(deflocal smallerMetaList (for [a b c d e f g h i j] myMetaList [a e i]))
|
||||
(doFor [a e i] smallerMetaList
|
||||
(Assert.equals 1 a)
|
||||
(Assert.equals 5 e)
|
||||
(Assert.equals 10 i)))
|
||||
|
||||
(defun myOptionalFunc [a &opt b c]
|
||||
(Assert.equals 5 a)
|
||||
(Assert.equals null b)
|
||||
(Assert.equals 6 (or c 6))) // (or [optionalVar] [defaultValue]) is the convention for default values
|
||||
|
||||
(defun myRestSum [firstOne &rest :List<Int> others]
|
||||
(deflocal &mut sum firstOne)
|
||||
(doFor nextOne others (set sum (+ sum nextOne)))
|
||||
sum)
|
||||
|
||||
(defvar myRest1 (myRestSum 5))
|
||||
(defvar myRest2 (myRestSum 1 1 1 1 1))
|
||||
(defvar myRest3 (myRestSum 1 2 2))
|
||||
|
||||
(defun myCombinedOptRest [firstOne &opt secondOne &rest :List<String> thirdAndMore]
|
||||
(deflocal &mut concatString (+ firstOne (or secondOne "boop")))
|
||||
(doFor str thirdAndMore (set concatString (+ concatString str)))
|
||||
concatString)
|
||||
|
||||
(defvar myCombined1 (myCombinedOptRest "a" "b" "c" "d"))
|
||||
(defvar myCombined2 (myCombinedOptRest "a"))
|
||||
(defvar myCombined3 (myCombinedOptRest "a" "b"))
|
||||
|
||||
(defun _testFieldExps []
|
||||
(Assert.equals "hey" (.trim " hey "))
|
||||
(Assert.equals "e" (.charAt (.trim " hey ") 1)))
|
||||
|
||||
(defun _testBreakContinue []
|
||||
(let [[a b c]
|
||||
(for val [1 2 3 4 5 6 7 8]
|
||||
(if (> val 6)
|
||||
(break)
|
||||
(if (% val 2)
|
||||
(continue)
|
||||
val)))]
|
||||
(Assert.equals 2 a)
|
||||
(Assert.equals 4 b)
|
||||
(Assert.equals 6 c)))
|
||||
|
||||
(defun _testAssert []
|
||||
(try
|
||||
(assert false (+ "false " "should " "have " "been " "true"))
|
||||
(catch [:String message]
|
||||
(Assert.equals "Assertion false failed: false should have been true" message)))
|
||||
|
||||
(assert true)
|
||||
(assert ![]))
|
||||
|
||||
(defun _testApply []
|
||||
(Assert.equals 6 (apply + [1 2 3])))
|
||||
|
||||
(defun applyWithMethod [obj]
|
||||
(apply .multiply obj [6]))
|
||||
|
||||
(defun _testAnonymousObject []
|
||||
(let [obj
|
||||
(object
|
||||
a "string A"
|
||||
b 5)]
|
||||
(Assert.equals "string A" obj.a)
|
||||
(Assert.equals 5 obj.b)))
|
||||
4
kiss/src/test/cases/CommentAtEndOfListTestCase.kiss
Normal file
4
kiss/src/test/cases/CommentAtEndOfListTestCase.kiss
Normal file
@@ -0,0 +1,4 @@
|
||||
(defun myFun []
|
||||
(deflocal something 5)
|
||||
// TODO This comment causes a hard-to-track-down error!
|
||||
)
|
||||
22
kiss/src/test/cases/ReaderMacroTestCase.hx
Normal file
22
kiss/src/test/cases/ReaderMacroTestCase.hx
Normal file
@@ -0,0 +1,22 @@
|
||||
package test.cases;
|
||||
|
||||
import utest.Test;
|
||||
import utest.Assert;
|
||||
import kiss.Prelude;
|
||||
|
||||
@:build(kiss.Kiss.build("kiss/src/test/cases/ReaderMacroTestCase.kiss"))
|
||||
class ReaderMacroTestCase extends Test {
|
||||
function testReadBang() {
|
||||
Assert.equals("String that takes the rest of the line", ReaderMacroTestCase.myLine());
|
||||
}
|
||||
|
||||
function testDefAlias() {
|
||||
Assert.equals(9, ReaderMacroTestCase.mySum);
|
||||
}
|
||||
|
||||
function testMultipleInitiators() {
|
||||
Assert.equals("a", ReaderMacroTestCase.str1);
|
||||
Assert.equals("b", ReaderMacroTestCase.str2);
|
||||
Assert.equals("c", ReaderMacroTestCase.str3);
|
||||
}
|
||||
}
|
||||
18
kiss/src/test/cases/ReaderMacroTestCase.kiss
Normal file
18
kiss/src/test/cases/ReaderMacroTestCase.kiss
Normal file
@@ -0,0 +1,18 @@
|
||||
(defreadermacro "!" [stream] #|ReaderExp.StrExp(stream.expect("a string line", function () stream.takeLine()))|#)
|
||||
|
||||
(defun myLine []
|
||||
!String that takes the rest of the line
|
||||
)
|
||||
|
||||
(defalias pluppers +)
|
||||
(defalias fluffers 5)
|
||||
(defalias buffers 4)
|
||||
|
||||
(defvar mySum (pluppers fluffers buffers))
|
||||
|
||||
// Read a b c directly as strings
|
||||
(defreadermacro ["a" "b" "c"] [stream] #|ReaderExp.StrExp(stream.expect("a, b, or c", function () stream.takeChars(1)))|#)
|
||||
|
||||
(defvar str1 a)
|
||||
(defvar str2 b)
|
||||
(defvar str3 c)
|
||||
Reference in New Issue
Block a user