reorganize kiss into its own directory

This commit is contained in:
2020-12-05 18:35:09 -07:00
parent cebe5c23a6
commit 7ff7bab555
38 changed files with 21 additions and 21 deletions

View File

@@ -0,0 +1,5 @@
-lib hscript
-lib uuid
-lib tink_macro
-cp kiss/src
-D analyzer-optimize

View File

@@ -0,0 +1,3 @@
-lib utest
-D test
--main test.TestMain

View File

@@ -0,0 +1,2 @@
#! /bin/bash
./bin/cpp/test/TestMain

View File

@@ -0,0 +1,3 @@
-lib hxcpp
-cpp bin/cpp/test
-cmd bash kiss/build-scripts/cpp/test-cpp.sh

View File

@@ -0,0 +1 @@
--interp

View File

@@ -0,0 +1,2 @@
--js bin/js/test.js
--cmd node bin/js/test.js

View File

@@ -0,0 +1,3 @@
-lib hxnodejs
--js bin/nodejs/test.js
--cmd node bin/nodejs/test.js

View 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

View 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
View 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
View 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": ""
}
}

View 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();
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,2 @@
(defun main []
(print "Hello, world!"))

92
kiss/src/kiss/Operand.hx Normal file
View 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
View 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
View 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()}';
}
}
}

View 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
View 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
View 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();
}
}

View 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;
}
}

View 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)))

View File

@@ -0,0 +1,4 @@
(defun myFun []
(deflocal something 5)
// TODO This comment causes a hard-to-track-down error!
)

View 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);
}
}

View 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)