String interpolation

This commit is contained in:
2021-01-24 18:23:32 -07:00
parent 9dd9c2d13d
commit 2c039cc485
5 changed files with 91 additions and 19 deletions

View File

@@ -10,29 +10,29 @@ Kiss is a work in progress. (See: [Who should use Kiss?](#who-should-use-kiss))
**Kiss aims to be:**
- [ ] A statically typed Lisp
- [x] A statically typed Lisp
- [ ] that runs correctly almost anywhere,
- [ ] is usable at any stage of its development,
- [x] is usable at any stage of its development,
- [ ] doesn't break downstream code when it updates,
- [ ] and doesn't require full-time maintenance
**Main features:**
- [ ] Traditional Lisp macros
- [ ] [Reader macros](https://gist.github.com/chaitanyagupta/9324402)
- [ ] Plug-and-play with every pure-Haxe library on Haxelib
- [ ] Smooth FFI with any non-Haxe library you can find or write Haxe bindings for
- [ ] helpful compiler errors
- [x] [Reader macros](https://gist.github.com/chaitanyagupta/9324402)
- [x] Plug-and-play with every pure-Haxe library on Haxelib
- [x] Smooth FFI with any non-Haxe library you can find or write Haxe bindings for
- [x] helpful compiler errors
**Extra goodies:**
- [ ] string interpolation
- [ ] Rust-style raw string literals
- [x] string interpolation
- [x] Rust-style raw string literals
- [ ] null-safe arrays
- [x] negative indexing
- [ ] list comprehensions
- [x] list comprehensions
- [x] immutability by default
- [ ] destructuring assignment
- [x] destructuring assignment
## How does it work?

View File

@@ -47,6 +47,10 @@ class Reader {
readTable["("] = (stream, k) -> CallExp(assertRead(stream, k), readExpArray(stream, ")", k));
readTable["["] = (stream, k) -> ListExp(readExpArray(stream, "]", k));
// Provides a nice syntactic sugar for (if... {[then block]} {[else block]}),
// and also handles string interpolation cases like "${}more"
readTable["{"] = (stream:Stream, k) -> CallExp(Symbol("begin").withPos(stream.position()), readExpArray(stream, "}", k));
readTable['"'] = readString;
readTable["#"] = readRawString;
@@ -96,7 +100,7 @@ class Reader {
return readTable;
}
public static final terminators = [")", "]", "/*", "\n", " "];
public static final terminators = [")", "]", "}", '"', "/*", "\n", " "];
public static function nextToken(stream:Stream, expect:String) {
var tok = stream.expect(expect, () -> stream.takeUntilOneOf(terminators));
@@ -217,19 +221,73 @@ class Reader {
}
static function readString(stream:Stream, k:KissState) {
return StrExp(stream.expect("closing \"", () -> stream.takeUntilAndDrop("\"")));
var pos = stream.position();
var stringParts:Array<ReaderExp> = [];
var currentStringPart = "";
function endCurrentStringPart() {
stringParts.push(StrExp(currentStringPart).withPos(pos));
currentStringPart = "";
}
do {
var next = stream.expect('closing "', () -> stream.takeChars(1));
switch (next) {
case '$':
endCurrentStringPart();
var wrapInIf = false;
var firstAfterDollar = stream.expect('interpolation expression', () -> stream.peekChars(1));
if (firstAfterDollar == "?") {
wrapInIf = true;
stream.dropChars(1);
}
var interpExpression = assertRead(stream, k);
interpExpression = CallExp(Symbol("Std.string").withPos(pos), [interpExpression]).withPos(pos);
if (wrapInIf) {
interpExpression = CallExp(Symbol("if").withPos(pos), [interpExpression, interpExpression, StrExp("").withPos(pos)]).withPos(pos);
}
stringParts.push(interpExpression);
case '\\':
var escapeSequence = stream.expect('valid escape sequence', () -> stream.takeChars(1));
switch (escapeSequence) {
case '\\':
currentStringPart += "\\";
case 't':
currentStringPart += "\t";
case 'n':
currentStringPart += "\n";
case 'r':
currentStringPart += "\r";
case '"':
currentStringPart += '"';
case '$':
currentStringPart += '$';
default:
error(stream, 'unsupported escape sequence \\$escapeSequence');
return null;
}
case '"':
endCurrentStringPart();
return if (stringParts.length == 1) {
stringParts[0].def;
} else {
CallExp(Symbol("+").withPos(pos), stringParts);
};
default:
currentStringPart += next;
}
} while (true);
}
static function readRawString(stream:Stream, k:KissState) {
var terminator = '"#';
do {
var next = stream.expect('# or "', () -> stream.peekChars(1));
var next = stream.expect('# or "', () -> stream.takeChars(1));
switch (next) {
case "#":
terminator += "#";
stream.dropChars(1);
case '"':
stream.dropChars(1);
break;
default:
error(stream, 'Invalid syntax for raw string. Delete $next');
@@ -239,7 +297,7 @@ class Reader {
return StrExp(stream.expect('closing $terminator', () -> stream.takeUntilAndDrop(terminator)));
}
static function error(stream:Stream, message:String) {
public static function error(stream:Stream, message:String) {
Sys.stderr().writeString('Kiss reader error!\n');
Sys.stderr().writeString(stream.position().toPrint() + ': $message\n');
Sys.exit(1);

View File

@@ -2,6 +2,7 @@ package kiss;
import sys.io.File;
import haxe.ds.Option;
import kiss.Reader;
using StringTools;
using Lambda;
@@ -124,7 +125,7 @@ class Stream {
public function dropString(s:String) {
var toDrop = content.substr(0, s.length);
if (toDrop != s) {
throw 'Expected $s at ${position()}';
Reader.error(this, 'Expected $s');
}
dropChars(s.length);
}
@@ -167,7 +168,8 @@ class Stream {
case Some(s):
return s;
default:
throw 'Expected $whatToExpect at $position';
Reader.error(this, 'Expected $whatToExpect');
return null;
}
}
}

View File

@@ -277,6 +277,10 @@ class BasicTestCase extends Test {
function testRawString() {
_testRawString();
}
function testKissStrings() {
_testKissStrings();
}
}
class BasicObject {

View File

@@ -458,4 +458,12 @@
(defun _testRawString []
(Assert.equals #| "\\" |# #"\"#)
(Assert.equals #| "\"#" |# ##""#"##))
(Assert.equals #| "\"#" |# ##""#"##))
(defun _testKissStrings []
(Assert.equals #| "\\\t\r\n\"$" |# "\\\t\r\n\"\$")
(let [str "it's"
num 3
l1 ["a" "b" "c"]
l2 [1 2 3]]
(Assert.equals "it's 3asy as [a,b,c] [1,2,3]" "$str ${num}asy as $l1 $l2")))