Skip to content

Commit 7c188ce

Browse files
committed
feat(static): add "static" package as a DCE-friendly alternative
The package is a drop-in replacement with two features removed: - functions in environment - Eval() method Fix #863.
1 parent 3a46b19 commit 7c188ce

10 files changed

Lines changed: 452 additions & 49 deletions

File tree

builtin/lib.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -560,11 +560,10 @@ func get(params ...any) (out any, err error) {
560560
}
561561

562562
// Methods can be defined on any type.
563-
if v.NumMethod() > 0 {
563+
if runtime.MethodByNameHook != nil && v.NumMethod() > 0 {
564564
if methodName, ok := i.(string); ok {
565-
method := v.MethodByName(methodName)
566-
if method.IsValid() {
567-
return method.Interface(), nil
565+
if m, ok := runtime.MethodByNameHook(v, methodName); ok {
566+
return m, nil
568567
}
569568
}
570569
}

checker/info.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ func MethodIndex(c *Cache, env Nature, node ast.Node) (bool, int, string) {
3737
}
3838
case *ast.MemberNode:
3939
if name, ok := n.Property.(*ast.StringNode); ok {
40-
base := n.Node.Type()
41-
if base != nil && base.Kind() != reflect.Interface {
42-
if m, ok := base.MethodByName(name.Value); ok {
43-
return true, m.Index, name.Value
40+
base := n.Node.Nature()
41+
if base != nil && base.Kind != reflect.Interface {
42+
if m, ok := base.MethodByName(c, name.Value); ok && m.TypeData != nil {
43+
return true, m.MethodIndex, name.Value
4444
}
4545
}
4646
}

checker/nature/nature.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ func ArrayFromType(c *Cache, t reflect.Type) Nature {
213213
}
214214

215215
func (n *Nature) IsAny(c *Cache) bool {
216-
return n.Type != nil && n.Kind == reflect.Interface && n.NumMethods(c) == 0
216+
return n.Type != nil && n.Kind == reflect.Interface && n.Type.NumMethod() == 0
217217
}
218218

219219
func (n *Nature) IsUnknown(c *Cache) bool {
@@ -294,7 +294,20 @@ func (n *Nature) NumMethods(c *Cache) int {
294294
return 0
295295
}
296296

297+
// MethodByNameHook, when non-nil, looks up a method on a Nature by name. If
298+
// nil, expressions must use Function-registered callables instead.
299+
var MethodByNameHook func(c *Cache, n *Nature, name string) (Nature, bool)
300+
297301
func (n *Nature) MethodByName(c *Cache, name string) (Nature, bool) {
302+
if MethodByNameHook == nil {
303+
return Nature{}, false
304+
}
305+
return MethodByNameHook(c, n, name)
306+
}
307+
308+
// LookupMethod is the reference implementation of MethodByNameHook. It
309+
// transitively reaches reflect.Type.Method via the methodset cache.
310+
func LookupMethod(c *Cache, n *Nature, name string) (Nature, bool) {
298311
if s := n.getMethodset(c); s != nil {
299312
if m := s.method(c, name); m != nil {
300313
return m.nature, true

compiler/compiler.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -783,9 +783,12 @@ func (c *compiler) CallNode(node *ast.CallNode) {
783783
switch callee := node.Callee.(type) {
784784
case *ast.MemberNode:
785785
if prop, ok := callee.Property.(*ast.StringNode); ok {
786-
if _, ok = callee.Node.Type().MethodByName(prop.Value); ok && callee.Node.Type().Kind() != reflect.Interface {
787-
fnInOffset = 1
788-
fnNumIn--
786+
base := callee.Node.Nature()
787+
if base != nil && base.Kind != reflect.Interface {
788+
if _, ok := base.MethodByName(c.ntCache, prop.Value); ok {
789+
fnInOffset = 1
790+
fnNumIn--
791+
}
789792
}
790793
}
791794
case *ast.IdentifierNode:

expr.go

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"github.com/expr-lang/expr/conf"
1414
"github.com/expr-lang/expr/file"
1515
"github.com/expr-lang/expr/optimizer"
16-
"github.com/expr-lang/expr/parser"
1716
"github.com/expr-lang/expr/patcher"
1817
"github.com/expr-lang/expr/vm"
1918
)
@@ -25,7 +24,8 @@ type Option func(c *conf.Config)
2524
// If struct is passed, all fields will be treated as variables,
2625
// as well as all fields of embedded structs and struct itself.
2726
// If map is passed, all items will be treated as variables.
28-
// Methods defined on this type will be available as functions.
27+
// Methods defined on this type will be available as functions,
28+
// unless imported through github.com/expr-lang/expr/static.
2929
func Env(env any) Option {
3030
return func(c *conf.Config) {
3131
c.WithEnv(env)
@@ -264,27 +264,3 @@ func Compile(input string, ops ...Option) (*vm.Program, error) {
264264
func Run(program *vm.Program, env any) (any, error) {
265265
return vm.Run(program, env)
266266
}
267-
268-
// Eval parses, compiles and runs given input.
269-
func Eval(input string, env any) (any, error) {
270-
if _, ok := env.(Option); ok {
271-
return nil, fmt.Errorf("misused expr.Eval: second argument (env) should be passed without expr.Env")
272-
}
273-
274-
tree, err := parser.Parse(input)
275-
if err != nil {
276-
return nil, err
277-
}
278-
279-
program, err := compiler.Compile(tree, nil)
280-
if err != nil {
281-
return nil, err
282-
}
283-
284-
output, err := Run(program, env)
285-
if err != nil {
286-
return nil, err
287-
}
288-
289-
return output, nil
290-
}

reflectmethods.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package expr
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
7+
"github.com/expr-lang/expr/checker/nature"
8+
"github.com/expr-lang/expr/compiler"
9+
"github.com/expr-lang/expr/parser"
10+
"github.com/expr-lang/expr/vm/runtime"
11+
)
12+
13+
// This file installs the method-dispatch hooks that the shared packages
14+
// (vm/runtime, checker/nature, builtin) consult to perform reflective method
15+
// lookups. The hooks are installed only when the parent expr package is
16+
// imported.
17+
//
18+
// All four reflect.* method-resolution call sites that the linker treats as
19+
// REFLECTMETHOD live exclusively in this file (or in functions reachable
20+
// only from this file's hooks):
21+
//
22+
// - reflect.Value.MethodByName in fetchMethodByName
23+
// - reflect.Value.Method in fetchMethodIndexed
24+
// - reflect.Type.Method in nature.LookupMethod (transitively)
25+
// - reflect.Type.MethodByName not used
26+
27+
func init() {
28+
runtime.MethodByNameHook = fetchMethodByName
29+
runtime.MethodIndexedHook = fetchMethodIndexed
30+
nature.MethodByNameHook = nature.LookupMethod
31+
}
32+
33+
func fetchMethodByName(v reflect.Value, name string) (any, bool) {
34+
method := v.MethodByName(name)
35+
if method.IsValid() {
36+
return method.Interface(), true
37+
}
38+
return nil, false
39+
}
40+
41+
func fetchMethodIndexed(v reflect.Value, index int) (any, bool) {
42+
method := v.Method(index)
43+
if method.IsValid() {
44+
return method.Interface(), true
45+
}
46+
return nil, false
47+
}
48+
49+
// Eval parses, compiles and runs given input.
50+
func Eval(input string, env any) (any, error) {
51+
if _, ok := env.(Option); ok {
52+
return nil, fmt.Errorf("misused expr.Eval: second argument (env) should be passed without expr.Env")
53+
}
54+
55+
tree, err := parser.Parse(input)
56+
if err != nil {
57+
return nil, err
58+
}
59+
60+
program, err := compiler.Compile(tree, nil)
61+
if err != nil {
62+
return nil, err
63+
}
64+
65+
output, err := Run(program, env)
66+
if err != nil {
67+
return nil, err
68+
}
69+
70+
return output, nil
71+
}

static/docs.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Package expr (imported as github.com/expr-lang/expr/static) is a
2+
// dead-code-elimination-friendly entry point for expr.
3+
//
4+
// It exposes the same API as the parent expr, without the `Eval()` function. It
5+
// does not trigger the Go linker's REFLECTMETHOD analysis and methods of every
6+
// reachable type can be eliminated by dead-code elimination. Calling a method
7+
// defined on the Env type does NOT work. Compilation fails with "unknown name
8+
// <method>". To expose a function, use the Function option.
9+
package expr
10+
11+
//go:generate cp ../expr.go expr.go

0 commit comments

Comments
 (0)