expr-lang / expr

Expression language and expression evaluation for Go
https://expr-lang.org
MIT License
5.85k stars 378 forks source link

Feature request: UNKNOWN type #666

Open Virviil opened 1 month ago

Virviil commented 1 month ago

I want to perform some dry-run calculations with dirty custom functions. Sometimes i don't know the value that will be in real run, but i know that this value will be.

I cant use nil or any another value, because it can change the attitude of an expression. For example foo ?? download(bar) , when foo is nil will force to download bar while it is not what's desired if foo is definitely known to be not nil

My proposal is to add type UNKNOWN and UNKNOWN_NOT_NIL, which will during computation propogate it's unknoness. These values can be used as any other type. At the same time, calling dirty custom functions can check if the value is UNKNOWN and perform accordingly.

Predefined functions, while being pure, can just return UNKNOWN as there result. Important to calculate UNKNOWN <-> UNKNOWN_NOT_NIL conversions properly.

This change is fully backward compatible, because not giving any UNKNOWN value to env will just work as is.

antonmedv commented 1 month ago

Those unknow for the run step?

Virviil commented 1 month ago

@antonmedv Yep.

Let's say, each download cost money, so i want to dry-run the expression and tell, how much will it cost.

The problem is that I want to chain some expr's, so second expression can be done only after first, which can result in different values for second one.

This problem is quite unsolvable, but at least I want to calculate the ceiling of price for these downloads.

The problem seems to be a hard one because the ub for each opcode that returns boolean.

For example, is IsEqual(unknown, unknown) probably should return unknown, but it will definitely break the control flow. So, probably let's say IsEqual(unknown, unknown) should return false...

antonmedv commented 1 month ago

I actually just finished a PR which brings unknown values to the type checker https://github.com/expr-lang/expr/pull/665 See bunch of return unknown. And those types will work on type checker step.

What you can do to estimate upper bound is using visitor traverse AST of the expression and count how many times download() function were called.

Take a look on, for example, on visitor which collects names of all used variables: https://expr-lang.org/docs/visitor

Virviil commented 1 month ago

@antonmedv Can you please share example to use these unknowns?

Unfortunately counting "download" function in program code is not giving any information because it can be behind "if" not used branch (so no need to count it) or inside "map" (which can be called multiple times)

mei-rune commented 1 month ago

I hope add a dynamic type, this expression is invalid as follows: 我希望增加一个 dynamic type, 当前下面的表达式是错误的:

out, err := expr.Compile(`value1 + value2`)
// err: invalid operation + (mismatched types string and int)
// | name + age
// | .....^

I hope add a option to enable dynamic support ( backward compatible while disable dynamic), 我希望增中一个选项来开启 dynamic type 的支持

program, err := expr.Compile(`value1 + value2`, EnableDynamicType())
// ok

output, err := expr.Run(program, map[string]interface{}{
   "value1": 1,
   "value2": 2,
})
// ok, err == nil  and output = 3

output, err := expr.Run(program, map[string]interface{}{
   "value1": 1.1,
   "value2": 2,
})
// ok, err == nil  and output = 3.1

output, err := expr.Run(program, map[string]interface{}{
   "value1": "1",
   "value2": "2",
})
// ok, err == nil  and output = 12

output, err := expr.Run(program, map[string]interface{}{
   "value1": 1,
   "value2": "2",
})
// panic, err != nil
antonmedv commented 1 month ago

@Virviil those are internal type checker structs.

I'm actually working on something very similar: estimated expression costs. You can assign a cost for each field/array/function and Expr will calculate estimated cost:

download() = 100
arr = 10
map(arr, download(#)) + map(arr, download(#))

Estimated cost will be: 10 * 100 + 10 * 100

Maybe it makes sense to add "units" to this system, so we can calculate in term of different units.

download() = 100d