diff --git a/src/kiss/Reader.hx b/src/kiss/Reader.hx index 0e04e2e..abe00d6 100644 --- a/src/kiss/Reader.hx +++ b/src/kiss/Reader.hx @@ -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 = []; + 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); diff --git a/src/kiss/Stream.hx b/src/kiss/Stream.hx index fdd2853..ac63b9e 100644 --- a/src/kiss/Stream.hx +++ b/src/kiss/Stream.hx @@ -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; } } } diff --git a/src/test/cases/BasicTestCase.hx b/src/test/cases/BasicTestCase.hx index f6d687a..518b222 100644 --- a/src/test/cases/BasicTestCase.hx +++ b/src/test/cases/BasicTestCase.hx @@ -277,6 +277,10 @@ class BasicTestCase extends Test { function testRawString() { _testRawString(); } + + function testKissStrings() { + _testKissStrings(); + } } class BasicObject { diff --git a/src/test/cases/BasicTestCase.kiss b/src/test/cases/BasicTestCase.kiss index a812544..3845a5d 100644 --- a/src/test/cases/BasicTestCase.kiss +++ b/src/test/cases/BasicTestCase.kiss @@ -458,4 +458,12 @@ (defun _testRawString [] (Assert.equals #| "\\" |# #"\"#) - (Assert.equals #| "\"#" |# ##""#"##)) \ No newline at end of file + (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"))) \ No newline at end of file