tables/arrays

This commit is contained in:
Sky Johnson 2025-06-09 12:39:37 -05:00
parent a744c12baf
commit 2db5c3bfe5
5 changed files with 261 additions and 1 deletions

View File

@ -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
}

View File

@ -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()

View File

@ -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:

View File

@ -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:

View File

@ -22,6 +22,9 @@ const (
// Delimiters
LPAREN // (
RPAREN // )
LBRACE // {
RBRACE // }
COMMA // ,
// Keywords
VAR