add table access

This commit is contained in:
Sky Johnson 2025-06-10 10:25:56 -05:00
parent b2a1b7a79b
commit 4b5faae944
7 changed files with 337 additions and 36 deletions

View File

@ -34,7 +34,7 @@ func (p *Program) String() string {
// AssignStatement represents variable assignment
type AssignStatement struct {
Name *Identifier
Name Expression // Changed from *Identifier to Expression for member access
Value Expression
}
@ -157,6 +157,28 @@ func (ie *InfixExpression) String() string {
return fmt.Sprintf("(%s %s %s)", ie.Left.String(), ie.Operator, ie.Right.String())
}
// IndexExpression represents table[key] access
type IndexExpression struct {
Left Expression
Index Expression
}
func (ie *IndexExpression) expressionNode() {}
func (ie *IndexExpression) String() string {
return fmt.Sprintf("%s[%s]", ie.Left.String(), ie.Index.String())
}
// DotExpression represents table.key access
type DotExpression struct {
Left Expression
Key string
}
func (de *DotExpression) expressionNode() {}
func (de *DotExpression) String() string {
return fmt.Sprintf("%s.%s", de.Left.String(), de.Key)
}
// TablePair represents a key-value pair in a table
type TablePair struct {
Key Expression // nil for array-style elements

View File

@ -218,6 +218,8 @@ func (l *Lexer) NextToken() Token {
tok = Token{Type: STAR, Literal: string(l.ch), Line: l.line, Column: l.column}
case '/':
tok = Token{Type: SLASH, Literal: string(l.ch), Line: l.line, Column: l.column}
case '.':
tok = Token{Type: DOT, Literal: string(l.ch), Line: l.line, Column: l.column}
case '(':
tok = Token{Type: LPAREN, Literal: string(l.ch), Line: l.line, Column: l.column}
case ')':
@ -226,18 +228,20 @@ func (l *Lexer) NextToken() Token {
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()
case '[':
if l.peekChar() == '[' {
tok.Type = STRING
tok.Literal = l.readMultilineString()
} else {
tok = Token{Type: ILLEGAL, Literal: string(l.ch), Line: l.line, Column: l.column}
tok = Token{Type: LBRACKET, Literal: string(l.ch), Line: l.line, Column: l.column}
}
case ']':
tok = Token{Type: RBRACKET, 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()
case 0:
tok.Literal = ""
tok.Type = EOF

View File

@ -54,6 +54,8 @@ func NewParser(lexer *Lexer) *Parser {
p.registerInfix(MINUS, p.parseInfixExpression)
p.registerInfix(SLASH, p.parseInfixExpression)
p.registerInfix(STAR, p.parseInfixExpression)
p.registerInfix(DOT, p.parseDotExpression)
p.registerInfix(LBRACKET, p.parseIndexExpression)
// Read two tokens, so curToken and peekToken are both set
p.nextToken()
@ -98,11 +100,8 @@ func (p *Parser) ParseProgram() *Program {
func (p *Parser) parseStatement() Statement {
switch p.curToken.Type {
case IDENT:
if p.peekTokenIs(ASSIGN) {
return p.parseAssignStatement()
}
p.addError("unexpected identifier, expected assignment or declaration")
return nil
// Try to parse as assignment (handles both simple and member access)
return p.parseAssignStatement()
case IF:
return p.parseIfStatement()
case ECHO:
@ -125,12 +124,27 @@ func (p *Parser) parseStatement() Statement {
func (p *Parser) parseAssignStatement() *AssignStatement {
stmt := &AssignStatement{}
if !p.curTokenIs(IDENT) {
p.addError("expected identifier for assignment")
// Parse left-hand side expression (can be identifier or member access)
stmt.Name = p.ParseExpression(LOWEST)
if stmt.Name == nil {
p.addError("expected expression for assignment left-hand side")
return nil
}
stmt.Name = &Identifier{Value: p.curToken.Literal}
// Check if next token is assignment operator
if !p.peekTokenIs(ASSIGN) {
p.addError("unexpected identifier, expected assignment or declaration")
return nil
}
// Validate assignment target
switch stmt.Name.(type) {
case *Identifier, *DotExpression, *IndexExpression:
// Valid assignment targets
default:
p.addError("invalid assignment target")
return nil
}
if !p.expectPeek(ASSIGN) {
return nil
@ -467,6 +481,38 @@ func (p *Parser) parseInfixExpression(left Expression) Expression {
return expression
}
func (p *Parser) parseDotExpression(left Expression) Expression {
if !p.expectPeek(IDENT) {
p.addError("expected identifier after '.'")
return nil
}
return &DotExpression{
Left: left,
Key: p.curToken.Literal,
}
}
func (p *Parser) parseIndexExpression(left Expression) Expression {
p.nextToken() // move past '['
index := p.ParseExpression(LOWEST)
if index == nil {
p.addError("expected expression inside brackets")
return nil
}
if !p.expectPeek(RBRACKET) {
p.addError("expected ']' after index expression")
return nil
}
return &IndexExpression{
Left: left,
Index: index,
}
}
// Helper methods
func (p *Parser) curTokenIs(t TokenType) bool {
return p.curToken.Type == t
@ -517,6 +563,8 @@ func (p *Parser) noPrefixParseFnError(t TokenType) {
message = "unexpected closing parenthesis"
case RBRACE:
message = "unexpected closing brace"
case RBRACKET:
message = "unexpected closing bracket"
case EOF:
message = "unexpected end of input"
default:
@ -582,6 +630,8 @@ func tokenTypeString(t TokenType) string {
return "*"
case SLASH:
return "/"
case DOT:
return "."
case LPAREN:
return "("
case RPAREN:
@ -590,6 +640,10 @@ func tokenTypeString(t TokenType) string {
return "{"
case RBRACE:
return "}"
case LBRACKET:
return "["
case RBRACKET:
return "]"
case COMMA:
return ","
case VAR:

View File

@ -36,8 +36,14 @@ func TestAssignStatements(t *testing.T) {
t.Fatalf("expected AssignStatement, got %T", program.Statements[0])
}
if stmt.Name.Value != tt.expectedIdentifier {
t.Errorf("expected identifier %s, got %s", tt.expectedIdentifier, stmt.Name.Value)
// Check that Name is an Identifier
ident, ok := stmt.Name.(*parser.Identifier)
if !ok {
t.Fatalf("expected Identifier for Name, got %T", stmt.Name)
}
if ident.Value != tt.expectedIdentifier {
t.Errorf("expected identifier %s, got %s", tt.expectedIdentifier, ident.Value)
}
if tt.isExpression {
@ -60,6 +66,74 @@ func TestAssignStatements(t *testing.T) {
}
}
func TestMemberAccessAssignment(t *testing.T) {
tests := []struct {
input string
expected string
description string
}{
{"table.key = 42", "table.key = 42.00", "dot notation assignment"},
{"arr[1] = \"hello\"", "arr[1.00] = \"hello\"", "bracket notation assignment"},
{"obj.nested.deep = true", "obj.nested.deep = true", "chained dot assignment"},
{"matrix[1][2] = 3.14", "matrix[1.00][2.00] = 3.14", "chained bracket assignment"},
{"data[\"key\"].value = nil", "data[\"key\"].value = nil", "mixed access assignment"},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
l := parser.NewLexer(tt.input)
p := parser.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].(*parser.AssignStatement)
if !ok {
t.Fatalf("expected AssignStatement, got %T", program.Statements[0])
}
if stmt.String() != tt.expected {
t.Errorf("expected %s, got %s", tt.expected, stmt.String())
}
})
}
}
func TestMemberAccessExpressions(t *testing.T) {
tests := []struct {
input string
expected string
description string
}{
{"echo table.key", "echo table.key", "dot access"},
{"echo arr[1]", "echo arr[1.00]", "bracket access"},
{"echo obj.nested.deep", "echo obj.nested.deep", "chained dot access"},
{"echo matrix[1][2]", "echo matrix[1.00][2.00]", "chained bracket access"},
{"echo data[\"key\"].value", "echo data[\"key\"].value", "mixed access"},
{"echo table.key + arr[0]", "echo (table.key + arr[0.00])", "member access in expression"},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
l := parser.NewLexer(tt.input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 1 {
t.Fatalf("expected 1 statement, got %d", len(program.Statements))
}
if program.Statements[0].String() != tt.expected {
t.Errorf("expected %s, got %s", tt.expected, program.Statements[0].String())
}
})
}
}
func TestTableAssignments(t *testing.T) {
tests := []struct {
input string
@ -89,8 +163,14 @@ func TestTableAssignments(t *testing.T) {
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)
// Check that Name is an Identifier
ident, ok := stmt.Name.(*parser.Identifier)
if !ok {
t.Fatalf("expected Identifier for Name, got %T", stmt.Name)
}
if ident.Value != tt.identifier {
t.Errorf("expected identifier %s, got %s", tt.identifier, ident.Value)
}
table, ok := stmt.Value.(*parser.TableLiteral)

View File

@ -37,8 +37,14 @@ end`
t.Fatalf("expected AssignStatement in body, got %T", stmt.Body[0])
}
if bodyStmt.Name.Value != "x" {
t.Errorf("expected identifier 'x', got %s", bodyStmt.Name.Value)
// Check that Name is an Identifier
ident, ok := bodyStmt.Name.(*parser.Identifier)
if !ok {
t.Fatalf("expected Identifier for Name, got %T", bodyStmt.Name)
}
if ident.Value != "x" {
t.Errorf("expected identifier 'x', got %s", ident.Value)
}
}
@ -78,8 +84,14 @@ end`
t.Fatalf("expected AssignStatement in else, got %T", stmt.Else[0])
}
if elseStmt.Name.Value != "x" {
t.Errorf("expected identifier 'x', got %s", elseStmt.Name.Value)
// Check that Name is an Identifier
ident, ok := elseStmt.Name.(*parser.Identifier)
if !ok {
t.Fatalf("expected Identifier for Name, got %T", elseStmt.Name)
}
if ident.Value != "x" {
t.Errorf("expected identifier 'x', got %s", ident.Value)
}
}
@ -122,6 +134,63 @@ end`
}
}
func TestConditionalWithMemberAccess(t *testing.T) {
input := `if table.flag then
arr[1] = "updated"
obj.nested.count = obj.nested.count + 1
end`
l := parser.NewLexer(input)
p := parser.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].(*parser.IfStatement)
if !ok {
t.Fatalf("expected IfStatement, got %T", program.Statements[0])
}
// Check condition is dot expression
dotExpr, ok := stmt.Condition.(*parser.DotExpression)
if !ok {
t.Fatalf("expected DotExpression condition, got %T", stmt.Condition)
}
if dotExpr.Key != "flag" {
t.Errorf("expected key 'flag', got %s", dotExpr.Key)
}
if len(stmt.Body) != 2 {
t.Fatalf("expected 2 body statements, got %d", len(stmt.Body))
}
// First assignment: arr[1] = "updated"
assign1, ok := stmt.Body[0].(*parser.AssignStatement)
if !ok {
t.Fatalf("expected AssignStatement, got %T", stmt.Body[0])
}
_, ok = assign1.Name.(*parser.IndexExpression)
if !ok {
t.Fatalf("expected IndexExpression for assignment target, got %T", assign1.Name)
}
// Second assignment: obj.nested.count = obj.nested.count + 1
assign2, ok := stmt.Body[1].(*parser.AssignStatement)
if !ok {
t.Fatalf("expected AssignStatement, got %T", stmt.Body[1])
}
_, ok = assign2.Name.(*parser.DotExpression)
if !ok {
t.Fatalf("expected DotExpression for assignment target, got %T", assign2.Name)
}
}
func TestConditionalExpressions(t *testing.T) {
input := `if 1 + 2 then
x = 3 * 4

View File

@ -27,8 +27,13 @@ z = true + false`
t.Fatalf("statement %d: expected AssignStatement, got %T", i, program.Statements[i])
}
if stmt.Name.Value != expectedIdent {
t.Errorf("statement %d: expected identifier %s, got %s", i, expectedIdent, stmt.Name.Value)
ident, ok := stmt.Name.(*parser.Identifier)
if !ok {
t.Fatalf("statement %d: expected Identifier for Name, got %T", i, stmt.Name)
}
if ident.Value != expectedIdent {
t.Errorf("statement %d: expected identifier %s, got %s", i, expectedIdent, ident.Value)
}
}
}
@ -57,8 +62,12 @@ arr = {a = 1, b = 2}`
if !ok {
t.Fatalf("statement 0: expected AssignStatement, got %T", program.Statements[0])
}
if stmt1.Name.Value != "x" {
t.Errorf("expected identifier 'x', got %s", stmt1.Name.Value)
ident1, ok := stmt1.Name.(*parser.Identifier)
if !ok {
t.Fatalf("expected Identifier for Name, got %T", stmt1.Name)
}
if ident1.Value != "x" {
t.Errorf("expected identifier 'x', got %s", ident1.Value)
}
// Second statement: if statement
@ -84,6 +93,63 @@ arr = {a = 1, b = 2}`
}
}
func TestMemberAccessProgram(t *testing.T) {
input := `table = {key = "value", nested = {inner = 42}}
table.key = "new value"
table["key"] = "bracket syntax"
table.nested.inner = 100
echo table[table.key]`
l := parser.NewLexer(input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 5 {
t.Fatalf("expected 5 statements, got %d", len(program.Statements))
}
// Second statement: dot assignment
stmt2, ok := program.Statements[1].(*parser.AssignStatement)
if !ok {
t.Fatalf("statement 1: expected AssignStatement, got %T", program.Statements[1])
}
_, ok = stmt2.Name.(*parser.DotExpression)
if !ok {
t.Fatalf("expected DotExpression for assignment target, got %T", stmt2.Name)
}
// Third statement: bracket assignment
stmt3, ok := program.Statements[2].(*parser.AssignStatement)
if !ok {
t.Fatalf("statement 2: expected AssignStatement, got %T", program.Statements[2])
}
_, ok = stmt3.Name.(*parser.IndexExpression)
if !ok {
t.Fatalf("expected IndexExpression for assignment target, got %T", stmt3.Name)
}
// Fourth statement: chained dot assignment
stmt4, ok := program.Statements[3].(*parser.AssignStatement)
if !ok {
t.Fatalf("statement 3: expected AssignStatement, got %T", program.Statements[3])
}
_, ok = stmt4.Name.(*parser.DotExpression)
if !ok {
t.Fatalf("expected DotExpression for assignment target, got %T", stmt4.Name)
}
// Fifth statement: echo with nested access
stmt5, ok := program.Statements[4].(*parser.EchoStatement)
if !ok {
t.Fatalf("statement 4: expected EchoStatement, got %T", program.Statements[4])
}
_, ok = stmt5.Value.(*parser.IndexExpression)
if !ok {
t.Fatalf("expected IndexExpression in echo, got %T", stmt5.Value)
}
}
func TestNestedConditionals(t *testing.T) {
input := `if a then
if b then

View File

@ -18,13 +18,16 @@ const (
MINUS // -
STAR // *
SLASH // /
DOT // .
// Delimiters
LPAREN // (
RPAREN // )
LBRACE // {
RBRACE // }
COMMA // ,
LPAREN // (
RPAREN // )
LBRACE // {
RBRACE // }
LBRACKET // [
RBRACKET // ]
COMMA // ,
// Keywords
VAR
@ -56,16 +59,19 @@ const (
LOWEST
SUM // +
PRODUCT // *
MEMBER // table[key], table.key
PREFIX // -x, !x
CALL // function()
)
// precedences maps token types to their precedence levels
var precedences = map[TokenType]Precedence{
PLUS: SUM,
MINUS: SUM,
SLASH: PRODUCT,
STAR: PRODUCT,
PLUS: SUM,
MINUS: SUM,
SLASH: PRODUCT,
STAR: PRODUCT,
DOT: MEMBER,
LBRACKET: MEMBER,
}
// lookupIdent checks if an identifier is a keyword