Files
kiss/src/kiss/Prelude.hx

657 lines
21 KiB
Haxe

package kiss;
using Std;
import kiss.ReaderExp;
import haxe.ds.Either;
import haxe.Constraints;
import haxe.DynamicAccess;
#if (!macro && hxnodejs)
import js.node.ChildProcess;
import js.node.Buffer;
#elseif sys
import sys.io.Process;
#end
#if (sys || hxnodejs)
import sys.FileSystem;
import sys.io.File;
#end
#if python
import python.lib.subprocess.Popen;
import python.Bytearray;
#end
import uuid.Uuid;
import haxe.io.Path;
import haxe.Json;
using StringTools;
using uuid.Uuid;
/** What functions that process Lists should do when there are more elements than expected **/
enum ExtraElementHandling {
Keep; // Keep the extra elements
Drop; // Drop the extra elements
Throw; // Throw an error
}
enum KissTarget {
Cpp;
CSharp;
Haxe;
JavaScript;
NodeJS;
Python;
Macro;
}
class Prelude {
static function stringOrFloat(d:Dynamic):Either<String, Float> {
return switch (Type.typeof(d)) {
case TInt | TFloat: Right(0.0 + d);
default:
if (Std.isOfType(d, String)) {
Left(d);
} else {
throw 'cannot use $d in multiplication';
};
};
}
// Kiss arithmetic will incur overhead because of these switch statements, but the results will not be platform-dependent
static function _add(values:Array<Dynamic>):Dynamic {
var sum:Dynamic = values[0];
for (value in values.slice(1))
sum += value;
return sum;
}
public static var add:Function = Reflect.makeVarArgs(_add);
static function _subtract(values:Array<Dynamic>):Dynamic {
var difference:Float = values[0];
for (value in values.slice(1))
difference -= value;
return difference;
}
public static var subtract:Function = Reflect.makeVarArgs(_subtract);
static function _multiply2(a:Dynamic, b:Dynamic):Dynamic {
return switch ([stringOrFloat(a), stringOrFloat(b)]) {
case [Right(f), Right(f2)]:
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;
}
result;
default:
throw 'cannot multiply $a and $b';
};
}
static function _multiply(values:Array<Dynamic>):Dynamic {
var product = values[0];
for (value in values.slice(1))
product = _multiply2(product, value);
return product;
}
public static var multiply:Function = Reflect.makeVarArgs(_multiply);
static function _divide(values:Array<Dynamic>):Dynamic {
var quotient:Float = values[0];
for (value in values.slice(1))
quotient /= value;
return quotient;
}
public static var divide:Function = Reflect.makeVarArgs(_divide);
public static function mod(top:Dynamic, bottom:Dynamic):Dynamic {
return top % bottom;
}
public static function pow(base:Dynamic, exponent:Dynamic):Dynamic {
return Math.pow(base, exponent);
}
static function _min(values:Array<Dynamic>):Dynamic {
var min = values[0];
for (value in values.slice(1))
min = Math.min(min, value);
return min;
}
public static var min:Function = Reflect.makeVarArgs(_min);
static function _max(values:Array<Dynamic>):Dynamic {
var max = values[0];
for (value in values.slice(1))
max = Math.max(max, value);
return max;
}
public static var max:Function = Reflect.makeVarArgs(_max);
static function _comparison(op:String, values:Array<Dynamic>):Bool {
for (idx in 1...values.length) {
var a:Dynamic = values[idx - 1];
var b:Dynamic = values[idx];
var check = switch (op) {
case ">": a > b;
case ">=": a >= b;
case "<": a < b;
case "<=": a <= b;
case "==": a == b;
default: throw 'Unreachable case';
}
if (!check)
return false;
}
return true;
}
public static var greaterThan:Function = Reflect.makeVarArgs(_comparison.bind(">"));
public static var greaterEqual:Function = Reflect.makeVarArgs(_comparison.bind(">="));
public static var lessThan:Function = Reflect.makeVarArgs(_comparison.bind("<"));
public static var lesserEqual:Function = Reflect.makeVarArgs(_comparison.bind("<="));
public static var areEqual:Function = Reflect.makeVarArgs(_comparison.bind("=="));
public static function sort<T>(a:Array<T>, ?comp:(T, T) -> Int):kiss.List<T> {
if (comp == null)
comp = Reflect.compare;
var sorted = a.copy();
sorted.sort(comp);
return sorted;
}
public static function groups<T>(a:Array<T>, size, extraHandling = Drop):kiss.List<kiss.List<T>> {
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) {
switch (extraHandling) {
case Throw:
throw 'groups was given a non-divisible number of elements: $a, $size';
case Keep:
fullGroups.push(a.slice(numFullGroups * size));
case Drop:
}
}
return fullGroups;
}
static function _concat(arrays:Array<Dynamic>):kiss.List<Dynamic> {
var arr:Array<Dynamic> = arrays[0];
for (nextArr in arrays.slice(1)) {
arr = arr.concat(nextArr);
}
return arr;
}
public static var concat:Function = Reflect.makeVarArgs(_concat);
static function _zip(iterables:Array<Dynamic>, extraHandling:ExtraElementHandling):kiss.List<kiss.List<Dynamic>> {
var lists = [];
var iterators = [for (iterable in iterables) iterable.iterator()];
while (true) {
var zipped:Array<Dynamic> = [];
var someNonNull = false;
for (it in iterators) {
switch (extraHandling) {
case Keep:
zipped.push(
if (it.hasNext()) {
someNonNull = true;
it.next();
} else {
null;
});
default:
if (it.hasNext())
zipped.push(it.next());
}
}
switch (extraHandling) {
case _ if (zipped.length == 0):
break;
case Keep if (!someNonNull):
break;
case Drop if (zipped.length != iterators.length):
break;
case Throw if (zipped.length != iterators.length):
throw 'zip${extraHandling} was given iterables of mis-matched size: $iterables';
default:
}
lists.push(zipped);
}
return lists;
}
public static var zipKeep:Function = Reflect.makeVarArgs(_zip.bind(_, Keep));
public static var zipDrop:Function = Reflect.makeVarArgs(_zip.bind(_, Drop));
public static var zipThrow:Function = Reflect.makeVarArgs(_zip.bind(_, Throw));
public static function enumerate(l:kiss.List<Dynamic>, startingIdx = 0):kiss.List<kiss.List<Dynamic>> {
return zipThrow(range(startingIdx, startingIdx + l.length, 1), l);
}
public static function pairs(l:kiss.List<Dynamic>, loopAround = false):kiss.List<kiss.List<Dynamic>> {
var l1 = l.slice(0, l.length - 1);
var l2 = l.slice(1, l.length);
if (loopAround) {
l1.push(l[-1]);
l2.unshift(l[0]);
}
return zipThrow(l1, l2);
}
public static function reversed<T>(l:kiss.List<T>):kiss.List<T> {
var c = l.copy();
c.reverse();
return c;
}
// Ranges with a min, exclusive max, and step size, just like Python.
public static function range(min, max, step):Iterator<Int>
& Iterable<Int>
{
if (step <= 0 || max < min)
throw "(range...) can only count up";
var count = min;
var iterator = {
next: () -> {
var oldCount = count;
count += step;
oldCount;
},
hasNext: () -> {
count < max;
}
};
return {
iterator: () -> iterator,
next: () -> iterator.next(),
hasNext: () -> iterator.hasNext()
};
}
static function _joinPath(parts:Array<Dynamic>) {
return Path.join([for (part in parts) cast(part, String)]);
}
public static var joinPath:Function = Reflect.makeVarArgs(_joinPath);
public static dynamic function truthy<T>(v:T) {
return switch (Type.typeof(v)) {
case TNull: false;
case TBool: cast(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 chooseRandom<T>(l:kiss.List<T>) {
return l[Std.random(l.length)];
}
// Based on: http://old.haxe.org/doc/snip/memoize
public static function memoize(func:Function, ?caller:Dynamic, ?jsonFile:String, ?jsonArgMap:Map<String, Dynamic>):Function {
var argMap = if (jsonArgMap != null) {
jsonArgMap;
} else {
new Map<String, Dynamic>();
}
var f = (args:Array<Dynamic>) -> {
var argString = args.join('|');
return if (argMap.exists(argString)) {
argMap[argString];
} else {
var ret = Reflect.callMethod(caller, func, args);
argMap[argString] = ret;
#if (sys || hxnodejs)
if (jsonFile != null) {
File.saveContent(jsonFile, Json.stringify(argMap));
}
#end
ret;
};
};
f = Reflect.makeVarArgs(f);
return f;
}
#if (sys || hxnodejs)
public static function fsMemoize(func:Function, funcName:String, ?caller:Dynamic):Function {
var fileName = '${funcName}.memoized';
if (!FileSystem.exists(fileName))
File.saveContent(fileName, "{}");
var pastResults:DynamicAccess<Dynamic> = Json.parse(File.getContent(fileName));
var argMap:Map<String, Dynamic> = [for (key => value in pastResults) key => value];
return memoize(func, caller, fileName, argMap);
}
#end
public static function _printStr(s:String) {
#if (sys || hxnodejs)
Sys.println(s);
#else
trace(s);
#end
}
#if (sys || hxnodejs)
static var externLogFile = "externLog.txt";
public static function _externPrintStr(s:String) {
var logContent = try {
File.getContent(externLogFile);
} catch (e) {
"";
}
File.saveContent(externLogFile, '${logContent}${s}\n');
}
#end
public static var printStr:(String) -> Void = _printStr;
public static function print<T>(v:T, label = ""):T {
var toPrint = label;
if (label.length > 0) {
toPrint += ": ";
}
toPrint += Std.string(v);
printStr(toPrint);
return v;
}
public static function symbolNameValue(s:ReaderExp, allowTyped = false):String {
return switch (s.def) {
case Symbol(name): name;
case TypedExp(_, innerExp) if (allowTyped): symbolNameValue(innerExp, false);
default: throw 'expected $s to be a plain symbol';
};
}
// ReaderExp helpers for macros:
public static function symbol(?name:String):ReaderExpDef {
if (name == null)
name = '_${Uuid.v4().toShort()}'; // TODO the underscore will make fields defined with gensym names all PRIVATE!
return Symbol(name);
}
public static function symbolName(s:ReaderExp, allowTyped = false):ReaderExpDef {
return switch (s.def) {
case Symbol(name): StrExp(name);
case TypedExp(_, innerExp) if (allowTyped): symbolName(innerExp, false);
default: throw 'expected $s to be a plain symbol';
};
}
public static function expList(s:ReaderExp):kiss.List<ReaderExp> {
return switch (s.def) {
case ListExp(exps):
exps;
default: throw 'expected $s to be a list expression';
};
}
public static function isListExp(s:ReaderExp):Bool {
return switch (s.def) {
case ListExp(exps):
true;
default:
false;
};
}
#if sys
private static var kissProcess:Process = null;
#end
public static function walkDirectory(basePath, directory, processFile:(String) -> Void, ?processFolderBefore:(String) -> Void,
?processFolderAfter:(String) -> Void) {
#if (sys || hxnodejs)
for (fileOrFolder in FileSystem.readDirectory(joinPath(basePath, directory))) {
switch (fileOrFolder) {
case folder if (FileSystem.isDirectory(joinPath(basePath, directory, folder))):
var subdirectory = joinPath(directory, folder);
if (processFolderBefore != null) {
processFolderBefore(subdirectory);
}
walkDirectory(basePath, subdirectory, processFile, processFolderBefore, processFolderAfter);
if (processFolderAfter != null) {
processFolderAfter(subdirectory);
}
case file:
processFile(joinPath(directory, file));
}
}
#else
throw "Can't walk a directory on this target.";
#end
}
public static function purgeDirectory(directory) {
#if (sys || hxnodejs)
walkDirectory("", directory, FileSystem.deleteFile, null, FileSystem.deleteDirectory);
FileSystem.deleteDirectory(directory);
#else
throw "Can't delete files/folders on this target.";
#end
}
/**
* On Sys targets and nodejs, Kiss can be converted to hscript at runtime
* NOTE on non-nodejs targets, after the first time calling this function,
* it will be much faster -- but things like reader macros will get stuck in the KissState, which you may not intend
* NOTE on non-nodejs sys targets, newlines in raw strings will be stripped away.
* So don't use raw string literals in Kiss you want parsed and evaluated at runtime.
*/
public static function convertToHScript(kissStr:String):String {
#if (!macro && hxnodejs)
var hscript = try {
assertProcess("haxelib", ["run", "kiss", "convert", "--all", "--hscript"], kissStr.split('\n'));
} catch (e) {
throw 'failed to convert ${kissStr} to hscript:\n$e';
}
if (hscript.startsWith(">>> ")) {
hscript = hscript.substr(4);
}
return hscript.trim();
#elseif (!macro && python)
var hscript = try {
assertProcess("haxelib", ["run", "kiss", "convert", "--hscript"], [kissStr.replace('\n', ' ')], false);
} catch (e) {
throw 'failed to convert ${kissStr} to hscript:\n$e';
}
if (hscript.startsWith(">>> ")) {
hscript = hscript.substr(4);
}
return hscript.trim();
#elseif sys
if (kissProcess == null)
kissProcess = new Process("haxelib", ["run", "kiss", "convert", "--hscript"]);
kissProcess.stdin.writeString('${kissStr.replace("\n", " ")}\n');
try {
var output = kissProcess.stdout.readLine();
if (output.startsWith(">>> ")) {
output = output.substr(4);
}
return output;
} catch (e) {
var error = kissProcess.stderr.readAll().toString();
throw 'failed to convert ${kissStr} to hscript: ${error}';
}
#else
throw "Can't convert Kiss to HScript on this target.";
#end
}
#if (sys || hxnodejs)
public static var cachedConvertToHScript:String->String = cast(fsMemoize(convertToHScript, "convertToHScript"));
#end
public static function getTarget():KissTarget {
return #if cpp
Cpp;
#elseif cs
CSharp;
#elseif interp
Haxe;
#elseif hxnodejs
NodeJS;
#elseif js
JavaScript;
#elseif python
Python;
#elseif macro
Macro;
#else
throw "Unsupported target language for Kiss";
#end
}
public static function assertProcess(command:String, args:Array<String>, ?inputLines:Array<String>, fullProcess = true):String {
#if test
Prelude.print('running $command $args $inputLines from ${Prelude.getTarget()}');
#end
if (inputLines != null) {
for (line in inputLines) {
if (line.indexOf("\n") != -1) {
throw 'newline is not allowed in the middle of a process input line: "${line.replace("\n", "\\n")}"';
}
}
}
#if python
// on Python, after new Process() is called, writing inputLines to stdin becomes impossible. Use python.lib.subprocess instead
var p = Popen.create([command].concat(args), {
stdin: -1, // -1 represents PIPE which allows communication
stdout: -1,
stderr: -1,
});
if (inputLines != null) {
for (line in inputLines) {
p.stdin.write(new Bytearray('$line\n', "utf-8"));
}
}
var output = if (fullProcess) {
if (p.wait() == 0) {
p.stdout.readall().decode().trim();
} else {
throw 'process $command $args failed:\n${p.stdout.readall().decode().trim() + p.stderr.readall().decode().trim();}';
}
} else {
// The haxe extern for FileIO.readline() says it's a string, but it's not, it's bytes!
var bytes:Dynamic = p.stdout.readline();
var s:String = bytes.decode();
s.trim();
}
p.terminate();
return output;
#elseif sys
var p = new Process(command, args);
if (inputLines != null) {
for (line in inputLines) {
p.stdin.writeString('$line\n');
}
}
var output = if (fullProcess) {
if (p.exitCode() == 0) {
p.stdout.readAll().toString().trim();
} else {
throw 'process $command $args failed:\n${p.stdout.readAll().toString().trim() + p.stderr.readAll().toString().trim()}';
}
} else {
p.stdout.readLine().toString().trim();
}
p.kill();
p.close();
return output;
#elseif hxnodejs
var p = if (inputLines != null) {
ChildProcess.spawnSync(command, args, {input: inputLines.join("\n")});
} else {
ChildProcess.spawnSync(command, args);
}
var output = switch (p.status) {
case 0:
var output:Buffer = p.stdout;
if (output == null) output = Buffer.alloc(0);
output.toString();
default:
var output:Buffer = p.stdout;
if (output == null) output = Buffer.alloc(0);
var error:Buffer = p.stderr;
if (error == null) error = Buffer.alloc(0);
throw 'process $command $args failed:\n${output.toString() + error.toString()}';
}
return output;
#else
throw "Can't run a subprocess on this target.";
#end
}
// Get the path to a haxelib the user has installed
public static function libPath(haxelibName:String) {
return assertProcess("haxelib", ["libpath", haxelibName]).trim();
}
public static function filter<T>(l:Iterable<T>, ?p:(T) -> Bool):kiss.List<T> {
if (p == null)
p = Prelude.truthy;
return Lambda.filter(l, p);
}
#if (sys || hxnodejs)
public static function readDirectory(dir:String) {
return [for (file in FileSystem.readDirectory(dir)) {
joinPath(dir, file);
}];
}
#end
public static function substr(str:String, startIdx:Int, ?endIdx:Int) {
function negIdx(idx) {
return if (idx < 0) str.length + idx else idx;
}
if (endIdx == null) endIdx = str.length;
return str.substr(negIdx(startIdx), negIdx(endIdx));
}
public static var newLine = "\n";
public static var backSlash = "\\";
}