Skip to content

feat: String interpolation #1427

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ under the licensing terms detailed in LICENSE:
* Guido Zuidhof <[email protected]>
* ncave <[email protected]>
* Andrew Davis <[email protected]>
* Fred <[email protected]>

Portions of this software are derived from third-party works licensed under
the following terms:
Expand Down
22 changes: 21 additions & 1 deletion src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,13 @@ export abstract class Node {
return new StringLiteralExpression(value, range);
}

static createTemplateLiteralExpression(
value: string,
range: Range
): StringLiteralExpression {
return new TemplateLiteralExpression(value, range);
}

static createSuperExpression(
range: Range
): SuperExpression {
Expand Down Expand Up @@ -1089,7 +1096,8 @@ export enum LiteralKind {
STRING,
REGEXP,
ARRAY,
OBJECT
OBJECT,
TEMPLATE,
}

/** Base class of all literal expressions. */
Expand Down Expand Up @@ -1432,6 +1440,18 @@ export class StringLiteralExpression extends LiteralExpression {
}
}

/** Represents a string template literal expression. */
export class TemplateLiteralExpression extends LiteralExpression {
constructor(
/** String value without quotes. */
public expresssionParts: Expression[],
/** Source range. */
range: Range
) {
super(LiteralKind.TEMPLATE, range);
}
}

