From c9ce5a25920d5d72632b57b8ff533f3fca5d3aee Mon Sep 17 00:00:00 2001 From: dplotnikov Date: Mon, 17 Aug 2026 13:27:34 -0700 Subject: [PATCH 1/2] Add parser benchmarks --- parser/bench/BUILD.bazel | 41 +++++++++ parser/bench/bench.go | 173 +++++++++++++++++++++++++++++++++++++ parser/bench/bench_test.go | 106 +++++++++++++++++++++++ 3 files changed, 320 insertions(+) create mode 100644 parser/bench/BUILD.bazel create mode 100644 parser/bench/bench.go create mode 100644 parser/bench/bench_test.go diff --git a/parser/bench/BUILD.bazel b/parser/bench/BUILD.bazel new file mode 100644 index 000000000..e97556fd6 --- /dev/null +++ b/parser/bench/BUILD.bazel @@ -0,0 +1,41 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +package( + licenses = ["notice"], # Apache 2.0 +) + +go_library( + name = "go_default_library", + srcs = [ + "bench.go", + ], + importpath = "github.com/google/cel-go/parser/bench", + visibility = ["//visibility:public"], + deps = [ + "//common:go_default_library", + "//common/ast:go_default_library", + "//common/operators:go_default_library", + "//common/types:go_default_library", + "//parser:go_default_library", + ], +) + +go_test( + name = "bench_test", + size = "small", + srcs = [ + "bench_test.go", + ], + embed = [ + ":go_default_library", + ], + deps = [ + "//common:go_default_library", + "//parser:go_default_library", + ], +) + +alias( + name = "go_default_test", + actual = ":bench_test", +) diff --git a/parser/bench/bench.go b/parser/bench/bench.go new file mode 100644 index 000000000..1d03fc961 --- /dev/null +++ b/parser/bench/bench.go @@ -0,0 +1,173 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package bench defines benchmark test cases and utilities for CEL parsers. +package bench + +import ( + "strings" + + "github.com/google/cel-go/common" + "github.com/google/cel-go/common/ast" + "github.com/google/cel-go/common/operators" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/parser" +) + +// ParseResult indicates whether a parse is expected to succeed or fail. +type ParseResult int + +const ( + // ParseResultSuccess indicates an expression that is expected to parse without error. + ParseResultSuccess ParseResult = iota + // ParseResultError indicates an expression that is expected to produce parse error(s). + ParseResultError +) + +// TestCase represents an expression to parse and the expected parse result. +type TestCase struct { + Expr string + Result ParseResult +} + +// ErrorCase returns a TestCase expecting a parse error. +func ErrorCase(expr string) TestCase { + return TestCase{ + Expr: expr, + Result: ParseResultError, + } +} + +// SuccessCase returns a TestCase expecting successful parsing. +func SuccessCase(expr string) TestCase { + return TestCase{ + Expr: expr, + Result: ParseResultSuccess, + } +} + +// Category represents a named group of test cases for benchmarking and verification. +type Category struct { + Name string + Cases []TestCase +} + +// OptMapMacro expands `m.optMap(v, f)` into a conditional comprehension. +var OptMapMacro = parser.NewReceiverMacro("optMap", 2, optMapExpander) + +func optMapExpander(meh parser.ExprHelper, target ast.Expr, args []ast.Expr) (ast.Expr, *common.Error) { + varIdent := args[0] + varName := "" + switch varIdent.Kind() { + case ast.IdentKind: + varName = varIdent.AsIdent() + default: + return nil, meh.NewError(varIdent.ID(), "optMap() variable name must be a simple identifier") + } + mapExpr := args[1] + return meh.NewCall( + operators.Conditional, + meh.NewMemberCall("hasValue", target), + meh.NewCall("optional.of", + meh.NewComprehension( + meh.NewList(), + "#unused", + varName, + meh.NewMemberCall("value", meh.Copy(target)), + meh.NewLiteral(types.False), + meh.NewIdent(varName), + mapExpr, + ), + ), + meh.NewCall("optional.none"), + ), nil +} + +// GetCategories returns benchmark and correctness test cases organized by category. +func GetCategories() []Category { + return categories +} + +// GetTestCases returns benchmark and correctness test cases flattened across all categories. +func GetTestCases() []TestCase { + var allCases []TestCase + for _, cat := range categories { + allCases = append(allCases, cat.Cases...) + } + return allCases +} + +var categories = []Category{ + // Simple: common, representative CEL expressions covering basic syntax, operators, calls, and literals + { + Name: "Simple", + Cases: []TestCase{ + SuccessCase("x * 2 + y / 3"), + SuccessCase(`foo.bar.baz(1, 2, "abc")`), + SuccessCase(`a > 5 && b < 10 || c == "xyz"`), + SuccessCase("x ? y : z"), + SuccessCase(`{"foo": 1, "bar": [2, 3]}`), + SuccessCase("a[b]"), + SuccessCase("a.b.c"), + SuccessCase("a.`b-c`"), + SuccessCase("\"\\a\\b\\f\\n\\r\\t\\v'\\\"\\\\ Legal escapes \\u2764\""), + }, + }, + + // Complex: expressions with deep chaining, nesting, precedence, and complex structures + { + Name: "Complex", + Cases: []TestCase{ + SuccessCase("a" + strings.Repeat(" + a", 49)), + SuccessCase("a" + strings.Repeat(" || a", 49)), + SuccessCase("a" + strings.Repeat(".f", 49)), + SuccessCase(strings.Repeat("(", 20) + "a" + strings.Repeat(")", 20)), + SuccessCase(`SomeMessage{foo: 5, bar: "xyz"}`), + SuccessCase("1 + 2 * 3 - 1 / 2 == 6 % 1"), + SuccessCase("[] + [1, 2, 3] + [4]"), + }, + }, + + // Macros: standard and receiver comprehension macros, optional syntax traversal + { + Name: "Macros", + Cases: []TestCase{ + SuccessCase("has(m.f)"), + SuccessCase("[1, 2, 3].all(x, x > 0)"), + SuccessCase("m.map(v, v * 2)"), + SuccessCase("m.filter(v, v > 0)"), + SuccessCase("m.exists_one(v, v == 1)"), + SuccessCase("x.filter(y, y.exists(z, has(z.a)))"), + SuccessCase("a.?b[?0] && a[?c]"), + SuccessCase("m.optMap(v, v + 1)"), + }, + }, + + // Errors: representative syntax errors, invalid tokens, keywords, and unclosed delimiters + { + Name: "Errors", + Cases: []TestCase{ + ErrorCase("x * 2 + y /"), + ErrorCase(`foo.bar.baz(1, 2, "abc"`), + ErrorCase("a > 5 && && b < 10"), + ErrorCase(`{"foo": 1, "bar": [2, 3`), + ErrorCase("1 + $"), + ErrorCase("break"), + ErrorCase(`"\xFh"`), + ErrorCase("a" + strings.Repeat(" + a", 49) + " +"), + ErrorCase(strings.Repeat("(", 20) + "a"), + ErrorCase("f(*" + strings.Repeat(", *", 9) + ")"), + }, + }, +} diff --git a/parser/bench/bench_test.go b/parser/bench/bench_test.go new file mode 100644 index 000000000..d016d0955 --- /dev/null +++ b/parser/bench/bench_test.go @@ -0,0 +1,106 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bench + +import ( + "fmt" + "testing" + + "github.com/google/cel-go/common" + "github.com/google/cel-go/parser" +) + +func newBenchmarkParser(tb testing.TB) *parser.Parser { + tb.Helper() + p, err := parser.NewParser( + parser.Macros(append(parser.AllMacros, OptMapMacro)...), + parser.EnableOptionalSyntax(true), + parser.EnableIdentEscapeSyntax(true), + parser.MaxRecursionDepth(512), + ) + if err != nil { + tb.Fatalf("parser.NewParser() failed: %v", err) + } + return p +} + +func TestExpectedResult(t *testing.T) { + p := newBenchmarkParser(t) + for _, cat := range GetCategories() { + t.Run(cat.Name, func(t *testing.T) { + for i, tc := range cat.Cases { + t.Run(fmt.Sprintf("%d_%s", i, tc.Expr), func(t *testing.T) { + src := common.NewTextSource(tc.Expr) + _, errs := p.Parse(src) + hasErr := len(errs.GetErrors()) > 0 + switch tc.Result { + case ParseResultSuccess: + if hasErr { + t.Errorf("p.Parse(%q) failed unexpectedly: %v", tc.Expr, errs.ToDisplayString()) + } + case ParseResultError: + if !hasErr { + t.Errorf("p.Parse(%q) succeeded unexpectedly, wanted error", tc.Expr) + } + } + }) + } + }) + } +} + +// BenchmarkParse benchmarks parsing organized by workload categories. +func BenchmarkParse(b *testing.B) { + p := newBenchmarkParser(b) + for _, cat := range GetCategories() { + b.Run(cat.Name, func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, tc := range cat.Cases { + src := common.NewTextSource(tc.Expr) + _, errs := p.Parse(src) + hasErr := len(errs.GetErrors()) > 0 + expectedErr := tc.Result == ParseResultError + if hasErr != expectedErr { + b.Fatalf("p.Parse(%q) got error: %v, expected error: %v", tc.Expr, hasErr, expectedErr) + } + } + } + }) + } +} + +// BenchmarkParseParallel benchmarks parsing concurrently across goroutines by category. +func BenchmarkParseParallel(b *testing.B) { + p := newBenchmarkParser(b) + for _, cat := range GetCategories() { + b.Run(cat.Name, func(b *testing.B) { + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + for _, tc := range cat.Cases { + src := common.NewTextSource(tc.Expr) + _, errs := p.Parse(src) + hasErr := len(errs.GetErrors()) > 0 + expectedErr := tc.Result == ParseResultError + if hasErr != expectedErr { + b.Fatalf("p.Parse(%q) got error: %v, expected error: %v", tc.Expr, hasErr, expectedErr) + } + } + } + }) + }) + } +} From b02bd0a2ced3b32f53c2483394398869d554be04 Mon Sep 17 00:00:00 2001 From: dplotnikov Date: Tue, 18 Aug 2026 15:28:52 -0700 Subject: [PATCH 2/2] Fold by-category benchmark tests into parser_test.go --- parser/BUILD.bazel | 1 + parser/bench/BUILD.bazel | 41 ------- parser/bench/bench.go | 173 -------------------------- parser/bench/bench_test.go | 106 ---------------- parser/parser_test.go | 243 +++++++++++++++++++++++++++++++++++++ 5 files changed, 244 insertions(+), 320 deletions(-) delete mode 100644 parser/bench/BUILD.bazel delete mode 100644 parser/bench/bench.go delete mode 100644 parser/bench/bench_test.go diff --git a/parser/BUILD.bazel b/parser/BUILD.bazel index 97bc9bd43..4a102840f 100644 --- a/parser/BUILD.bazel +++ b/parser/BUILD.bazel @@ -48,6 +48,7 @@ go_test( deps = [ "//common/ast:go_default_library", "//common/debug:go_default_library", + "//common/operators:go_default_library", "//common/types:go_default_library", "//parser/gen:go_default_library", "//test:go_default_library", diff --git a/parser/bench/BUILD.bazel b/parser/bench/BUILD.bazel deleted file mode 100644 index e97556fd6..000000000 --- a/parser/bench/BUILD.bazel +++ /dev/null @@ -1,41 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") - -package( - licenses = ["notice"], # Apache 2.0 -) - -go_library( - name = "go_default_library", - srcs = [ - "bench.go", - ], - importpath = "github.com/google/cel-go/parser/bench", - visibility = ["//visibility:public"], - deps = [ - "//common:go_default_library", - "//common/ast:go_default_library", - "//common/operators:go_default_library", - "//common/types:go_default_library", - "//parser:go_default_library", - ], -) - -go_test( - name = "bench_test", - size = "small", - srcs = [ - "bench_test.go", - ], - embed = [ - ":go_default_library", - ], - deps = [ - "//common:go_default_library", - "//parser:go_default_library", - ], -) - -alias( - name = "go_default_test", - actual = ":bench_test", -) diff --git a/parser/bench/bench.go b/parser/bench/bench.go deleted file mode 100644 index 1d03fc961..000000000 --- a/parser/bench/bench.go +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package bench defines benchmark test cases and utilities for CEL parsers. -package bench - -import ( - "strings" - - "github.com/google/cel-go/common" - "github.com/google/cel-go/common/ast" - "github.com/google/cel-go/common/operators" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/parser" -) - -// ParseResult indicates whether a parse is expected to succeed or fail. -type ParseResult int - -const ( - // ParseResultSuccess indicates an expression that is expected to parse without error. - ParseResultSuccess ParseResult = iota - // ParseResultError indicates an expression that is expected to produce parse error(s). - ParseResultError -) - -// TestCase represents an expression to parse and the expected parse result. -type TestCase struct { - Expr string - Result ParseResult -} - -// ErrorCase returns a TestCase expecting a parse error. -func ErrorCase(expr string) TestCase { - return TestCase{ - Expr: expr, - Result: ParseResultError, - } -} - -// SuccessCase returns a TestCase expecting successful parsing. -func SuccessCase(expr string) TestCase { - return TestCase{ - Expr: expr, - Result: ParseResultSuccess, - } -} - -// Category represents a named group of test cases for benchmarking and verification. -type Category struct { - Name string - Cases []TestCase -} - -// OptMapMacro expands `m.optMap(v, f)` into a conditional comprehension. -var OptMapMacro = parser.NewReceiverMacro("optMap", 2, optMapExpander) - -func optMapExpander(meh parser.ExprHelper, target ast.Expr, args []ast.Expr) (ast.Expr, *common.Error) { - varIdent := args[0] - varName := "" - switch varIdent.Kind() { - case ast.IdentKind: - varName = varIdent.AsIdent() - default: - return nil, meh.NewError(varIdent.ID(), "optMap() variable name must be a simple identifier") - } - mapExpr := args[1] - return meh.NewCall( - operators.Conditional, - meh.NewMemberCall("hasValue", target), - meh.NewCall("optional.of", - meh.NewComprehension( - meh.NewList(), - "#unused", - varName, - meh.NewMemberCall("value", meh.Copy(target)), - meh.NewLiteral(types.False), - meh.NewIdent(varName), - mapExpr, - ), - ), - meh.NewCall("optional.none"), - ), nil -} - -// GetCategories returns benchmark and correctness test cases organized by category. -func GetCategories() []Category { - return categories -} - -// GetTestCases returns benchmark and correctness test cases flattened across all categories. -func GetTestCases() []TestCase { - var allCases []TestCase - for _, cat := range categories { - allCases = append(allCases, cat.Cases...) - } - return allCases -} - -var categories = []Category{ - // Simple: common, representative CEL expressions covering basic syntax, operators, calls, and literals - { - Name: "Simple", - Cases: []TestCase{ - SuccessCase("x * 2 + y / 3"), - SuccessCase(`foo.bar.baz(1, 2, "abc")`), - SuccessCase(`a > 5 && b < 10 || c == "xyz"`), - SuccessCase("x ? y : z"), - SuccessCase(`{"foo": 1, "bar": [2, 3]}`), - SuccessCase("a[b]"), - SuccessCase("a.b.c"), - SuccessCase("a.`b-c`"), - SuccessCase("\"\\a\\b\\f\\n\\r\\t\\v'\\\"\\\\ Legal escapes \\u2764\""), - }, - }, - - // Complex: expressions with deep chaining, nesting, precedence, and complex structures - { - Name: "Complex", - Cases: []TestCase{ - SuccessCase("a" + strings.Repeat(" + a", 49)), - SuccessCase("a" + strings.Repeat(" || a", 49)), - SuccessCase("a" + strings.Repeat(".f", 49)), - SuccessCase(strings.Repeat("(", 20) + "a" + strings.Repeat(")", 20)), - SuccessCase(`SomeMessage{foo: 5, bar: "xyz"}`), - SuccessCase("1 + 2 * 3 - 1 / 2 == 6 % 1"), - SuccessCase("[] + [1, 2, 3] + [4]"), - }, - }, - - // Macros: standard and receiver comprehension macros, optional syntax traversal - { - Name: "Macros", - Cases: []TestCase{ - SuccessCase("has(m.f)"), - SuccessCase("[1, 2, 3].all(x, x > 0)"), - SuccessCase("m.map(v, v * 2)"), - SuccessCase("m.filter(v, v > 0)"), - SuccessCase("m.exists_one(v, v == 1)"), - SuccessCase("x.filter(y, y.exists(z, has(z.a)))"), - SuccessCase("a.?b[?0] && a[?c]"), - SuccessCase("m.optMap(v, v + 1)"), - }, - }, - - // Errors: representative syntax errors, invalid tokens, keywords, and unclosed delimiters - { - Name: "Errors", - Cases: []TestCase{ - ErrorCase("x * 2 + y /"), - ErrorCase(`foo.bar.baz(1, 2, "abc"`), - ErrorCase("a > 5 && && b < 10"), - ErrorCase(`{"foo": 1, "bar": [2, 3`), - ErrorCase("1 + $"), - ErrorCase("break"), - ErrorCase(`"\xFh"`), - ErrorCase("a" + strings.Repeat(" + a", 49) + " +"), - ErrorCase(strings.Repeat("(", 20) + "a"), - ErrorCase("f(*" + strings.Repeat(", *", 9) + ")"), - }, - }, -} diff --git a/parser/bench/bench_test.go b/parser/bench/bench_test.go deleted file mode 100644 index d016d0955..000000000 --- a/parser/bench/bench_test.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package bench - -import ( - "fmt" - "testing" - - "github.com/google/cel-go/common" - "github.com/google/cel-go/parser" -) - -func newBenchmarkParser(tb testing.TB) *parser.Parser { - tb.Helper() - p, err := parser.NewParser( - parser.Macros(append(parser.AllMacros, OptMapMacro)...), - parser.EnableOptionalSyntax(true), - parser.EnableIdentEscapeSyntax(true), - parser.MaxRecursionDepth(512), - ) - if err != nil { - tb.Fatalf("parser.NewParser() failed: %v", err) - } - return p -} - -func TestExpectedResult(t *testing.T) { - p := newBenchmarkParser(t) - for _, cat := range GetCategories() { - t.Run(cat.Name, func(t *testing.T) { - for i, tc := range cat.Cases { - t.Run(fmt.Sprintf("%d_%s", i, tc.Expr), func(t *testing.T) { - src := common.NewTextSource(tc.Expr) - _, errs := p.Parse(src) - hasErr := len(errs.GetErrors()) > 0 - switch tc.Result { - case ParseResultSuccess: - if hasErr { - t.Errorf("p.Parse(%q) failed unexpectedly: %v", tc.Expr, errs.ToDisplayString()) - } - case ParseResultError: - if !hasErr { - t.Errorf("p.Parse(%q) succeeded unexpectedly, wanted error", tc.Expr) - } - } - }) - } - }) - } -} - -// BenchmarkParse benchmarks parsing organized by workload categories. -func BenchmarkParse(b *testing.B) { - p := newBenchmarkParser(b) - for _, cat := range GetCategories() { - b.Run(cat.Name, func(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - for _, tc := range cat.Cases { - src := common.NewTextSource(tc.Expr) - _, errs := p.Parse(src) - hasErr := len(errs.GetErrors()) > 0 - expectedErr := tc.Result == ParseResultError - if hasErr != expectedErr { - b.Fatalf("p.Parse(%q) got error: %v, expected error: %v", tc.Expr, hasErr, expectedErr) - } - } - } - }) - } -} - -// BenchmarkParseParallel benchmarks parsing concurrently across goroutines by category. -func BenchmarkParseParallel(b *testing.B) { - p := newBenchmarkParser(b) - for _, cat := range GetCategories() { - b.Run(cat.Name, func(b *testing.B) { - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - for _, tc := range cat.Cases { - src := common.NewTextSource(tc.Expr) - _, errs := p.Parse(src) - hasErr := len(errs.GetErrors()) > 0 - expectedErr := tc.Result == ParseResultError - if hasErr != expectedErr { - b.Fatalf("p.Parse(%q) got error: %v, expected error: %v", tc.Expr, hasErr, expectedErr) - } - } - } - }) - }) - } -} diff --git a/parser/parser_test.go b/parser/parser_test.go index 88527d813..4a453a197 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -24,6 +24,7 @@ import ( "github.com/google/cel-go/common" "github.com/google/cel-go/common/ast" "github.com/google/cel-go/common/debug" + "github.com/google/cel-go/common/operators" "github.com/google/cel-go/common/types" "github.com/google/cel-go/test" ) @@ -2393,6 +2394,248 @@ func BenchmarkParseParallel(b *testing.B) { }) } +type benchTestInfo struct { + // I contains the input expression to be parsed. + I string + + // E indicates whether an error is expected. + E bool +} + +type benchCategory struct { + name string + cases []benchTestInfo +} + +var benchCategories = []benchCategory{ + // Simple: common, representative CEL expressions covering basic syntax, operators, calls, and literals + { + name: "Simple", + cases: []benchTestInfo{ + { + I: "x * 2 + y / 3", + }, + { + I: `foo.bar.baz(1, 2, "abc")`, + }, + { + I: `a > 5 && b < 10 || c == "xyz"`, + }, + { + I: "x ? y : z", + }, + { + I: `{"foo": 1, "bar": [2, 3]}`, + }, + { + I: "a[b]", + }, + { + I: "a.b.c", + }, + { + I: "a.`b-c`", + }, + { + I: "\"\\a\\b\\f\\n\\r\\t\\v'\\\"\\\\ Legal escapes \\u2764\"", + }, + }, + }, + + // Complex: expressions with deep chaining, nesting, precedence, and complex structures + { + name: "Complex", + cases: []benchTestInfo{ + { + I: "a" + strings.Repeat(" + a", 49), + }, + { + I: "a" + strings.Repeat(" || a", 49), + }, + { + I: "a" + strings.Repeat(".f", 49), + }, + { + I: strings.Repeat("(", 20) + "a" + strings.Repeat(")", 20), + }, + { + I: `SomeMessage{foo: 5, bar: "xyz"}`, + }, + { + I: "1 + 2 * 3 - 1 / 2 == 6 % 1", + }, + { + I: "[] + [1, 2, 3] + [4]", + }, + }, + }, + + // Macros: standard and receiver comprehension macros, optional syntax traversal + { + name: "Macros", + cases: []benchTestInfo{ + { + I: "has(m.f)", + }, + { + I: "[1, 2, 3].all(x, x > 0)", + }, + { + I: "m.map(v, v * 2)", + }, + { + I: "m.filter(v, v > 0)", + }, + { + I: "m.exists_one(v, v == 1)", + }, + { + I: "x.filter(y, y.exists(z, has(z.a)))", + }, + { + I: "a.?b[?0] && a[?c]", + }, + { + I: "m.optMap(v, v + 1)", + }, + }, + }, + + // Errors: representative syntax errors, invalid tokens, keywords, and unclosed delimiters + { + name: "Errors", + cases: []benchTestInfo{ + { + I: "x * 2 + y /", + E: true, + }, + { + I: `foo.bar.baz(1, 2, "abc"`, + E: true, + }, + { + I: "a > 5 && && b < 10", + E: true, + }, + { + I: `{"foo": 1, "bar": [2, 3`, + E: true, + }, + { + I: "1 + $", + E: true, + }, + { + I: "break", + E: true, + }, + { + I: `"\xFh"`, + E: true, + }, + { + I: "a" + strings.Repeat(" + a", 49) + " +", + E: true, + }, + { + I: strings.Repeat("(", 20) + "a", + E: true, + }, + { + I: "f(*" + strings.Repeat(", *", 9) + ")", + E: true, + }, + }, + }, +} + +// BenchmarkByCategory benchmarks parsing organized by workload categories. +func BenchmarkByCategory(b *testing.B) { + p := newBenchmarkCategoryParser(b) + for _, cat := range benchCategories { + b.Run(cat.name, func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, tc := range cat.cases { + src := common.NewTextSource(tc.I) + _, errs := p.Parse(src) + hasErr := len(errs.GetErrors()) > 0 + if hasErr != tc.E { + b.Fatalf("p.Parse(%q) got error: %v, expected error: %v", tc.I, hasErr, tc.E) + } + } + } + }) + } +} + +// BenchmarkParallelByCategory benchmarks parsing concurrently across goroutines by category. +func BenchmarkParallelByCategory(b *testing.B) { + p := newBenchmarkCategoryParser(b) + for _, cat := range benchCategories { + b.Run(cat.name, func(b *testing.B) { + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + for _, tc := range cat.cases { + src := common.NewTextSource(tc.I) + _, errs := p.Parse(src) + hasErr := len(errs.GetErrors()) > 0 + if hasErr != tc.E { + b.Fatalf("p.Parse(%q) got error: %v, expected error: %v", tc.I, hasErr, tc.E) + } + } + } + }) + }) + } +} + +// optMapMacro expands `m.optMap(v, f)` into a conditional comprehension. +var optMapMacro = NewReceiverMacro("optMap", 2, optMapExpander) + +func optMapExpander(meh ExprHelper, target ast.Expr, args []ast.Expr) (ast.Expr, *common.Error) { + varIdent := args[0] + varName := "" + switch varIdent.Kind() { + case ast.IdentKind: + varName = varIdent.AsIdent() + default: + return nil, meh.NewError(varIdent.ID(), "optMap() variable name must be a simple identifier") + } + mapExpr := args[1] + return meh.NewCall( + operators.Conditional, + meh.NewMemberCall("hasValue", target), + meh.NewCall("optional.of", + meh.NewComprehension( + meh.NewList(), + "#unused", + varName, + meh.NewMemberCall("value", meh.Copy(target)), + meh.NewLiteral(types.False), + meh.NewIdent(varName), + mapExpr, + ), + ), + meh.NewCall("optional.none"), + ), nil +} + +func newBenchmarkCategoryParser(tb testing.TB) *Parser { + tb.Helper() + p, err := NewParser( + Macros(append(AllMacros, optMapMacro)...), + EnableOptionalSyntax(true), + EnableIdentEscapeSyntax(true), + MaxRecursionDepth(512), + ) + if err != nil { + tb.Fatalf("NewParser() failed: %v", err) + } + return p +} + func TestParseErrorData(t *testing.T) { p := newTestParser(t) src := common.NewTextSource(`a.?b`)