Skip to content
This repository has been archived by the owner on Nov 5, 2024. It is now read-only.

Commit

Permalink
feat: implement string interpolation
Browse files Browse the repository at this point in the history
  • Loading branch information
R-unic committed Oct 5, 2023
1 parent 95d8398 commit ba6d144
Show file tree
Hide file tree
Showing 12 changed files with 159 additions and 31 deletions.
9 changes: 5 additions & 4 deletions src/code-analysis/parser/ast/expressions/literal.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Token } from "../../../syntax/token";
import { ValueType } from "../../../type-checker";
import type { ValueType } from "../../../type-checker";
import type { Token } from "../../../syntax/token";
import type Syntax from "../../../syntax/syntax-type";
import AST from "..";

export class LiteralExpression<T extends ValueType = ValueType> extends AST.Expression {
export class LiteralExpression<V extends ValueType = ValueType, S extends Syntax = Syntax> extends AST.Expression {
public constructor(
public readonly token: Token<T>
public readonly token: Token<V, S>
) { super(); }

public accept<R>(visitor: AST.Visitor.Expression<R>): R {
Expand Down
17 changes: 17 additions & 0 deletions src/code-analysis/parser/ast/expressions/string-interpolation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Token } from "../../../syntax/token";
import type { LiteralExpression } from "./literal";
import AST from "..";

export class StringInterpolationExpression extends AST.Expression {
public constructor(
public readonly parts: (LiteralExpression<string> | AST.Expression)[]
) { super(); }

public accept<R>(visitor: AST.Visitor.Expression<R>): R {
return visitor.visitStringInterpolationExpression(this);
}

public get token(): Token {
return this.parts[0].token;
}
}
4 changes: 4 additions & 0 deletions src/code-analysis/parser/ast/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import util from "util";
import type { Token } from "../../syntax/token";
import type { LiteralExpression } from "./expressions/literal";
import type { ArrayLiteralExpression } from "./expressions/array-literal";
import type { StringInterpolationExpression } from "./expressions/string-interpolation";
import type { ParenthesizedExpression } from "./expressions/parenthesized";
import type { UnaryExpression } from "./expressions/unary";
import type { BinaryExpression } from "./expressions/binary";
Expand Down Expand Up @@ -43,6 +44,7 @@ import type BoundIfStatement from "../../type-checker/binder/bound-statements/if
import type BoundWhileStatement from "../../type-checker/binder/bound-statements/if";
import type BoundFunctionDeclarationStatement from "../../type-checker/binder/bound-statements/function-declaration";
import type BoundReturnStatement from "../../type-checker/binder/bound-statements/return";
import type BoundStringInterpolationExpression from "../../type-checker/binder/bound-expressions/string-interpolation";

namespace AST {
export abstract class Node {
Expand Down Expand Up @@ -73,6 +75,7 @@ namespace AST {
public abstract visitBinaryExpression(expr: BinaryExpression): R
public abstract visitUnaryExpression(expr: UnaryExpression): R
public abstract visitParenthesizedExpression(expr: ParenthesizedExpression): R
public abstract visitStringInterpolationExpression(expr: StringInterpolationExpression): R
public abstract visitArrayLiteralExpression(expr: ArrayLiteralExpression): R
public abstract visitLiteralExpression(expr: LiteralExpression): R
}
Expand Down Expand Up @@ -100,6 +103,7 @@ namespace AST {
public abstract visitBinaryExpression(expr: BoundBinaryExpression): R
public abstract visitUnaryExpression(expr: BoundUnaryExpression): R
public abstract visitParenthesizedExpression(expr: BoundParenthesizedExpression): R
public abstract visitStringInterpolationExpression(expr: BoundStringInterpolationExpression): R
public abstract visitArrayLiteralExpression(expr: BoundArrayLiteralExpression): R
public abstract visitLiteralExpression(expr: BoundLiteralExpression): R
}
Expand Down
51 changes: 45 additions & 6 deletions src/code-analysis/parser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { WhileStatement } from "./ast/statements/while";
import { PropertyAssignmentExpression } from "./ast/expressions/property-assignment";
import { FunctionDeclarationStatement } from "./ast/statements/function-declaration";
import { ReturnStatement } from "./ast/statements/return";
import { StringInterpolationExpression } from "./ast/expressions/string-interpolation";
const { UNARY_SYNTAXES, LITERAL_SYNTAXES, COMPOUND_ASSIGNMENT_SYNTAXES } = SyntaxSets;

type SyntaxSet = (typeof SyntaxSets)[keyof typeof SyntaxSets];
Expand Down Expand Up @@ -121,7 +122,7 @@ export default class Parser extends ArrayStepper<Token> {
|| syntax === Syntax.RBracket
|| syntax === Syntax.Question;

return (this.check(Syntax.Mut) ? this.checkType(1) : this.checkType(1))
return (this.check(Syntax.Mut) ? this.checkType(1) : this.checkType())
&& (isVariableDeclarationSyntax(nextSyntax) || isVariableDeclarationSyntax(nextNextSyntax));
}

Expand Down Expand Up @@ -398,9 +399,12 @@ export default class Parser extends ArrayStepper<Token> {
}

private parsePrimary(): AST.Expression {
if (this.matchSet(LITERAL_SYNTAXES))
return new LiteralExpression(this.previous());
if (this.match(Syntax.LBracket)) {
if (this.matchSet(LITERAL_SYNTAXES)) {
const token = this.previous();
return token.syntax === Syntax.String && token.lexeme.includes("%{") ?
this.parseStringInterpolation(<Token<string, Syntax.String>>token)
: new LiteralExpression(token);
} if (this.match(Syntax.LBracket)) {
const bracket = this.previous<undefined>();
const elements = this.parseExpressionList();
this.consume(Syntax.RBracket, "']'");
Expand All @@ -415,7 +419,42 @@ export default class Parser extends ArrayStepper<Token> {
return new ParenthesizedExpression(expr);
}

throw new ParsingError("Expected expression", this.current);
throw new ParsingError(`Expected expression, got '${this.current.lexeme}'`, this.current);
}

private parseStringInterpolation(string: Token<string, Syntax.String>): StringInterpolationExpression {
const rawParts = this.extractInterpolationParts(string.lexeme);
const parts: (LiteralExpression<string> | AST.Expression)[] = [];

for (const part of rawParts) {
if (part.startsWith("%{")) {
const interpolationParser = new Parser(part.slice(2, -1));
const expression = interpolationParser.parseExpression();
parts.push(expression);
} else
parts.push(new LiteralExpression(fakeToken(Syntax.String, part, part)));
}

return new StringInterpolationExpression(parts);
}

private extractInterpolationParts(string: string): string[] {
const rawParts: string[] = [];
const pattern = /%\{([^{}]+)\}/;
const match = string.match(pattern);

if (match !== null) {
rawParts.push(match.input!.slice(0, match.index!));
rawParts.push(match[0]);

if (pattern.test(match.input!.slice(match.index! + match[0].length))) {
rawParts.push(...this.extractInterpolationParts(match.input!.slice(match.index! + match[0].length)));
} else {
rawParts.push(match.input!.slice(match.index! + match[0].length));
}
}

return rawParts;
}

private parseType(): AST.TypeRef {
Expand Down Expand Up @@ -520,7 +559,7 @@ export default class Parser extends ArrayStepper<Token> {
}

private checkType(offset = 0): boolean {
return this.checkMultiple([Syntax.Identifier, Syntax.Undefined, Syntax.Null], offset) && this.isTypeDefined(this.current.lexeme);
return this.checkMultiple([Syntax.Identifier, Syntax.Undefined, Syntax.Null], offset) && this.isTypeDefined(this.peek(offset)!.lexeme);
}

private isTypeDefined(name: string): boolean {
Expand Down
6 changes: 6 additions & 0 deletions src/code-analysis/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type { IfStatement } from "./parser/ast/statements/if";
import type { WhileStatement } from "./parser/ast/statements/while";
import type { FunctionDeclarationStatement } from "./parser/ast/statements/function-declaration";
import type { ReturnStatement } from "./parser/ast/statements/return";
import { StringInterpolationExpression } from "./parser/ast/expressions/string-interpolation";

export default class Resolver implements AST.Visitor.Expression<void>, AST.Visitor.Statement<void> {
public readonly locals = new Map<AST.Node, number>;
Expand Down Expand Up @@ -138,6 +139,11 @@ export default class Resolver implements AST.Visitor.Expression<void>, AST.Visit
this.resolve(expr.expression);
}

public visitStringInterpolationExpression(expr: StringInterpolationExpression): void {
for (const part of expr.parts)
this.resolve(part);
}

public visitArrayLiteralExpression(expr: ArrayLiteralExpression): void {
for (const element of expr.elements)
this.resolve(element);
Expand Down
6 changes: 3 additions & 3 deletions src/code-analysis/syntax/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ export class LocationSpan {
}
}

export class Token<T extends ValueType = ValueType> {
export class Token<V extends ValueType = ValueType, S extends Syntax = Syntax> {
public constructor(
public readonly syntax: Syntax,
public readonly syntax: S,
public readonly lexeme: string,
public readonly value: T,
public readonly value: V,
public readonly locationSpan: LocationSpan
) {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { BoundExpression } from "../bound-node";
import type { Token } from "../../../syntax/token";
import type { ValueType } from "../../../type-checker";
import type { Type } from "../../types/type";
import type Syntax from "../../../syntax/syntax-type";
import AST from "../../../parser/ast";

export default class BoundLiteralExpression<V extends ValueType = ValueType> extends BoundExpression {
export default class BoundLiteralExpression<V extends ValueType = ValueType, S extends Syntax = Syntax> extends BoundExpression {
public constructor(
public readonly token: Token<V>,
public readonly token: Token<V, S>,
public readonly type: Type
) { super(); }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Token } from "../../../syntax/token";
import { BoundExpression } from "../bound-node";
import BoundLiteralExpression from "./literal";
import SingularType from "../../types/singular-type";
import AST from "../../../parser/ast";

export default class BoundStringInterpolationExpression extends BoundExpression {
public override readonly type = new SingularType("string");

public constructor(
public readonly parts: (BoundLiteralExpression<string> | BoundExpression)[]
) { super(); }

public accept<R>(visitor: AST.Visitor.BoundExpression<R>): R {
return visitor.visitStringInterpolationExpression(this);
}

public get token(): Token {
return this.parts[0].token;
}
}
45 changes: 34 additions & 11 deletions src/code-analysis/type-checker/binder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import AST from "../../parser/ast";

import type { LiteralExpression } from "../../parser/ast/expressions/literal";
import type { ArrayLiteralExpression } from "../../parser/ast/expressions/array-literal";
import type { StringInterpolationExpression } from "../../parser/ast/expressions/string-interpolation";
import type { ParenthesizedExpression } from "../../parser/ast/expressions/parenthesized";
import type { UnaryExpression } from "../../parser/ast/expressions/unary";
import type { BinaryExpression } from "../../parser/ast/expressions/binary";
Expand Down Expand Up @@ -59,9 +60,14 @@ import BoundWhileStatement from "./bound-statements/while";
import BoundFunctionDeclarationStatement from "./bound-statements/function-declaration";
import FunctionType from "../types/function-type";
import BoundReturnStatement from "./bound-statements/return";
import BoundStringInterpolationExpression from "./bound-expressions/string-interpolation";

export default class Binder implements AST.Visitor.Expression<BoundExpression>, AST.Visitor.Statement<BoundStatement> {
private readonly variables: VariableSymbol[] = [];
private readonly variableScopes: VariableSymbol[][] = [];

public constructor() {
this.beginScope();
}

public visitReturnStatement(stmt: ReturnStatement): BoundReturnStatement {
const expr = this.bind(stmt.expression);
Expand Down Expand Up @@ -95,7 +101,9 @@ export default class Binder implements AST.Visitor.Expression<BoundExpression>,
}

public visitBlockStatement(stmt: BlockStatement): BoundBlockStatement {
this.beginScope();
const boundStatements = this.bindStatements(stmt.statements);
this.endScope();
return new BoundBlockStatement(stmt.token, boundStatements);
}

Expand Down Expand Up @@ -154,9 +162,6 @@ export default class Binder implements AST.Visitor.Expression<BoundExpression>,

public visitIdentifierExpression(expr: IdentifierExpression): BoundIdentifierExpression {
const variableSymbol = this.findSymbol(expr.token);
if (!variableSymbol)
throw new BindingError(`Failed to find variable symbol for '${expr.token.lexeme}'`, expr.token)

return new BoundIdentifierExpression(expr.name, variableSymbol.type);
}

Expand Down Expand Up @@ -184,6 +189,10 @@ export default class Binder implements AST.Visitor.Expression<BoundExpression>,
return new BoundParenthesizedExpression(this.bind(expr.expression));
}

public visitStringInterpolationExpression(expr: StringInterpolationExpression): BoundStringInterpolationExpression {
return new BoundStringInterpolationExpression(expr.parts.map(part => this.bind(part)));
}

public visitArrayLiteralExpression(expr: ArrayLiteralExpression): BoundExpression {
const elements = expr.elements.map(element => this.bind(element));
let elementType: Type = new SingularType("undefined");
Expand Down Expand Up @@ -220,25 +229,39 @@ export default class Binder implements AST.Visitor.Expression<BoundExpression>,
return new BoundLiteralExpression(expr.token, type);
}

public bindStatements(statements: AST.Statement[]): BoundStatement[] {
return statements.map(statement => this.bind(statement));
}

public defineSymbol<T extends Type = Type>(name: Token, type: T): VariableSymbol<T> {
const variableSymbol = new VariableSymbol<T>(name, type);
this.variables.push(variableSymbol);
const scope = this.variableScopes.at(-1);
scope?.push(variableSymbol);
return variableSymbol;
}

public bindStatements(statements: AST.Statement[]): BoundStatement[] {
return statements.map(statement => this.bind(statement));
}

private bind<T extends AST.Expression | AST.Statement = AST.Expression | AST.Statement, R extends BoundExpression | BoundStatement = T extends AST.Expression ? BoundExpression : BoundStatement>(node: T): R {
return <R>(node instanceof AST.Expression ?
node.accept<BoundExpression>(this)
: node.accept<BoundStatement>(this));
}

private beginScope(): void {
this.variableScopes.push([]);
}

private endScope<T extends Type = Type>(): VariableSymbol<T>[] {
return <VariableSymbol<T>[]>this.variableScopes.pop()!;
}

private findSymbol(name: Token): VariableSymbol {
return this.variables
.find(symbol => symbol.name.lexeme === name.lexeme)!;
for (let i = this.variableScopes.length - 1; i >= 0; i--) {
const scope = this.variableScopes[i];
const symbol = scope.find(symbol => symbol.name.lexeme === name.lexeme)!;
if (!symbol) continue;
return symbol;
}
throw new BindingError(`Failed to find variable symbol for '${name.lexeme}'`, name)
}

private getTypeFromTypeRef(node: AST.TypeRef): Type {
Expand Down
10 changes: 7 additions & 3 deletions src/code-analysis/type-checker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ import { TypeError } from "../../errors";
import { BoundExpression, BoundNode, BoundStatement } from "./binder/bound-node";
import type { Type } from "./types/type";
import type PValue from "../../runtime/types/value";
import type Resolver from "../resolver";
import type FunctionType from "./types/function-type";
import ArrayType from "./types/array-type";
import SingularType from "./types/singular-type";
import UnionType from "./types/union-type";
import ScopeContext from "../scope-context";
import AST from "../parser/ast";

import BoundArrayLiteralExpression from "./binder/bound-expressions/array-literal";
import type BoundStringInterpolationExpression from "./binder/bound-expressions/string-interpolation";
import type BoundParenthesizedExpression from "./binder/bound-expressions/parenthesized";
import type BoundUnaryExpression from "./binder/bound-expressions/unary";
import type BoundBinaryExpression from "./binder/bound-expressions/binary";
Expand All @@ -28,7 +27,7 @@ import type BoundBlockStatement from "./binder/bound-statements/block";
import type BoundIfStatement from "./binder/bound-statements/if";
import type BoundWhileStatement from "./binder/bound-statements/while";
import type BoundFunctionDeclarationStatement from "./binder/bound-statements/function-declaration";
import BoundReturnStatement from "./binder/bound-statements/return";
import type BoundReturnStatement from "./binder/bound-statements/return";

export type ValueType = PValue | string | number | boolean | null | undefined | void | ValueType[];

Expand Down Expand Up @@ -153,6 +152,11 @@ export class TypeChecker implements AST.Visitor.BoundExpression<void>, AST.Visit
this.check(expr.expression);
}

public visitStringInterpolationExpression(expr: BoundStringInterpolationExpression): void {
for (const part of expr.parts)
this.check(part);
}

public visitArrayLiteralExpression(expr: BoundArrayLiteralExpression): void {
for (const element of expr.elements) {
this.check(element);
Expand Down
12 changes: 12 additions & 0 deletions src/runtime/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import P from "../../tools/p";

import { LiteralExpression } from "../code-analysis/parser/ast/expressions/literal";
import type { ArrayLiteralExpression } from "../code-analysis/parser/ast/expressions/array-literal";
import type { StringInterpolationExpression } from "../code-analysis/parser/ast/expressions/string-interpolation";
import type { ParenthesizedExpression } from "../code-analysis/parser/ast/expressions/parenthesized";
import type { UnaryExpression } from "../code-analysis/parser/ast/expressions/unary";
import { BinaryExpression } from "../code-analysis/parser/ast/expressions/binary";
Expand Down Expand Up @@ -268,6 +269,17 @@ export default class Interpreter implements AST.Visitor.Expression<ValueType>, A
return this.evaluate(expr.expression);
}

public visitStringInterpolationExpression(expr: StringInterpolationExpression): ValueType {
return expr.parts.map(part =>
part === undefined ?
"undefined"
: (part === null ?
"null"
: this.evaluate(part)!.toString()
)
).join("");
}

public visitArrayLiteralExpression(expr: ArrayLiteralExpression): ValueType {
return expr.elements.map(element => this.evaluate(element));
}
Expand Down
Loading

0 comments on commit ba6d144

Please sign in to comment.