Improve type checker for named types

This commit is contained in:
Anton Medvedev
2025-03-08 15:33:37 +01:00
parent 3d52b8e334
commit 8cab964afb
9 changed files with 218 additions and 95 deletions
+1 -1
View File
@@ -164,7 +164,7 @@ var Builtins = []*Function{
if len(args) != 1 {
return anyType, fmt.Errorf("invalid number of arguments (expected 1, got %d)", len(args))
}
switch kind(args[0]) {
switch kind(deref.Type(args[0])) {
case reflect.Interface:
return integerType, nil
case reflect.Float32, reflect.Float64, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+13
View File
@@ -638,3 +638,16 @@ func Test_int_unwraps_underlying_value(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, true, out)
}
func TestBuiltin_int_with_deref(t *testing.T) {
x := 42
env := map[string]any{
"x": &x,
}
program, err := expr.Compile(`int(x)`, expr.Env(env))
require.NoError(t, err)
out, err := expr.Run(program, env)
require.NoError(t, err)
assert.Equal(t, 42, out)
}
+1 -1
View File
@@ -180,7 +180,7 @@ func Round(x any) any {
}
func Int(x any) any {
switch x := x.(type) {
switch x := deref.Deref(x).(type) {
case float32:
return int(x)
case float64:
+20 -2
View File
@@ -319,7 +319,10 @@ func (v *checker) BinaryNode(node *ast.BinaryNode) Nature {
if isTime(l) && isTime(r) {
return boolNature
}
if or(l, r, isNumber, isString, isTime) {
if isDuration(l) && isDuration(r) {
return boolNature
}
if or(l, r, isNumber, isString, isTime, isDuration) {
return boolNature
}
@@ -333,6 +336,9 @@ func (v *checker) BinaryNode(node *ast.BinaryNode) Nature {
if isTime(l) && isDuration(r) {
return timeNature
}
if isDuration(l) && isDuration(r) {
return durationNature
}
if or(l, r, isNumber, isTime, isDuration) {
return unknown
}
@@ -341,7 +347,16 @@ func (v *checker) BinaryNode(node *ast.BinaryNode) Nature {
if isNumber(l) && isNumber(r) {
return combined(l, r)
}
if or(l, r, isNumber) {
if isNumber(l) && isDuration(r) {
return durationNature
}
if isDuration(l) && isNumber(r) {
return durationNature
}
if isDuration(l) && isDuration(r) {
return durationNature
}
if or(l, r, isNumber, isDuration) {
return unknown
}
@@ -382,6 +397,9 @@ func (v *checker) BinaryNode(node *ast.BinaryNode) Nature {
if isDuration(l) && isTime(r) {
return timeNature
}
if isDuration(l) && isDuration(r) {
return durationNature
}
if or(l, r, isNumber, isString, isTime, isDuration) {
return unknown
}
+7
View File
@@ -161,6 +161,13 @@ func (n Nature) FieldByName(name string) (Nature, bool) {
return Nature{Type: field.Type, FieldIndex: field.Index}, ok
}
func (n Nature) PkgPath() string {
if n.Type == nil {
return ""
}
return n.Type.PkgPath()
}
func (n Nature) IsFastMap() bool {
if n.Type == nil {
return false
+19 -11
View File
@@ -89,7 +89,7 @@ func isInteger(nt Nature) bool {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
fallthrough
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return true
return nt.PkgPath() == ""
}
return false
}
@@ -97,7 +97,7 @@ func isInteger(nt Nature) bool {
func isFloat(nt Nature) bool {
switch nt.Kind() {
case reflect.Float32, reflect.Float64:
return true
return nt.PkgPath() == ""
}
return false
}
@@ -178,15 +178,23 @@ func kind(t reflect.Type) reflect.Kind {
}
func isComparable(l, r Nature) bool {
switch {
case l.Kind() == r.Kind():
return true
case isNumber(l) && isNumber(r):
return true
case isNil(l) || isNil(r):
return true
case isUnknown(l) || isUnknown(r):
if isUnknown(l) || isUnknown(r) {
return true
}
return false
if isNil(l) || isNil(r) {
return true
}
if isNumber(l) && isNumber(r) {
return true
}
if isDuration(l) && isDuration(r) {
return true
}
if isTime(l) && isTime(r) {
return true
}
if isArray(l) && isArray(r) {
return true
}
return l.AssignableTo(r)
}
+4 -80
View File
@@ -1119,6 +1119,10 @@ func TestExpr(t *testing.T) {
`duration("1h") + duration("1m")`,
time.Hour + time.Minute,
},
{
`duration("1h") - duration("1m")`,
time.Hour - time.Minute,
},
{
`7 * duration("1h")`,
7 * time.Hour,
@@ -2328,86 +2332,6 @@ func TestIssue432(t *testing.T) {
assert.Equal(t, float64(10), out)
}
func TestIssue461(t *testing.T) {
type EnvStr string
type EnvField struct {
S EnvStr
Str string
}
type Env struct {
S EnvStr
Str string
EnvField EnvField
}
var tests = []struct {
input string
env Env
want bool
}{
{
input: "Str == S",
env: Env{S: "string", Str: "string"},
want: false,
},
{
input: "Str == Str",
env: Env{Str: "string"},
want: true,
},
{
input: "S == S",
env: Env{Str: "string"},
want: true,
},
{
input: `Str == "string"`,
env: Env{Str: "string"},
want: true,
},
{
input: `S == "string"`,
env: Env{Str: "string"},
want: false,
},
{
input: "EnvField.Str == EnvField.S",
env: Env{EnvField: EnvField{S: "string", Str: "string"}},
want: false,
},
{
input: "EnvField.Str == EnvField.Str",
env: Env{EnvField: EnvField{Str: "string"}},
want: true,
},
{
input: "EnvField.S == EnvField.S",
env: Env{EnvField: EnvField{Str: "string"}},
want: true,
},
{
input: `EnvField.Str == "string"`,
env: Env{EnvField: EnvField{Str: "string"}},
want: true,
},
{
input: `EnvField.S == "string"`,
env: Env{EnvField: EnvField{Str: "string"}},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
program, err := expr.Compile(tt.input, expr.Env(tt.env), expr.AsBool())
out, err := expr.Run(program, tt.env)
require.NoError(t, err)
require.Equal(t, tt.want, out)
})
}
}
func TestIssue462(t *testing.T) {
env := map[string]any{
"foo": func() (string, error) {
+93
View File
@@ -0,0 +1,93 @@
package issue_test
import (
"testing"
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/internal/testify/require"
)
func TestIssue461(t *testing.T) {
type EnvStr string
type EnvField struct {
S EnvStr
Str string
}
type Env struct {
S EnvStr
Str string
EnvField EnvField
}
var tests = []struct {
input string
env Env
want bool
err string
}{
{
input: "Str == S",
env: Env{S: "string", Str: "string"},
err: "invalid operation: == (mismatched types string and issue_test.EnvStr)",
},
{
input: "Str == Str",
env: Env{Str: "string"},
want: true,
},
{
input: "S == S",
env: Env{Str: "string"},
want: true,
},
{
input: `Str == "string"`,
env: Env{Str: "string"},
want: true,
},
{
input: `S == "string"`,
env: Env{Str: "string"},
err: "invalid operation: == (mismatched types issue_test.EnvStr and string)",
},
{
input: "EnvField.Str == EnvField.S",
env: Env{EnvField: EnvField{S: "string", Str: "string"}},
err: "invalid operation: == (mismatched types string and issue_test.EnvStr)",
},
{
input: "EnvField.Str == EnvField.Str",
env: Env{EnvField: EnvField{Str: "string"}},
want: true,
},
{
input: "EnvField.S == EnvField.S",
env: Env{EnvField: EnvField{Str: "string"}},
want: true,
},
{
input: `EnvField.Str == "string"`,
env: Env{EnvField: EnvField{Str: "string"}},
want: true,
},
{
input: `EnvField.S == "string"`,
env: Env{EnvField: EnvField{Str: "string"}},
err: "invalid operation: == (mismatched types issue_test.EnvStr and string)",
},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
program, err := expr.Compile(tt.input, expr.Env(tt.env), expr.AsBool())
if tt.err != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tt.err)
} else {
out, err := expr.Run(program, tt.env)
require.NoError(t, err)
require.Equal(t, tt.want, out)
}
})
}
}
+60
View File
@@ -0,0 +1,60 @@
package issue_test
import (
"testing"
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/internal/testify/require"
)
type ModeEnum int
const (
ModeEnumA ModeEnum = 1
)
type Env struct {
Mode *ModeEnum
}
func TestIssue730(t *testing.T) {
code := `int(Mode) == 1`
tmp := ModeEnumA
env := map[string]any{
"Mode": &tmp,
}
program, err := expr.Compile(code, expr.Env(env))
require.NoError(t, err)
output, err := expr.Run(program, env)
require.NoError(t, err)
require.True(t, output.(bool))
}
func TestIssue730_warn_about_different_types(t *testing.T) {
code := `Mode == 1`
_, err := expr.Compile(code, expr.Env(Env{}))
require.Error(t, err)
require.Contains(t, err.Error(), "invalid operation: == (mismatched types issue_test.ModeEnum and int)")
}
func TestIssue730_eval(t *testing.T) {
code := `Mode == 1`
tmp := ModeEnumA
env := map[string]any{
"Mode": &tmp,
}
// Golang also does not allow this:
// _ = ModeEnumA == int(1) // will not compile
out, err := expr.Eval(code, env)
require.NoError(t, err)
require.False(t, out.(bool))
}