diff --git a/src/Hl7.Fhir.Base/FhirPath/DiagnosticsDebugTracer.cs b/src/Hl7.Fhir.Base/FhirPath/DiagnosticsDebugTracer.cs new file mode 100644 index 0000000000..fd0282725c --- /dev/null +++ b/src/Hl7.Fhir.Base/FhirPath/DiagnosticsDebugTracer.cs @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2015, Firely (info@fire.ly) and contributors + * See the file CONTRIBUTORS for details. + * + * This file is licensed under the BSD 3-Clause license + * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE + */ + +#nullable enable + +using Hl7.Fhir.ElementModel; +using Hl7.FhirPath.Expressions; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Hl7.FhirPath +{ + + public class DiagnosticsDebugTracer : IDebugTracer + { + + public void TraceCall( + Expression expr, + IEnumerable? focus, + IEnumerable? thisValue, + ITypedElement? index, + IEnumerable totalValue, + IEnumerable result, + IEnumerable>> variables) + { + string exprName; + + switch (expr) + { + case IdentifierExpression _: + return; + + case ConstantExpression ce: + Trace.WriteLine($"{expr.Location.LineNumber},{expr.Location.LinePosition},constant"); + exprName = "constant"; + break; + + case ChildExpression child: + Trace.WriteLine($"{expr.Location.LineNumber},{expr.Location.LinePosition},{child.ChildName}"); + exprName = child.ChildName; + break; + + case IndexerExpression _: + Trace.WriteLine($"{expr.Location.LineNumber},{expr.Location.LinePosition},[]"); + exprName = "[]"; + break; + + case UnaryExpression ue: + Trace.WriteLine($"{expr.Location.LineNumber},{expr.Location.LinePosition},{ue.Op}"); + exprName = ue.Op; + break; + + case BinaryExpression be: + Trace.WriteLine($"{expr.Location.LineNumber},{expr.Location.LinePosition},{be.Op}"); + exprName = be.Op; + break; + + case FunctionCallExpression fe: + Trace.WriteLine($"{expr.Location.LineNumber},{expr.Location.LinePosition},{fe.FunctionName}"); + exprName = fe.FunctionName; + break; + + case NewNodeListInitExpression _: + Trace.WriteLine($"{expr.Location.LineNumber},{expr.Location.LinePosition},{{}} (empty)"); + exprName = "{}"; + break; + + case AxisExpression ae: + if (ae.AxisName == "that") + return; + Trace.WriteLine($"Evaluated: {ae.AxisName} results: {result.Count()}"); + exprName = "$" + ae.AxisName; + break; + + case VariableRefExpression ve: + Trace.WriteLine($"{expr.Location.LineNumber},{expr.Location.LinePosition},%{ve.Name}"); + exprName = "%" + ve.Name; + break; + + default: + exprName = expr.GetType().Name; +#if DEBUG + Debugger.Break(); +#endif + throw new Exception($"Unknown expression type: {expr.GetType().Name}"); + // Trace.WriteLine($"Evaluated: {expr} results: {result.Count()}"); + } + + if (focus != null) + { + foreach (var item in focus) + { + DebugTraceValue($"$focus", item); + } + } + + if (thisValue != null) + { + foreach (var item in thisValue) + { + DebugTraceValue("$this", item); + } + } + + if (index != null) + { + DebugTraceValue("$index", index); + } + + if (totalValue != null) + { + foreach (var item in totalValue) + { + DebugTraceValue($"{exprName} »", item); + } + } + + if (result != null) + { + foreach (var item in result) + { + DebugTraceValue($"{exprName} »", item); + } + } + } + + private static void DebugTraceValue(string exprName, ITypedElement? item) + { + if (item == null) + return; // possible with a null focus to kick things off + if (item.Location == "@primitivevalue@" || item.Location == "@QuantityAsPrimitiveValue@") + Trace.WriteLine($" {exprName}:\t{item.Value}\t({item.InstanceType})"); + else + Trace.WriteLine($" {exprName}:\t{item.Value}\t({item.InstanceType})\t{item.Location}"); + } + } +} diff --git a/src/Hl7.Fhir.Base/FhirPath/Expressions/Closure.cs b/src/Hl7.Fhir.Base/FhirPath/Expressions/Closure.cs index 650f7e9175..15923843dc 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Expressions/Closure.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Expressions/Closure.cs @@ -63,6 +63,11 @@ public static Closure Root(ITypedElement root, EvaluationContext ctx = null) private Dictionary> _namedValues = new Dictionary>(); + internal IEnumerable>> Variables() + { + return _namedValues; + } + public virtual void SetValue(string name, IEnumerable value) { _namedValues.Remove(name); diff --git a/src/Hl7.Fhir.Base/FhirPath/Expressions/DynaDispatcher.cs b/src/Hl7.Fhir.Base/FhirPath/Expressions/DynaDispatcher.cs index 5548c9a249..eacae4d4ca 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Expressions/DynaDispatcher.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Expressions/DynaDispatcher.cs @@ -1,7 +1,7 @@ -/* +/* * Copyright (c) 2015, Firely (info@fire.ly) and contributors * See the file CONTRIBUTORS for details. - * + * * This file is licensed under the BSD 3-Clause license * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE */ @@ -11,6 +11,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using FocusCollection = System.Collections.Generic.IEnumerable; namespace Hl7.FhirPath.Expressions { @@ -25,17 +26,17 @@ public DynaDispatcher(string name, SymbolTable scope) private readonly string _name; private readonly SymbolTable _scope; - public IEnumerable Dispatcher(Closure context, IEnumerable args) + public FocusCollection Dispatcher(Closure context, IEnumerable args, out FocusCollection focus) { - var actualArgs = new List>(); + var actualArgs = new List(); - var focus = args.First()(context, InvokeeFactory.EmptyArgs); + focus = args.First()(context, InvokeeFactory.EmptyArgs, out _); if (!focus.Any()) return ElementNode.EmptyList; actualArgs.Add(focus); var newCtx = context.Nest(focus); - actualArgs.AddRange(args.Skip(1).Select(a => a(newCtx, InvokeeFactory.EmptyArgs))); + actualArgs.AddRange(args.Skip(1).Select(a => a(newCtx, InvokeeFactory.EmptyArgs, out _))); if (actualArgs.Any(aa => !aa.Any())) return ElementNode.EmptyList; var entry = _scope.DynamicGet(_name, actualArgs); @@ -46,9 +47,8 @@ public IEnumerable Dispatcher(Closure context, IEnumerable; namespace Hl7.FhirPath.Expressions { internal class EvaluatorVisitor : FP.ExpressionVisitor { + private Invokee WrapForDebugTracer(Invokee invokee, Expression expression) + { + if (_debugTrace != null) + { + return (Closure context, IEnumerable arguments, out FocusCollection focus) => { + var result = invokee(context, arguments, out focus); + _debugTrace?.TraceCall(expression, focus, context.GetThis(), context.GetIndex()?.FirstOrDefault(), context.GetTotal(), result, context.Variables()); + return result; + }; + } + return invokee; + } + public SymbolTable Symbols { get; } + private IDebugTracer _debugTrace; - public EvaluatorVisitor(SymbolTable symbols) + public EvaluatorVisitor(SymbolTable symbols, IDebugTracer debugTrace = null) { Symbols = symbols; + _debugTrace = debugTrace; } public override Invokee VisitConstant(FP.ConstantExpression expression) { - return InvokeeFactory.Return(ElementNode.ForPrimitive(expression.Value)); + return WrapForDebugTracer(InvokeeFactory.Return(ElementNode.ForPrimitive(expression.Value)), expression); } public override Invokee VisitFunctionCall(FP.FunctionCallExpression expression) { - var focus = expression.Focus.ToEvaluator(Symbols); + var focus = expression.Focus.ToEvaluator(Symbols, _debugTrace); var arguments = new List() { focus }; - arguments.AddRange(expression.Arguments.Select(arg => arg.ToEvaluator(Symbols))); + arguments.AddRange(expression.Arguments.Select(arg => arg.ToEvaluator(Symbols, _debugTrace))); // We have no real type information, so just pass object as the type var types = new List() { typeof(object) }; // for the focus; @@ -42,19 +58,19 @@ public override Invokee VisitFunctionCall(FP.FunctionCallExpression expression) // Now locate the function based on the types and name Invokee boundFunction = resolve(Symbols, expression.FunctionName, types); - return InvokeeFactory.Invoke(expression.FunctionName, arguments, boundFunction); + return WrapForDebugTracer(InvokeeFactory.Invoke(expression.FunctionName, arguments, boundFunction), expression); } public override Invokee VisitNewNodeListInit(FP.NewNodeListInitExpression expression) { - return InvokeeFactory.Return(ElementNode.EmptyList); + return WrapForDebugTracer(InvokeeFactory.Return(ElementNode.EmptyList), expression); } public override Invokee VisitVariableRef(FP.VariableRefExpression expression) { // HACK, for now, $this is special, and we handle in run-time, not compile time... if (expression.Name == "builtin.this") - return InvokeeFactory.GetThis; + return WrapForDebugTracer(InvokeeFactory.GetThis, expression); // HACK, for now, $this is special, and we handle in run-time, not compile time... if (expression.Name == "builtin.that") @@ -62,32 +78,42 @@ public override Invokee VisitVariableRef(FP.VariableRefExpression expression) // HACK, for now, $total is special, and we handle in run-time, not compile time... if (expression.Name == "builtin.total") - return InvokeeFactory.GetTotal; + return WrapForDebugTracer(InvokeeFactory.GetTotal, expression); // HACK, for now, $index is special, and we handle in run-time, not compile time... if (expression.Name == "builtin.index") - return InvokeeFactory.GetIndex; + return WrapForDebugTracer(InvokeeFactory.GetIndex, expression); // HACK, for now, %context is special, and we handle in run-time, not compile time... if (expression.Name == "context") - return InvokeeFactory.GetContext; + return WrapForDebugTracer(InvokeeFactory.GetContext, expression); // HACK, for now, %resource is special, and we handle in run-time, not compile time... if (expression.Name == "resource") - return InvokeeFactory.GetResource; + return WrapForDebugTracer(InvokeeFactory.GetResource, expression); // HACK, for now, %rootResource is special, and we handle in run-time, not compile time... if (expression.Name == "rootResource") - return InvokeeFactory.GetRootResource; + return WrapForDebugTracer(InvokeeFactory.GetRootResource, expression); + + return WrapForDebugTracer(chainResolves, expression); - return chainResolves; - - IEnumerable chainResolves(Closure context, IEnumerable invokees) + FocusCollection chainResolves(Closure context, IEnumerable invokees, out FocusCollection focus) { - return context.ResolveValue(expression.Name) ?? resolve(Symbols, expression.Name, Enumerable.Empty())(context, []); + var value = context.ResolveValue(expression.Name); + if (value != null) + { + // this was in the context, so the scope was $this (the context) + focus = context.GetThis(); + return value; + } + else + { + return resolve(Symbols, expression.Name, Enumerable.Empty())(context, [], out focus); + } } } - + private static Invokee resolve(SymbolTable scope, string name, IEnumerable argumentTypes) { // For now, we don't have the types or the parameters statically, so we just match on name @@ -113,7 +139,7 @@ private static Invokee resolve(SymbolTable scope, string name, IEnumerable } else { - // No function could be found, but there IS a function with the given name, + // No function could be found, but there IS a function with the given name, // report an error about the fact that the function is known, but could not be bound throw Error.Argument("Unknown symbol '{0}'".FormatWith(name)); } @@ -123,11 +149,10 @@ private static Invokee resolve(SymbolTable scope, string name, IEnumerable internal static class EvaluatorExpressionExtensions { - public static Invokee ToEvaluator(this FP.Expression expr, SymbolTable scope) + public static Invokee ToEvaluator(this FP.Expression expr, SymbolTable scope, IDebugTracer debugTrace = null) { - var compiler = new EvaluatorVisitor(scope); + var compiler = new EvaluatorVisitor(scope, debugTrace); return expr.Accept(compiler); } } - } diff --git a/src/Hl7.Fhir.Base/FhirPath/Expressions/ExpressionNode.cs b/src/Hl7.Fhir.Base/FhirPath/Expressions/ExpressionNode.cs index 1a34650a26..effcf16b41 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Expressions/ExpressionNode.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Expressions/ExpressionNode.cs @@ -356,12 +356,12 @@ public string DebuggerDisplay public class ChildExpression : FunctionCallExpression, Sprache.IPositionAware { public ChildExpression(Expression focus, string name) : base(focus, OP_PREFIX + "children", TypeSpecifier.Any, - new ConstantExpression(name, TypeSpecifier.String)) + new IdentifierExpression(name, TypeSpecifier.String)) { } public ChildExpression(Expression focus, string name, ISourcePositionInfo location) : base(focus, OP_PREFIX + "children", TypeSpecifier.Any, - new ConstantExpression(name, TypeSpecifier.String), location) + new IdentifierExpression(name, TypeSpecifier.String), location) { } diff --git a/src/Hl7.Fhir.Base/FhirPath/Expressions/Invokee.cs b/src/Hl7.Fhir.Base/FhirPath/Expressions/Invokee.cs index d492d0db12..09fc79608a 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Expressions/Invokee.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Expressions/Invokee.cs @@ -18,30 +18,49 @@ namespace Hl7.FhirPath.Expressions; -internal delegate FocusCollection Invokee(Closure context, IEnumerable arguments); +internal delegate FocusCollection Invokee(Closure context, IEnumerable arguments, out FocusCollection focus); internal static class InvokeeFactory { public static readonly IEnumerable EmptyArgs = []; - public static FocusCollection GetThis(Closure context, IEnumerable _) => context.GetThis(); + public static FocusCollection GetThis(Closure context, IEnumerable _, out FocusCollection focus) => focus = context.GetThis(); - public static FocusCollection GetTotal(Closure context, IEnumerable _) => context.GetTotal(); + public static FocusCollection GetTotal(Closure context, IEnumerable _, out FocusCollection focus) + { + focus = context.GetThis(); + return context.GetTotal(); + } - public static FocusCollection GetContext(Closure context, IEnumerable _) => - context.GetOriginalContext(); + public static FocusCollection GetContext(Closure context, IEnumerable _, out FocusCollection focus) + { + focus = context.GetThis(); + return context.GetOriginalContext(); + } - public static FocusCollection GetResource(Closure context, IEnumerable _) => - context.GetResource(); + public static FocusCollection GetResource(Closure context, IEnumerable _, out FocusCollection focus) + { + focus = context.GetThis(); + return context.GetResource(); + } - public static FocusCollection GetRootResource(Closure context, IEnumerable arguments) => - context.GetRootResource(); + public static FocusCollection GetRootResource(Closure context, IEnumerable arguments, out FocusCollection focus) + { + focus = context.GetThis(); + return context.GetRootResource(); + } - public static FocusCollection GetThat(Closure context, IEnumerable _) => - context.GetThat(); + public static FocusCollection GetThat(Closure context, IEnumerable _, out FocusCollection focus) + { + focus = context.GetThis(); + return context.GetThat(); + } - public static FocusCollection GetIndex(Closure context, IEnumerable args) => - context.GetIndex(); + public static FocusCollection GetIndex(Closure context, IEnumerable args, out FocusCollection focus) + { + focus = context.GetThis(); + return context.GetIndex(); + } private static readonly Predicate PROPAGATE_WHEN_EMPTY = focus => !focus.Any(); private static readonly Predicate PROPAGATE_NEVER = _ => false; @@ -70,50 +89,110 @@ true when isPrimitiveDotNetType(argType) => PROPAGATE_EMPTY_PRIMITIVE, public static Invokee Wrap(Func func) { - return (_, _) => Typecasts.CastTo(func()); + return (Closure ctx, IEnumerable _, out FocusCollection focus) => + { + focus = ctx.GetThis(); + return Typecasts.CastTo(func()); + }; } public static Invokee Wrap(Func func, bool propNull) { - return (ctx, args) => + return (Closure ctx, IEnumerable args, out FocusCollection focus) => { if (typeof(A) != typeof(EvaluationContext)) { - var focus = args.First()(ctx, EmptyArgs); - if (getPropagator(propNull, typeof(A))(focus)) return ElementNode.EmptyList; + focus = args.First()(ctx, EmptyArgs, out _); + if (getPropagator(propNull, typeof(A))(focus)) + return ElementNode.EmptyList; return Typecasts.CastTo(func(Typecasts.CastTo(focus))); } + else + { + focus = ctx.GetThis(); + } A lastPar = (A)(object)ctx.EvaluationContext; return Typecasts.CastTo(func(lastPar)); }; } + /// /// Wraps a function that is only supposed to propagate null in the focus, not in the other arguments. /// internal static Invokee WrapWithPropNullForFocus(Func func) { - return (ctx, args) => + return (Closure ctx, IEnumerable args, out FocusCollection focus) => { - // propagate only null for focus - var focus = args.First()(ctx, EmptyArgs); - if (getPropagator(true,typeof(A))(focus)) return ElementNode.EmptyList; + // Get the original focus first before any processing + var originalFocus = args.First()(ctx, EmptyArgs, out _); + + // Preserve the original focus for the debug tracer + focus = originalFocus; + + // Check for null propagation condition + if (getPropagator(true, typeof(A))(originalFocus)) + { + return ElementNode.EmptyList; + } - return Wrap(func, false)(ctx, args); + // For the actual function execution, we need a new Invokee that handles the arguments + // but doesn't modify the focus for debug tracing + if (typeof(B) != typeof(EvaluationContext)) + { + var argA = args.Skip(1).First()(ctx, EmptyArgs, out _); + if (getPropagator(false, typeof(B))(argA)) return ElementNode.EmptyList; + + if (typeof(C) != typeof(EvaluationContext)) + { + var argB = args.Skip(2).First()(ctx, EmptyArgs, out _); + if (getPropagator(false, typeof(C))(argB)) return ElementNode.EmptyList; + + return Typecasts.CastTo(func(Typecasts.CastTo(originalFocus), + Typecasts.CastTo(argA), + Typecasts.CastTo(argB))); + } + else + { + C lastPar = (C)(object)ctx.EvaluationContext; + return Typecasts.CastTo(func(Typecasts.CastTo(originalFocus), + Typecasts.CastTo(argA), lastPar)); + } + } + else + { + B argA = (B)(object)ctx.EvaluationContext; + + if (typeof(C) != typeof(EvaluationContext)) + { + var argB = args.Skip(2).First()(ctx, EmptyArgs, out _); + if (getPropagator(false, typeof(C))(argB)) return ElementNode.EmptyList; + + return Typecasts.CastTo(func(Typecasts.CastTo(originalFocus), + argA, + Typecasts.CastTo(argB))); + } + else + { + C lastPar = (C)(object)ctx.EvaluationContext; + return Typecasts.CastTo(func(Typecasts.CastTo(originalFocus), + argA, lastPar)); + } + } }; } public static Invokee Wrap(Func func, bool propNull) { - return (ctx, args) => + return (Closure ctx, IEnumerable args, out FocusCollection focus) => { - var focus = args.First()(ctx, EmptyArgs); + focus = args.First()(ctx, EmptyArgs, out _); if (getPropagator(propNull, typeof(A))(focus)) return ElementNode.EmptyList; if (typeof(B) != typeof(EvaluationContext)) { - var argA = args.Skip(1).First()(ctx, EmptyArgs); + var argA = args.Skip(1).First()(ctx, EmptyArgs, out _); if (getPropagator(propNull, typeof(B))(argA)) return ElementNode.EmptyList; return Typecasts.CastTo(func(Typecasts.CastTo(focus), Typecasts.CastTo(argA))); @@ -128,17 +207,17 @@ public static Invokee Wrap(Func func, bool propNull) public static Invokee Wrap(Func func, bool propNull) { - return (ctx, args) => + return (Closure ctx, IEnumerable args, out FocusCollection focus) => { - var focus = args.First()(ctx, EmptyArgs); + focus = args.First()(ctx, EmptyArgs, out _); if (getPropagator(propNull,typeof(A))(focus)) return ElementNode.EmptyList; - var argA = args.Skip(1).First()(ctx, EmptyArgs); + var argA = args.Skip(1).First()(ctx, EmptyArgs, out _); if (getPropagator(propNull, typeof(B))(argA)) return ElementNode.EmptyList; if (typeof(C) != typeof(EvaluationContext)) { - var argB = args.Skip(2).First()(ctx, EmptyArgs); + var argB = args.Skip(2).First()(ctx, EmptyArgs, out _); if (getPropagator(propNull, typeof(C))(argB)) return ElementNode.EmptyList; return Typecasts.CastTo(func(Typecasts.CastTo(focus), Typecasts.CastTo(argA), @@ -155,19 +234,19 @@ public static Invokee Wrap(Func func, bool propNull) public static Invokee Wrap(Func func, bool propNull) { - return (ctx, args) => + return (Closure ctx, IEnumerable args, out FocusCollection focus) => { - var focus = args.First()(ctx, EmptyArgs); + focus = args.First()(ctx, EmptyArgs, out _); if (getPropagator(propNull, typeof(A))(focus)) return ElementNode.EmptyList; - var argA = args.Skip(1).First()(ctx, EmptyArgs); + var argA = args.Skip(1).First()(ctx, EmptyArgs, out _); if (getPropagator(propNull, typeof(B))(argA)) return ElementNode.EmptyList; - var argB = args.Skip(2).First()(ctx, EmptyArgs); + var argB = args.Skip(2).First()(ctx, EmptyArgs, out _); if (getPropagator(propNull, typeof(C))(argB)) return ElementNode.EmptyList; if (typeof(D) != typeof(EvaluationContext)) { - var argC = args.Skip(3).First()(ctx, EmptyArgs); + var argC = args.Skip(3).First()(ctx, EmptyArgs, out _); if (getPropagator(propNull, typeof(D))(argC)) return ElementNode.EmptyList; return Typecasts.CastTo(func(Typecasts.CastTo(focus), @@ -186,31 +265,41 @@ public static Invokee Wrap(Func func, bool propNul public static Invokee WrapLogic(Func, Func, bool?> func) { - return (ctx, args) => + return (Closure ctx, IEnumerable args, out FocusCollection focus) => { // Ignore focus - // NOT GOOD, arguments need to be evaluated in the context of the focus to give "$that" meaning. + // Arguments to functions (except iterative functions like `where` and `select` that update the value of $this) are not processed on the focus, they are processed on $this. + focus = ctx.GetThis(); var left = args.Skip(1).First(); var right = args.Skip(2).First(); // Return function that actually executes the Invokee at the last moment return Typecasts.CastTo( - func(() => left(ctx, EmptyArgs).BooleanEval(), () => right(ctx, EmptyArgs).BooleanEval())); + func(() => left(ctx, EmptyArgs, out _).BooleanEval(), + () => right(ctx, EmptyArgs, out _).BooleanEval())); }; } - public static Invokee Return(ITypedElement value) => (_, _) => [value]; + public static Invokee Return(ITypedElement value) => (Closure ctx, IEnumerable _, out FocusCollection focus) => + { + focus = ctx.GetThis(); + return [value]; + }; - public static Invokee Return(FocusCollection value) => (_, _) => value; + public static Invokee Return(FocusCollection value) => (Closure ctx, IEnumerable _, out FocusCollection focus) => + { + focus = ctx.GetThis(); + return value; + }; public static Invokee Invoke(string functionName, IEnumerable arguments, Invokee invokee) { - return (ctx, _) => + return (Closure ctx, IEnumerable _, out FocusCollection focus) => { try { var wrappedArguments = arguments.Skip(1).Select(wrapWithNextContext); - return invokee(ctx, [arguments.First(),.. wrappedArguments]); + return invokee(ctx, [arguments.First(),.. wrappedArguments], out focus); } catch (Exception e) { @@ -221,7 +310,7 @@ public static Invokee Invoke(string functionName, IEnumerable arguments static Invokee wrapWithNextContext(Invokee unwrappedArgument) { - return (ctx, args) => unwrappedArgument(ctx.Nest(ctx.GetThis()), args); + return (Closure ctx, IEnumerable args, out FocusCollection focus) => unwrappedArgument(ctx.Nest(ctx.GetThis()), args, out focus); } string formatFunctionName(string name) diff --git a/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableInit.cs b/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableInit.cs index 4b2ba6f94f..64b4b989e2 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableInit.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableInit.cs @@ -1,7 +1,7 @@ -/* +/* * Copyright (c) 2015, Firely (info@fire.ly) and contributors * See the file CONTRIBUTORS for details. - * + * * This file is licensed under the BSD 3-Clause license * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE */ @@ -17,6 +17,7 @@ using System.Linq; using System.Text.RegularExpressions; using P = Hl7.Fhir.ElementModel.Types; +using FocusCollection = System.Collections.Generic.IEnumerable; namespace Hl7.FhirPath.Expressions; @@ -243,13 +244,13 @@ internal static void AddBuiltinChildren(this SymbolTable table) table.Add(new CallSignature("builtin.children", typeof(IEnumerable), typeof(IEnumerable), - typeof(string)), ( - ctx, invokees) => + typeof(string)), (Closure ctx, IEnumerable invokees, out FocusCollection focus) => { var iks = invokees.ToArray(); - var focus = iks[0].Invoke(ctx, InvokeeFactory.EmptyArgs); - var name = (string?)iks[1].Invoke(ctx, InvokeeFactory.EmptyArgs).First().Value; - var result= focus.Navigate(name); + var focusCollection = iks[0](ctx, InvokeeFactory.EmptyArgs, out _); + focus = focusCollection; + var name = (string?)iks[1](ctx, InvokeeFactory.EmptyArgs, out _).First().Value; + var result = focusCollection.Navigate(name); return result; }); @@ -265,78 +266,80 @@ private static string getCoreValueSetUrl(string id) return "http://hl7.org/fhir/ValueSet/" + id; } - private static IEnumerable runAggregate(Closure ctx, IEnumerable arguments) + private static IEnumerable runAggregate(Closure ctx, IEnumerable arguments, out FocusCollection focus) { - var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); + var focusCollection = arguments.First()(ctx, InvokeeFactory.EmptyArgs, out _); + focus = focusCollection; var incrExpre = arguments.Skip(1).First(); IEnumerable initialValue = ElementNode.EmptyList; if (arguments.Count() > 2) { var initialValueExpr = arguments.Skip(2).First(); - initialValue = initialValueExpr(ctx, InvokeeFactory.EmptyArgs); + initialValue = initialValueExpr(ctx, InvokeeFactory.EmptyArgs, out _); } var totalContext = ctx.Nest(); totalContext.SetTotal(initialValue); - foreach (ITypedElement element in focus) + foreach (ITypedElement element in focusCollection) { var newFocus = ElementNode.CreateList(element); var newContext = totalContext.Nest(newFocus); newContext.SetThis(newFocus); newContext.SetTotal(totalContext.GetTotal()); - var newTotalResult = incrExpre(newContext, InvokeeFactory.EmptyArgs); + var newTotalResult = incrExpre(newContext, InvokeeFactory.EmptyArgs, out _); totalContext.SetTotal(newTotalResult); } return totalContext.GetTotal(); } - private static IEnumerable Trace(Closure ctx, IEnumerable arguments) + private static IEnumerable Trace(Closure ctx, IEnumerable arguments, out FocusCollection focus) { - var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); - var name = arguments.Skip(1).First()(ctx, InvokeeFactory.EmptyArgs).FirstOrDefault()?.Value as string; + focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs, out _); + var name = arguments.Skip(1).First()(ctx, InvokeeFactory.EmptyArgs, out _).FirstOrDefault()?.Value as string; List selectArgs = [arguments.First(), .. arguments.Skip(2)]; - var selectResults = runSelect(ctx, selectArgs); + var selectResults = runSelect(ctx, selectArgs, out _); ctx?.EvaluationContext?.Tracer?.Invoke(name, selectResults); return focus; } - private static IEnumerable DefineVariable(Closure ctx, IEnumerable arguments) + private static IEnumerable DefineVariable(Closure ctx, IEnumerable arguments, out FocusCollection focus) { Invokee[] enumerable = arguments as Invokee[] ?? arguments.ToArray(); - var focus = enumerable[0](ctx, InvokeeFactory.EmptyArgs); - var name = enumerable[1](ctx, InvokeeFactory.EmptyArgs).FirstOrDefault()?.Value as string; + var focusCollection = enumerable[0](ctx, InvokeeFactory.EmptyArgs, out _); + focus = focusCollection; + var name = enumerable[1](ctx, InvokeeFactory.EmptyArgs, out _).FirstOrDefault()?.Value as string; if(ctx.ResolveValue(name) is not null) throw new InvalidOperationException($"Variable {name} is already defined in this scope"); if (enumerable.Length == 2) { - ctx.SetValue(name, focus); + ctx.SetValue(name, focusCollection); } else { - var newContext = ctx.Nest(focus); - newContext.SetThis(focus); - var result = enumerable[2](newContext, InvokeeFactory.EmptyArgs); + var newContext = ctx.Nest(focusCollection); + newContext.SetThis(focusCollection); + var result = enumerable[2](newContext, InvokeeFactory.EmptyArgs, out _); ctx.SetValue(name, result); } - return focus; + return focusCollection; } - private static IEnumerable runIif(Closure ctx, IEnumerable arguments) + private static IEnumerable runIif(Closure ctx, IEnumerable arguments, out FocusCollection focus) { // iif(criterion: expression, true-result: collection [, otherwise-result: collection]) : collection // note: short-circuit behavior is expected in this function - var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); + focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs, out _); var newContext = ctx.Nest(focus); newContext.SetThis(focus); - var expression = arguments.Skip(1).First()(newContext, InvokeeFactory.EmptyArgs); + var expression = arguments.Skip(1).First()(newContext, InvokeeFactory.EmptyArgs, out _); var trueResult = arguments.Skip(2).First(); var otherResult = arguments.Skip(3).FirstOrDefault(); @@ -344,13 +347,14 @@ private static IEnumerable runIif(Closure ctx, IEnumerable runWhere(Closure ctx, IEnumerable arguments) + private static IEnumerable runWhere(Closure ctx, IEnumerable arguments, out FocusCollection focus) { - var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); + var focusCollection = arguments.First()(ctx, InvokeeFactory.EmptyArgs, out _); + focus = focusCollection; var lambda = arguments.Skip(1).First(); return CachedEnumerable.Create(runForeach()); @@ -359,7 +363,7 @@ IEnumerable runForeach() { var index = 0; - foreach (ITypedElement element in focus) + foreach (ITypedElement element in focusCollection) { var newFocus = ElementNode.CreateList(element); var newContext = ctx.Nest(newFocus); @@ -367,15 +371,16 @@ IEnumerable runForeach() newContext.SetIndex(ElementNode.CreateList(index)); index++; - if (lambda(newContext, InvokeeFactory.EmptyArgs).BooleanEval() == true) + if (lambda(newContext, InvokeeFactory.EmptyArgs, out _).BooleanEval() == true) yield return element; } } } - private static IEnumerable runSelect(Closure ctx, IEnumerable arguments) + private static IEnumerable runSelect(Closure ctx, IEnumerable arguments, out FocusCollection focus) { - var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); + var focusCollection = arguments.First()(ctx, InvokeeFactory.EmptyArgs, out _); + focus = focusCollection; var lambda = arguments.Skip(1).First(); return CachedEnumerable.Create(runForeach()); @@ -384,7 +389,7 @@ IEnumerable runForeach() { var index = 0; - foreach (ITypedElement element in focus) + foreach (ITypedElement element in focusCollection) { var newFocus = ElementNode.CreateList(element); var newContext = ctx.Nest(newFocus); @@ -392,16 +397,17 @@ IEnumerable runForeach() newContext.SetIndex(ElementNode.CreateList(index)); index++; - var result = lambda(newContext, InvokeeFactory.EmptyArgs); + var result = lambda(newContext, InvokeeFactory.EmptyArgs, out _); foreach (var resultElement in result) // implement SelectMany() yield return resultElement; } } } - private static IEnumerable runRepeat(Closure ctx, IEnumerable arguments) + private static IEnumerable runRepeat(Closure ctx, IEnumerable arguments, out FocusCollection focus) { - var newNodes = arguments.First()(ctx, InvokeeFactory.EmptyArgs).ToList(); + var newNodes = arguments.First()(ctx, InvokeeFactory.EmptyArgs, out _).ToList(); + focus = newNodes.ToArray(); var lambda = arguments.Skip(1).First(); var fullResult = new List(); @@ -420,7 +426,7 @@ private static IEnumerable runRepeat(Closure ctx, IEnumerable runRepeat(Closure ctx, IEnumerable runAll(Closure ctx, IEnumerable arguments) + private static IEnumerable runAll(Closure ctx, IEnumerable arguments, out FocusCollection focus) { - var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); + var focusCollection = arguments.First()(ctx, InvokeeFactory.EmptyArgs, out _); + focus = focusCollection; var lambda = arguments.Skip(1).First(); var index = 0; - foreach (ITypedElement element in focus) + foreach (ITypedElement element in focusCollection) { var newFocus = ElementNode.CreateList(element); var newContext = ctx.Nest(newFocus); @@ -446,7 +453,7 @@ private static IEnumerable runAll(Closure ctx, IEnumerable runAll(Closure ctx, IEnumerable runAny(Closure ctx, IEnumerable arguments) + private static IEnumerable runAny(Closure ctx, IEnumerable arguments, out FocusCollection focus) { - var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); + var focusCollection = arguments.First()(ctx, InvokeeFactory.EmptyArgs, out _); + focus = focusCollection; var lambda = arguments.Skip(1).First(); var index = 0; - foreach (ITypedElement element in focus) + foreach (ITypedElement element in focusCollection) { var newFocus = ElementNode.CreateList(element); var newContext = ctx.Nest(newFocus); @@ -468,7 +476,7 @@ private static IEnumerable runAny(Closure ctx, IEnumerable + /// Compiles a parsed FHIRPath expression into a delegate that can be used to evaluate the expression + /// + /// the parsed fhirpath expression to compile + /// public CompiledExpression Compile(Expression expression) { Invokee inv = expression.ToEvaluator(Symbols); @@ -55,13 +60,46 @@ public CompiledExpression Compile(Expression expression) return (ITypedElement focus, EvaluationContext ctx) => { var closure = Closure.Root(focus, ctx); - return inv(closure, InvokeeFactory.EmptyArgs); + return inv(closure, InvokeeFactory.EmptyArgs, out _); }; } + /// + /// Compiles a parsed FHIRPath expression into a delegate that can be used to evaluate the expression + /// + /// the parsed fhirpath expression to compile + /// An optional delegate to wire into the compilation that traces the processing steps + /// + public CompiledExpression Compile(Expression expression, IDebugTracer debugTrace) + { + Invokee inv = expression.ToEvaluator(Symbols, debugTrace); + + return (ITypedElement focus, EvaluationContext ctx) => + { + var closure = Closure.Root(focus, ctx); + return inv(closure, InvokeeFactory.EmptyArgs, out _); + }; + } + + /// + /// Compiles a FHIRPath expression string into a delegate that can be used to evaluate the expression + /// + /// the fhirpath expression to parse then compile + /// public CompiledExpression Compile(string expression) { return Compile(Parse(expression)); } + + /// + /// Compiles a FHIRPath expression string into a delegate that can be used to evaluate the expression + /// + /// the fhirpath expression to parse then compile + /// An optional delegate to wire into the compilation that traces the processing steps + /// + public CompiledExpression Compile(string expression, IDebugTracer debugTrace) + { + return Compile(Parse(expression), debugTrace); + } } } diff --git a/src/Hl7.Fhir.Base/FhirPath/IDebugTracer.cs b/src/Hl7.Fhir.Base/FhirPath/IDebugTracer.cs new file mode 100644 index 0000000000..990ff9165d --- /dev/null +++ b/src/Hl7.Fhir.Base/FhirPath/IDebugTracer.cs @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2015, Firely (info@fire.ly) and contributors + * See the file CONTRIBUTORS for details. + * + * This file is licensed under the BSD 3-Clause license + * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE + */ +using Hl7.Fhir.ElementModel; +using Hl7.FhirPath.Expressions; +using System.Collections.Generic; + +namespace Hl7.FhirPath +{ + /// + /// An interface for tracing FHIRPath expression results during evaluation. + /// + public interface IDebugTracer + { + void TraceCall(Expression expr, + IEnumerable focus, + IEnumerable thisValue, + ITypedElement index, + IEnumerable totalValue, + IEnumerable result, + IEnumerable>> variables); + } +} diff --git a/src/Hl7.Fhir.Base/FhirPath/Parser/Grammar.cs b/src/Hl7.Fhir.Base/FhirPath/Parser/Grammar.cs index a019b18eed..d4b1bf9796 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Parser/Grammar.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Parser/Grammar.cs @@ -1,7 +1,7 @@ -/* +/* * Copyright (c) 2015, Firely (info@fire.ly) and contributors * See the file CONTRIBUTORS for details. - * + * * This file is licensed under the BSD 3-Clause license * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE */ @@ -131,7 +131,7 @@ public static Parser FunctionParameter(string name) public static Parser FunctionInvocation(Expression focus) { return Function(focus) - .Or(WhitespaceOrComments().Then(wsLeading => Lexer.Identifier.Select(i => new ConstantExpression(i).WithLeadingWS(wsLeading)).Positioned()).Select(i => new ChildExpression(focus, i)).Positioned()) + .Or(WhitespaceOrComments().Then(wsLeading => Lexer.Identifier.Select(i => new IdentifierExpression(i).WithLeadingWS(wsLeading)).Positioned()).Select(i => new ChildExpression(focus, i)).Positioned()) //.XOr(Lexer.Axis.Select(a => new AxisExpression(a))) ; } @@ -152,11 +152,11 @@ select l.CaptureWhitespaceAndComments(wsLeading, wsTrailing) public static Expression BuildVariableRefExpression(SubToken name) { if (name.Value.StartsWith("ext-")) - return new FunctionCallExpression(AxisExpression.That, "builtin.coreexturl", null, null, TypeSpecifier.String, new ConstantExpression(name.Value.Substring(4)).UsePositionFrom(name.Location)); + return new FunctionCallExpression(AxisExpression.That, "builtin.coreexturl", null, null, TypeSpecifier.String, new ConstantExpression(name.Value.Substring(4)).UsePositionFrom(name.Location)).UsePositionFrom(name.Location); #pragma warning disable IDE0046 // Convert to conditional expression else if (name.Value.StartsWith("vs-")) #pragma warning restore IDE0046 // Convert to conditional expression - return new FunctionCallExpression(AxisExpression.That, "builtin.corevsurl", null, null, TypeSpecifier.String, new ConstantExpression(name.Value.Substring(3)).UsePositionFrom(name.Location)); + return new FunctionCallExpression(AxisExpression.That, "builtin.corevsurl", null, null, TypeSpecifier.String, new ConstantExpression(name.Value.Substring(3)).UsePositionFrom(name.Location)).UsePositionFrom(name.Location); else return new VariableRefExpression(name.Value).UsePositionFrom(name.Location); } @@ -237,7 +237,7 @@ from indexer in InvocationExpression private static Parser WrapSubTokenParameter(Parser parser) { - return + return from wsLeading in WhitespaceOrComments() from p in parser.SubTokenWithLeadingWS(wsLeading) select p; @@ -314,7 +314,7 @@ from wsTrailing in WhitespaceOrComments() select op.WithTrailingWS(wsTrailing); // Whitespace or comments - private static Parser> WhitespaceOrComments() => + private static Parser> WhitespaceOrComments() => Parse.WhiteSpace.Many().Select(w => new WhitespaceSubToken(new string(w.ToArray()))).Positioned() .XOr(Lexer.Comment.Select(v => new CommentSubToken(v, false)).Positioned()) .XOr(Lexer.CommentBlock.Select(v => new CommentSubToken(v, true)).Positioned()) diff --git a/src/Hl7.FhirPath.R4.Tests/DebugTracerTests.cs b/src/Hl7.FhirPath.R4.Tests/DebugTracerTests.cs new file mode 100644 index 0000000000..52874b9be7 --- /dev/null +++ b/src/Hl7.FhirPath.R4.Tests/DebugTracerTests.cs @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2015, Firely (info@fire.ly) and contributors + * See the file CONTRIBUTORS for details. + * + * This file is licensed under the BSD 3-Clause license + * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE + */ + +// To introduce the DSTU2 FHIR specification +//extern alias dstu2; + +using Hl7.Fhir.ElementModel; +using Hl7.Fhir.FhirPath; +using Hl7.FhirPath.Expressions; +using Hl7.FhirPath.R4.Tests; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Hl7.FhirPath.Tests +{ + + [TestClass] + public class DebugTracerTest + { + static PatientFixture fixture; + static FhirPathCompiler compiler; + + [ClassInitialize] + public static void Initialize(TestContext ctx) + { + fixture = new PatientFixture(); + compiler = new FhirPathCompiler(); + } + + private class TestDebugTracer: IDebugTracer + { + public List traceOutput = new List(); + public void TraceCall( + Expression expr, + IEnumerable focus, + IEnumerable thisValue, + ITypedElement index, + IEnumerable totalValue, + IEnumerable result, + IEnumerable>> variables) + { + var exprName = TraceExpressionNodeName(expr); + if (exprName == null) + return; // this is a node that we aren't interested in tracing (Identifier and $that) + var pi = expr.Location as FhirPathExpressionLocationInfo; + string output = $"{pi.RawPosition},{pi.Length},{exprName}:" + + $" focus={focus?.Count() ?? 0} result={result?.Count() ?? 0}"; + traceOutput.Add(output); + } + + public string TraceExpressionNodeName(Expression expr) + { + switch (expr) + { + case IdentifierExpression _: + return null; // we don't trace IdentifierExpressions, they are just names + case ConstantExpression ce: + return "constant"; + case ChildExpression child: + return child.ChildName; + case IndexerExpression indexer: + return "[]"; + case UnaryExpression ue: + return ue.Op; + case BinaryExpression be: + return be.Op; + case FunctionCallExpression fe: + return fe.FunctionName; + case NewNodeListInitExpression: + return "{}"; + case AxisExpression ae: + { + if (ae.AxisName == "that") + return null; + return "$" + ae.AxisName; + } + case VariableRefExpression ve: + return "%" + ve.Name; + } +#if DEBUG + Debugger.Break(); +#endif + throw new Exception($"Unknown expression type: {expr.GetType().Name}"); + } + + public void DumpDiagnostics() + { + foreach (var item in traceOutput) + { + System.Diagnostics.Trace.WriteLine(item); + } + } + } + + + + [TestMethod] + public void testDebugTrace_PropertyWalking() + { + var expression = "Patient.birthDate.toString().substring(0, 4)"; + var input = fixture.PatientExample.ToTypedElement().ToScopedNode(); + var tracer = new TestDebugTracer(); + var expr = compiler.Compile(expression, tracer); + var results = expr(input, new FhirEvaluationContext()).ToFhirValues().ToList(); + System.Diagnostics.Trace.WriteLine("Expression: " + expression); + tracer.DumpDiagnostics(); + + Assert.AreEqual(1, results.Count()); + Assert.AreEqual("1974", results[0].ToString()); + + Assert.AreEqual(6, tracer.traceOutput.Count()); + Assert.AreEqual("0,7,Patient: focus=1 result=1", tracer.traceOutput[0]); + Assert.AreEqual("8,9,birthDate: focus=1 result=1", tracer.traceOutput[1]); + Assert.AreEqual("18,8,toString: focus=1 result=1", tracer.traceOutput[2]); + Assert.AreEqual("39,1,constant: focus=1 result=1", tracer.traceOutput[3]); + Assert.AreEqual("42,1,constant: focus=1 result=1", tracer.traceOutput[4]); + Assert.AreEqual("29,9,substring: focus=1 result=1", tracer.traceOutput[5]); + } + + [TestMethod] + public void testDebugTrace_WhereClause() + { + var expression = "name.where(use='official' or use='usual').given"; + + var input = fixture.PatientExample.ToTypedElement().ToScopedNode(); + var tracer = new TestDebugTracer(); + var expr = compiler.Compile(expression, tracer); + var results = expr(input, new FhirEvaluationContext()).ToList(); + System.Diagnostics.Trace.WriteLine("Expression: " + expression); + tracer.DumpDiagnostics(); + + Assert.AreEqual(3, results.Count()); + Assert.AreEqual("Peter", results[0].Value.ToString()); + Assert.AreEqual("James", results[1].Value.ToString()); + Assert.AreEqual("Jim", results[2].Value.ToString()); + + Assert.AreEqual("Patient.name[0].given[0]", results[0].Location); + Assert.AreEqual("Patient.name[0].given[1]", results[1].Location); + Assert.AreEqual("Patient.name[1].given[0]", results[2].Location); + + Assert.AreEqual(14, tracer.traceOutput.Count()); + Assert.AreEqual("0,4,name: focus=1 result=2", tracer.traceOutput[0]); + Assert.AreEqual("11,3,use: focus=1 result=1", tracer.traceOutput[1]); + Assert.AreEqual("15,10,constant: focus=1 result=1", tracer.traceOutput[2]); + Assert.AreEqual("14,1,=: focus=1 result=1", tracer.traceOutput[3]); + Assert.AreEqual("26,2,or: focus=1 result=1", tracer.traceOutput[4]); + Assert.AreEqual("11,3,use: focus=1 result=1", tracer.traceOutput[5]); + Assert.AreEqual("15,10,constant: focus=1 result=1", tracer.traceOutput[6]); + Assert.AreEqual("14,1,=: focus=1 result=1", tracer.traceOutput[7]); + Assert.AreEqual("29,3,use: focus=1 result=1", tracer.traceOutput[8]); + Assert.AreEqual("33,7,constant: focus=1 result=1", tracer.traceOutput[9]); + Assert.AreEqual("32,1,=: focus=1 result=1", tracer.traceOutput[10]); + Assert.AreEqual("26,2,or: focus=1 result=1", tracer.traceOutput[11]); + Assert.AreEqual("5,5,where: focus=2 result=2", tracer.traceOutput[12]); + Assert.AreEqual("42,5,given: focus=2 result=3", tracer.traceOutput[13]); + } + + [TestMethod] + public void testDebugTrace_ConstantValues() + { + var expression = "'42'"; + + var input = fixture.PatientExample.ToTypedElement().ToScopedNode(); + var tracer = new TestDebugTracer(); + var expr = compiler.Compile(expression, tracer); + var results = expr(input, new FhirEvaluationContext()).ToFhirValues().ToList(); + System.Diagnostics.Trace.WriteLine("Expression: " + expression); + tracer.DumpDiagnostics(); + + Assert.AreEqual(1, results.Count()); + Assert.AreEqual("42", results[0].ToString()); + + Assert.AreEqual(1, tracer.traceOutput.Count()); + Assert.AreEqual("0,4,constant: focus=1 result=1", tracer.traceOutput[0]); + } + + [TestMethod] + public void testDebugTrace_GroupedOr() + { + var expression = "id='official' or id='example'"; + + var input = fixture.PatientExample.ToTypedElement().ToScopedNode(); + var tracer = new TestDebugTracer(); + var expr = compiler.Compile(expression, tracer); + var results = expr(input, new FhirEvaluationContext()).ToFhirValues().ToList(); + System.Diagnostics.Trace.WriteLine("Expression: " + expression); + tracer.DumpDiagnostics(); + + Assert.AreEqual(1, results.Count()); + Assert.AreEqual("true", results[0].ToString()); + + Assert.AreEqual(7, tracer.traceOutput.Count()); + Assert.AreEqual("0,2,id: focus=1 result=1", tracer.traceOutput[0]); + Assert.AreEqual("3,10,constant: focus=1 result=1", tracer.traceOutput[1]); + Assert.AreEqual("2,1,=: focus=1 result=1", tracer.traceOutput[2]); + Assert.AreEqual("17,2,id: focus=1 result=1", tracer.traceOutput[3]); + Assert.AreEqual("20,9,constant: focus=1 result=1", tracer.traceOutput[4]); + Assert.AreEqual("19,1,=: focus=1 result=1", tracer.traceOutput[5]); + Assert.AreEqual("14,2,or: focus=1 result=1", tracer.traceOutput[6]); + } + } +} \ No newline at end of file diff --git a/src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathEvaluatorTest.cs b/src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathEvaluatorTest.cs index 5b620b5da0..26bec1eb4f 100644 --- a/src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathEvaluatorTest.cs +++ b/src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathEvaluatorTest.cs @@ -1,7 +1,7 @@ -/* +/* * Copyright (c) 2015, Firely (info@fire.ly) and contributors * See the file CONTRIBUTORS for details. - * + * * This file is licensed under the BSD 3-Clause license * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE */ @@ -86,14 +86,23 @@ public void IsBoolean(string expr, bool result) new XElement("output", new XAttribute("type", "boolean"), new XText(result ? "true" : "false"))); Xdoc.Elements().First().Add(testXml); - Assert.IsTrue(TestInput.IsBoolean(expr, result)); + Assert.IsTrue(IsBoolean(TestInput, expr, result)); } + public bool IsBoolean(Base baseInput, string expression, bool value, EvaluationContext? ctx = null) + { + var input = baseInput.ToTypedElement().ToScopedNode(); + + // Don't use the expression cache as we need to inject the debug tracer + var compiler = new FhirPathCompiler(); + var evaluator = compiler.Compile(expression, new DiagnosticsDebugTracer()); + return evaluator.IsBoolean(value, input, ctx ?? new EvaluationContext()); + } public void IsTrue(string expr, Base input) { - Assert.IsTrue(input.IsBoolean(expr, true)); + Assert.IsTrue(IsBoolean(input, expr, true)); } } @@ -479,7 +488,7 @@ public void use_of_a_variable_in_separate_contexts() [TestMethod] public void use_of_a_variable_in_separate_contexts_defined_in_2_but_used_in_1() { - // this example defines the same variable name in 2 different contexts, + // this example defines the same variable name in 2 different contexts, // but only uses it in the second. This ensures that the first context doesn't remain when using it in another context var expr = "defineVariable('n1', name.first()).where(active.not()) | defineVariable('n1', name.skip(1).first()).select(%n1.given)"; var r = fixture.PatientExample.Select(expr).ToList(); @@ -522,7 +531,7 @@ public void composite_variable_use() } - + [TestMethod] public void use_of_a_variable_outside_context_throws_error() { @@ -554,14 +563,14 @@ public void use_undefined_variable_throws_error() ex.Message.Should().Contain("Unknown symbol 'fam'"); } } - + [TestMethod] public void redefining_variable_throws_error() { var expr = "defineVariable('v1').defineVariable('v1').select(%v1)"; Assert.ThrowsException(() => fixture.PatientExample.Select(expr).ToList()); } - + [TestMethod] public void sequence_of_variable_definitions_tweak() @@ -588,7 +597,7 @@ public void sequence_of_variable_definitions_original() // .toStrictEqual([true, "JimJim"]); } - + [TestMethod] public void multi_tree_vars_valid() { @@ -599,7 +608,7 @@ public void multi_tree_vars_valid() Assert.AreEqual("r1-v2", r.Skip(1).First().ToString()); // .toStrictEqual(["r1-v1", "r1-v2"]); } - + [TestMethod] public void defineVariable_with_compile_success() { diff --git a/src/Hl7.FhirPath.Tests/Tests/BasicFunctionTests.cs b/src/Hl7.FhirPath.Tests/Tests/BasicFunctionTests.cs index 8016f1e7b4..ab079d26a4 100644 --- a/src/Hl7.FhirPath.Tests/Tests/BasicFunctionTests.cs +++ b/src/Hl7.FhirPath.Tests/Tests/BasicFunctionTests.cs @@ -1,7 +1,7 @@ -/* +/* * Copyright (c) 2015, Firely (info@fire.ly) and contributors * See the file CONTRIBUTORS for details. - * + * * This file is licensed under the BSD 3-Clause license * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE */ @@ -23,14 +23,26 @@ public class BasicFunctionsTest { private static void isB(string expr, object value = null) { - ITypedElement dummy = ElementNode.ForPrimitive(value ?? true); - Assert.IsTrue(dummy.IsBoolean(expr, true)); + ITypedElement dummy = ElementNode.ForPrimitive(value ?? true).ToScopedNode(); + var compiler = new FhirPathCompiler(); + var evaluator = compiler.Compile(expr, new DiagnosticsDebugTracer()); + Assert.IsTrue(evaluator.IsBoolean(true, dummy, new EvaluationContext())); } private static object scalar(string expr) { - ITypedElement dummy = ElementNode.ForPrimitive(true); - return dummy.Scalar(expr); + ITypedElement dummy = ElementNode.ForPrimitive(true).ToScopedNode(); + var compiler = new FhirPathCompiler(); + var evaluator = compiler.Compile(expr, new DiagnosticsDebugTracer()); + return evaluator.Scalar(dummy, new EvaluationContext()); + } + + private static object scalar(ITypedElement dummy, string expr) + { + dummy = dummy.ToScopedNode(); + var compiler = new FhirPathCompiler(); + var evaluator = compiler.Compile(expr, new DiagnosticsDebugTracer()); + return evaluator.Scalar(dummy, new EvaluationContext()); } [TestMethod] @@ -41,7 +53,7 @@ public void TestDynaBinding() SourceNode.Valued("child", "Hello world!"), SourceNode.Valued("child", "4")).ToTypedElement(); #pragma warning restore CS0618 // Type or member is internal - Assert.AreEqual("ello", input.Scalar(@"$this.child[0].substring(1,%context.child[1].toInteger())")); + Assert.AreEqual("ello", scalar(input, @"$this.child[0].substring(1,%context.child[1].toInteger())")); } [TestMethod] @@ -217,23 +229,23 @@ public void StringConcatenationAndEmpty() { ITypedElement dummy = ElementNode.ForPrimitive(true); - Assert.AreEqual("ABCDEF", dummy.Scalar("'ABC' + '' + 'DEF'")); - Assert.AreEqual("DEF", dummy.Scalar("'' + 'DEF'")); - Assert.AreEqual("DEF", dummy.Scalar("'DEF' + ''")); + Assert.AreEqual("ABCDEF", scalar(dummy, "'ABC' + '' + 'DEF'")); + Assert.AreEqual("DEF", scalar(dummy, "'' + 'DEF'")); + Assert.AreEqual("DEF", scalar(dummy, "'DEF' + ''")); - Assert.IsNull(dummy.Scalar("{} + 'DEF'")); - Assert.IsNull(dummy.Scalar("'ABC' + {} + 'DEF'")); - Assert.IsNull(dummy.Scalar("'ABC' + {}")); + Assert.IsNull(scalar(dummy, "{} + 'DEF'")); + Assert.IsNull(scalar(dummy, "'ABC' + {} + 'DEF'")); + Assert.IsNull(scalar(dummy, "'ABC' + {}")); - Assert.AreEqual("ABCDEF", dummy.Scalar("'ABC' & '' & 'DEF'")); - Assert.AreEqual("DEF", dummy.Scalar("'' & 'DEF'")); - Assert.AreEqual("DEF", dummy.Scalar("'DEF' & ''")); + Assert.AreEqual("ABCDEF", scalar(dummy, "'ABC' & '' & 'DEF'")); + Assert.AreEqual("DEF", scalar(dummy, "'' & 'DEF'")); + Assert.AreEqual("DEF", scalar(dummy, "'DEF' & ''")); - Assert.AreEqual("DEF", dummy.Scalar("{} & 'DEF'")); - Assert.AreEqual("ABCDEF", dummy.Scalar("'ABC' & {} & 'DEF'")); - Assert.AreEqual("ABC", dummy.Scalar("'ABC' & {}")); + Assert.AreEqual("DEF", scalar(dummy, "{} & 'DEF'")); + Assert.AreEqual("ABCDEF", scalar(dummy, "'ABC' & {} & 'DEF'")); + Assert.AreEqual("ABC", scalar(dummy, "'ABC' & {}")); - Assert.IsNull(dummy.Scalar("'ABC' & {} & 'DEF' + {}")); + Assert.IsNull(scalar(dummy, "'ABC' & {} & 'DEF' + {}")); } [TestMethod] @@ -258,7 +270,7 @@ public void TestStringSplit() Assert.IsNotNull(result); CollectionAssert.AreEqual(new[] { "", "ONE", "", "TWO", "", "", "THREE", "", "" }, result.Select(r => r.Value.ToString()).ToArray()); } - + [DataTestMethod] [DataRow("(1 | 2 | 3).indexOf(3)", 2)] [DataRow("((1 | 2 | 3).combine(2)).indexOf(2, 2)", 3)] diff --git a/src/Hl7.FhirPath.Tests/Tests/FhirPathGrammarTest.cs b/src/Hl7.FhirPath.Tests/Tests/FhirPathGrammarTest.cs index cf63bc92c6..376128d989 100644 --- a/src/Hl7.FhirPath.Tests/Tests/FhirPathGrammarTest.cs +++ b/src/Hl7.FhirPath.Tests/Tests/FhirPathGrammarTest.cs @@ -58,7 +58,7 @@ public void FhirPath_Gramm_Invocation() AxisExpression.This, AxisExpression.Index, new FunctionCallExpression(AxisExpression.That, "somethingElse", TypeSpecifier.Any, new ConstantExpression(true)))); - AssertParser.SucceedsMatch(parser, "as(Patient)", new FunctionCallExpression(AxisExpression.That, "as", TypeSpecifier.Any, new ConstantExpression("Patient"))); + AssertParser.SucceedsMatch(parser, "as(Patient)", new FunctionCallExpression(AxisExpression.That, "as", TypeSpecifier.Any, new IdentifierExpression("Patient"))); var fexRaw = parser.Parse("as(Patient)"); if (fexRaw is FunctionCallExpression fex)