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)