From 68ae108c28f703c8d22e346b2befb9cf85549dd2 Mon Sep 17 00:00:00 2001 From: Suyash Kumar Date: Wed, 10 Jul 2024 17:30:02 -0700 Subject: [PATCH] Implement [] and Indexer operators for List and Strings. This is needed for https://github.com/google/cql/issues/39. This fixes https://github.com/google/cql/issues/49. PiperOrigin-RevId: 651204863 --- interpreter/operator_dispatcher.go | 11 +++ interpreter/operator_list.go | 22 ++++++ interpreter/operator_string.go | 21 ++++++ model/model.go | 6 ++ parser/expressions.go | 2 + parser/operator_expressions.go | 9 +++ parser/operator_expressions_test.go | 26 +++++++ parser/operators.go | 25 +++++++ parser/operators_test.go | 26 +++++++ tests/enginetests/operator_list_test.go | 84 +++++++++++++++++++++++ tests/enginetests/operator_string_test.go | 78 +++++++++++++++++++++ tests/spectests/exclusions/exclusions.go | 2 - 12 files changed, 310 insertions(+), 2 deletions(-) diff --git a/interpreter/operator_dispatcher.go b/interpreter/operator_dispatcher.go index 34392d1..c3a3333 100644 --- a/interpreter/operator_dispatcher.go +++ b/interpreter/operator_dispatcher.go @@ -849,6 +849,17 @@ func (i *interpreter) binaryOverloads(m model.IBinaryExpression) ([]convert.Over Result: i.evalSplit, }, }, nil + case *model.Indexer: + return []convert.Overload[evalBinarySignature]{ + { + Operands: []types.IType{types.String, types.Integer}, + Result: i.evalIndexerString, + }, + { + Operands: []types.IType{&types.List{ElementType: types.Any}, types.Integer}, + Result: i.evalIndexerList, + }, + }, nil default: return nil, fmt.Errorf("unsupported Binary Expression %v", m.GetName()) } diff --git a/interpreter/operator_list.go b/interpreter/operator_list.go index f6f9b82..93a0834 100644 --- a/interpreter/operator_list.go +++ b/interpreter/operator_list.go @@ -121,3 +121,25 @@ func evalSingletonFrom(m model.IUnaryExpression, listObj result.Value) (result.V return result.Value{}, fmt.Errorf("singleton from requires a list of length 0 or 1, but got length %d", len(list)) } } + +// Indexer(argument List, index Integer) T +// [](argument List, index Integer) T +// https://cql.hl7.org/09-b-cqlreference.html#indexer-1 +// Indexer is also defined for String, see operator_string.go for that implementation. +func (i *interpreter) evalIndexerList(m model.IBinaryExpression, lObj, rObj result.Value) (result.Value, error) { + if result.IsNull(lObj) || result.IsNull(rObj) { + return result.New(nil) + } + list, err := result.ToSlice(lObj) + if err != nil { + return result.Value{}, err + } + idx, err := result.ToInt32(rObj) + if err != nil { + return result.Value{}, err + } + if idx < 0 || idx >= int32(len(list)) { + return result.New(nil) + } + return list[idx], nil +} diff --git a/interpreter/operator_string.go b/interpreter/operator_string.go index a91f700..3463412 100644 --- a/interpreter/operator_string.go +++ b/interpreter/operator_string.go @@ -219,6 +219,27 @@ func (i *interpreter) evalCombine(m model.INaryExpression, operands []result.Val return result.New(resultBuilder.String()) } +// Indexer(argument String, index Integer) String +// https://cql.hl7.org/09-b-cqlreference.html#indexer +// Indexer is also defined for List, see operator_list.go for that implementation. +func (i *interpreter) evalIndexerString(m model.IBinaryExpression, lObj, rObj result.Value) (result.Value, error) { + if result.IsNull(lObj) || result.IsNull(rObj) { + return result.New(nil) + } + str, err := result.ToString(lObj) + if err != nil { + return result.Value{}, err + } + idx, err := result.ToInt32(rObj) + if err != nil { + return result.Value{}, err + } + if idx < 0 || idx >= int32(len(str)) { + return result.New(nil) + } + return result.New(string([]rune(str)[idx])) +} + // convert a quantity value to a string func quantityToString(q result.Quantity) string { f := strconv.FormatFloat(q.Value, 'f', -1, 64) diff --git a/model/model.go b/model/model.go index 350cbce..9cbd5c5 100644 --- a/model/model.go +++ b/model/model.go @@ -932,6 +932,9 @@ type Union struct{ *BinaryExpression } // it always takes two arguments. type Split struct{ *BinaryExpression } +// Indexer ELM Expression https://cql.hl7.org/04-logicalspecification.html#indexer. +type Indexer struct{ *BinaryExpression } + // BinaryExpressionWithPrecision represents a BinaryExpression with a precision property. type BinaryExpressionWithPrecision struct { *BinaryExpression @@ -1371,3 +1374,6 @@ func (a *Split) GetName() string { return "Split" } // GetName returns the name of the system operator. func (a *Combine) GetName() string { return "Combine" } + +// GetName returns the name of the system operator. +func (i *Indexer) GetName() string { return "Indexer" } diff --git a/parser/expressions.go b/parser/expressions.go index 08973b3..f640f7d 100644 --- a/parser/expressions.go +++ b/parser/expressions.go @@ -122,6 +122,8 @@ func (v *visitor) VisitExpression(tree antlr.Tree) model.IExpression { m = v.VisitTypeExtentExpressionTermContext(t) case *cql.ElementExtractorExpressionTermContext: m = v.VisitElementExtractorExpressionTerm(t) + case *cql.IndexedExpressionTermContext: + m = v.VisitIndexedExpressionTermContext(t) // All cases that have a single child and recurse to the child are handled below. For example in // the CQL grammar the only child of QueryExpression is Query, so QueryExpression can be handled diff --git a/parser/operator_expressions.go b/parser/operator_expressions.go index 55e710a..c8e9bb2 100644 --- a/parser/operator_expressions.go +++ b/parser/operator_expressions.go @@ -669,3 +669,12 @@ func (v *visitor) VisitSuccessorExpressionTerm(ctx *cql.SuccessorExpressionTermC } return m } + +func (v *visitor) VisitIndexedExpressionTermContext(ctx *cql.IndexedExpressionTermContext) model.IExpression { + baseExpr := ctx.ExpressionTerm() + m, err := v.parseFunction("", "Indexer", []antlr.Tree{baseExpr, ctx.Expression()}, false) + if err != nil { + return v.badExpression(err.Error(), ctx) + } + return m +} diff --git a/parser/operator_expressions_test.go b/parser/operator_expressions_test.go index 83a5467..be4d34c 100644 --- a/parser/operator_expressions_test.go +++ b/parser/operator_expressions_test.go @@ -999,6 +999,32 @@ func TestOperatorExpressions(t *testing.T) { }, }, }, + { + name: "Indexer [] syntax for List", + cql: "{1, 2, 3}[0]", + want: &model.Indexer{ + BinaryExpression: &model.BinaryExpression{ + Expression: model.ResultType(types.Integer), + Operands: []model.IExpression{ + model.NewList([]string{"1", "2", "3"}, types.Integer), + model.NewLiteral("0", types.Integer), + }, + }, + }, + }, + { + name: "Indexer [] syntax for String", + cql: "'abc'[0]", + want: &model.Indexer{ + BinaryExpression: &model.BinaryExpression{ + Expression: model.ResultType(types.String), + Operands: []model.IExpression{ + model.NewLiteral("abc", types.String), + model.NewLiteral("0", types.Integer), + }, + }, + }, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { diff --git a/parser/operators.go b/parser/operators.go index fabe70b..fbc57b4 100644 --- a/parser/operators.go +++ b/parser/operators.go @@ -116,6 +116,18 @@ func (v *visitor) resolveFunction(libraryName, funcName string, operands []model // Last(List) T is a special case because the ResultType is not known until invocation. listType := resolved.WrappedOperands[0].GetResultType().(*types.List) t.Expression = model.ResultType(listType.ElementType) + case *model.Indexer: + switch opType := resolved.WrappedOperands[0].GetResultType().(type) { + case types.System: + if opType != types.String { + return nil, fmt.Errorf("internal error - expected Indexer(String, Integer) overload during parsing, but got Indexer(%v, _)", resolved.WrappedOperands[0].GetResultType()) + } + t.Expression = model.ResultType(types.String) + case *types.List: + t.Expression = model.ResultType(opType.ElementType) + default: + return nil, fmt.Errorf("internal error -- upsupported Indexer operand types") + } case *model.Predecessor: t.Expression = model.ResultType(resolved.WrappedOperands[0].GetResultType()) case *model.Successor: @@ -920,6 +932,19 @@ func (p *Parser) loadSystemOperators() error { } }, }, + { + name: "Indexer", + operands: [][]types.IType{ + {types.String, types.Integer}, + {&types.List{ElementType: types.Any}, types.Integer}, + }, + model: func() model.IExpression { + return &model.Indexer{ + // The result type is set in the resolveFunction(). + BinaryExpression: &model.BinaryExpression{}, + } + }, + }, // DATE AND TIME OPERATORS - https://cql.hl7.org/09-b-cqlreference.html#datetime-operators-2 { name: "Add", diff --git a/parser/operators_test.go b/parser/operators_test.go index c4f4ae5..195fca2 100644 --- a/parser/operators_test.go +++ b/parser/operators_test.go @@ -687,6 +687,32 @@ func TestBuiltInFunctions(t *testing.T) { }, }, }, + { + name: "Indexer functional form for List", + cql: "Indexer({1}, 0)", + want: &model.Indexer{ + BinaryExpression: &model.BinaryExpression{ + Expression: model.ResultType(types.Integer), + Operands: []model.IExpression{ + model.NewList([]string{"1"}, types.Integer), + model.NewLiteral("0", types.Integer), + }, + }, + }, + }, + { + name: "Indexer functional form for String", + cql: "Indexer('abc', 0)", + want: &model.Indexer{ + BinaryExpression: &model.BinaryExpression{ + Expression: model.ResultType(types.String), + Operands: []model.IExpression{ + model.NewLiteral("abc", types.String), + model.NewLiteral("0", types.Integer), + }, + }, + }, + }, // DATE AND TIME OPERATORS - https://cql.hl7.org/09-b-cqlreference.html#datetime-operators-2 { name: "After", diff --git a/tests/enginetests/operator_list_test.go b/tests/enginetests/operator_list_test.go index 27e3fc5..7c6486f 100644 --- a/tests/enginetests/operator_list_test.go +++ b/tests/enginetests/operator_list_test.go @@ -467,3 +467,87 @@ func TestListOperatorSingletonFrom_Error(t *testing.T) { }) } } + +func TestIndexerList(t *testing.T) { + tests := []struct { + name string + cql string + wantModel model.IExpression + wantResult result.Value + }{ + { + name: "Indexer [] syntax for List", + cql: "{1, 2}[1]", + wantModel: &model.Indexer{ + BinaryExpression: &model.BinaryExpression{ + Expression: model.ResultType(types.Integer), + Operands: []model.IExpression{ + model.NewList([]string{"1", "2"}, types.Integer), + model.NewLiteral("1", types.Integer), + }, + }, + }, + wantResult: newOrFatal(t, 2), + }, + { + name: "Indexer [] syntax for List", + cql: "{'a', 'b'}[1]", + wantModel: &model.Indexer{ + BinaryExpression: &model.BinaryExpression{ + Expression: model.ResultType(types.String), + Operands: []model.IExpression{ + model.NewList([]string{"a", "b"}, types.String), + model.NewLiteral("1", types.Integer), + }, + }, + }, + wantResult: newOrFatal(t, "b"), + }, + { + name: "Indexer functional form", + cql: "Indexer({1, 2}, 1)", + wantResult: newOrFatal(t, 2), + }, + { + name: "Indexer with index too large", + cql: "{1, 2}[100]", + wantResult: newOrFatal(t, nil), + }, + { + name: "Indexer with index smaller than 0", + cql: "{1, 2}[-100]", + wantResult: newOrFatal(t, nil), + }, + { + name: "Indexer on null", + cql: "(null as List)[1]", + wantResult: newOrFatal(t, nil), + }, + { + name: "Indexer with null index", + cql: "{1, 2}[null as Integer]", + wantResult: newOrFatal(t, nil), + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + p := newFHIRParser(t) + parsedLibs, err := p.Libraries(context.Background(), wrapInLib(t, tc.cql), parser.Config{}) + if err != nil { + t.Fatalf("Parse returned unexpected error: %v", err) + } + if diff := cmp.Diff(tc.wantModel, getTESTRESULTModel(t, parsedLibs)); tc.wantModel != nil && diff != "" { + t.Errorf("Parse diff (-want +got):\n%s", diff) + } + + results, err := interpreter.Eval(context.Background(), parsedLibs, defaultInterpreterConfig(t, p)) + if err != nil { + t.Fatalf("Eval returned unexpected error: %v", err) + } + if diff := cmp.Diff(tc.wantResult, getTESTRESULT(t, results), protocmp.Transform()); diff != "" { + t.Errorf("Eval diff (-want +got)\n%v", diff) + } + + }) + } +} diff --git a/tests/enginetests/operator_string_test.go b/tests/enginetests/operator_string_test.go index cbde8a1..3f825db 100644 --- a/tests/enginetests/operator_string_test.go +++ b/tests/enginetests/operator_string_test.go @@ -389,3 +389,81 @@ func TestCombine(t *testing.T) { }) } } + +func TestIndexerString(t *testing.T) { + tests := []struct { + name string + cql string + wantModel model.IExpression + wantResult result.Value + }{ + { + name: "[] Indexer", + cql: "'abc'[1]", + wantModel: &model.Indexer{ + BinaryExpression: &model.BinaryExpression{ + Expression: model.ResultType(types.String), + Operands: []model.IExpression{ + model.NewLiteral("abc", types.String), + model.NewLiteral("1", types.Integer), + }, + }, + }, + wantResult: newOrFatal(t, "b"), + }, + { + name: "Indexer functional form", + cql: "Indexer('abc', 1)", + wantModel: &model.Indexer{ + BinaryExpression: &model.BinaryExpression{ + Expression: model.ResultType(types.String), + Operands: []model.IExpression{ + model.NewLiteral("abc", types.String), + model.NewLiteral("1", types.Integer), + }, + }, + }, + wantResult: newOrFatal(t, "b"), + }, + { + name: "Indexer with index too large", + cql: "'abc'[100]", + wantResult: newOrFatal(t, nil), + }, + { + name: "Indexer with index smaller than 0", + cql: "'abc'[-100]", + wantResult: newOrFatal(t, nil), + }, + { + name: "Indexer on null", + cql: "(null as String)[1]", + wantResult: newOrFatal(t, nil), + }, + { + name: "Indexer with null index", + cql: "'abc'[null as Integer]", + wantResult: newOrFatal(t, nil), + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + p := newFHIRParser(t) + parsedLibs, err := p.Libraries(context.Background(), wrapInLib(t, tc.cql), parser.Config{}) + if err != nil { + t.Fatalf("Parse returned unexpected error: %v", err) + } + if diff := cmp.Diff(tc.wantModel, getTESTRESULTModel(t, parsedLibs)); tc.wantModel != nil && diff != "" { + t.Errorf("Parse diff (-want +got):\n%s", diff) + } + + results, err := interpreter.Eval(context.Background(), parsedLibs, defaultInterpreterConfig(t, p)) + if err != nil { + t.Fatalf("Eval returned unexpected error: %v", err) + } + if diff := cmp.Diff(tc.wantResult, getTESTRESULT(t, results), protocmp.Transform()); diff != "" { + t.Errorf("Eval diff (-want +got)\n%v", diff) + } + }) + } +} diff --git a/tests/spectests/exclusions/exclusions.go b/tests/spectests/exclusions/exclusions.go index 132e98d..0d7383f 100644 --- a/tests/spectests/exclusions/exclusions.go +++ b/tests/spectests/exclusions/exclusions.go @@ -318,7 +318,6 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions { "Flatten", "Includes", "IncludedIn", - "Indexer", "IndexOf", "Intersect", "Length", @@ -359,7 +358,6 @@ func XMLTestFileExclusionDefinitions() map[string]XMLTestFileExclusions { GroupExcludes: []string{ // TODO: b/342061715 - unsupported operators. "EndsWith", - "Indexer", "LastPositionOf", "Length", "Lower",