Browse Source

§2.4: passing LetStatement parsing, silent errors.

Frederic G. MARAND 5 years ago
parent
commit
f12ba8036f
10 changed files with 259 additions and 29 deletions
  1. 1 0
      .gitignore
  2. 13 0
      .idea/runConfigurations/Test_All.xml
  3. 0 13
      .idea/runConfigurations/Test_Lexer.xml
  4. 54 0
      ast/ast.go
  5. 1 1
      lexer/lexer.go
  6. 10 10
      lexer/lexer_test.go
  7. 2 2
      main.go
  8. 105 0
      parser/parser.go
  9. 70 0
      parser/parser_test.go
  10. 3 3
      repl/repl.go

+ 1 - 0
.gitignore

@@ -1 +1,2 @@
 .idea/workspace.xml
+coverage.out

+ 13 - 0
.idea/runConfigurations/Test_All.xml

@@ -0,0 +1,13 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="Test All" type="GoTestRunConfiguration" factoryName="Go Test" singleton="true">
+    <module name="waiig15" />
+    <working_directory value="$PROJECT_DIR$" />
+    <go_parameters value="-i" />
+    <framework value="gotest" />
+    <kind value="DIRECTORY" />
+    <package value="fgm/waiig15" />
+    <directory value="$PROJECT_DIR$" />
+    <filePath value="$PROJECT_DIR$/parser/parser_test.go" />
+    <method v="2" />
+  </configuration>
+</component>

+ 0 - 13
.idea/runConfigurations/Test_Lexer.xml

@@ -1,13 +0,0 @@
-<component name="ProjectRunConfigurationManager">
-  <configuration default="false" name="Test Lexer" type="GoTestRunConfiguration" factoryName="Go Test" singleton="true">
-    <module name="waiig15" />
-    <working_directory value="$PROJECT_DIR$/lexer" />
-    <go_parameters value="-i" />
-    <framework value="gotest" />
-    <kind value="FILE" />
-    <package value="fgm/waiig15" />
-    <directory value="$PROJECT_DIR$/" />
-    <filePath value="$PROJECT_DIR$/lexer/lexer_test.go" />
-    <method v="2" />
-  </configuration>
-</component>

+ 54 - 0
ast/ast.go

@@ -0,0 +1,54 @@
+package ast
+
+import "code.osinet.fr/fgm/waiig15/token"
+
+// Every node in the AST implements Node.
+type Node interface {
+	// TokenLiteral returns the literal value of the token it's associated to.
+	TokenLiteral() string
+}
+
+type Statement interface {
+	// Expression extends Node.
+	Node
+	statementNode()
+}
+
+type Expression interface {
+	// Expression extends Node.
+	Node
+	expressionNode()
+}
+
+type Program struct {
+	Statements []Statement
+}
+
+func (p *Program) TokenLiteral() string {
+	if len(p.Statements) > 0 {
+		return p.Statements[0].TokenLiteral()
+	} else {
+		return ""
+	}
+}
+
+type LetStatement struct {
+	Token token.Token // the token.LET token. Why do we need it ?
+	Name  *Identifier
+	Value Expression
+}
+
+func (ls *LetStatement) statementNode() {}
+func (ls *LetStatement) TokenLiteral() string {
+	return ls.Token.Literal
+}
+
+type Identifier struct {
+	Token token.Token // the token.IDENT token. Why do we need it ?
+	Value string      // The identifier string.
+}
+
+func (i *Identifier) expressionNode() {}
+func (i *Identifier) TokenLiteral() string {
+	return i.Token.Literal
+}

+ 1 - 1
lexer/lexer.go

