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:** **Kiss aims to be:**
- [ ] A statically typed Lisp - [x] A statically typed Lisp
- [ ] that runs correctly almost anywhere, - [ ] 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, - [ ] doesn't break downstream code when it updates,
- [ ] and doesn't require full-time maintenance - [ ] and doesn't require full-time maintenance
**Main features:** **Main features:**
- [ ] Traditional Lisp macros - [ ] Traditional Lisp macros
- [ ] [Reader macros](https://gist.github.com/chaitanyagupta/9324402) - [x] [Reader macros](https://gist.github.com/chaitanyagupta/9324402)
- [ ] Plug-and-play with every pure-Haxe library on Haxelib - [x] 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 - [x] Smooth FFI with any non-Haxe library you can find or write Haxe bindings for
- [ ] helpful compiler errors - [x] helpful compiler errors
**Extra goodies:** **Extra goodies:**
- [ ] string interpolation - [x] string interpolation
- [ ] Rust-style raw string literals - [x] Rust-style raw string literals
- [ ] null-safe arrays - [ ] null-safe arrays
- [x] negative indexing - [x] negative indexing
- [ ] list comprehensions - [x] list comprehensions
- [x] immutability by default - [x] immutability by default
- [ ] destructuring assignment - [x] destructuring assignment
## How does it work? ## 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) -> CallExp(assertRead(stream, k), readExpArray(stream, ")", k));
readTable["["] = (stream, k) -> ListExp(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['"'] = readString;
readTable["#"] = readRawString; readTable["#"] = readRawString;
@@ -96,7 +100,7 @@ class Reader {
return readTable; return readTable;
} }
public static final terminators = [")", "]", "/*", "\n", " "]; public static final terminators = [")", "]", "}", '"', "/*", "\n", " "];
public static function nextToken(stream:Stream, expect:String) { public static function nextToken(stream:Stream, expect:String) {
var tok = stream.expect(expect, () -> stream.takeUntilOneOf(terminators)); var tok = stream.expect(expect, () -> stream.takeUntilOneOf(terminators));
@@ -217,19 +221,73 @@ class Reader {
} }
static function readString(stream:Stream, k:KissState) { 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) { static function readRawString(stream:Stream, k:KissState) {
var terminator = '"#'; var terminator = '"#';
do { do {
var next = stream.expect('# or "', () -> stream.peekChars(1)); var next = stream.expect('# or "', () -> stream.takeChars(1));
switch (next) { switch (next) {
case "#": case "#":
terminator += "#"; terminator += "#";
stream.dropChars(1);
case '"': case '"':
stream.dropChars(1);
break; break;
default: default:
error(stream, 'Invalid syntax for raw string. Delete $next'); error(stream, 'Invalid syntax for raw string. Delete $next');
@@ -239,7 +297,7 @@ class Reader {
return StrExp(stream.expect('closing $terminator', () -> stream.takeUntilAndDrop(terminator))); 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('Kiss reader error!\n');
Sys.stderr().writeString(stream.position().toPrint() + ': $message\n'); Sys.stderr().writeString(stream.position().toPrint() + ': $message\n');
Sys.exit(1); Sys.exit(1);

View File

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

View File

@@ -459,3 +459,11 @@
(defun _testRawString [] (defun _testRawString []
(Assert.equals #| "\\" |# #"\"#) (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")))