Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions builtin/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,9 @@ var Builtins = []*Function{
return
}
}()
return runtime.Fetch(args[0], 0), nil

value, _ := runtime.Fetch(args[0], 0)
return value, nil
},
Validate: func(args []reflect.Type) (reflect.Type, error) {
if len(args) != 1 {
Expand All @@ -624,7 +626,9 @@ var Builtins = []*Function{
return
}
}()
return runtime.Fetch(args[0], -1), nil

value, _ := runtime.Fetch(args[0], -1)
return value, nil
},
Validate: func(args []reflect.Type) (reflect.Type, error) {
if len(args) != 1 {
Expand Down
6 changes: 3 additions & 3 deletions builtin/lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -548,13 +548,13 @@ func get(params ...any) (out any, err error) {
return nil, fmt.Errorf("invalid number of arguments (expected 2, got %d)", len(params))
}
from := params[0]
i := params[1]
v := reflect.ValueOf(from)

if from == nil {
return nil, nil
}

i := params[1]
v := reflect.ValueOf(from)

if v.Kind() == reflect.Invalid {
panic(fmt.Sprintf("cannot fetch %v from %T", i, from))
}
Expand Down
5 changes: 5 additions & 0 deletions compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ func Compile(tree *parser.Tree, config *conf.Config) (program *Program, err erro

c.compile(tree.Node)

nilSafe := false

if c.config != nil {
nilSafe = c.config.NilSafe

switch c.config.Expect {
case reflect.Int:
c.emit(OpCast, 0)
Expand Down Expand Up @@ -77,6 +81,7 @@ func Compile(tree *parser.Tree, config *conf.Config) (program *Program, err erro
c.functions,
c.debugInfo,
span,
nilSafe,
)
return
}
Expand Down
12 changes: 11 additions & 1 deletion conf/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ type Config struct {
// When enabled, the lexer treats `if`/`else` as identifiers and the parser
// will not parse `if` statements.
DisableIfOperator bool
// NilSafe enables nil-safe navigation for all expressions,
// allowing access to fields and methods on nil values without panicking.
NilSafe bool
}

// CreateNew creates new config with default values.
Expand Down Expand Up @@ -77,7 +80,14 @@ func (c *Config) ConstExpr(name string) {
if c.EnvObject == nil {
panic("no environment is specified for ConstExpr()")
}
fn := reflect.ValueOf(runtime.Fetch(c.EnvObject, name))

field, ok := runtime.Fetch(c.EnvObject, name)
if !ok {
panic(fmt.Errorf("cannot fetch %q in the environment", name))
}

fn := reflect.ValueOf(field)

if fn.Kind() != reflect.Func {
panic(fmt.Errorf("const expression %q must be a function", name))
}
Expand Down
6 changes: 6 additions & 0 deletions expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,12 @@ func MaxNodes(n uint) Option {
}
}

func NilSafe() Option {
return func(c *conf.Config) {
c.NilSafe = true
}
}

// Compile parses and compiles given input expression to bytecode program.
func Compile(input string, ops ...Option) (*vm.Program, error) {
config := conf.CreateNew()
Expand Down
4 changes: 4 additions & 0 deletions vm/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ type Program struct {
functions []Function
debugInfo map[string]string
span *Span

nilSafe bool
}

// NewProgram returns a new Program. It's used by the compiler.
Expand All @@ -42,6 +44,7 @@ func NewProgram(
functions []Function,
debugInfo map[string]string,
span *Span,
nilSafe bool,
) *Program {
return &Program{
source: source,
Expand All @@ -54,6 +57,7 @@ func NewProgram(
functions: functions,
debugInfo: debugInfo,
span: span,
nilSafe: nilSafe,
}
}

Expand Down
21 changes: 13 additions & 8 deletions vm/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ type fieldCacheKey struct {
f string
}

func Fetch(from, i any) any {
func Fetch(from, i any) (any, bool) {
if from == nil {
return nil, false
}

v := reflect.ValueOf(from)
if v.Kind() == reflect.Invalid {
panic(fmt.Sprintf("cannot fetch %v from %T", i, from))
Expand All @@ -29,7 +33,7 @@ func Fetch(from, i any) any {
if methodName, ok := i.(string); ok {
method := v.MethodByName(methodName)
if method.IsValid() {
return method.Interface()
return method.Interface(), true
}
}
}
Expand All @@ -52,7 +56,7 @@ func Fetch(from, i any) any {
}
value := v.Index(index)
if value.IsValid() {
return value.Interface()
return value.Interface(), true
}

case reflect.Map:
Expand All @@ -63,10 +67,10 @@ func Fetch(from, i any) any {
value = v.MapIndex(reflect.ValueOf(i))
}
if value.IsValid() {
return value.Interface()
return value.Interface(), true
} else {
elem := reflect.TypeOf(from).Elem()
return reflect.Zero(elem).Interface()
return reflect.Zero(elem).Interface(), true
}

case reflect.Struct:
Expand All @@ -77,7 +81,7 @@ func Fetch(from, i any) any {
f: fieldName,
}
if cv, ok := fieldCache.Load(key); ok {
return v.FieldByIndex(cv.([]int)).Interface()
return v.FieldByIndex(cv.([]int)).Interface(), true
}
field, ok := t.FieldByNameFunc(func(name string) bool {
field, _ := t.FieldByName(name)
Expand All @@ -94,11 +98,12 @@ func Fetch(from, i any) any {
value := v.FieldByIndex(field.Index)
if value.IsValid() {
fieldCache.Store(key, field.Index)
return value.Interface()
return value.Interface(), true
}
}
}
panic(fmt.Sprintf("cannot fetch %v from %T", i, from))

return nil, false
}

type Field struct {
Expand Down
17 changes: 15 additions & 2 deletions vm/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,12 @@ func (vm *VM) Run(program *Program, env any) (_ any, err error) {
vm.push(vm.Variables[arg])

case OpLoadConst:
vm.push(runtime.Fetch(env, program.Constants[arg]))
value, ok := runtime.Fetch(env, program.Constants[arg])
if !ok && !program.nilSafe {
panic(fmt.Sprintf("cannot fetch %v in the environment", program.Constants[arg]))
}

vm.push(value)

case OpLoadField:
vm.push(runtime.FetchField(env, program.Constants[arg].(*runtime.Field)))
Expand All @@ -139,7 +144,12 @@ func (vm *VM) Run(program *Program, env any) (_ any, err error) {
case OpFetch:
b := vm.pop()
a := vm.pop()
vm.push(runtime.Fetch(a, b))

value, ok := runtime.Fetch(a, b)
if !ok && !program.nilSafe {
panic(fmt.Sprintf("cannot fetch %v from %T", b, a))
}
vm.push(value)

case OpFetchField:
a := vm.pop()
Expand Down Expand Up @@ -609,6 +619,9 @@ func (vm *VM) Run(program *Program, env any) (_ any, err error) {
a := vm.pop()
s := vm.allocScope()
switch v := a.(type) {
case nil:
s.Len = 0
s.Anys = nil
case []int:
s.Ints = v
s.Len = len(v)
Expand Down
20 changes: 12 additions & 8 deletions vm/vm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -694,8 +694,9 @@ func TestVM_DirectCallOpcodes(t *testing.T) {
tt.bytecode,
tt.args,
tt.funcs,
nil, // debugInfo
nil, // span
nil, // debugInfo
nil, // span
false, // nilSafe
)
vm := &vm.VM{}
got, err := vm.Run(program, nil)
Expand Down Expand Up @@ -819,9 +820,10 @@ func TestVM_IndexAndCountOperations(t *testing.T) {
tt.consts,
tt.bytecode,
tt.args,
nil, // functions
nil, // debugInfo
nil, // span
nil, // functions
nil, // debugInfo
nil, // span
false, // nilSafe
)
vm := &vm.VM{}
got, err := vm.Run(program, nil)
Expand Down Expand Up @@ -1288,9 +1290,10 @@ func TestVM_DirectBasicOpcodes(t *testing.T) {
tt.consts,
tt.bytecode,
tt.args,
nil, // functions
nil, // debugInfo
nil, // span
nil, // functions
nil, // debugInfo
nil, // span
false, // nilSafe
)
vm := &vm.VM{}
got, err := vm.Run(program, tt.env)
Expand Down Expand Up @@ -1460,6 +1463,7 @@ func TestVM_OpJump_NegativeOffset(t *testing.T) {
nil,
nil,
nil,
false, // nilSafe
)

_, err := vm.Run(program, nil)
Expand Down