Skip to content

Commit

Permalink
refactoring code for resolving conflicts
Browse files Browse the repository at this point in the history
  • Loading branch information
ckganesan committed Feb 27, 2024
1 parent 2347a4e commit 7df20d6
Show file tree
Hide file tree
Showing 10 changed files with 279 additions and 113 deletions.
6 changes: 6 additions & 0 deletions checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,12 @@ func (v *checker) MemberNode(node *ast.MemberNode) (reflect.Type, info) {
}
return base.Elem(), info{}

case reflect.String:
if !isInteger(prop) && !isAny(prop) {
return v.error(node.Property, "string elements can only be selected using an integer (got %v)", prop)
}
return stringType, info{}

case reflect.Struct:
if name, ok := node.Property.(*ast.StringNode); ok {
propertyName := name.Value
Expand Down
6 changes: 5 additions & 1 deletion compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,11 @@ func (c *compiler) MemberNode(node *ast.MemberNode) {

if op == OpFetch {
c.compile(node.Property)
c.emit(OpFetch)
if node.Optional {
c.emit(OpOptionalFetch)
} else {
c.emit(op)
}
} else {
c.emitLocation(node.Location(), op, c.addConstant(
&runtime.Field{Index: index, Path: path},
Expand Down
82 changes: 79 additions & 3 deletions compiler/compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,71 @@ func TestCompile_optimizes_jumps(t *testing.T) {
{vm.OpNil, 0},
},
},
{
`let m = {"a": {"b": {"c": 1}}}; m.a.b.c ?? 'nil coalescing'`,
[]op{
{vm.OpPush, 0},
{vm.OpPush, 1},
{vm.OpPush, 2},
{vm.OpPush, 3},
{vm.OpPush, 3},
{vm.OpMap, 0},
{vm.OpPush, 3},
{vm.OpMap, 0},
{vm.OpPush, 3},
{vm.OpMap, 0},
{vm.OpStore, 0},
{vm.OpLoadVar, 0},
{vm.OpJumpIfNil, 8},
{vm.OpPush, 0},
{vm.OpOptionalFetch, 0},
{vm.OpJumpIfNil, 5},
{vm.OpPush, 1},
{vm.OpOptionalFetch, 0},
{vm.OpJumpIfNil, 2},
{vm.OpPush, 2},
{vm.OpOptionalFetch, 0},
{vm.OpDeref, 0},
{vm.OpJumpIfNotNil, 2},
{vm.OpPop, 0},
{vm.OpPush, 4},
},
},
{
`let m = [{"a": {"b": {"c": 1}}}]; m[5].a.b.c ?? 'nil coalescing'`,
[]op{
{vm.OpPush, 0},
{vm.OpPush, 1},
{vm.OpPush, 2},
{vm.OpPush, 3},
{vm.OpPush, 3},
{vm.OpMap, 0},
{vm.OpPush, 3},
{vm.OpMap, 0},
{vm.OpPush, 3},
{vm.OpMap, 0},
{vm.OpPush, 3},
{vm.OpArray, 0},
{vm.OpStore, 0},
{vm.OpLoadVar, 0},
{vm.OpJumpIfNil, 11},
{vm.OpPush, 4},
{vm.OpOptionalFetch, 0},
{vm.OpJumpIfNil, 8},
{vm.OpPush, 0},
{vm.OpOptionalFetch, 0},
{vm.OpJumpIfNil, 5},
{vm.OpPush, 1},
{vm.OpOptionalFetch, 0},
{vm.OpJumpIfNil, 2},
{vm.OpPush, 2},
{vm.OpOptionalFetch, 0},
{vm.OpDeref, 0},
{vm.OpJumpIfNotNil, 2},
{vm.OpPop, 0},
{vm.OpPush, 5},
},
},
{
`let m = {"a": {"b": {"c": 1}}}; m?.a?.b?.c`,
[]op{
Expand All @@ -532,13 +597,24 @@ func TestCompile_optimizes_jumps(t *testing.T) {
{vm.OpLoadVar, 0},
{vm.OpJumpIfNil, 8},
{vm.OpPush, 0},
{vm.OpFetch, 0},
{vm.OpOptionalFetch, 0},
{vm.OpJumpIfNil, 5},
{vm.OpPush, 1},
{vm.OpFetch, 0},
{vm.OpOptionalFetch, 0},
{vm.OpJumpIfNil, 2},
{vm.OpPush, 2},
{vm.OpFetch, 0},
{vm.OpOptionalFetch, 0},
},
},
{
`let m = [1, 2, 3]; m?.[5]`,
[]op{
{vm.OpPush, 0},
{vm.OpStore, 0},
{vm.OpLoadVar, 0},
{vm.OpJumpIfNil, 2},
{vm.OpPush, 1},
{vm.OpOptionalFetch, 0},
},
},
}
Expand Down
30 changes: 30 additions & 0 deletions expr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1273,6 +1273,18 @@ func TestExpr(t *testing.T) {
`1 < 2 < 3 == true`,
true,
},
{
`[1, 2, 3]?.[5]`,
nil,
},
{
`'string'?.[5]`,
"g",
},
{
`'string'?.[7]`,
nil,
},
}

for _, tt := range tests {
Expand Down Expand Up @@ -1967,6 +1979,24 @@ func TestRun_NilCoalescingOperator(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "default", out)
})

t.Run("default without chain", func(t *testing.T) {
p, err := expr.Compile(`foo.foo.bar ?? "default"`, expr.Env(env))
assert.NoError(t, err)

out, err := expr.Run(p, map[string]any{})
assert.NoError(t, err)
assert.Equal(t, "default", out)
})

t.Run("array default without chain", func(t *testing.T) {
p, err := expr.Compile(`foo.foo[10].bar ?? "default"`, expr.Env(env))
assert.NoError(t, err)

out, err := expr.Run(p, map[string]any{})
assert.NoError(t, err)
assert.Equal(t, "default", out)
})
}

func TestEval_nil_in_maps(t *testing.T) {
Expand Down
126 changes: 67 additions & 59 deletions parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ func (p *parser) parseExpression(precedence int) Node {
nodeRight = p.parseExpression(op.Precedence)
}

if opToken.Value == "??" {
nodeLeft = p.flattenChainedOptionals(nodeLeft)
}

nodeLeft = &BinaryNode{
Operator: opToken.Value,
Left: nodeLeft,
Expand Down Expand Up @@ -579,65 +583,66 @@ end:
}

func (p *parser) parsePostfixExpression(node Node) Node {
postfixToken := p.current
for (postfixToken.Is(Operator) || postfixToken.Is(Bracket)) && p.err == nil {
optional := postfixToken.Value == "?."
parseToken:
if postfixToken.Value == "." || postfixToken.Value == "?." {
p.next()

propertyToken := p.current
if optional && propertyToken.Is(Bracket, "[") {
postfixToken = propertyToken
goto parseToken
}
for postfixToken := p.current; (postfixToken.Is(Operator) || postfixToken.Is(Bracket)) && p.err == nil; postfixToken = p.current {
switch postfixToken.Value {
case ".", "?.":
p.next()

if propertyToken.Kind != Identifier &&
// Operators like "not" and "matches" are valid methods or property names.
(propertyToken.Kind != Operator || !utils.IsValidIdentifier(propertyToken.Value)) {
p.error("expected name")
}

property := &StringNode{Value: propertyToken.Value}
property.SetLocation(propertyToken.Location)

chainNode, isChain := node.(*ChainNode)
optional := postfixToken.Value == "?."

if isChain {
node = chainNode.Node
}
propertyToken := p.current
isOptional := postfixToken.Value == "?."

memberNode := &MemberNode{
Node: node,
Property: property,
Optional: optional,
}
memberNode.SetLocation(propertyToken.Location)

if p.current.Is(Bracket, "(") {
memberNode.Method = true
node = &CallNode{
Callee: memberNode,
Arguments: p.parseArguments([]Node{}),
if isOptional && p.current.Is(Bracket, "[") {
p.next()
node = &MemberNode{
Node: node,
Property: p.parseExpression(0),
Optional: true,
}
node.SetLocation(propertyToken.Location)
p.expect(Bracket, "]")
} else {
node = memberNode
}
p.next()

if propertyToken.Kind != Identifier &&
// Operators like "not" and "matches" are valid methods or property names.
(propertyToken.Kind != Operator || !utils.IsValidIdentifier(propertyToken.Value)) {
p.error("expected name")
}

if isChain || optional {
property := &StringNode{Value: propertyToken.Value}
property.SetLocation(propertyToken.Location)

memberNode := &MemberNode{
Node: node,
Property: property,
Optional: isOptional,
}
memberNode.SetLocation(propertyToken.Location)

if p.current.Is(Bracket, "(") {
memberNode.Method = true
node = &CallNode{
Callee: memberNode,
Arguments: p.parseArguments([]Node{}),
}
node.SetLocation(propertyToken.Location)
} else {
node = memberNode
}
}
if isChain || isOptional {
node = &ChainNode{Node: node}
}

} else if postfixToken.Value == "[" {
case "[":
p.next()
var from, to Node

if p.current.Is(Operator, ":") { // slice without from [:1]
p.next()

var to Node
if !p.current.Is(Bracket, "]") { // slice without from and to [:]
to = p.parseExpression(0)
}
Expand All @@ -646,16 +651,13 @@ func (p *parser) parsePostfixExpression(node Node) Node {
Node: node,
To: to,
}
node.SetLocation(postfixToken.Location)
p.expect(Bracket, "]")

} else {

from = p.parseExpression(0)
from := p.parseExpression(0)

if p.current.Is(Operator, ":") {
p.next()

var to Node
if !p.current.Is(Bracket, "]") { // slice without to [1:]
to = p.parseExpression(0)
}
Expand All @@ -665,28 +667,20 @@ func (p *parser) parsePostfixExpression(node Node) Node {
From: from,
To: to,
}
node.SetLocation(postfixToken.Location)
p.expect(Bracket, "]")

} else {
// Slice operator [:] was not found,
// it should be just an index node.
node = &MemberNode{
Node: node,
Property: from,
Optional: optional,
}
node.SetLocation(postfixToken.Location)
if optional {
node = &ChainNode{Node: node}
}
p.expect(Bracket, "]")
}
}
} else {
break
node.SetLocation(postfixToken.Location)
p.expect(Bracket, "]")
default:
return node
}
postfixToken = p.current
}
return node
}
Expand Down Expand Up @@ -721,3 +715,17 @@ func (p *parser) parseComparison(left Node, token Token, precedence int) Node {
}
return rootNode
}

func (p *parser) flattenChainedOptionals(nodeLeft Node) Node {
switch node := nodeLeft.(type) {
case *MemberNode:
node.Optional = true
node.Node = p.flattenChainedOptionals(node.Node)
return &ChainNode{Node: node}
case *ChainNode:
node.Node = p.flattenChainedOptionals(node.Node)
return node
default:
return nodeLeft
}
}
19 changes: 19 additions & 0 deletions parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,25 @@ world`},
Right: &IdentifierNode{Value: "bar"}},
Right: &IdentifierNode{Value: "baz"}},
},
{
"foo.bar.baz ?? 'nil'",
&BinaryNode{Operator: "??",
Left: &ChainNode{
Node: &MemberNode{
Node: &ChainNode{
Node: &MemberNode{
Node: &IdentifierNode{Value: "foo"},
Property: &StringNode{Value: "bar"},
Optional: true,
},
},
Property: &StringNode{Value: "baz"},
Optional: true,
},
},
Right: &StringNode{Value: "nil"},
},
},
{
"foo ?? (bar || baz)",
&BinaryNode{Operator: "??",
Expand Down
1 change: 1 addition & 0 deletions vm/opcodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const (
OpLoadEnv
OpFetch
OpFetchField
OpOptionalFetch
OpMethod
OpTrue
OpFalse
Expand Down
Loading

0 comments on commit 7df20d6

Please sign in to comment.