diff --git a/cache.go b/cache.go new file mode 100644 index 0000000..8eca106 --- /dev/null +++ b/cache.go @@ -0,0 +1,75 @@ +//go:build go1.24 && cache + +package govaluate + +import ( + "sync" + "weak" +) + +var ( + paramMap = sync.Map{} + + constMap = sync.Map{} +) + +func getParameterStage(name string) (*evaluationStage, error) { + stage, ok := getParamFromMap(name) + if ok { + return stage, nil + } + + operator := makeParameterStage(name) + ret := &evaluationStage{ + operator: operator, + } + storeVal := weak.Make(ret) + paramMap.Store(name, storeVal) + return ret, nil +} + +func getParamFromMap(name string) (*evaluationStage, bool) { + stage, ok := paramMap.Load(name) + if ok { + ptr, ok := stage.(weak.Pointer[evaluationStage]) + if ok { + ret := ptr.Value() + if ret != nil { + return ret, true + } + paramMap.Delete(name) + } + } + return nil, false +} + +func getConstantStage(value any) (*evaluationStage, error) { + stage, ok := getConstantFromMap(value) + if ok { + return stage, nil + } + + operator := makeLiteralStage(value) + ret := &evaluationStage{ + symbol: LITERAL, + operator: operator, + } + storeVal := weak.Make(ret) + constMap.Store(value, storeVal) + return ret, nil +} + +func getConstantFromMap(value any) (*evaluationStage, bool) { + stage, ok := constMap.Load(value) + if ok { + ptr, ok := stage.(weak.Pointer[evaluationStage]) + if ok { + ret := ptr.Value() + if ret != nil { + return ret, true + } + constMap.Delete(value) + } + } + return nil, false +} diff --git a/noCache.go b/noCache.go new file mode 100644 index 0000000..afec35c --- /dev/null +++ b/noCache.go @@ -0,0 +1,18 @@ +//go:build !go1.24 || !cache + +package govaluate + +func getParameterStage(name string) (*evaluationStage, error) { + operator := makeParameterStage(name) + return &evaluationStage{ + operator: operator, + }, nil +} + +func getConstantStage(value interface{}) (*evaluationStage, error) { + operator := makeLiteralStage(value) + return &evaluationStage{ + symbol: LITERAL, + operator: operator, + }, nil +} diff --git a/stagePlanner.go b/stagePlanner.go index 85bbe60..770af5a 100644 --- a/stagePlanner.go +++ b/stagePlanner.go @@ -403,7 +403,10 @@ func planValue(stream *tokenStream) (*evaluationStage, error) { // clauses with single elements don't trigger SEPARATE stage planner // this ensures that when used as part of an "in" comparison, the array requirement passes if prev.Kind == COMPARATOR && prev.Value == "in" && ret.symbol == LITERAL { - ret.operator = ensureSliceStage(ret.operator) + // We need to copy this in case we are using the cached value... + tmp := *ret + tmp.operator = ensureSliceStage(ret.operator) + ret = &tmp } // advance past the CLAUSE_CLOSE token. We know that it's a CLAUSE_CLOSE, because at parse-time we check for unbalanced parens. @@ -428,7 +431,7 @@ func planValue(stream *tokenStream) (*evaluationStage, error) { return nil, nil case VARIABLE: - operator = makeParameterStage(token.Value.(string)) + return getParameterStage(token.Value.(string)) case NUMERIC: fallthrough @@ -437,11 +440,9 @@ func planValue(stream *tokenStream) (*evaluationStage, error) { case PATTERN: fallthrough case BOOLEAN: - symbol = LITERAL - operator = makeLiteralStage(token.Value) + return getConstantStage(token.Value) case TIME: - symbol = LITERAL - operator = makeLiteralStage(float64(token.Value.(time.Time).Unix())) + return getConstantStage(float64(token.Value.(time.Time).Unix())) case PREFIX: stream.rewind()