From 78655c68b0ceb9e6643acba86c338238652fa3cd Mon Sep 17 00:00:00 2001 From: Bob Nystrom Date: Wed, 11 Nov 2015 07:55:48 -0800 Subject: [PATCH] Simple string interpolation. This allows "%(...)" inside a string literal to interpolate the stringified result of an expression. It doesn't support custom interpolators or format strings, but we can consider extending that later. --- example/syntax.wren | 26 +-- src/vm/wren_compiler.c | 181 ++++++++++++++---- src/vm/wren_core.c | 26 +++ src/vm/wren_core.wren | 17 +- src/vm/wren_core.wren.inc | 17 +- test/api/call.wren | 4 +- test/api/foreign_class.wren | 2 +- test/benchmark/binary_trees.wren | 13 +- test/benchmark/binary_trees_gc.wren | 13 +- test/benchmark/delta_blue.wren | 2 +- test/benchmark/fib.wren | 2 +- test/benchmark/fibers.wren | 2 +- test/benchmark/for.wren | 2 +- test/benchmark/map_numeric.wren | 2 +- test/benchmark/map_string.wren | 2 +- test/benchmark/method_call.wren | 2 +- test/benchmark/string_equals.py | 2 +- test/benchmark/string_equals.wren | 4 +- test/core/function/call_extra_arguments.wren | 6 +- test/core/map/to_string.wren | 12 +- test/io/stdin/read_line.wren | 4 +- test/language/break/nested_for_loop.wren | 4 +- test/language/break/nested_while_loop.wren | 4 +- test/language/constructor/superclass.wren | 4 +- test/language/interpolation/empty.wren | 2 + .../language/interpolation/interpolation.wren | 15 ++ .../runtime_error_in_expression.wren | 1 + test/language/interpolation/switch_fiber.wren | 8 + test/language/interpolation/unterminated.wren | 2 + .../unterminated_expression.wren | 2 + test/language/method/newlines.wren | 4 +- test/language/method/operators.wren | 28 +-- test/language/method/static.wren | 4 +- test/language/method/subscript_operators.wren | 12 +- test/language/module/cyclic_import/a.wren | 6 +- test/language/module/cyclic_import/b.wren | 4 +- test/language/module/shared_import/a.wren | 2 +- test/language/module/shared_import/b.wren | 2 +- test/language/string/escapes.wren | 3 +- test/limit/interpolation_nesting.wren | 1 + .../limit/too_much_interpolation_nesting.wren | 1 + 41 files changed, 321 insertions(+), 129 deletions(-) create mode 100644 test/language/interpolation/empty.wren create mode 100644 test/language/interpolation/interpolation.wren create mode 100644 test/language/interpolation/runtime_error_in_expression.wren create mode 100644 test/language/interpolation/switch_fiber.wren create mode 100644 test/language/interpolation/unterminated.wren create mode 100644 test/language/interpolation/unterminated_expression.wren create mode 100644 test/limit/interpolation_nesting.wren create mode 100644 test/limit/too_much_interpolation_nesting.wren diff --git a/example/syntax.wren b/example/syntax.wren index 52d440f5..bb60e35b 100644 --- a/example/syntax.wren +++ b/example/syntax.wren @@ -61,19 +61,19 @@ class SyntaxExample { print(a, b) { System.print(a + b) } // Operators - +(other) { "infix + " + other } - -(other) { "infix - " + other } - *(other) { "infix * " + other } - /(other) { "infix / " + other } - %(other) { "infix % " + other } - <(other) { "infix < " + other } - >(other) { "infix > " + other } - <=(other) { "infix <= " + other } - >=(other) { "infix >= " + other } - ==(other) { "infix == " + other } - !=(other) { "infix != " + other } - &(other) { "infix & " + other } - |(other) { "infix | " + other } + +(other) { "infix + %(other)" } + -(other) { "infix - %(other)" } + *(other) { "infix * %(other)" } + /(other) { "infix / %(other)" } + %(other) { "infix \% %(other)" } + <(other) { "infix < %(other)" } + >(other) { "infix > %(other)" } + <=(other) { "infix <= %(other)" } + >=(other) { "infix >= %(other)" } + ==(other) { "infix == %(other)" } + !=(other) { "infix != %(other)" } + &(other) { "infix & %(other)" } + |(other) { "infix | %(other)" } ! { "prefix !" } ~ { "prefix ~" } diff --git a/src/vm/wren_compiler.c b/src/vm/wren_compiler.c index de1c50a0..2ccf4bca 100644 --- a/src/vm/wren_compiler.c +++ b/src/vm/wren_compiler.c @@ -24,17 +24,23 @@ // Note that this limitation is also explicit in the bytecode. Since // `CODE_LOAD_LOCAL` and `CODE_STORE_LOCAL` use a single argument byte to // identify the local, only 256 can be in scope at one time. -#define MAX_LOCALS (256) +#define MAX_LOCALS 256 // The maximum number of upvalues (i.e. variables from enclosing functions) // that a function can close over. -#define MAX_UPVALUES (256) +#define MAX_UPVALUES 256 // The maximum number of distinct constants that a function can contain. This // value is explicit in the bytecode since `CODE_CONSTANT` only takes a single // two-byte argument. #define MAX_CONSTANTS (1 << 16) +// The maximum depth that interpolation can nest. For example, this string has +// three levels: +// +// "outside %(one + "%(two + "%(three)")")" +#define MAX_INTERPOLATION_NESTING 8 + typedef enum { TOKEN_LEFT_PAREN, @@ -95,7 +101,24 @@ typedef enum TOKEN_STATIC_FIELD, TOKEN_NAME, TOKEN_NUMBER, + + // A string literal without any interpolation, or the last section of a + // string following the last interpolated expression. TOKEN_STRING, + + // A portion of a string literal preceding an interpolated expression. This + // string: + // + // "a %(b) c %(d) e" + // + // is tokenized to: + // + // TOKEN_INTERPOLATION "a " + // TOKEN_NAME b + // TOKEN_INTERPOLATION " c " + // TOKEN_NAME d + // TOKEN_STRING " e" + TOKEN_INTERPOLATION, TOKEN_LINE, @@ -115,6 +138,9 @@ typedef struct // The 1-based line where the token appears. int line; + + // The parsed value if the token is a literal. + Value value; } Token; typedef struct @@ -141,7 +167,23 @@ typedef struct // The most recently consumed/advanced token. Token previous; - + + // Tracks the lexing state when tokenizing interpolated strings. + // + // Interpolated strings make the lexer not strictly regular: we don't know + // whether a ")" should be treated as a RIGHT_PAREN token or as ending an + // interpolated expression unless we know whether we are inside a string + // interpolation and how many unmatched "(" there are. This is particularly + // complex because interpolation can nest: + // + // " %( " %( inner ) " ) " + // + // This tracks that state. The parser maintains a stack of ints, one for each + // level of current interpolation nesting. Each value is the number of + // unmatched "(" that are waiting to be closed. + int parens[MAX_INTERPOLATION_NESTING]; + int numParens; + // If subsequent newline tokens should be discarded. bool skipNewlines; @@ -150,9 +192,6 @@ typedef struct // If a syntax or compile error has occurred. bool hasError; - - // The parsed value if the current token is a literal. - Value value; } Parser; typedef struct @@ -520,7 +559,7 @@ static void makeToken(Parser* parser, TokenType type) parser->current.start = parser->tokenStart; parser->current.length = (int)(parser->currentChar - parser->tokenStart); parser->current.line = parser->currentLine; - + // Make line tokens appear on the line containing the "\n". if (type == TOKEN_LINE) parser->current.line--; } @@ -596,13 +635,13 @@ static void makeNumber(Parser* parser, bool isHex) // We don't check that the entire token is consumed because we've already // scanned it ourselves and know it's valid. - parser->value = NUM_VAL(isHex ? strtol(parser->tokenStart, NULL, 16) - : strtod(parser->tokenStart, NULL)); - + parser->current.value = NUM_VAL(isHex ? strtol(parser->tokenStart, NULL, 16) + : strtod(parser->tokenStart, NULL)); + if (errno == ERANGE) { lexError(parser, "Number literal was too large."); - parser->value = NUM_VAL(0); + parser->current.value = NUM_VAL(0); } makeToken(parser, TOKEN_NUMBER); @@ -720,8 +759,9 @@ static void readUnicodeEscape(Parser* parser, ByteBuffer* string, int length) static void readString(Parser* parser) { ByteBuffer string; + TokenType type = TOKEN_STRING; wrenByteBufferInit(&string); - + for (;;) { char c = nextChar(parser); @@ -737,12 +777,30 @@ static void readString(Parser* parser) break; } + if (c == '%') + { + if (parser->numParens < MAX_INTERPOLATION_NESTING) + { + // TODO: Allow format string. + if (nextChar(parser) != '(') lexError(parser, "Expect '(' after '%'."); + + parser->parens[parser->numParens] = 1; + parser->numParens++; + type = TOKEN_INTERPOLATION; + break; + } + + lexError(parser, "Interpolation may only nest %d levels deep.", + MAX_INTERPOLATION_NESTING); + } + if (c == '\\') { switch (nextChar(parser)) { case '"': wrenByteBufferWrite(parser->vm, &string, '"'); break; case '\\': wrenByteBufferWrite(parser->vm, &string, '\\'); break; + case '%': wrenByteBufferWrite(parser->vm, &string, '%'); break; case '0': wrenByteBufferWrite(parser->vm, &string, '\0'); break; case 'a': wrenByteBufferWrite(parser->vm, &string, '\a'); break; case 'b': wrenByteBufferWrite(parser->vm, &string, '\b'); break; @@ -770,9 +828,11 @@ static void readString(Parser* parser) } } - parser->value = wrenNewString(parser->vm, (char*)string.data, string.count); + parser->current.value = wrenNewString(parser->vm, + (char*)string.data, string.count); + wrenByteBufferClear(parser->vm, &string); - makeToken(parser, TOKEN_STRING); + makeToken(parser, type); } // Lex the next token and store it in [parser.current]. @@ -784,7 +844,7 @@ static void nextToken(Parser* parser) // copy the TOKEN_EOF to previous so that code that expects it to be consumed // will still work. if (parser->current.type == TOKEN_EOF) return; - + while (peekChar(parser) != '\0') { parser->tokenStart = parser->currentChar; @@ -792,8 +852,27 @@ static void nextToken(Parser* parser) char c = nextChar(parser); switch (c) { - case '(': makeToken(parser, TOKEN_LEFT_PAREN); return; - case ')': makeToken(parser, TOKEN_RIGHT_PAREN); return; + case '(': + // If we are inside an interpolated expression, count the unmatched "(". + if (parser->numParens > 0) parser->parens[parser->numParens - 1]++; + makeToken(parser, TOKEN_LEFT_PAREN); + return; + + case ')': + // If we are inside an interpolated expression, count the ")". + if (parser->numParens > 0 && + --parser->parens[parser->numParens - 1] == 0) + { + // This is the final ")", so the interpolation expression has ended. + // This ")" now begins the next section of the template string. + parser->numParens--; + readString(parser); + return; + } + + makeToken(parser, TOKEN_RIGHT_PAREN); + return; + case '[': makeToken(parser, TOKEN_LEFT_BRACKET); return; case ']': makeToken(parser, TOKEN_RIGHT_BRACKET); return; case '{': makeToken(parser, TOKEN_LEFT_BRACE); return; @@ -1835,15 +1914,9 @@ static void list(Compiler* compiler, bool allowAssignment) // Stop if we hit the end of the list. if (peek(compiler) == TOKEN_RIGHT_BRACKET) break; - // Push a copy of the list since the add() call will consume it. - emit(compiler, CODE_DUP); - // The element. expression(compiler); - callMethod(compiler, 1, "add(_)", 6); - - // Discard the result of the add() call. - emit(compiler, CODE_POP); + callMethod(compiler, 1, "addCore_(_)", 11); } while (match(compiler, TOKEN_COMMA)); // Allow newlines before the closing ']'. @@ -1867,9 +1940,6 @@ static void map(Compiler* compiler, bool allowAssignment) // Stop if we hit the end of the map. if (peek(compiler) == TOKEN_RIGHT_BRACE) break; - // Push a copy of the map since the subscript call will consume it. - emit(compiler, CODE_DUP); - // The key. parsePrecedence(compiler, false, PREC_PRIMARY); consume(compiler, TOKEN_COLON, "Expect ':' after map key."); @@ -1877,10 +1947,7 @@ static void map(Compiler* compiler, bool allowAssignment) // The value. expression(compiler); - callMethod(compiler, 2, "[_]=(_)", 7); - - // Discard the result of the setter call. - emit(compiler, CODE_POP); + callMethod(compiler, 2, "addCore_(_,_)", 13); } while (match(compiler, TOKEN_COMMA)); // Allow newlines before the closing '}'. @@ -2138,7 +2205,46 @@ static void null(Compiler* compiler, bool allowAssignment) // A number or string literal. static void literal(Compiler* compiler, bool allowAssignment) { - emitConstant(compiler, compiler->parser->value); + emitConstant(compiler, compiler->parser->previous.value); +} + +static void stringInterpolation(Compiler* compiler, bool allowAssignment) +{ + // TODO: Allow other expressions here so that user-defined classes can control + // interpolation processing like "tagged template strings" in ES6. + loadCoreVariable(compiler, "String"); + + // Instantiate a new list. + loadCoreVariable(compiler, "List"); + callMethod(compiler, 0, "new()", 5); + + do + { + // The opening string part. + literal(compiler, false); + callMethod(compiler, 1, "addCore_(_)", 11); + + ignoreNewlines(compiler); + + // Compile the interpolated expression part to a function. + Compiler fnCompiler; + initCompiler(&fnCompiler, compiler->parser, compiler, true); + expression(&fnCompiler); + emit(&fnCompiler, CODE_RETURN); + endCompiler(&fnCompiler, "interpolation", 9); + + callMethod(compiler, 1, "addCore_(_)", 11); + + ignoreNewlines(compiler); + } while (match(compiler, TOKEN_INTERPOLATION)); + + // The trailing string part. + consume(compiler, TOKEN_STRING, "Expect end of string interpolation."); + literal(compiler, false); + callMethod(compiler, 1, "addCore_(_)", 11); + + // Call .interpolate() with the list of interpolation parts. + callMethod(compiler, 1, "interpolate_(_)", 15); } static void super_(Compiler* compiler, bool allowAssignment) @@ -2482,6 +2588,7 @@ GrammarRule rules[] = /* TOKEN_NAME */ { name, NULL, namedSignature, PREC_NONE, NULL }, /* TOKEN_NUMBER */ PREFIX(literal), /* TOKEN_STRING */ PREFIX(literal), + /* TOKEN_INTERPOLATION */ PREFIX(stringInterpolation), /* TOKEN_LINE */ UNUSED, /* TOKEN_ERROR */ UNUSED, /* TOKEN_EOF */ UNUSED @@ -3131,7 +3238,7 @@ static void classDefinition(Compiler* compiler, bool isForeign) static void import(Compiler* compiler) { consume(compiler, TOKEN_STRING, "Expect a string after 'import'."); - int moduleConstant = addConstant(compiler, compiler->parser->value); + int moduleConstant = addConstant(compiler, compiler->parser->previous.value); // Load the module. emitShortArg(compiler, CODE_LOAD_MODULE, moduleConstant); @@ -3221,11 +3328,11 @@ ObjFn* wrenCompile(WrenVM* vm, ObjModule* module, const char* source, parser.vm = vm; parser.module = module; parser.source = source; - parser.value = UNDEFINED_VAL; parser.tokenStart = source; parser.currentChar = source; parser.currentLine = 1; + parser.numParens = 0; // Zero-init the current token. This will get copied to previous when // advance() is called below. @@ -3233,6 +3340,7 @@ ObjFn* wrenCompile(WrenVM* vm, ObjModule* module, const char* source, parser.current.start = source; parser.current.length = 0; parser.current.line = 0; + parser.current.value = UNDEFINED_VAL; // Ignore leading newlines. parser.skipNewlines = true; @@ -3272,7 +3380,7 @@ ObjFn* wrenCompile(WrenVM* vm, ObjModule* module, const char* source, parser.module->variableNames.data[i].buffer); } } - + return endCompiler(&compiler, "(script)", 8); } @@ -3344,7 +3452,8 @@ void wrenBindMethodCode(ObjClass* classObj, ObjFn* fn) void wrenMarkCompiler(WrenVM* vm, Compiler* compiler) { - wrenGrayValue(vm, compiler->parser->value); + wrenGrayValue(vm, compiler->parser->current.value); + wrenGrayValue(vm, compiler->parser->previous.value); // Walk up the parent chain to mark the outer compilers too. The VM only // tracks the innermost one. diff --git a/src/vm/wren_core.c b/src/vm/wren_core.c index eeae093e..79ef463a 100644 --- a/src/vm/wren_core.c +++ b/src/vm/wren_core.c @@ -264,6 +264,17 @@ DEF_PRIMITIVE(list_add) RETURN_VAL(args[1]); } +// Adds an element to the list and then returns the list itself. This is called +// by the compiler when compiling list literals instead of using add() to +// minimize stack churn. +DEF_PRIMITIVE(list_addCore) +{ + wrenValueBufferWrite(vm, &AS_LIST(args[0])->elements, args[1]); + + // Return the list. + RETURN_VAL(args[0]); +} + DEF_PRIMITIVE(list_clear) { wrenValueBufferClear(vm, &AS_LIST(args[0])->elements); @@ -394,6 +405,19 @@ DEF_PRIMITIVE(map_subscriptSetter) RETURN_VAL(args[2]); } +// Adds an entry to the map and then returns the map itself. This is called by +// the compiler when compiling map literals instead of using [_]=(_) to +// minimize stack churn. +DEF_PRIMITIVE(map_addCore) +{ + if (!validateKey(vm, args[1])) return false; + + wrenMapSet(vm, AS_MAP(args[0]), args[1], args[2]); + + // Return the map itself. + RETURN_VAL(args[0]); +} + DEF_PRIMITIVE(map_clear) { wrenMapClear(vm, AS_MAP(args[0])); @@ -1229,6 +1253,7 @@ void wrenInitializeCore(WrenVM* vm) PRIMITIVE(vm->listClass, "[_]", list_subscript); PRIMITIVE(vm->listClass, "[_]=(_)", list_subscriptSetter); PRIMITIVE(vm->listClass, "add(_)", list_add); + PRIMITIVE(vm->listClass, "addCore_(_)", list_addCore); PRIMITIVE(vm->listClass, "clear()", list_clear); PRIMITIVE(vm->listClass, "count", list_count); PRIMITIVE(vm->listClass, "insert(_,_)", list_insert); @@ -1240,6 +1265,7 @@ void wrenInitializeCore(WrenVM* vm) PRIMITIVE(vm->mapClass->obj.classObj, "new()", map_new); PRIMITIVE(vm->mapClass, "[_]", map_subscript); PRIMITIVE(vm->mapClass, "[_]=(_)", map_subscriptSetter); + PRIMITIVE(vm->mapClass, "addCore_(_,_)", map_addCore); PRIMITIVE(vm->mapClass, "clear()", map_clear); PRIMITIVE(vm->mapClass, "containsKey(_)", map_containsKey); PRIMITIVE(vm->mapClass, "count", map_count); diff --git a/src/vm/wren_core.wren b/src/vm/wren_core.wren index c35e4d1d..303bcb52 100644 --- a/src/vm/wren_core.wren +++ b/src/vm/wren_core.wren @@ -131,6 +131,19 @@ class WhereSequence is Sequence { class String is Sequence { bytes { StringByteSequence.new(this) } codePoints { StringCodePointSequence.new(this) } + + static interpolate_(parts) { + var result = "" + for (part in parts) { + if (part is String) { + result = result + part + } else { + result = result + part.call().toString + } + } + + return result + } } class StringByteSequence is Sequence { @@ -165,7 +178,7 @@ class List is Sequence { return other } - toString { "[" + join(", ") + "]" } + toString { "[%(join(", "))]" } +(other) { var result = this[0..-1] @@ -187,7 +200,7 @@ class Map { for (key in keys) { if (!first) result = result + ", " first = false - result = result + key.toString + ": " + this[key].toString + result = result + "%(key): %(this[key])" } return result + "}" diff --git a/src/vm/wren_core.wren.inc b/src/vm/wren_core.wren.inc index 1fce5623..d8f97f2f 100644 --- a/src/vm/wren_core.wren.inc +++ b/src/vm/wren_core.wren.inc @@ -133,6 +133,19 @@ static const char* coreModuleSource = "class String is Sequence {\n" " bytes { StringByteSequence.new(this) }\n" " codePoints { StringCodePointSequence.new(this) }\n" +"\n" +" static interpolate_(parts) {\n" +" var result = \"\"\n" +" for (part in parts) {\n" +" if (part is String) {\n" +" result = result + part\n" +" } else {\n" +" result = result + part.call().toString\n" +" }\n" +" }\n" +"\n" +" return result\n" +" }\n" "}\n" "\n" "class StringByteSequence is Sequence {\n" @@ -167,7 +180,7 @@ static const char* coreModuleSource = " return other\n" " }\n" "\n" -" toString { \"[\" + join(\", \") + \"]\" }\n" +" toString { \"[%(join(\", \"))]\" }\n" "\n" " +(other) {\n" " var result = this[0..-1]\n" @@ -189,7 +202,7 @@ static const char* coreModuleSource = " for (key in keys) {\n" " if (!first) result = result + \", \"\n" " first = false\n" -" result = result + key.toString + \": \" + this[key].toString\n" +" result = result + \"%(key): %(this[key])\"\n" " }\n" "\n" " return result + \"}\"\n" diff --git a/test/api/call.wren b/test/api/call.wren index f04b7578..a174ae2e 100644 --- a/test/api/call.wren +++ b/test/api/call.wren @@ -8,7 +8,7 @@ class Api { } static one(one) { - System.print("one " + one.toString) + System.print("one %(one)") } static two(one, two) { @@ -17,7 +17,7 @@ class Api { two = two.bytes.toList } - System.print("two " + one.toString + " " + two.toString) + System.print("two %(one) %(two)") } static getValue(value) { diff --git a/test/api/foreign_class.wren b/test/api/foreign_class.wren index 46ff130a..3f878271 100644 --- a/test/api/foreign_class.wren +++ b/test/api/foreign_class.wren @@ -30,7 +30,7 @@ foreign class Point is PointBase { } construct new(x, y, z) { - System.print(x.toString + ", " + y.toString + ", " + z.toString) + System.print("%(x), %(y), %(z)") } foreign translate(x, y, z) diff --git a/test/benchmark/binary_trees.wren b/test/benchmark/binary_trees.wren index 05028c06..50e42ad0 100644 --- a/test/benchmark/binary_trees.wren +++ b/test/benchmark/binary_trees.wren @@ -26,8 +26,8 @@ var stretchDepth = maxDepth + 1 var start = System.clock -System.print("stretch tree of depth " + stretchDepth.toString + " check: " + - Tree.new(0, stretchDepth).check.toString) +System.print("stretch tree of depth %(stretchDepth) check: " + + "%(Tree.new(0, stretchDepth).check)") var longLivedTree = Tree.new(0, maxDepth) @@ -44,12 +44,11 @@ while (depth < stretchDepth) { check = check + Tree.new(i, depth).check + Tree.new(-i, depth).check } - System.print((iterations * 2).toString + " trees of depth " + - depth.toString + " check: " + check.toString) + System.print("%(iterations * 2) trees of depth %(depth) check: %(check)") iterations = iterations / 4 depth = depth + 2 } -System.print("long lived tree of depth " + maxDepth.toString + " check: " + - longLivedTree.check.toString) -System.print("elapsed: " + (System.clock - start).toString) +System.print( + "long lived tree of depth %(maxDepth) check: %(longLivedTree.check)") +System.print("elapsed: %(System.clock - start)") diff --git a/test/benchmark/binary_trees_gc.wren b/test/benchmark/binary_trees_gc.wren index 83eb4ffc..ee54d432 100644 --- a/test/benchmark/binary_trees_gc.wren +++ b/test/benchmark/binary_trees_gc.wren @@ -26,8 +26,8 @@ var stretchDepth = maxDepth + 1 var start = System.clock -System.print("stretch tree of depth " + stretchDepth.toString + " check: " + - Tree.new(0, stretchDepth).check.toString) +System.print("stretch tree of depth %(stretchDepth) check: " + + "%(Tree.new(0, stretchDepth).check)") for (i in 1...1000) System.gc() var longLivedTree = Tree.new(0, maxDepth) @@ -45,16 +45,15 @@ while (depth < stretchDepth) { check = check + Tree.new(i, depth).check + Tree.new(-i, depth).check } - System.print((iterations * 2).toString + " trees of depth " + - depth.toString + " check: " + check.toString) + System.print("%(iterations * 2) trees of depth %(depth) check: %(check)") for (i in 1...1000) System.gc() iterations = iterations / 4 depth = depth + 2 } -System.print("long lived tree of depth " + maxDepth.toString + " check: " + - longLivedTree.check.toString) +System.print( + "long lived tree of depth %(maxDepth) check: %(longLivedTree.check)") for (i in 1...1000) System.gc() -System.print("elapsed: " + (System.clock - start).toString) +System.print("elapsed: %(System.clock - start)") diff --git a/test/benchmark/delta_blue.wren b/test/benchmark/delta_blue.wren index 8311bf0a..8e2229cb 100644 --- a/test/benchmark/delta_blue.wren +++ b/test/benchmark/delta_blue.wren @@ -696,4 +696,4 @@ for (i in 0...40) { } System.print(total) -System.print("elapsed: " + (System.clock - start).toString) +System.print("elapsed: %(System.clock - start)") diff --git a/test/benchmark/fib.wren b/test/benchmark/fib.wren index dcad88f0..10824f22 100644 --- a/test/benchmark/fib.wren +++ b/test/benchmark/fib.wren @@ -9,4 +9,4 @@ var start = System.clock for (i in 1..5) { System.print(Fib.get(28)) } -System.print("elapsed: " + (System.clock - start).toString) +System.print("elapsed: %(System.clock - start)") diff --git a/test/benchmark/fibers.wren b/test/benchmark/fibers.wren index a30da99c..2da8a104 100644 --- a/test/benchmark/fibers.wren +++ b/test/benchmark/fibers.wren @@ -13,4 +13,4 @@ for (i in 0...100000) { fibers[0].call() System.print(sum) -System.print("elapsed: " + (System.clock - start).toString) +System.print("elapsed: %(System.clock - start)") diff --git a/test/benchmark/for.wren b/test/benchmark/for.wren index 1546c909..478274d2 100644 --- a/test/benchmark/for.wren +++ b/test/benchmark/for.wren @@ -7,4 +7,4 @@ var sum = 0 for (i in list) sum = sum + i System.print(sum) -System.print("elapsed: " + (System.clock - start).toString) +System.print("elapsed: %(System.clock - start)") diff --git a/test/benchmark/map_numeric.wren b/test/benchmark/map_numeric.wren index e6b226a6..252bfa0b 100644 --- a/test/benchmark/map_numeric.wren +++ b/test/benchmark/map_numeric.wren @@ -16,4 +16,4 @@ for (i in 1..1000000) { map.remove(i) } -System.print("elapsed: " + (System.clock - start).toString) +System.print("elapsed: %(System.clock - start)") diff --git a/test/benchmark/map_string.wren b/test/benchmark/map_string.wren index b31a4137..b01a002f 100644 --- a/test/benchmark/map_string.wren +++ b/test/benchmark/map_string.wren @@ -97,4 +97,4 @@ for (key in keys) { } System.print(sum) -System.print("elapsed: " + (System.clock - start).toString) +System.print("elapsed: %(System.clock - start)") diff --git a/test/benchmark/method_call.wren b/test/benchmark/method_call.wren index 22739e41..93367cb7 100644 --- a/test/benchmark/method_call.wren +++ b/test/benchmark/method_call.wren @@ -65,4 +65,4 @@ for (i in 0...n) { } System.print(ntoggle.value) -System.print("elapsed: " + (System.clock - start).toString) +System.print("elapsed: %(System.clock - start)") diff --git a/test/benchmark/string_equals.py b/test/benchmark/string_equals.py index f42947de..8829b5c2 100644 --- a/test/benchmark/string_equals.py +++ b/test/benchmark/string_equals.py @@ -18,7 +18,7 @@ for i in range(0, 1000000): count = count + 1 if "abc" == "abcd": count = count + 1 - if "changed one character" == "changed %ne character": + if "changed one character" == "changed !ne character": count = count + 1 if "123" == 123: count = count + 1 if "a slightly longer string" == \ diff --git a/test/benchmark/string_equals.wren b/test/benchmark/string_equals.wren index 6d95af3e..0c87bcc2 100644 --- a/test/benchmark/string_equals.wren +++ b/test/benchmark/string_equals.wren @@ -10,7 +10,7 @@ for (i in 1..1000000) { if ("" == "abc") count = count + 1 if ("abc" == "abcd") count = count + 1 - if ("changed one character" == "changed %ne character") count = count + 1 + if ("changed one character" == "changed !ne character") count = count + 1 if ("123" == 123) count = count + 1 if ("a slightly longer string" == "a slightly longer string!") count = count + 1 @@ -21,4 +21,4 @@ for (i in 1..1000000) { } System.print(count) -System.print("elapsed: " + (System.clock - start).toString) +System.print("elapsed: %(System.clock - start)") diff --git a/test/core/function/call_extra_arguments.wren b/test/core/function/call_extra_arguments.wren index 2a0c22bd..37e2022a 100644 --- a/test/core/function/call_extra_arguments.wren +++ b/test/core/function/call_extra_arguments.wren @@ -1,7 +1,7 @@ var f0 = Fn.new { System.print("zero") } -var f1 = Fn.new {|a| System.print("one " + a) } -var f2 = Fn.new {|a, b| System.print("two " + a + " " + b) } -var f3 = Fn.new {|a, b, c| System.print("three " + a + " " + b + " " + c) } +var f1 = Fn.new {|a| System.print("one %(a)") } +var f2 = Fn.new {|a, b| System.print("two %(a) %(b)") } +var f3 = Fn.new {|a, b, c| System.print("three %(a) %(b) %(c)") } f0.call("a") // expect: zero f0.call("a", "b") // expect: zero diff --git a/test/core/map/to_string.wren b/test/core/map/to_string.wren index 370d3979..635293b2 100644 --- a/test/core/map/to_string.wren +++ b/test/core/map/to_string.wren @@ -19,10 +19,10 @@ System.print({1: Foo.new()}) // expect: {1: Foo.toString} // will be. var s = {1: 2, 3: 4, 5: 6}.toString System.print(s == "{1: 2, 3: 4, 5: 6}" || - s == "{1: 2, 5: 6, 3: 4}" || - s == "{3: 4, 1: 2, 5: 6}" || - s == "{3: 4, 5: 6, 1: 2}" || - s == "{5: 6, 1: 2, 3: 4}" || - s == "{5: 6, 3: 4, 1: 2}") // expect: true + s == "{1: 2, 5: 6, 3: 4}" || + s == "{3: 4, 1: 2, 5: 6}" || + s == "{3: 4, 5: 6, 1: 2}" || + s == "{5: 6, 1: 2, 3: 4}" || + s == "{5: 6, 3: 4, 1: 2}") // expect: true -// TODO: Handle maps that contain themselves. \ No newline at end of file +// TODO: Handle maps that contain themselves. diff --git a/test/io/stdin/read_line.wren b/test/io/stdin/read_line.wren index f0e3e869..5532e922 100644 --- a/test/io/stdin/read_line.wren +++ b/test/io/stdin/read_line.wren @@ -1,9 +1,9 @@ import "io" for Stdin System.write("> ") -System.print("1 " + Stdin.readLine()) +System.print("1 %(Stdin.readLine())") System.write("> ") -System.print("2 " + Stdin.readLine()) +System.print("2 %(Stdin.readLine())") // stdin: first // stdin: second diff --git a/test/language/break/nested_for_loop.wren b/test/language/break/nested_for_loop.wren index 167a4e91..dcb7ca84 100644 --- a/test/language/break/nested_for_loop.wren +++ b/test/language/break/nested_for_loop.wren @@ -1,9 +1,9 @@ for (i in 0..2) { - System.print("outer " + i.toString) + System.print("outer %(i)") if (i > 1) break for (j in 0..2) { - System.print("inner " + j.toString) + System.print("inner %(j)") if (j > 1) break } } diff --git a/test/language/break/nested_while_loop.wren b/test/language/break/nested_while_loop.wren index 8146613c..0c932336 100644 --- a/test/language/break/nested_while_loop.wren +++ b/test/language/break/nested_while_loop.wren @@ -1,11 +1,11 @@ var i = 0 while (true) { - System.print("outer " + i.toString) + System.print("outer %(i)") if (i > 1) break var j = 0 while (true) { - System.print("inner " + j.toString) + System.print("inner %(j)") if (j > 1) break j = j + 1 diff --git a/test/language/constructor/superclass.wren b/test/language/constructor/superclass.wren index f645f39e..78e69038 100644 --- a/test/language/constructor/superclass.wren +++ b/test/language/constructor/superclass.wren @@ -1,6 +1,6 @@ class A { construct new(arg) { - System.print("new A " + arg) + System.print("new A %(arg)") _field = arg } @@ -10,7 +10,7 @@ class A { class B is A { construct new(arg1, arg2) { super(arg2) - System.print("new B " + arg1) + System.print("new B %(arg1)") _field = arg1 } diff --git a/test/language/interpolation/empty.wren b/test/language/interpolation/empty.wren new file mode 100644 index 00000000..6e0562e1 --- /dev/null +++ b/test/language/interpolation/empty.wren @@ -0,0 +1,2 @@ +// expect error line 3 +" %() " diff --git a/test/language/interpolation/interpolation.wren b/test/language/interpolation/interpolation.wren new file mode 100644 index 00000000..b3e0ccd3 --- /dev/null +++ b/test/language/interpolation/interpolation.wren @@ -0,0 +1,15 @@ +// Full string. +System.print("%(1 + 2)") // expect: 3 + +// Multiple in one string. +System.print("str%(1 + 2)(%(3 + 4)\%%(5 + 6)") // expect: str3(7%11 + +// Nested. +System.print("[%("{%("in" + "ner")}")]") // expect: [{inner}] + +// Ignore newlines in template. +System.print("[%( + +"template" + +)]") // expect: [template] diff --git a/test/language/interpolation/runtime_error_in_expression.wren b/test/language/interpolation/runtime_error_in_expression.wren new file mode 100644 index 00000000..e1dd7ddf --- /dev/null +++ b/test/language/interpolation/runtime_error_in_expression.wren @@ -0,0 +1 @@ +System.print("%(123.badMethod)") // expect runtime error: Num does not implement 'badMethod'. diff --git a/test/language/interpolation/switch_fiber.wren b/test/language/interpolation/switch_fiber.wren new file mode 100644 index 00000000..3688b7c9 --- /dev/null +++ b/test/language/interpolation/switch_fiber.wren @@ -0,0 +1,8 @@ +var fiber = Fiber.new { + System.print("in fiber") + Fiber.yield("result") +} + +System.print("outer %(fiber.call()) string") +// expect: in fiber +// expect: outer result string diff --git a/test/language/interpolation/unterminated.wren b/test/language/interpolation/unterminated.wren new file mode 100644 index 00000000..bbe6f6e4 --- /dev/null +++ b/test/language/interpolation/unterminated.wren @@ -0,0 +1,2 @@ +" %( +// expect error \ No newline at end of file diff --git a/test/language/interpolation/unterminated_expression.wren b/test/language/interpolation/unterminated_expression.wren new file mode 100644 index 00000000..d258052e --- /dev/null +++ b/test/language/interpolation/unterminated_expression.wren @@ -0,0 +1,2 @@ +// expect error line 2 +" %(123" \ No newline at end of file diff --git a/test/language/method/newlines.wren b/test/language/method/newlines.wren index 33b41def..e6a837ac 100644 --- a/test/language/method/newlines.wren +++ b/test/language/method/newlines.wren @@ -1,7 +1,7 @@ class Foo { construct new() {} - method(a, b) { "method " + a + " " + b } - [a, b] { "subscript " + a + " " + b } + method(a, b) { "method %(a) %(b)" } + [a, b] { "subscript %(a) %(b)" } } var foo = Foo.new() diff --git a/test/language/method/operators.wren b/test/language/method/operators.wren index baa645aa..e2759ccd 100644 --- a/test/language/method/operators.wren +++ b/test/language/method/operators.wren @@ -1,20 +1,20 @@ class Foo { construct new() {} - +(other) { "infix + " + other } - -(other) { "infix - " + other } - *(other) { "infix * " + other } - /(other) { "infix / " + other } - %(other) { "infix % " + other } - <(other) { "infix < " + other } - >(other) { "infix > " + other } - <=(other) { "infix <= " + other } - >=(other) { "infix >= " + other } - ==(other) { "infix == " + other } - !=(other) { "infix != " + other } - &(other) { "infix & " + other } - |(other) { "infix | " + other } - is(other) { "infix is " + other } + +(other) { "infix + %(other)" } + -(other) { "infix - %(other)" } + *(other) { "infix * %(other)" } + /(other) { "infix / %(other)" } + %(other) { "infix \% %(other)" } + <(other) { "infix < %(other)" } + >(other) { "infix > %(other)" } + <=(other) { "infix <= %(other)" } + >=(other) { "infix >= %(other)" } + ==(other) { "infix == %(other)" } + !=(other) { "infix != %(other)" } + &(other) { "infix & %(other)" } + |(other) { "infix | %(other)" } + is(other) { "infix is %(other)" } ! { "prefix !" } ~ { "prefix ~" } diff --git a/test/language/method/static.wren b/test/language/method/static.wren index 0921779e..06926828 100644 --- a/test/language/method/static.wren +++ b/test/language/method/static.wren @@ -3,8 +3,8 @@ class Foo { bar { "on instance" } static bar { "on metaclass" } - bar(arg) { "on instance " + arg } - static bar(arg) { "on metaclass " + arg } + bar(arg) { "on instance %(arg)" } + static bar(arg) { "on metaclass %(arg)" } } System.print(Foo.new().bar) // expect: on instance diff --git a/test/language/method/subscript_operators.wren b/test/language/method/subscript_operators.wren index 6ce5c1b4..2eef305f 100644 --- a/test/language/method/subscript_operators.wren +++ b/test/language/method/subscript_operators.wren @@ -1,11 +1,11 @@ class Foo { construct new() {} - [a] { "1-subscript " + a } - [a, b] { "2-subscript " + a + " " + b } - [a, b, c] { "3-subscript " + a + " " + b + " " + c } - [a]=(value) { "1-subscript setter " + a + " = " + value } - [a, b]=(value) { "2-subscript setter " + a + " " + b + " = " + value } - [a, b, c]=(value) { "3-subscript setter " + a + " " + b + " " + c + " = " + value } + [a] { "1-subscript %(a)" } + [a, b] { "2-subscript %(a) %(b)" } + [a, b, c] { "3-subscript %(a) %(b) %(c)" } + [a]=(value) { "1-subscript setter %(a) = %(value)" } + [a, b]=(value) { "2-subscript setter %(a) %(b) = %(value)" } + [a, b, c]=(value) { "3-subscript setter %(a) %(b) %(c) = %(value)" } } var foo = Foo.new() diff --git a/test/language/module/cyclic_import/a.wren b/test/language/module/cyclic_import/a.wren index 54e0a80d..c2806adb 100644 --- a/test/language/module/cyclic_import/a.wren +++ b/test/language/module/cyclic_import/a.wren @@ -2,8 +2,8 @@ System.print("start a") var A = "a value" -System.print("a defined " + A) +System.print("a defined %(A)") import "b" for B -System.print("a imported " + B) +System.print("a imported %(B)") -System.print("end a") \ No newline at end of file +System.print("end a") diff --git a/test/language/module/cyclic_import/b.wren b/test/language/module/cyclic_import/b.wren index 3671f9d5..21fb44ae 100644 --- a/test/language/module/cyclic_import/b.wren +++ b/test/language/module/cyclic_import/b.wren @@ -2,8 +2,8 @@ System.print("start b") var B = "b value" -System.print("b defined " + B) +System.print("b defined %(B)") import "a" for A -System.print("b imported " + A) +System.print("b imported %(A)") System.print("end b") diff --git a/test/language/module/shared_import/a.wren b/test/language/module/shared_import/a.wren index 96dd90a2..6478e5a7 100644 --- a/test/language/module/shared_import/a.wren +++ b/test/language/module/shared_import/a.wren @@ -1,5 +1,5 @@ // nontest System.print("a") import "shared" for Shared -var A = "a " + Shared +var A = "a %(Shared)" System.print("a done") diff --git a/test/language/module/shared_import/b.wren b/test/language/module/shared_import/b.wren index 7dd9f4ab..9449134f 100644 --- a/test/language/module/shared_import/b.wren +++ b/test/language/module/shared_import/b.wren @@ -1,5 +1,5 @@ // nontest System.print("b") import "shared" for Shared -var B = "b " + Shared +var B = "b %(Shared)" System.print("b done") diff --git a/test/language/string/escapes.wren b/test/language/string/escapes.wren index 70628b45..dc7388c0 100644 --- a/test/language/string/escapes.wren +++ b/test/language/string/escapes.wren @@ -2,6 +2,7 @@ System.print("\"") // expect: " System.print("\\") // expect: \ System.print("(\n)") // expect: ( - // expect: ) + // expect: ) +System.print("\%") // expect: % // TODO: Non-printing escapes like \t. diff --git a/test/limit/interpolation_nesting.wren b/test/limit/interpolation_nesting.wren new file mode 100644 index 00000000..90a65267 --- /dev/null +++ b/test/limit/interpolation_nesting.wren @@ -0,0 +1 @@ +System.print("0 %("1 %("2 %("3 %("4 %("5 %("6 %("7 %(8)")")")")")")")") // expect: 0 1 2 3 4 5 6 7 8 diff --git a/test/limit/too_much_interpolation_nesting.wren b/test/limit/too_much_interpolation_nesting.wren new file mode 100644 index 00000000..b9d77919 --- /dev/null +++ b/test/limit/too_much_interpolation_nesting.wren @@ -0,0 +1 @@ +System.print("0 %("1 %("2 %("3 %("4 %("5 %("6 %("7 %("8 %(9)")")")")")")")")") // expect error