tables/arrays
This commit is contained in:
parent
a744c12baf
commit
2db5c3bfe5
@ -97,3 +97,59 @@ func (ie *InfixExpression) expressionNode() {}
|
||||
func (ie *InfixExpression) String() 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}
|
||||
case ')':
|
||||
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 '"':
|
||||
tok.Type = STRING
|
||||
tok.Literal = l.readString()
|
||||
|
@ -46,6 +46,7 @@ func NewParser(lexer *Lexer) *Parser {
|
||||
p.registerPrefix(FALSE, p.parseBooleanLiteral)
|
||||
p.registerPrefix(NIL, p.parseNilLiteral)
|
||||
p.registerPrefix(LPAREN, p.parseGroupedExpression)
|
||||
p.registerPrefix(LBRACE, p.parseTableLiteral)
|
||||
|
||||
p.infixParseFns = make(map[TokenType]func(Expression) Expression)
|
||||
p.registerInfix(PLUS, p.parseInfixExpression)
|
||||
@ -215,6 +216,76 @@ func (p *Parser) parseGroupedExpression() Expression {
|
||||
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 {
|
||||
expression := &InfixExpression{
|
||||
Left: left,
|
||||
@ -281,6 +352,8 @@ func (p *Parser) noPrefixParseFnError(t TokenType) {
|
||||
message = fmt.Sprintf("unexpected operator '%s', missing left operand", tokenTypeString(t))
|
||||
case RPAREN:
|
||||
message = "unexpected closing parenthesis"
|
||||
case RBRACE:
|
||||
message = "unexpected closing brace"
|
||||
case EOF:
|
||||
message = "unexpected end of input"
|
||||
default:
|
||||
@ -350,6 +423,12 @@ func tokenTypeString(t TokenType) string {
|
||||
return "("
|
||||
case RPAREN:
|
||||
return ")"
|
||||
case LBRACE:
|
||||
return "{"
|
||||
case RBRACE:
|
||||
return "}"
|
||||
case COMMA:
|
||||
return ","
|
||||
case VAR:
|
||||
return "var"
|
||||
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) {
|
||||
tests := []struct {
|
||||
input string
|
||||
@ -162,6 +275,9 @@ func TestParsingErrors(t *testing.T) {
|
||||
{"1 +", "expected expression after operator '+'", 1, 3},
|
||||
{"@", "unexpected token '@'", 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 {
|
||||
@ -171,7 +287,7 @@ func TestParsingErrors(t *testing.T) {
|
||||
|
||||
// Decide parsing strategy based on the type of error we're testing
|
||||
switch tt.input {
|
||||
case "(1 + 2", "+ 5", "1 +":
|
||||
case "(1 + 2", "+ 5", "1 +", "{1, 2", "{a =", "{a = 1,":
|
||||
// These are expression-level errors
|
||||
p.parseExpression(LOWEST)
|
||||
default:
|
||||
|
@ -22,6 +22,9 @@ const (
|
||||
// Delimiters
|
||||
LPAREN // (
|
||||
RPAREN // )
|
||||
LBRACE // {
|
||||
RBRACE // }
|
||||
COMMA // ,
|
||||
|
||||
// Keywords
|
||||
VAR
|
||||
|
Loading…
x
Reference in New Issue
Block a user