/** Represents a `super` expression. */
export class SuperExpression extends IdentifierExpression {
constructor(
Expand Down
19 changes: 17 additions & 2 deletions src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ import {
NamedTypeNode,

findDecorator,
isTypeOmitted
isTypeOmitted,
TemplateLiteralExpression
} from "./ast";

import {
Expand Down Expand Up @@ -8763,6 +8764,10 @@ export class Compiler extends DiagnosticEmitter {
assert(!implicitlyNegate);
return this.compileStringLiteral(<StringLiteralExpression>expression, constraints);
}
case LiteralKind.TEMPLATE: {
assert(!implicitlyNegate);
return this.compileTemplateLiteral(<TemplateLiteralExpression>expression, constraints);
}
case LiteralKind.OBJECT: {
assert(!implicitlyNegate);
return this.compileObjectLiteral(<ObjectLiteralExpression>expression, contextualType);
Expand All @@ -8781,7 +8786,17 @@ export class Compiler extends DiagnosticEmitter {
return module.unreachable();
}

private compileStringLiteral(
compileTemplateLiteral(expr: TemplateLiteralExpression, constraints: Constraints): ExpressionRef {
const innerExpressions: ExpressionRef[] | Expression[] = expr.expresssionParts;

var innerConcat = innerExpressions.reduce(function(preV, elem) {
return Number(preV + elem.toString());
}, 0);

return innerConcat;
}

compileStringLiteral(
expression: StringLiteralExpression,
constraints: Constraints
): ExpressionRef {
Expand Down
83 changes: 69 additions & 14 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ import {
} from "./diagnostics";

import {
normalizePath
normalizePath, CharCode
} from "./util";

import { concat3 } from "../std/assembly/util/string";
import {
Node,
NodeKind,
Expand Down Expand Up @@ -367,7 +368,7 @@ export class Parser extends DiagnosticEmitter {
statement = this.parseExport(tn, startPos, (flags & CommonFlags.DECLARE) != 0);
}

// handle non-declaration statements
// handle non-declaration statements
} else {
if (exportEnd) {
this.error(
Expand Down Expand Up @@ -551,25 +552,25 @@ export class Parser extends DiagnosticEmitter {
return null;
}

// 'void'
// 'void'
} else if (token == Token.VOID) {
type = Node.createNamedType(
Node.createSimpleTypeName("void", tn.range()), [], false, tn.range(startPos, tn.pos)
);

// 'this'
// 'this'
} else if (token == Token.THIS) {
type = Node.createNamedType(
Node.createSimpleTypeName("this", tn.range()), [], false, tn.range(startPos, tn.pos)
);

// 'true'
// 'true'
} else if (token == Token.TRUE || token == Token.FALSE) {
type = Node.createNamedType(
Node.createSimpleTypeName("bool", tn.range()), [], false, tn.range(startPos, tn.pos)
);

// 'null'
// 'null'
} else if (token == Token.NULL) {
type = Node.createNamedType(
Node.createSimpleTypeName("null", tn.range()), [], false, tn.range(startPos, tn.pos)
Expand All @@ -582,7 +583,7 @@ export class Parser extends DiagnosticEmitter {
Node.createSimpleTypeName("string", tn.range()), [], false, tn.range(startPos, tn.pos)
);

// Identifier
// Identifier
} else if (token == Token.IDENTIFIER) {
let name = this.parseTypeName(tn);
if (!name) return null;
Expand Down Expand Up @@ -664,7 +665,7 @@ export class Parser extends DiagnosticEmitter {
}
type = Node.createNamedType(
Node.createSimpleTypeName("Array", bracketRange),
[ type ],
[type],
nullable,
tn.range(startPos, tn.pos)
);
Expand Down Expand Up @@ -2244,7 +2245,7 @@ export class Parser extends DiagnosticEmitter {
name.range
);

// field: (':' Type)? ('=' Expression)? ';'?
// field: (':' Type)? ('=' Expression)? ';'?
} else {
if (flags & CommonFlags.ABSTRACT) {
this.error(
Expand Down Expand Up @@ -2501,7 +2502,7 @@ export class Parser extends DiagnosticEmitter {
let internalPath = assert(ret.internalPath);
let source = tn.source;
let exportPaths = source.exportPaths;
if (!exportPaths) source.exportPaths = [ internalPath ];
if (!exportPaths) source.exportPaths = [internalPath];
else if (!exportPaths.includes(internalPath)) exportPaths.push(internalPath);
if (!this.seenlog.has(internalPath)) {
this.dependees.set(internalPath, new Dependee(currentSource, path));
Expand Down Expand Up @@ -3239,7 +3240,7 @@ export class Parser extends DiagnosticEmitter {

var startPos = tn.tokenPos;
var statements: Statement[],
statement: Statement | null;
statement: Statement | null;

// 'case' Expression ':' Statement*

Expand Down Expand Up @@ -3797,7 +3798,11 @@ export class Parser extends DiagnosticEmitter {
return this.maybeParseCallExpression(tn, expr);
}
case Token.STRINGLITERAL: {
return Node.createStringLiteralExpression(tn.readString(), tn.range(startPos, tn.pos));
return this.parseStringLiteral(tn, startPos);
}
case Token.TEMPLATELITERAL: {
return this.parseTemplateLiteralExpression(tn);
// return Node.createTemplateLiteralExpression(tn.readString(), tn.range(startPos, tn.pos));
}
case Token.INTEGERLITERAL: {
return Node.createIntegerLiteralExpression(tn.readInteger(), tn.range(startPos, tn.pos));
Expand Down Expand Up @@ -3846,6 +3851,56 @@ export class Parser extends DiagnosticEmitter {
}
}
}
parseStringLiteral(tn: Tokenizer, startPos: i32, quote: i32 = -1): Expression {
return Node.createStringLiteralExpression(tn.readString(quote), tn.range(startPos, tn.pos));
}

parseTemplateLiteralExpression(tn: Tokenizer): Expression | null {
var startPos = tn.pos;
tn.inStringTemplate = true;

// at `(String*${ Expression }*String*)*`
const parts: Expression[] = [this.parseStringLiteral(tn, startPos)];

var token = tn.peek();
while (token == Token.DOLLAR) {
tn.skip(token);
tn.skip(Token.OPENBRACE);
let expr = this.parseExpressionStart(tn);
if (expr == null) return null;
parts.push(expr);
tn.skip(Token.CLOSEBRACE);
token = tn.next();
if (token == Token.TEMPLATELITERAL) {
break;
}
if (token == Token.DOLLAR) {
continue;
}
startPos = tn.pos;
parts.push(this.parseStringLiteral(tn, startPos, CharCode.BACKTICK));
token = tn.next();
}
if (token == Token.TEMPLATELITERAL) {
tn.advance();
tn.inStringTemplate = false;
}
if (parts.length == 1) {
return parts[0];
}
return parts.reduce((acc: Expression | null, expr: Expression) => {
if (acc == null) return expr;
if (parts.length == 1) {
return isDefined(parts[0]) ? parts[0] : parts[0].toString();
} else if (parts.length == 2) {
return Node.createBinaryExpression(Token.PLUS, parts[0], parts[1], tn.range(startPos, tn.pos));
} else if (parts.length == 3) {
return concat3(parts[0], parts[1], parts[2]);
} else {
return Node.createArrayLiteralExpression(parts, tn.range(startPos, tn.pos));
}
}, null);
}

tryParseTypeArgumentsBeforeArguments(
tn: Tokenizer
Expand All @@ -3866,7 +3921,7 @@ export class Parser extends DiagnosticEmitter {
tn.reset(state);
return null;
}
if (!typeArguments) typeArguments = [ type ];
if (!typeArguments) typeArguments = [type];
else typeArguments.push(type);
} while (tn.skip(Token.COMMA));
if (tn.skip(Token.GREATERTHAN)) {
Expand Down Expand Up @@ -4036,7 +4091,7 @@ export class Parser extends DiagnosticEmitter {
}
// CommaExpression
case Token.COMMA: {
let commaExprs: Expression[] = [ expr ];
let commaExprs: Expression[] = [expr];
do {
expr = this.parseExpression(tn, Precedence.COMMA + 1);
if (!expr) return null;
Expand Down
22 changes: 20 additions & 2 deletions src/tokenizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,13 @@ export enum Token {
BAR_EQUALS,
CARET_EQUALS,
AT,
DOLLAR,

// literals

IDENTIFIER,
STRINGLITERAL,
TEMPLATELITERAL,
INTEGERLITERAL,
FLOATLITERAL,

Expand Down Expand Up @@ -458,6 +460,7 @@ export class Tokenizer extends DiagnosticEmitter {
nextTokenOnNewLine: bool = false;

onComment: CommentHandler | null = null;
public inStringTemplate: bool = false;

/** Constructs a new tokenizer. */
constructor(source: Source, diagnostics: DiagnosticMessage[] | null = null) {
Expand Down Expand Up @@ -553,7 +556,9 @@ export class Tokenizer extends DiagnosticEmitter {
return Token.EXCLAMATION;
}
case CharCode.DOUBLEQUOTE:
case CharCode.SINGLEQUOTE:
case CharCode.SINGLEQUOTE: {
return Token.STRINGLITERAL;
}
case CharCode.BACKTICK: { // TODO
this.pos = pos;
return Token.STRINGLITERAL; // expects a call to readString
Expand Down Expand Up @@ -904,6 +909,13 @@ export class Tokenizer extends DiagnosticEmitter {
this.pos = pos + 1;
return Token.AT;
}
case CharCode.DOLLAR: {
if (this.inStringTemplate) {
++this.pos;
return Token.DOLLAR;
}
// fall through to identifier
}
default: {
if (isIdentifierStart(c)) {
if (isKeywordCharacter(c)) {
Expand Down Expand Up @@ -1014,6 +1026,11 @@ export class Tokenizer extends DiagnosticEmitter {
}
}

advance() {
this.nextToken = -1;
++this.pos;
}

mark(): State {
var state = reusableState;
if (state) {
Expand Down Expand Up @@ -1063,7 +1080,7 @@ export class Tokenizer extends DiagnosticEmitter {
return text.substring(start, pos);
}

readString(): string {
readString(quote: i32 = -1): string {
var text = this.source.text;
var end = this.end;
var pos = this.pos;
Expand Down Expand Up @@ -1128,6 +1145,7 @@ export class Tokenizer extends DiagnosticEmitter {
case CharCode.r: return "\r";
case CharCode.SINGLEQUOTE: return "'";
case CharCode.DOUBLEQUOTE: return "\"";
case CharCode.BACKTICK: return "`";
case CharCode.u: {
if (
this.pos < end &&
Expand Down
1 change: 1 addition & 0 deletions src/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from "./math";
export * from "./path";
export * from "./text";
export * from "./vector";

Loading