tables/arrays
This commit is contained in:
parent
a744c12baf
commit
2db5c3bfe5
@ -97,3 +97,59 @@ func (ie *InfixExpression) expressionNode() {}
|
|||||||
func (ie *InfixExpression) String() string {
|
func (ie *InfixExpression) String() string {
|
||||||
return fmt.Sprintf("(%s %s %s)", ie.Left.String(), ie.Operator, ie.Right.String())
|
return fmt.Sprintf("(%s %s %s)", ie.Left.String(), ie.Operator, ie.Right.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TablePair represents a key-value pair in a table
|
||||||
|
type TablePair struct {
|
||||||
|
Key Expression // nil for array-style elements
|
||||||
|
Value Expression
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tp *TablePair) String() string {
|
||||||
|
if tp.Key == nil {
|
||||||
|
return tp.Value.String()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s = %s", tp.Key.String(), tp.Value.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableLiteral represents table literals {}
|
||||||
|
type TableLiteral struct {
|
||||||
|
Pairs []TablePair
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tl *TableLiteral) expressionNode() {}
|
||||||
|
func (tl *TableLiteral) String() string {
|
||||||
|
var pairs []string
|
||||||
|
for _, pair := range tl.Pairs {
|
||||||
|
pairs = append(pairs, pair.String())
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("{%s}", joinStrings(pairs, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsArray returns true if this table contains only array-style elements
|
||||||
|
func (tl *TableLiteral) IsArray() bool {
|
||||||
|
for _, pair := range tl.Pairs {
|
||||||
|
if pair.Key != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// joinStrings joins string slice with separator
|
||||||
|
func joinStrings(strs []string, sep string) string {
|
||||||
|
if len(strs) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if len(strs) == 1 {
|
||||||
|
return strs[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
var result string
|
||||||
|
for i, s := range strs {
|
||||||
|
if i > 0 {
|
||||||
|
result += sep
|
||||||
|
}
|
||||||
|
result += s
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
@ -149,6 +149,12 @@ func (l *Lexer) NextToken() Token {
|
|||||||
tok = Token{Type: LPAREN, Literal: string(l.ch), Line: l.line, Column: l.column}
|
tok = Token{Type: LPAREN, Literal: string(l.ch), Line: l.line, Column: l.column}
|
||||||
case ')':
|
case ')':
|
||||||
tok = Token{Type: RPAREN, Literal: string(l.ch), Line: l.line, Column: l.column}
|
tok = Token{Type: RPAREN, Literal: string(l.ch), Line: l.line, Column: l.column}
|
||||||
|
case '{':
|
||||||
|
tok = Token{Type: LBRACE, Literal: string(l.ch), Line: l.line, Column: l.column}
|
||||||
|
case '}':
|
||||||
|
tok = Token{Type: RBRACE, Literal: string(l.ch), Line: l.line, Column: l.column}
|
||||||
|
case ',':
|
||||||
|
tok = Token{Type: COMMA, Literal: string(l.ch), Line: l.line, Column: l.column}
|
||||||
case '"':
|
case '"':
|
||||||
tok.Type = STRING
|
tok.Type = STRING
|
||||||
tok.Literal = l.readString()
|
tok.Literal = l.readString()
|
||||||
|
@ -46,6 +46,7 @@ func NewParser(lexer *Lexer) *Parser {
|
|||||||
p.registerPrefix(FALSE, p.parseBooleanLiteral)
|
p.registerPrefix(FALSE, p.parseBooleanLiteral)
|
||||||
p.registerPrefix(NIL, p.parseNilLiteral)
|
p.registerPrefix(NIL, p.parseNilLiteral)
|
||||||
p.registerPrefix(LPAREN, p.parseGroupedExpression)
|
p.registerPrefix(LPAREN, p.parseGroupedExpression)
|
||||||
|
p.registerPrefix(LBRACE, p.parseTableLiteral)
|
||||||
|
|
||||||
p.infixParseFns = make(map[TokenType]func(Expression) Expression)
|
p.infixParseFns = make(map[TokenType]func(Expression) Expression)
|
||||||
p.registerInfix(PLUS, p.parseInfixExpression)
|
p.registerInfix(PLUS, p.parseInfixExpression)
|
||||||
@ -215,6 +216,76 @@ func (p *Parser) parseGroupedExpression() Expression {
|
|||||||
return exp
|
return exp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Parser) parseTableLiteral() Expression {
|
||||||
|
table := &TableLiteral{}
|
||||||
|
table.Pairs = []TablePair{}
|
||||||
|
|
||||||
|
if p.peekTokenIs(RBRACE) {
|
||||||
|
p.nextToken()
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
|
||||||
|
p.nextToken()
|
||||||
|
|
||||||
|
for {
|
||||||
|
// Check for EOF
|
||||||
|
if p.curTokenIs(EOF) {
|
||||||
|
p.addError("unexpected end of input, expected }")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pair := TablePair{}
|
||||||
|
|
||||||
|
// Check if this is a key=value pair
|
||||||
|
if p.curTokenIs(IDENT) && p.peekTokenIs(ASSIGN) {
|
||||||
|
pair.Key = &Identifier{Value: p.curToken.Literal}
|
||||||
|
p.nextToken() // move to =
|
||||||
|
p.nextToken() // move past =
|
||||||
|
|
||||||
|
// Check for EOF after =
|
||||||
|
if p.curTokenIs(EOF) {
|
||||||
|
p.addError("expected expression after assignment operator")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pair.Value = p.parseExpression(LOWEST)
|
||||||
|
} else {
|
||||||
|
// Array-style element
|
||||||
|
pair.Value = p.parseExpression(LOWEST)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pair.Value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Pairs = append(table.Pairs, pair)
|
||||||
|
|
||||||
|
if !p.peekTokenIs(COMMA) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
p.nextToken() // consume comma
|
||||||
|
p.nextToken() // move to next element
|
||||||
|
|
||||||
|
// Allow trailing comma
|
||||||
|
if p.curTokenIs(RBRACE) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for EOF after comma
|
||||||
|
if p.curTokenIs(EOF) {
|
||||||
|
p.addError("expected next token to be }")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.expectPeek(RBRACE) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Parser) parseInfixExpression(left Expression) Expression {
|
func (p *Parser) parseInfixExpression(left Expression) Expression {
|
||||||
expression := &InfixExpression{
|
expression := &InfixExpression{
|
||||||
Left: left,
|
Left: left,
|
||||||
@ -281,6 +352,8 @@ func (p *Parser) noPrefixParseFnError(t TokenType) {
|
|||||||
message = fmt.Sprintf("unexpected operator '%s', missing left operand", tokenTypeString(t))
|
message = fmt.Sprintf("unexpected operator '%s', missing left operand", tokenTypeString(t))
|
||||||
case RPAREN:
|
case RPAREN:
|
||||||
message = "unexpected closing parenthesis"
|
message = "unexpected closing parenthesis"
|
||||||
|
case RBRACE:
|
||||||
|
message = "unexpected closing brace"
|
||||||
case EOF:
|
case EOF:
|
||||||
message = "unexpected end of input"
|
message = "unexpected end of input"
|
||||||
default:
|
default:
|
||||||
@ -350,6 +423,12 @@ func tokenTypeString(t TokenType) string {
|
|||||||
return "("
|
return "("
|
||||||
case RPAREN:
|
case RPAREN:
|
||||||
return ")"
|
return ")"
|
||||||
|
case LBRACE:
|
||||||
|
return "{"
|
||||||
|
case RBRACE:
|
||||||
|
return "}"
|
||||||
|
case COMMA:
|
||||||
|
return ","
|
||||||
case VAR:
|
case VAR:
|
||||||
return "var"
|
return "var"
|
||||||
case EOF:
|
case EOF:
|
||||||
|
@ -40,6 +40,119 @@ func TestLiterals(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTableLiterals(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expectedPairs int
|
||||||
|
isArray bool
|
||||||
|
description string
|
||||||
|
}{
|
||||||
|
{"{}", 0, true, "empty table"},
|
||||||
|
{"{1, 2, 3}", 3, true, "array-like table"},
|
||||||
|
{"{a = 1, b = 2}", 2, false, "hash-like table"},
|
||||||
|
{`{"hello", "world"}`, 2, true, "string array"},
|
||||||
|
{"{x = true, y = false}", 2, false, "boolean hash"},
|
||||||
|
{"{1}", 1, true, "single element array"},
|
||||||
|
{"{key = nil}", 1, false, "nil value hash"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.description, func(t *testing.T) {
|
||||||
|
l := NewLexer(tt.input)
|
||||||
|
p := NewParser(l)
|
||||||
|
expr := p.parseExpression(LOWEST)
|
||||||
|
checkParserErrors(t, p)
|
||||||
|
|
||||||
|
table, ok := expr.(*TableLiteral)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected TableLiteral, got %T", expr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(table.Pairs) != tt.expectedPairs {
|
||||||
|
t.Errorf("expected %d pairs, got %d", tt.expectedPairs, len(table.Pairs))
|
||||||
|
}
|
||||||
|
|
||||||
|
if table.IsArray() != tt.isArray {
|
||||||
|
t.Errorf("expected IsArray() = %t, got %t", tt.isArray, table.IsArray())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTableAssignments(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
identifier string
|
||||||
|
pairCount int
|
||||||
|
isArray bool
|
||||||
|
description string
|
||||||
|
}{
|
||||||
|
{"arr = {1, 2, 3}", "arr", 3, true, "array assignment"},
|
||||||
|
{"hash = {x = 1, y = 2}", "hash", 2, false, "hash assignment"},
|
||||||
|
{"empty = {}", "empty", 0, true, "empty table assignment"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.description, func(t *testing.T) {
|
||||||
|
l := NewLexer(tt.input)
|
||||||
|
p := NewParser(l)
|
||||||
|
program := p.ParseProgram()
|
||||||
|
checkParserErrors(t, p)
|
||||||
|
|
||||||
|
if len(program.Statements) != 1 {
|
||||||
|
t.Fatalf("expected 1 statement, got %d", len(program.Statements))
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt, ok := program.Statements[0].(*AssignStatement)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected AssignStatement, got %T", program.Statements[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
if stmt.Name.Value != tt.identifier {
|
||||||
|
t.Errorf("expected identifier %s, got %s", tt.identifier, stmt.Name.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
table, ok := stmt.Value.(*TableLiteral)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected TableLiteral, got %T", stmt.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(table.Pairs) != tt.pairCount {
|
||||||
|
t.Errorf("expected %d pairs, got %d", tt.pairCount, len(table.Pairs))
|
||||||
|
}
|
||||||
|
|
||||||
|
if table.IsArray() != tt.isArray {
|
||||||
|
t.Errorf("expected IsArray() = %t, got %t", tt.isArray, table.IsArray())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTableStringRepresentation(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"{}", "{}"},
|
||||||
|
{"{1, 2}", "{1.00, 2.00}"},
|
||||||
|
{"{x = 1}", "{x = 1.00}"},
|
||||||
|
{"{a = 1, b = 2}", "{a = 1.00, b = 2.00}"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.input, func(t *testing.T) {
|
||||||
|
l := NewLexer(tt.input)
|
||||||
|
p := NewParser(l)
|
||||||
|
expr := p.parseExpression(LOWEST)
|
||||||
|
checkParserErrors(t, p)
|
||||||
|
|
||||||
|
if expr.String() != tt.expected {
|
||||||
|
t.Errorf("expected %s, got %s", tt.expected, expr.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAssignStatements(t *testing.T) {
|
func TestAssignStatements(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
input string
|
input string
|
||||||
@ -162,6 +275,9 @@ func TestParsingErrors(t *testing.T) {
|
|||||||
{"1 +", "expected expression after operator '+'", 1, 3},
|
{"1 +", "expected expression after operator '+'", 1, 3},
|
||||||
{"@", "unexpected token '@'", 1, 1},
|
{"@", "unexpected token '@'", 1, 1},
|
||||||
{"invalid@", "unexpected identifier", 1, 1},
|
{"invalid@", "unexpected identifier", 1, 1},
|
||||||
|
{"{1, 2", "expected next token to be }", 1, 6},
|
||||||
|
{"{a =", "expected expression after assignment operator", 1, 4},
|
||||||
|
{"{a = 1,", "expected next token to be }", 1, 8},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@ -171,7 +287,7 @@ func TestParsingErrors(t *testing.T) {
|
|||||||
|
|
||||||
// Decide parsing strategy based on the type of error we're testing
|
// Decide parsing strategy based on the type of error we're testing
|
||||||
switch tt.input {
|
switch tt.input {
|
||||||
case "(1 + 2", "+ 5", "1 +":
|
case "(1 + 2", "+ 5", "1 +", "{1, 2", "{a =", "{a = 1,":
|
||||||
// These are expression-level errors
|
// These are expression-level errors
|
||||||
p.parseExpression(LOWEST)
|
p.parseExpression(LOWEST)
|
||||||
default:
|
default:
|
||||||
|
@ -22,6 +22,9 @@ const (
|
|||||||
// Delimiters
|
// Delimiters
|
||||||
LPAREN // (
|
LPAREN // (
|
||||||
RPAREN // )
|
RPAREN // )
|
||||||
|
LBRACE // {
|
||||||
|
RBRACE // }
|
||||||
|
COMMA // ,
|
||||||
|
|
||||||
// Keywords
|
// Keywords
|
||||||
VAR
|
VAR
|
||||||
|
Loading…
x
Reference in New Issue
Block a user