add table access
This commit is contained in:
parent
b2a1b7a79b
commit
4b5faae944
@ -34,7 +34,7 @@ func (p *Program) String() string {
|
|||||||
|
|
||||||
// AssignStatement represents variable assignment
|
// AssignStatement represents variable assignment
|
||||||
type AssignStatement struct {
|
type AssignStatement struct {
|
||||||
Name *Identifier
|
Name Expression // Changed from *Identifier to Expression for member access
|
||||||
Value Expression
|
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())
|
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
|
// TablePair represents a key-value pair in a table
|
||||||
type TablePair struct {
|
type TablePair struct {
|
||||||
Key Expression // nil for array-style elements
|
Key Expression // nil for array-style elements
|
||||||
|
@ -218,6 +218,8 @@ func (l *Lexer) NextToken() Token {
|
|||||||
tok = Token{Type: STAR, Literal: string(l.ch), Line: l.line, Column: l.column}
|
tok = Token{Type: STAR, Literal: string(l.ch), Line: l.line, Column: l.column}
|
||||||
case '/':
|
case '/':
|
||||||
tok = Token{Type: SLASH, Literal: string(l.ch), Line: l.line, Column: l.column}
|
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 '(':
|
case '(':
|
||||||
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 ')':
|
||||||
@ -226,18 +228,20 @@ func (l *Lexer) NextToken() Token {
|
|||||||
tok = Token{Type: LBRACE, Literal: string(l.ch), Line: l.line, Column: l.column}
|
tok = Token{Type: LBRACE, Literal: string(l.ch), Line: l.line, Column: l.column}
|
||||||
case '}':
|
case '}':
|
||||||
tok = Token{Type: RBRACE, Literal: string(l.ch), Line: l.line, Column: l.column}
|
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 '[':
|
case '[':
|
||||||
if l.peekChar() == '[' {
|
if l.peekChar() == '[' {
|
||||||
tok.Type = STRING
|
tok.Type = STRING
|
||||||
tok.Literal = l.readMultilineString()
|
tok.Literal = l.readMultilineString()
|
||||||
} else {
|
} 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:
|
case 0:
|
||||||
tok.Literal = ""
|
tok.Literal = ""
|
||||||
tok.Type = EOF
|
tok.Type = EOF
|
||||||
|
@ -54,6 +54,8 @@ func NewParser(lexer *Lexer) *Parser {
|
|||||||
p.registerInfix(MINUS, p.parseInfixExpression)
|
p.registerInfix(MINUS, p.parseInfixExpression)
|
||||||
p.registerInfix(SLASH, p.parseInfixExpression)
|
p.registerInfix(SLASH, p.parseInfixExpression)
|
||||||
p.registerInfix(STAR, 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
|
// Read two tokens, so curToken and peekToken are both set
|
||||||
p.nextToken()
|
p.nextToken()
|
||||||
@ -98,11 +100,8 @@ func (p *Parser) ParseProgram() *Program {
|
|||||||
func (p *Parser) parseStatement() Statement {
|
func (p *Parser) parseStatement() Statement {
|
||||||
switch p.curToken.Type {
|
switch p.curToken.Type {
|
||||||
case IDENT:
|
case IDENT:
|
||||||
if p.peekTokenIs(ASSIGN) {
|
// Try to parse as assignment (handles both simple and member access)
|
||||||
return p.parseAssignStatement()
|
return p.parseAssignStatement()
|
||||||
}
|
|
||||||
p.addError("unexpected identifier, expected assignment or declaration")
|
|
||||||
return nil
|
|
||||||
case IF:
|
case IF:
|
||||||
return p.parseIfStatement()
|
return p.parseIfStatement()
|
||||||
case ECHO:
|
case ECHO:
|
||||||
@ -125,12 +124,27 @@ func (p *Parser) parseStatement() Statement {
|
|||||||
func (p *Parser) parseAssignStatement() *AssignStatement {
|
func (p *Parser) parseAssignStatement() *AssignStatement {
|
||||||
stmt := &AssignStatement{}
|
stmt := &AssignStatement{}
|
||||||
|
|
||||||
if !p.curTokenIs(IDENT) {
|
// Parse left-hand side expression (can be identifier or member access)
|
||||||
p.addError("expected identifier for assignment")
|
stmt.Name = p.ParseExpression(LOWEST)
|
||||||
|
if stmt.Name == nil {
|
||||||
|
p.addError("expected expression for assignment left-hand side")
|
||||||
return nil
|
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) {
|
if !p.expectPeek(ASSIGN) {
|
||||||
return nil
|
return nil
|
||||||
@ -467,6 +481,38 @@ func (p *Parser) parseInfixExpression(left Expression) Expression {
|
|||||||
return 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
|
// Helper methods
|
||||||
func (p *Parser) curTokenIs(t TokenType) bool {
|
func (p *Parser) curTokenIs(t TokenType) bool {
|
||||||
return p.curToken.Type == t
|
return p.curToken.Type == t
|
||||||
@ -517,6 +563,8 @@ func (p *Parser) noPrefixParseFnError(t TokenType) {
|
|||||||
message = "unexpected closing parenthesis"
|
message = "unexpected closing parenthesis"
|
||||||
case RBRACE:
|
case RBRACE:
|
||||||
message = "unexpected closing brace"
|
message = "unexpected closing brace"
|
||||||
|
case RBRACKET:
|
||||||
|
message = "unexpected closing bracket"
|
||||||
case EOF:
|
case EOF:
|
||||||
message = "unexpected end of input"
|
message = "unexpected end of input"
|
||||||
default:
|
default:
|
||||||
@ -582,6 +630,8 @@ func tokenTypeString(t TokenType) string {
|
|||||||
return "*"
|
return "*"
|
||||||
case SLASH:
|
case SLASH:
|
||||||
return "/"
|
return "/"
|
||||||
|
case DOT:
|
||||||
|
return "."
|
||||||
case LPAREN:
|
case LPAREN:
|
||||||
return "("
|
return "("
|
||||||
case RPAREN:
|
case RPAREN:
|
||||||
@ -590,6 +640,10 @@ func tokenTypeString(t TokenType) string {
|
|||||||
return "{"
|
return "{"
|
||||||
case RBRACE:
|
case RBRACE:
|
||||||
return "}"
|
return "}"
|
||||||
|
case LBRACKET:
|
||||||
|
return "["
|
||||||
|
case RBRACKET:
|
||||||
|
return "]"
|
||||||
case COMMA:
|
case COMMA:
|
||||||
return ","
|
return ","
|
||||||
case VAR:
|
case VAR:
|
||||||
|
@ -36,8 +36,14 @@ func TestAssignStatements(t *testing.T) {
|
|||||||
t.Fatalf("expected AssignStatement, got %T", program.Statements[0])
|
t.Fatalf("expected AssignStatement, got %T", program.Statements[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
if stmt.Name.Value != tt.expectedIdentifier {
|
// Check that Name is an Identifier
|
||||||
t.Errorf("expected identifier %s, got %s", tt.expectedIdentifier, stmt.Name.Value)
|
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 {
|
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) {
|
func TestTableAssignments(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
input string
|
input string
|
||||||
@ -89,8 +163,14 @@ func TestTableAssignments(t *testing.T) {
|
|||||||
t.Fatalf("expected AssignStatement, got %T", program.Statements[0])
|
t.Fatalf("expected AssignStatement, got %T", program.Statements[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
if stmt.Name.Value != tt.identifier {
|
// Check that Name is an Identifier
|
||||||
t.Errorf("expected identifier %s, got %s", tt.identifier, stmt.Name.Value)
|
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)
|
table, ok := stmt.Value.(*parser.TableLiteral)
|
||||||
|
@ -37,8 +37,14 @@ end`
|
|||||||
t.Fatalf("expected AssignStatement in body, got %T", stmt.Body[0])
|
t.Fatalf("expected AssignStatement in body, got %T", stmt.Body[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
if bodyStmt.Name.Value != "x" {
|
// Check that Name is an Identifier
|
||||||
t.Errorf("expected identifier 'x', got %s", bodyStmt.Name.Value)
|
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])
|
t.Fatalf("expected AssignStatement in else, got %T", stmt.Else[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
if elseStmt.Name.Value != "x" {
|
// Check that Name is an Identifier
|
||||||
t.Errorf("expected identifier 'x', got %s", elseStmt.Name.Value)
|
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) {
|
func TestConditionalExpressions(t *testing.T) {
|
||||||
input := `if 1 + 2 then
|
input := `if 1 + 2 then
|
||||||
x = 3 * 4
|
x = 3 * 4
|
||||||
|
@ -27,8 +27,13 @@ z = true + false`
|
|||||||
t.Fatalf("statement %d: expected AssignStatement, got %T", i, program.Statements[i])
|
t.Fatalf("statement %d: expected AssignStatement, got %T", i, program.Statements[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
if stmt.Name.Value != expectedIdent {
|
ident, ok := stmt.Name.(*parser.Identifier)
|
||||||
t.Errorf("statement %d: expected identifier %s, got %s", i, expectedIdent, stmt.Name.Value)
|
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 {
|
if !ok {
|
||||||
t.Fatalf("statement 0: expected AssignStatement, got %T", program.Statements[0])
|
t.Fatalf("statement 0: expected AssignStatement, got %T", program.Statements[0])
|
||||||
}
|
}
|
||||||
if stmt1.Name.Value != "x" {
|
ident1, ok := stmt1.Name.(*parser.Identifier)
|
||||||
t.Errorf("expected identifier 'x', got %s", stmt1.Name.Value)
|
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
|
// 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) {
|
func TestNestedConditionals(t *testing.T) {
|
||||||
input := `if a then
|
input := `if a then
|
||||||
if b then
|
if b then
|
||||||
|
@ -18,12 +18,15 @@ const (
|
|||||||
MINUS // -
|
MINUS // -
|
||||||
STAR // *
|
STAR // *
|
||||||
SLASH // /
|
SLASH // /
|
||||||
|
DOT // .
|
||||||
|
|
||||||
// Delimiters
|
// Delimiters
|
||||||
LPAREN // (
|
LPAREN // (
|
||||||
RPAREN // )
|
RPAREN // )
|
||||||
LBRACE // {
|
LBRACE // {
|
||||||
RBRACE // }
|
RBRACE // }
|
||||||
|
LBRACKET // [
|
||||||
|
RBRACKET // ]
|
||||||
COMMA // ,
|
COMMA // ,
|
||||||
|
|
||||||
// Keywords
|
// Keywords
|
||||||
@ -56,6 +59,7 @@ const (
|
|||||||
LOWEST
|
LOWEST
|
||||||
SUM // +
|
SUM // +
|
||||||
PRODUCT // *
|
PRODUCT // *
|
||||||
|
MEMBER // table[key], table.key
|
||||||
PREFIX // -x, !x
|
PREFIX // -x, !x
|
||||||
CALL // function()
|
CALL // function()
|
||||||
)
|
)
|
||||||
@ -66,6 +70,8 @@ var precedences = map[TokenType]Precedence{
|
|||||||
MINUS: SUM,
|
MINUS: SUM,
|
||||||
SLASH: PRODUCT,
|
SLASH: PRODUCT,
|
||||||
STAR: PRODUCT,
|
STAR: PRODUCT,
|
||||||
|
DOT: MEMBER,
|
||||||
|
LBRACKET: MEMBER,
|
||||||
}
|
}
|
||||||
|
|
||||||
// lookupIdent checks if an identifier is a keyword
|
// lookupIdent checks if an identifier is a keyword
|
||||||
|
Loading…
x
Reference in New Issue
Block a user