@@ -143,7 +143,7 @@ func isLetter(ch byte) bool {
 func (l *Lexer) skipWhitespace() {
 	for l.ch == ' ' ||
 		l.ch == '\r' ||
-	    l.ch == '\t' ||
+		l.ch == '\t' ||
 		l.ch == '\n' {
 		l.readChar()
 	}

+ 10 - 10
lexer/lexer_test.go

@@ -112,16 +112,16 @@ $
 		{token.RBRACE, "}"},
 
 		// 64
-		{ token.INT, "10"},
-		{ token.EQ, "=="},
-		{ token.INT, "10"},
-		{ token.SEMICOLON, ";"},
-		{ token.INT, "10"},
-		{ token.NOT_EQ, "!="},
-		{ token.INT, "9"},
-		{ token.SEMICOLON, ";"},
-
-		{ token.ILLEGAL, "$"},
+		{token.INT, "10"},
+		{token.EQ, "=="},
+		{token.INT, "10"},
+		{token.SEMICOLON, ";"},
+		{token.INT, "10"},
+		{token.NOT_EQ, "!="},
+		{token.INT, "9"},
+		{token.SEMICOLON, ";"},
+
+		{token.ILLEGAL, "$"},
 
 		{token.EOF, ""},
 	}

+ 2 - 2
main.go

@@ -1,10 +1,10 @@
 package main
 
 import (
-	"os/user"
-	"fmt"
 	"code.osinet.fr/fgm/waiig15/repl"
+	"fmt"
 	"os"
+	"os/user"
 )
 
 func main() {

+ 105 - 0
parser/parser.go

@@ -0,0 +1,105 @@
+package parser
+
+import (
+	"code.osinet.fr/fgm/waiig15/ast"
+	"code.osinet.fr/fgm/waiig15/lexer"
+	"code.osinet.fr/fgm/waiig15/token"
+)
+
+type Parser struct {
+	l *lexer.Lexer
+
+	curToken  token.Token
+	peekToken token.Token
+}
+
+func New(l *lexer.Lexer) *Parser {
+	p := &Parser{l: l}
+
+	// Read two tokens, so curToken and peeToken are both set.
+	p.nextToken()
+	p.nextToken()
+
+	return p
+}
+
+func (p *Parser) nextToken() {
+	p.curToken = p.peekToken
+	p.peekToken = p.l.NextToken()
+}
+
+func (p *Parser) ParseProgram() *ast.Program {
+	program := &ast.Program{
+		Statements: []ast.Statement{},
+	}
+	for !p.curTokenIs(token.EOF) {
+		stmt := p.parseStatement()
+		if stmt != nil {
+			program.Statements = append(program.Statements, stmt)
+		}
+		p.nextToken()
+	}
+
+	return program
+}
+
+func (p *Parser) parseStatement() ast.Statement {
+	switch p.curToken.Type {
+	case token.LET:
+		return p.parseLetStatement()
+	default:
+		return nil
+	}
+}
+
+func (p *Parser) parseLetStatement() *ast.LetStatement {
+	stmt := &ast.LetStatement{
+		Token: p.curToken,
+	}
+
+	// Let statement starts with an IDENT token, so if next token is not an
+	// IDENT, the next statement cannot be a Let statement.
+	if !p.expectPeek(token.IDENT) {
+		return nil
+	}
+
+	stmt.Name = &ast.Identifier{
+		Token: p.curToken,
+		Value: p.curToken.Literal,
+	}
+
+	// The previous expectPeek() call fetched the next token, so we should now
+	// be on the assignment.
+	if !p.expectPeek(token.ASSIGN) {
+		return nil
+	}
+
+	// Skip the expression for now, progress to the semicolon terminating the
+	// statement.
+	for !p.curTokenIs(token.SEMICOLON) {
+		p.nextToken()
+	}
+
+	return stmt
+}
+
+// Is the current token in the parser of the given type ?
+func (p *Parser) curTokenIs(t token.TokenType) bool {
+	return p.curToken.Type == t
+}
+
+// Is the next token in the parser of the given type ? Don't consume it.
+func (p *Parser) peekTokenIs(t token.TokenType) bool {
+	return p.peekToken.Type == t
+}
+
+// Is the next token in the parser of the given type ? If it is, consume it,
+// else don't.
+func (p *Parser) expectPeek(t token.TokenType) bool {
+	if p.peekTokenIs(t) {
+		p.nextToken()
+		return true
+	} else {
+		return false
+	}
+}

+ 70 - 0
parser/parser_test.go

@@ -0,0 +1,70 @@
+package parser
+
+import (
+	"code.osinet.fr/fgm/waiig15/ast"
+	"code.osinet.fr/fgm/waiig15/lexer"
+	"testing"
+)
+
+func TestLetStatements(t *testing.T) {
+	input := `
+let x = 5;
+let y = 10;
+let foobar = 838383;
+`
+	l := lexer.New(input)
+	p := New(l)
+
+	program := p.ParseProgram()
+	if program == nil {
+		t.Fatalf("ParseProgram() returned nil.")
+	}
+
+	if len(program.Statements) != 3 {
+		t.Fatalf("program.Statements does not contain 3 statements, got=%d",
+			len(program.Statements))
+	}
+
+	tests := []struct {
+		expectedIdentifier string
+	}{
+		{"x"},
+		{"y"},
+		{"foobar"},
+	}
+
+	for i, tt := range tests {
+		stmt := program.Statements[i]
+		if !testLetStatement(t, stmt, tt.expectedIdentifier) {
+			return
+		}
+	}
+}
+
+func testLetStatement(t *testing.T, s ast.Statement, name string) bool {
+	if s.TokenLiteral() != "let" {
+		t.Errorf("s.TokenLiteral not 'let', got=%q", s.TokenLiteral())
+		return false
+	}
+
+	// Statement is an interface, we need a concrete type for the value, and we
+	// just determined this looked like a LetStatement.
+	letStmt, ok := s.(*ast.LetStatement)
+	if !ok {
+		t.Errorf("s not *ast.LetStatement{}, got=%T", s)
+	}
+
+	if letStmt.Name.Value != name {
+		t.Errorf("letStmt.Name.Value not %s, got=%s",
+			name, letStmt.Name.Value)
+		return false
+	}
+
+	if letStmt.Name.TokenLiteral() != name {
+		t.Errorf("letStmt.Name.TokenLiteral not %s, got=%s",
+			name, letStmt.Name.TokenLiteral())
+		return false
+	}
+
+	return true
+}

+ 3 - 3
repl/repl.go

@@ -1,11 +1,11 @@
 package repl
 
 import (
-	"io"
 	"bufio"
-	"fmt"
 	"code.osinet.fr/fgm/waiig15/lexer"
 	"code.osinet.fr/fgm/waiig15/token"
+	"fmt"
+	"io"
 )
 
 const PROMPT = ">> "
@@ -27,4 +27,4 @@ func Start(in io.Reader, out io.Writer) {
 			fmt.Printf("%+v\n", tok)
 		}
 	}
-}
+}