Skip to content

Commit

Permalink
Implement [] and Indexer operators for List<T> and Strings.
Browse files Browse the repository at this point in the history
This is needed for #39.
This fixes #49.

PiperOrigin-RevId: 651204863
  • Loading branch information
suyashkumar authored and copybara-github committed Jul 11, 2024
1 parent f3940d2 commit 68ae108
Show file tree
Hide file tree
Showing 12 changed files with 310 additions and 2 deletions.
11 changes: 11 additions & 0 deletions interpreter/operator_dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down
22 changes: 22 additions & 0 deletions interpreter/operator_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>, index Integer) T
// [](argument List<T>, 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
}
21 changes: 21 additions & 0 deletions interpreter/operator_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>, 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)
Expand Down
6 changes: 6 additions & 0 deletions model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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" }
2 changes: 2 additions & 0 deletions parser/expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions parser/operator_expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
26 changes: 26 additions & 0 deletions parser/operator_expressions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,32 @@ func TestOperatorExpressions(t *testing.T) {
},
},
},
{
name: "Indexer [] syntax for List<T>",
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) {
Expand Down
25 changes: 25 additions & 0 deletions parser/operators.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,18 @@ func (v *visitor) resolveFunction(libraryName, funcName string, operands []model
// Last(List<T>) 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:
Expand Down Expand Up @@ -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",
Expand Down
26 changes: 26 additions & 0 deletions parser/operators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,32 @@ func TestBuiltInFunctions(t *testing.T) {
},
},
},
{
name: "Indexer functional form for List<T>",
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",
Expand Down
84 changes: 84 additions & 0 deletions tests/enginetests/operator_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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<Integer>",
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<String>",
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<Integer>)[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)
}

})
}
}
78 changes: 78 additions & 0 deletions tests/enginetests/operator_string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}
Loading

0 comments on commit 68ae108

Please sign in to comment.