Date: 08/2020, Repository: wafuwafu13/Interpreter-made-in-TypeScript
I wrote Writing An Interpreter In Go in TypeScript up to chapter 2.
This article will focus on the analysis of let statements.
environment building
I used TypeScript, webpack, Jest, ESLint, Prettier.
first commit
Lexer
It takes source code as input and returns a sequence of tokens representing that source code as output.
Replacing Go Struct with JavaScript class worked.
type Lexer struct {
input string
position int
readPosition int
ch byte
}
func New(input string) *Lexer {
l := &Lexer{input: input}
l.readChar()
return l
}
func (l *Lexer) readChar() {
if l.readPosition >= len(l.input) {
l.ch = 0
} else {
l.ch = l.input[l.readPosition]
}
l.position = l.readPosition
l.readPosition += 1
}
->
export interface LexerProps {
input: string;
position: number;
readPosition: number;
ch: string | number;
}
export class Lexer<T extends LexerProps> {
input: T['input'];
position: T['position'];
readPosition: T['readPosition'];
ch: T['ch'];
constructor(
input: T['input'],
position: T['position'] = 0,
readPosition: T['readPosition'] = 0,
ch: T['ch'] = '',
) {
this.input = input;
this.position = position;
this.readPosition = readPosition;
this.ch = ch;
this.readChar();
}
readChar(): void {
if (this.readPosition >= this.input.length) {
this.ch = 'EOF';
} else {
this.ch = this.input[this.readPosition];
}
this.position = this.readPosition;
this.readPosition += 1;
}
AST
It is used as an internal representation of the source code.
In Go, the type of the Value field is Expression, but in TypeScript, the type of the Identifier class was used as in the Name field.
type Node interface {
TokenLiteral() string
String() string
}
type Statement interface {
Node
statementNode()
}
type Expression interface {
Node
expressionNode()
}
type Program struct {
Statements []Statement
}
func (p *Program) TokenLiteral() string {
if len(p.Statements) > 0 {
return p.Statements[0].TokenLiteral()
} else {
return ""
}
}
func (p *Program) String() string {
var out bytes.Buffer
for _, s := range p.Statements {
out.WriteString(s.String())
}
return out.String()
}
type LetStatement struct {
Token token.Token
Name *Identifier
Value Expression
}
func (ls *LetStatement) statementNode() {}
func (ls *LetStatement) TokenLiteral() string { return ls.Token.Literal }
func (ls *LetStatement) String() string {
var out bytes.Buffer
out.WriteString(ls.TokenLiteral() + " ")
out.WriteString(ls.Name.String())
out.WriteString(" = ")
if ls.Value != nil {
out.WriteString(ls.Value.String())
}
out.WriteString(";")
return out.String()
}
type Identifier struct {
Token token.Token
Value string
}
func (i *Identifier) expressionNode() {}
func (i *Identifier) TokenLiteral() string { return i.Token.Literal }
func (i *Identifier) String() string { return i.Value }
->
export interface ProgramProps {
statements:
| LetStatement<LetStatementProps>[]
| ReturnStatement<ReturnStatementProps>[]
| ExpressionStatement<ExpressionStatementProps>[];
}
export class Program<T extends ProgramProps> {
statements: T['statements'];
constructor(statements: T['statements'] = []) {
this.statements = statements;
}
}
export interface LetStatementProps {
token: Token<TokenProps>;
name: Identifier<IdentifierProps>;
value?: Identifier<IdentifierProps>;
}
export class LetStatement<T extends LetStatementProps> {
token: T['token'];
name?: T['name'];
value?: T['value'];
constructor(token: T['token']) {
this.token = token;
}
tokenLiteral(): string | number {
return this.token.literal;
}
string(): string {
let statements = [];
statements.push(this.tokenLiteral() + ' ');
statements.push(this.name!.string());
statements.push(' = ');
if (this.value != null) {
statements.push(this.value.string());
}
statements.push(';');
return statements.join('');
}
}
export interface IdentifierProps {
token: Token<TokenProps>;
value: string | number;
}
export class Identifier<T extends IdentifierProps> {
token: T['token'];
value: T['value'];
constructor(token: T['token'], value: T['value']) {
this.token = token;
this.value = value;
}
tokenLiteral(): string | number {
return this.token.literal;
}
string(): string | number {
return this.value;
}
}
Parser
It takes input data and builds an abstract syntax tree data structure.
Since the Type Guard was cumbersome, I defined a new DEFAULT token.
commit
func (p *Parser) parseLetStatement() *ast.LetStatement {
stmt := &ast.LetStatement{Token: p.curToken}
if !p.expectPeek(token.IDENT) {
return nil
}
stmt.Name = &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal}
if !p.expectPeek(token.ASSIGN) {
return nil
}
p.nextToken()
stmt.Value = p.parseExpression(LOWEST)
if p.peekTokenIs(token.SEMICOLON) {
p.nextToken()
}
return stmt
}
->
parseLetStatement(): LetStatement<LetStatementProps> {
const stmt: LetStatement<LetStatementProps> = new LetStatement(
this.curToken,
);
if (!this.expectPeek(TokenDef.IDENT)) {
return stmt;
}
stmt.name = new Identifier(this.curToken, this.curToken.literal);
if (!this.expectPeek(TokenDef.ASSIGN)) {
return stmt;
}
while (!this.curTokenIs(TokenDef.SEMICOLON)) {
this.nextToken();
}
return stmt;
}
Testing
I used Jest.
func TestLetStatements(t *testing.T) {
tests := []struct {
input string
expectedIdentifier string
expectedValue interface{}
}{
{"let x = 5;", "x", 5},
{"let y = true;", "y", true},
{"let foobar = y;", "foobar", "y"},
}
for _, tt := range tests {
l := lexer.New(tt.input)
p := New(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 1 {
t.Fatalf("program.Statements does not contain 1 statements. got=%d",
len(program.Statements))
}
stmt := program.Statements[0]
if !testLetStatement(t, stmt, tt.expectedIdentifier) {
return
}
val := stmt.(*ast.LetStatement).Value
if !testLiteralExpression(t, val, tt.expectedValue) {
return
}
}
}
->
describe('testLetStatement', () => {
const tests = [
{ input: 'let x = 5;', expectedIdentifier: 'x', expectedValue: 5 },
{ input: 'let y = true;', expectedIdentifier: 'y', expectedValue: true },
{
input: 'let foobar = y;',
expectedIdentifier: 'foobar',
expectedValue: 'y',
},
];
for (const test of tests) {
const l = new Lexer(test['input']);
const p = new Parser(l);
const program: Program<ProgramProps> = p.parseProgram();
it('checkParserErrros', () => {
const errors = p.Errors();
if (errors.length != 0) {
for (let i = 0; i < errors.length; i++) {
console.log('parser error: %s', errors[i]);
}
}
expect(errors.length).toBe(0);
});
it('parseProgram', () => {
expect(program).not.toBe(null);
expect(program.statements.length).toBe(1);
});
const stmt: LetStatement<LetStatementProps> | any = program.statements[0];
it('letStatement', () => {
expect(stmt.token.literal).toBe('let');
expect(stmt.name.value).toBe(test['expectedIdentifier']);
expect(stmt.value.value).toBe(test['expectedValue']);
});
}
});
Debugging
It may just be that I am new to Go and debugging was not the right way to do it, but with TypeScript, I was able to debug the AST structure cleanly.


Top comments (0)