diff --git a/src/Hl7.Fhir.Base/FhirPath/DiagnosticsDebugTracer.cs b/src/Hl7.Fhir.Base/FhirPath/DiagnosticsDebugTracer.cs new file mode 100644 index 0000000000..ef6016f98f --- /dev/null +++ b/src/Hl7.Fhir.Base/FhirPath/DiagnosticsDebugTracer.cs @@ -0,0 +1,157 @@ +/* + * 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, + int contextId, + IEnumerable? focus, + IEnumerable? thisValue, + ITypedElement? index, + IEnumerable totalValue, + IEnumerable result, + IEnumerable>> variables) + { + DiagnosticsDebugTracer.DebugTraceCall(expr, contextId, focus, thisValue, index, totalValue, result, variables); + } + + public static void DebugTraceCall( + Expression expr, + int contextId, + 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 (ctx.id: {contextId})"); + exprName = "constant"; + break; + + case ChildExpression child: + Trace.WriteLine($"{expr.Location.LineNumber},{expr.Location.LinePosition},{child.ChildName} (ctx.id: {contextId})"); + exprName = child.ChildName; + break; + + case IndexerExpression _: + Trace.WriteLine($"{expr.Location.LineNumber},{expr.Location.LinePosition},[] (ctx.id: {contextId})"); + exprName = "[]"; + break; + + case UnaryExpression ue: + Trace.WriteLine($"{expr.Location.LineNumber},{expr.Location.LinePosition},{ue.Op} (ctx.id: {contextId})"); + exprName = ue.Op; + break; + + case BinaryExpression be: + Trace.WriteLine($"{expr.Location.LineNumber},{expr.Location.LinePosition},{be.Op} (ctx.id: {contextId})"); + exprName = be.Op; + break; + + case FunctionCallExpression fe: + Trace.WriteLine($"{expr.Location.LineNumber},{expr.Location.LinePosition},{fe.FunctionName} (ctx.id: {contextId})"); + exprName = fe.FunctionName; + break; + + case NewNodeListInitExpression _: + Trace.WriteLine($"{expr.Location.LineNumber},{expr.Location.LinePosition},{{}} (empty) (ctx.id: {contextId})"); + exprName = "{}"; + break; + + case AxisExpression ae: + if (ae.AxisName == "that") + return; + Trace.WriteLine($"{expr.Location.LineNumber},{expr.Location.LinePosition},${ae.AxisName} (ctx.id: {contextId})"); + exprName = "$" + ae.AxisName; + break; + + case VariableRefExpression ve: + Trace.WriteLine($"{expr.Location.LineNumber},{expr.Location.LinePosition},%{ve.Name} (ctx.id: {contextId})"); + exprName = "%" + ve.Name; + break; + + default: + exprName = expr.GetType().Name; +#if DEBUG + Debugger.Break(); +#endif + throw new Exception($"Unknown expression type: {expr.GetType().Name} (ctx.id: {contextId})"); + // Trace.WriteLine($"Evaluated: {expr} results: {result.Count()}"); + } + + if (result != null) + { + foreach (var item in result) + { + DebugTraceValue($"{exprName} »", item); + } + } + + if (focus != null) + { + foreach (var item in focus) + { + DebugTraceValue($"$focus", item); + } + } + + if (index != null) + { + DebugTraceValue("$index", index); + } + + if (thisValue != null) + { + foreach (var item in thisValue) + { + DebugTraceValue("$this", item); + } + } + + if (totalValue != null) + { + foreach (var item in totalValue) + { + 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/EvaluationContext.cs b/src/Hl7.Fhir.Base/FhirPath/EvaluationContext.cs index 63fb38c261..2ac01b6633 100644 --- a/src/Hl7.Fhir.Base/FhirPath/EvaluationContext.cs +++ b/src/Hl7.Fhir.Base/FhirPath/EvaluationContext.cs @@ -11,7 +11,9 @@ public class EvaluationContext [Obsolete("This method does not initialize any members and will be removed in a future version. Use the empty constructor instead.")] public static EvaluationContext CreateDefault() => new(); - + private int ClosuresCreated { get; set; } = 0; + internal int IncrementClosuresCreatedCount() => ClosuresCreated++; + public EvaluationContext() { // no defaults yet @@ -35,13 +37,13 @@ public EvaluationContext(ITypedElement? resource, ITypedElement? rootResource) Resource = resource; RootResource = rootResource ?? resource; } - + [Obsolete("%resource and %rootResource are inferred from scoped nodes by the evaluator. If you do not have access to a scoped node, or if you wish to explicitly override this behaviour, use the EvaluationContext.WithResourceOverrides() method. Environment can be set explicitly after construction of the base context")] public EvaluationContext(ITypedElement? resource, ITypedElement? rootResource, IDictionary> environment) : this(resource, rootResource) { Environment = environment; } - + /// /// The data represented by %rootResource. /// @@ -61,6 +63,11 @@ public EvaluationContext(ITypedElement? resource, ITypedElement? rootResource, I /// A delegate that handles the output for the trace() function. /// public Action>? Tracer { get; set; } + + /// + /// Gets or sets the tracer used for capturing debug information during evaluation + /// + public IDebugTracer? DebugTracer { get; set; } } public static class EvaluationContextExtensions diff --git a/src/Hl7.Fhir.Base/FhirPath/Expressions/Closure.cs b/src/Hl7.Fhir.Base/FhirPath/Expressions/Closure.cs index 650f7e9175..e2f104dfaa 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Expressions/Closure.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Expressions/Closure.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,31 +17,73 @@ namespace Hl7.FhirPath.Expressions { internal class Closure { - public Closure() + internal int Id { get; private set; } + + public Closure(EvaluationContext ctx) + { + EvaluationContext = ctx; + Id = ctx.IncrementClosuresCreatedCount(); + _debugTracerActive = ctx.DebugTracer != null; + } + + public Closure(Closure parent, EvaluationContext ctx) + { + Parent = parent; + EvaluationContext = ctx; + Id = ctx.IncrementClosuresCreatedCount(); + _debugTracerActive = ctx.DebugTracer != null; + } + + /// + /// When the debug/trace is enabled this property is used to record the focus of the closure. + ///
VALUE IS NOT USED OUTSIDE DEBUG - without debug/tracer, the value is not consistent. + ///
+ /// + /// It is set in the delegate produced for each node by the evaluator visitor. + /// The debug tracer will reset the focus in the closure after calling the delegate it's wrapping. + /// ensuring that argument evaluation doesn't impact the focus logged in the debug trace in other + /// calls. + /// + public IEnumerable focus { + get + { + if (!_debugTracerActive) + return ElementNode.EmptyList; + return _focus; + } + set + { + if (!_debugTracerActive) + return; + _focus = value; + } } + private IEnumerable _focus; + private bool _debugTracerActive = false; + public EvaluationContext EvaluationContext { get; private set; } public static Closure Root(ITypedElement root, EvaluationContext ctx = null) { var newContext = ctx ?? new EvaluationContext(); - + var node = root as ScopedNode; - + newContext.Resource ??= node != null // if the value has been manually set, we do nothing. Otherwise, if the root is a scoped node: ? getResourceFromNode(node) // we infer the resource from the scoped node : (root?.Definition?.IsResource is true // if we do not have a scoped node, we see if this is even a resource to begin with ? root // if it is, we use the root as the resource : null // if not, this breaks the spec in every way (but we will still continue, hopefully we do not need %resource or %rootResource) - ); - + ); + // Same thing, but we copy the resource into the root resource if we cannot infer it from the node. - newContext.RootResource ??= node != null - ? getRootResourceFromNode(node) - : newContext.Resource; - - var newClosure = new Closure() { EvaluationContext = ctx ?? new EvaluationContext() }; + newContext.RootResource ??= node != null + ? getRootResourceFromNode(node) + : newContext.Resource; + + var newClosure = new Closure(ctx ?? new EvaluationContext()); var input = new[] { root }; @@ -49,12 +91,12 @@ public static Closure Root(ITypedElement root, EvaluationContext ctx = null) { newClosure.SetValue(assignment.Key, assignment.Value); } - + newClosure.SetThis(input); newClosure.SetThat(input); newClosure.SetIndex(ElementNode.CreateList(0)); newClosure.SetOriginalContext(input); - + if (newContext.Resource != null) newClosure.SetResource(new[] { newContext.Resource }); if (newContext.RootResource != null) newClosure.SetRootResource(new[] { newContext.RootResource }); @@ -63,6 +105,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); @@ -74,11 +121,7 @@ public virtual void SetValue(string name, IEnumerable value) public virtual Closure Nest() { - return new Closure() - { - Parent = this, - EvaluationContext = this.EvaluationContext - }; + return new Closure(this, EvaluationContext); } @@ -100,7 +143,7 @@ public virtual IEnumerable ResolveValue(string name) } private static ScopedNode getResourceFromNode(ScopedNode node) => node.AtResource ? node : node.ParentResource; - + private static ScopedNode getRootResourceFromNode(ScopedNode node) { var resource = getResourceFromNode(node); diff --git a/src/Hl7.Fhir.Base/FhirPath/Expressions/DynaDispatcher.cs b/src/Hl7.Fhir.Base/FhirPath/Expressions/DynaDispatcher.cs index 5548c9a249..3ee814d80e 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,11 +26,12 @@ 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) { - var actualArgs = new List>(); + var actualArgs = new List(); var focus = args.First()(context, InvokeeFactory.EmptyArgs); + context.focus = focus; if (!focus.Any()) return ElementNode.EmptyList; actualArgs.Add(focus); @@ -46,9 +48,13 @@ public IEnumerable Dispatcher(Closure context, IEnumerable; namespace Hl7.FhirPath.Expressions { internal class EvaluatorVisitor : FP.ExpressionVisitor { + private Invokee WrapForDebugTracer(Invokee invokee, Expression expression) + { + if (_injectDebugHook) + { + return (Closure context, IEnumerable arguments) => { + var oldFocus = context.focus; + var result = invokee(context, arguments); + + context.EvaluationContext.DebugTracer?.TraceCall(expression, context.Id, context.focus, context.GetThis(), context.GetIndex()?.FirstOrDefault(), context.GetTotal(), result, context.Variables()); + + // restore the original focus to the context + context.focus = oldFocus; + return result; + }; + } + return invokee; + } + public SymbolTable Symbols { get; } + private bool _injectDebugHook; - public EvaluatorVisitor(SymbolTable symbols) + public EvaluatorVisitor(SymbolTable symbols, IDebugTracer debugTrace = null) { Symbols = symbols; + _injectDebugHook = true; } + public EvaluatorVisitor(SymbolTable symbols, bool injectDebugHook) + { + Symbols = symbols; + _injectDebugHook = injectDebugHook; + } 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, _injectDebugHook); var arguments = new List() { focus }; - arguments.AddRange(expression.Arguments.Select(arg => arg.ToEvaluator(Symbols))); + arguments.AddRange(expression.Arguments.Select(arg => arg.ToEvaluator(Symbols, _injectDebugHook))); // We have no real type information, so just pass object as the type var types = new List() { typeof(object) }; // for the focus; @@ -42,19 +68,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 +88,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) { - 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) + context.focus = context.GetThis(); + return value; + } + else + { + return resolve(Symbols, expression.Name, Enumerable.Empty())(context, []); + } } } - + 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 +149,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)); } @@ -128,6 +164,11 @@ public static Invokee ToEvaluator(this FP.Expression expr, SymbolTable scope) var compiler = new EvaluatorVisitor(scope); return expr.Accept(compiler); } - } + public static Invokee ToEvaluator(this FP.Expression expr, SymbolTable scope, bool injectDebugTraceHooks) + { + var compiler = new EvaluatorVisitor(scope, injectDebugTraceHooks); + 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..5ca6045c3a 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Expressions/Invokee.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Expressions/Invokee.cs @@ -24,24 +24,48 @@ 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 _) + { + var result = context.GetThis(); + context.focus = result; + return result; + } - public static FocusCollection GetTotal(Closure context, IEnumerable _) => context.GetTotal(); + public static FocusCollection GetTotal(Closure context, IEnumerable _) + { + context.focus = context.GetThis(); + return context.GetTotal(); + } - public static FocusCollection GetContext(Closure context, IEnumerable _) => - context.GetOriginalContext(); + public static FocusCollection GetContext(Closure context, IEnumerable _) + { + context.focus = context.GetThis(); + return context.GetOriginalContext(); + } - public static FocusCollection GetResource(Closure context, IEnumerable _) => - context.GetResource(); + public static FocusCollection GetResource(Closure context, IEnumerable _) + { + context.focus = context.GetThis(); + return context.GetResource(); + } - public static FocusCollection GetRootResource(Closure context, IEnumerable arguments) => - context.GetRootResource(); + public static FocusCollection GetRootResource(Closure context, IEnumerable arguments) + { + context.focus = context.GetThis(); + return context.GetRootResource(); + } - public static FocusCollection GetThat(Closure context, IEnumerable _) => - context.GetThat(); + public static FocusCollection GetThat(Closure context, IEnumerable _) + { + context.focus = context.GetThis(); + return context.GetThat(); + } - public static FocusCollection GetIndex(Closure context, IEnumerable args) => - context.GetIndex(); + public static FocusCollection GetIndex(Closure context, IEnumerable args) + { + context.focus = context.GetThis(); + return context.GetIndex(); + } private static readonly Predicate PROPAGATE_WHEN_EMPTY = focus => !focus.Any(); private static readonly Predicate PROPAGATE_NEVER = _ => false; @@ -70,45 +94,103 @@ true when isPrimitiveDotNetType(argType) => PROPAGATE_EMPTY_PRIMITIVE, public static Invokee Wrap(Func func) { - return (_, _) => Typecasts.CastTo(func()); + return (Closure ctx, IEnumerable _) => + { + ctx.focus = ctx.GetThis(); + return Typecasts.CastTo(func()); + }; } public static Invokee Wrap(Func func, bool propNull) { - return (ctx, args) => + return (Closure ctx, IEnumerable args) => { if (typeof(A) != typeof(EvaluationContext)) { var focus = args.First()(ctx, EmptyArgs); - if (getPropagator(propNull, typeof(A))(focus)) return ElementNode.EmptyList; + ctx.focus = focus; + if (getPropagator(propNull, typeof(A))(focus)) + return ElementNode.EmptyList; return Typecasts.CastTo(func(Typecasts.CastTo(focus))); } + else + { + ctx.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) => { - // propagate only null for focus + // Get the original focus first before any processing var focus = args.First()(ctx, EmptyArgs); - if (getPropagator(true,typeof(A))(focus)) return ElementNode.EmptyList; + ctx.focus = focus; + + // Check for null propagation condition + if (getPropagator(true, typeof(A))(focus)) 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 + // re-wrapping (as the old code did) will fully re-evaluate the focus, again. Which can be VERY expensive in some expressions. + if (typeof(B) != typeof(EvaluationContext)) + { + var argA = args.Skip(1).First()(ctx, EmptyArgs); + if (getPropagator(false, typeof(B))(argA)) return ElementNode.EmptyList; + + if (typeof(C) != typeof(EvaluationContext)) + { + var argB = args.Skip(2).First()(ctx, EmptyArgs); + if (getPropagator(false, typeof(C))(argB)) return ElementNode.EmptyList; + + return Typecasts.CastTo(func(Typecasts.CastTo(focus), + Typecasts.CastTo(argA), + Typecasts.CastTo(argB))); + } + else + { + C lastPar = (C)(object)ctx.EvaluationContext; + return Typecasts.CastTo(func(Typecasts.CastTo(focus), + Typecasts.CastTo(argA), lastPar)); + } + } + else + { + B argA = (B)(object)ctx.EvaluationContext; + + if (typeof(C) != typeof(EvaluationContext)) + { + var argB = args.Skip(2).First()(ctx, EmptyArgs); + if (getPropagator(false, typeof(C))(argB)) return ElementNode.EmptyList; + + return Typecasts.CastTo(func(Typecasts.CastTo(focus), + argA, + Typecasts.CastTo(argB))); + } + else + { + C lastPar = (C)(object)ctx.EvaluationContext; + return Typecasts.CastTo(func(Typecasts.CastTo(focus), + argA, lastPar)); + } + } }; } public static Invokee Wrap(Func func, bool propNull) { - return (ctx, args) => + return (Closure ctx, IEnumerable args) => { var focus = args.First()(ctx, EmptyArgs); + ctx.focus = focus; if (getPropagator(propNull, typeof(A))(focus)) return ElementNode.EmptyList; if (typeof(B) != typeof(EvaluationContext)) @@ -128,9 +210,10 @@ public static Invokee Wrap(Func func, bool propNull) public static Invokee Wrap(Func func, bool propNull) { - return (ctx, args) => + return (Closure ctx, IEnumerable args) => { var focus = args.First()(ctx, EmptyArgs); + ctx.focus = focus; if (getPropagator(propNull,typeof(A))(focus)) return ElementNode.EmptyList; var argA = args.Skip(1).First()(ctx, EmptyArgs); @@ -155,9 +238,10 @@ public static Invokee Wrap(Func func, bool propNull) public static Invokee Wrap(Func func, bool propNull) { - return (ctx, args) => + return (Closure ctx, IEnumerable args) => { var focus = args.First()(ctx, EmptyArgs); + ctx.focus = focus; if (getPropagator(propNull, typeof(A))(focus)) return ElementNode.EmptyList; var argA = args.Skip(1).First()(ctx, EmptyArgs); @@ -186,26 +270,37 @@ public static Invokee Wrap(Func func, bool propNul public static Invokee WrapLogic(Func, Func, bool?> func) { - return (ctx, args) => + return (Closure ctx, IEnumerable args) => { // 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. + ctx.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())); + var result = Typecasts.CastTo( + func(() => left(ctx, EmptyArgs).BooleanEval(), + () => right(ctx, EmptyArgs).BooleanEval())); + return result; }; } - public static Invokee Return(ITypedElement value) => (_, _) => [value]; + public static Invokee Return(ITypedElement value) => (Closure ctx, IEnumerable _) => + { + ctx.focus = ctx.GetThis(); + return [value]; + }; - public static Invokee Return(FocusCollection value) => (_, _) => value; + public static Invokee Return(FocusCollection value) => (Closure ctx, IEnumerable _) => + { + ctx.focus = ctx.GetThis(); + return value; + }; public static Invokee Invoke(string functionName, IEnumerable arguments, Invokee invokee) { - return (ctx, _) => + return (Closure ctx, IEnumerable _) => { try { @@ -221,7 +316,14 @@ 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) => + { + // Bring the context outside the call so that it is created before calling the invokee + // so that the debug tracer which will be injected gets the correct context object in it. + var newContext = ctx.Nest(ctx.GetThis()); + var result = unwrappedArgument(newContext, args); + return result; + }; } 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..4d37e0c6ef 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) => { 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 focus = iks[0](ctx, InvokeeFactory.EmptyArgs); + ctx.focus = focus; + var name = (string?)iks[1](ctx, InvokeeFactory.EmptyArgs).First().Value; + var result = focus.Navigate(name); return result; }); @@ -268,6 +269,7 @@ private static string getCoreValueSetUrl(string id) private static IEnumerable runAggregate(Closure ctx, IEnumerable arguments) { var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); + ctx.focus = focus; var incrExpre = arguments.Skip(1).First(); IEnumerable initialValue = ElementNode.EmptyList; if (arguments.Count() > 2) @@ -283,6 +285,7 @@ private static IEnumerable runAggregate(Closure ctx, IEnumerable< { var newFocus = ElementNode.CreateList(element); var newContext = totalContext.Nest(newFocus); + newContext.focus = newFocus; newContext.SetThis(newFocus); newContext.SetTotal(totalContext.GetTotal()); var newTotalResult = incrExpre(newContext, InvokeeFactory.EmptyArgs); @@ -295,11 +298,12 @@ private static IEnumerable runAggregate(Closure ctx, IEnumerable< private static IEnumerable Trace(Closure ctx, IEnumerable arguments) { var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); + ctx.focus = focus; var name = arguments.Skip(1).First()(ctx, InvokeeFactory.EmptyArgs).FirstOrDefault()?.Value as string; List selectArgs = [arguments.First(), .. arguments.Skip(2)]; var selectResults = runSelect(ctx, selectArgs); - ctx?.EvaluationContext?.Tracer?.Invoke(name, selectResults); + ctx.EvaluationContext?.Tracer?.Invoke(name, selectResults); return focus; } @@ -308,6 +312,7 @@ private static IEnumerable DefineVariable(Closure ctx, IEnumerabl { Invokee[] enumerable = arguments as Invokee[] ?? arguments.ToArray(); var focus = enumerable[0](ctx, InvokeeFactory.EmptyArgs); + ctx.focus = focus; var name = enumerable[1](ctx, InvokeeFactory.EmptyArgs).FirstOrDefault()?.Value as string; if(ctx.ResolveValue(name) is not null) throw new InvalidOperationException($"Variable {name} is already defined in this scope"); @@ -319,6 +324,7 @@ private static IEnumerable DefineVariable(Closure ctx, IEnumerabl else { var newContext = ctx.Nest(focus); + newContext.focus = focus; newContext.SetThis(focus); var result = enumerable[2](newContext, InvokeeFactory.EmptyArgs); ctx.SetValue(name, result); @@ -332,8 +338,10 @@ private static IEnumerable runIif(Closure ctx, IEnumerable runIif(Closure ctx, IEnumerable runWhere(Closure ctx, IEnumerable arguments) { var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); + ctx.focus = focus; var lambda = arguments.Skip(1).First(); return CachedEnumerable.Create(runForeach()); @@ -363,6 +372,7 @@ IEnumerable runForeach() { var newFocus = ElementNode.CreateList(element); var newContext = ctx.Nest(newFocus); + newContext.focus = newFocus; newContext.SetThis(newFocus); newContext.SetIndex(ElementNode.CreateList(index)); index++; @@ -376,6 +386,7 @@ IEnumerable runForeach() private static IEnumerable runSelect(Closure ctx, IEnumerable arguments) { var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); + ctx.focus = focus; var lambda = arguments.Skip(1).First(); return CachedEnumerable.Create(runForeach()); @@ -388,6 +399,7 @@ IEnumerable runForeach() { var newFocus = ElementNode.CreateList(element); var newContext = ctx.Nest(newFocus); + newContext.focus = newFocus; newContext.SetThis(newFocus); newContext.SetIndex(ElementNode.CreateList(index)); index++; @@ -402,6 +414,7 @@ IEnumerable runForeach() private static IEnumerable runRepeat(Closure ctx, IEnumerable arguments) { var newNodes = arguments.First()(ctx, InvokeeFactory.EmptyArgs).ToList(); + ctx.focus = newNodes; var lambda = arguments.Skip(1).First(); var fullResult = new List(); @@ -416,14 +429,15 @@ private static IEnumerable runRepeat(Closure ctx, IEnumerable runRepeat(Closure ctx, IEnumerable runAll(Closure ctx, IEnumerable arguments) { var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); + ctx.focus = focus; var lambda = arguments.Skip(1).First(); var index = 0; @@ -442,6 +457,7 @@ private static IEnumerable runAll(Closure ctx, IEnumerable runAll(Closure ctx, IEnumerable runAny(Closure ctx, IEnumerable arguments) { var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); + ctx.focus = focus; var lambda = arguments.Skip(1).First(); var index = 0; @@ -464,6 +481,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); @@ -59,9 +64,42 @@ public CompiledExpression Compile(Expression expression) }; } + /// + /// Compiles a parsed FHIRPath expression into a delegate that can be used to evaluate the expression + /// + /// the parsed fhirpath expression to compile + /// Inject the required hooks into the compiled evaluator to support debug tracing via the EvaluationContext + /// + public CompiledExpression Compile(Expression expression, bool injectDebugTraceHooks) + { + Invokee inv = expression.ToEvaluator(Symbols, injectDebugTraceHooks); + + return (ITypedElement focus, EvaluationContext ctx) => + { + var closure = Closure.Root(focus, ctx); + return inv(closure, InvokeeFactory.EmptyArgs); + }; + } + + /// + /// 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 + /// Inject the required hooks into the compiled evaluator to support debug tracing via the EvaluationContext + /// + public CompiledExpression Compile(string expression, bool injectDebugTraceHooks) + { + return Compile(Parse(expression), injectDebugTraceHooks); + } } } diff --git a/src/Hl7.Fhir.Base/FhirPath/IDebugTracer.cs b/src/Hl7.Fhir.Base/FhirPath/IDebugTracer.cs new file mode 100644 index 0000000000..62953a33c1 --- /dev/null +++ b/src/Hl7.Fhir.Base/FhirPath/IDebugTracer.cs @@ -0,0 +1,28 @@ +/* + * 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, + int contextId, + 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..0b44a8e243 --- /dev/null +++ b/src/Hl7.FhirPath.R4.Tests/DebugTracerTests.cs @@ -0,0 +1,509 @@ +/* + * 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; +using System.Runtime.ExceptionServices; + +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(); + private List exceptions = new List(); + + public void Assert() + { + if (exceptions.Count == 0) + return; // no exceptions to throw + System.Diagnostics.Trace.WriteLine($"Tracer exceptions: {exceptions.Count}"); + foreach (var item in exceptions) + { + item.Throw(); + } + } + + + public void TraceCall( + Expression expr, + int contextId, + IEnumerable focus, + IEnumerable thisValue, + ITypedElement index, + IEnumerable totalValue, + IEnumerable result, + IEnumerable>> variables) + { + // DiagnosticsDebugTracer.DebugTraceCall(expr, contextId, focus, thisValue, index, totalValue, result, 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); + if (TraceNode != null) + { + try + { + TraceNode(traceOutput.Count - 1, expr, contextId, + focus, thisValue, index, totalValue, result); + } + catch(Exception e) + { + // swallow the exception while tracing during testing, then after evaluation + // is complete, we can throw them. + exceptions.Add(ExceptionDispatchInfo.Capture(e)); + } + } + } + + public delegate void TraceNodeDelegate(int n, Expression expr, int contextId, + IEnumerable focus, + IEnumerable thisValue, + ITypedElement index, + IEnumerable totalValue, + IEnumerable result); + public TraceNodeDelegate TraceNode { get; set; } = null; + + 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() + { + System.Diagnostics.Trace.WriteLine("---"); + foreach (var item in traceOutput) + { + System.Diagnostics.Trace.WriteLine(item); + } + } + + public string DebugTraceValue(ITypedElement? item) + { + if (item == null) + return null; // possible with a null focus to kick things off + + if (item.Location == "@primitivevalue@" || item.Location == "@QuantityAsPrimitiveValue@") + return $"{item.Value}\t({item.InstanceType})"; + + return $"{item.Value}\t({item.InstanceType})\t{item.Location}"; + } + } + + [TestMethod] + public void testDebugTrace_PropertyWalking() + { + var expression = "Patient.birthDate.toString().substring(0, 4)"; + var input = fixture.PatientExample.ToTypedElement().ToScopedNode(); + var tracer = new TestDebugTracer(); + tracer.TraceNode = (n, expr, contextId, focus, thisValue, index, totalValue, result) => + { + DiagnosticsDebugTracer.DebugTraceCall(expr, contextId, focus, thisValue, index, totalValue, result, null); + var vThis = tracer.DebugTraceValue(thisValue?.FirstOrDefault()); + Assert.AreEqual("\t(Patient)\tPatient", vThis); + var vFocus = tracer.DebugTraceValue(focus?.FirstOrDefault()); + var vResult = tracer.DebugTraceValue(result?.FirstOrDefault()); + + if (n == 2) + { + // toString + Assert.AreEqual("1974-12-25\t(date)\tPatient.birthDate[0]", vFocus); + Assert.AreEqual("1974-12-25\t(System.String)", vResult); + } + if (n == 3) + { + // constant 0 + Assert.AreEqual("\t(Patient)\tPatient", vFocus); + Assert.AreEqual("0\t(System.Integer)", vResult); + } + if (n == 4) + { + // constant 4 + Assert.AreEqual("\t(Patient)\tPatient", vFocus); + Assert.AreEqual("4\t(System.Integer)", vResult); + } + if (n == 5) + { + // substring + Assert.AreEqual("1974-12-25\t(System.String)", vFocus); + Assert.AreEqual("1974\t(System.String)", vResult); + } + }; + var expr = compiler.Compile(expression, true); + Trace.WriteLine("Expression: " + expression + "\r\n"); + var results = expr(input, new FhirEvaluationContext() { DebugTracer = tracer }).ToFhirValues().ToList(); + 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]); + + // Now check the tracer assertions + tracer.Assert(); + } + + [TestMethod] + public void testDebugTrace_PropertyAndFunctionCalls() + { + var expression = "Patient.id.indexOf('am')"; + var input = fixture.PatientExample.ToTypedElement().ToScopedNode(); + var tracer = new TestDebugTracer(); + tracer.TraceNode = (n, expr, contextId, focus, thisValue, index, totalValue, result) => + { + DiagnosticsDebugTracer.DebugTraceCall(expr, contextId, focus, thisValue, index, totalValue, result, null); + var vThis = tracer.DebugTraceValue(thisValue?.FirstOrDefault()); + var vFocus = tracer.DebugTraceValue(focus?.FirstOrDefault()); + var vResult = tracer.DebugTraceValue(result?.FirstOrDefault()); + Assert.AreEqual("\t(Patient)\tPatient", vThis); // in this specific expression, this is always the patient + if (n == 2) + { + // the context and results of the constant 'am' call + Assert.AreEqual("\t(Patient)\tPatient", vFocus); + Assert.AreEqual("am\t(System.String)", vResult); + } + if (n == 3) + { + // the context and results of indexOf call + Assert.AreEqual("example\t(id)\tPatient.id[0]", vFocus); + Assert.AreEqual("2\t(System.Integer)", vResult); + } + }; + var expr = compiler.Compile(expression, true); + Trace.WriteLine("Expression: " + expression + "\r\n"); + var results = expr(input, new FhirEvaluationContext() { DebugTracer = tracer }).ToFhirValues().ToList(); + tracer.DumpDiagnostics(); + + Assert.AreEqual(1, results.Count()); + Assert.AreEqual("2", results[0].ToString()); + + Assert.AreEqual(4, tracer.traceOutput.Count()); + Assert.AreEqual("0,7,Patient: focus=1 result=1", tracer.traceOutput[0]); + Assert.AreEqual("8,2,id: focus=1 result=1", tracer.traceOutput[1]); + Assert.AreEqual("19,4,constant: focus=1 result=1", tracer.traceOutput[2]); + Assert.AreEqual("11,7,indexOf: focus=1 result=1", tracer.traceOutput[3]); + + // Now check the tracer assertions + tracer.Assert(); + } + + [TestMethod] + public void testDebugTrace_Aggregate() + { + var expression = "(1|2).aggregate($total+$this, 0)"; + var input = fixture.PatientExample.ToTypedElement().ToScopedNode(); + var tracer = new TestDebugTracer(); + tracer.TraceNode = (n, expr, contextId, focus, thisValue, index, totalValue, result) => + { + // TODO: Check the focus values. + if (n == 2) + { + // the results of the | operator + DiagnosticsDebugTracer.DebugTraceCall(expr, contextId, focus, thisValue, index, totalValue, result, null); + var vThis = tracer.DebugTraceValue(thisValue?.FirstOrDefault()); + var vFocus = tracer.DebugTraceValue(focus?.FirstOrDefault()); + var vResult1 = tracer.DebugTraceValue(result?.FirstOrDefault()); + var vResult2 = tracer.DebugTraceValue(result?.Skip(1)?.FirstOrDefault()); + Assert.AreEqual(0, contextId); + } + if (n == 3) { + // the results of the constant "0" for the init expression + DiagnosticsDebugTracer.DebugTraceCall(expr, contextId, focus, thisValue, index, totalValue, result, null); + var v1 = tracer.DebugTraceValue(focus?.FirstOrDefault()); + var v2 = tracer.DebugTraceValue(focus?.Skip(1)?.FirstOrDefault()); + // Assert.AreEqual(3, contextId); + } + }; + + var expr = compiler.Compile(expression, true); + Trace.WriteLine("Expression: " + expression + "\r\n"); + var results = expr(input, new FhirEvaluationContext() { DebugTracer = tracer }).ToFhirValues().ToList(); + tracer.DumpDiagnostics(); + + Assert.AreEqual(1, results.Count()); + Assert.AreEqual("3", results[0].ToString()); + + // Now check the tracer outputs + Assert.AreEqual(11, tracer.traceOutput.Count()); + int n = 0; + Assert.AreEqual("1,1,constant: focus=1 result=1", tracer.traceOutput[n++]); + Assert.AreEqual("3,1,constant: focus=1 result=1", tracer.traceOutput[n++]); + Assert.AreEqual("2,1,|: focus=1 result=2", tracer.traceOutput[n++]); + Assert.AreEqual("30,1,constant: focus=1 result=1", tracer.traceOutput[n++]); + Assert.AreEqual("16,6,$total: focus=1 result=1", tracer.traceOutput[n++]); + Assert.AreEqual("23,5,$this: focus=1 result=1", tracer.traceOutput[n++]); + Assert.AreEqual("22,1,+: focus=1 result=1", tracer.traceOutput[n++]); + Assert.AreEqual("16,6,$total: focus=1 result=1", tracer.traceOutput[n++]); + Assert.AreEqual("23,5,$this: focus=1 result=1", tracer.traceOutput[n++]); + Assert.AreEqual("22,1,+: focus=1 result=1", tracer.traceOutput[n++]); + Assert.AreEqual("6,9,aggregate: focus=2 result=1", tracer.traceOutput[n++]); + + // Now check the tracer assertions + tracer.Assert(); + } + + [TestMethod] + public void testDebugTrace_Operator() + { + var expression = "Patient.id.toString() = Patient.id"; + var input = fixture.PatientExample.ToTypedElement().ToScopedNode(); + var tracer = new TestDebugTracer(); + tracer.TraceNode = (n, expr, contextId, focus, thisValue, index, totalValue, result) => + { + DiagnosticsDebugTracer.DebugTraceCall(expr, contextId, focus, thisValue, index, totalValue, result, null); + var vThis = tracer.DebugTraceValue(thisValue?.FirstOrDefault()); + Assert.AreEqual("\t(Patient)\tPatient", vThis); + var vFocus = tracer.DebugTraceValue(focus?.FirstOrDefault()); + if (n == 2) + { + // the context and results of toString call + var vResult = tracer.DebugTraceValue(result?.FirstOrDefault()); + Assert.AreEqual("example\t(id)\tPatient.id[0]", vFocus); + Assert.AreEqual("example\t(System.String)", vResult); + } + }; + + var expr = compiler.Compile(expression, true); + Trace.WriteLine("Expression: " + expression + "\r\n"); + var results = expr(input, new FhirEvaluationContext() { DebugTracer = tracer }).ToFhirValues().ToList(); + tracer.DumpDiagnostics(); + + Assert.AreEqual(1, results.Count()); + Assert.AreEqual("true", results[0].ToString()); + + // Now check the tracer outputs + Assert.AreEqual(6, tracer.traceOutput.Count()); + int n = 0; + Assert.AreEqual("0,7,Patient: focus=1 result=1", tracer.traceOutput[n++]); + Assert.AreEqual("8,2,id: focus=1 result=1", tracer.traceOutput[n++]); + Assert.AreEqual("11,8,toString: focus=1 result=1", tracer.traceOutput[n++]); + Assert.AreEqual("24,7,Patient: focus=1 result=1", tracer.traceOutput[n++]); + Assert.AreEqual("32,2,id: focus=1 result=1", tracer.traceOutput[n++]); + Assert.AreEqual("22,1,=: focus=1 result=1", tracer.traceOutput[n++]); + + // Now check the tracer assertions + tracer.Assert(); + } + + [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(); + tracer.TraceNode = (n, expr, contextId, focus, thisValue, index, totalValue, result) => + { + DiagnosticsDebugTracer.DebugTraceCall(expr, contextId, focus, thisValue, index, totalValue, result, null); + var vThis = tracer.DebugTraceValue(thisValue?.FirstOrDefault()); + var vFocus = tracer.DebugTraceValue(focus?.FirstOrDefault()); + var vResult = tracer.DebugTraceValue(result?.FirstOrDefault()); + var vIndex= index?.Value; + if (n == 0) + { + // name + Assert.AreEqual("\t(Patient)\tPatient", vThis); + Assert.AreEqual("\t(Patient)\tPatient", vFocus); + Assert.AreEqual(2, result.Count()); + } + + if (n == 1 || n == 2 || n == 3 || n == 4) + { + Assert.AreEqual("\t(HumanName)\tPatient.name[0]", vThis); + Assert.AreEqual("\t(HumanName)\tPatient.name[0]", vFocus); + Assert.AreEqual(0, vIndex); + } + if (n >= 5 && n <= 11) + { + Assert.AreEqual("\t(HumanName)\tPatient.name[1]", vThis); + Assert.AreEqual("\t(HumanName)\tPatient.name[1]", vFocus); + Assert.AreEqual(1, vIndex); + } + + if (n == 12) + { + // Where clause + Assert.AreEqual("\t(Patient)\tPatient", vThis); + Assert.AreEqual("\t(HumanName)\tPatient.name[0]", vFocus); + Assert.AreEqual(2, focus.Count()); + Assert.AreEqual(2, result.Count()); + Assert.AreEqual("\t(HumanName)\tPatient.name[0]", vResult); + } + if (n == 13) + { + // The final given prop navigator + Assert.AreEqual("\t(Patient)\tPatient", vThis); + Assert.AreEqual("\t(HumanName)\tPatient.name[0]", vFocus); + Assert.AreEqual(2, focus.Count()); + Assert.AreEqual(3, result.Count()); + } + }; + var expr = compiler.Compile(expression, true); + Trace.WriteLine("Expression: " + expression + "\r\n"); + var results = expr(input, new FhirEvaluationContext() { DebugTracer = tracer }).ToList(); + 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]); + + // Now check the tracer assertions + tracer.Assert(); + } + + [TestMethod] + public void testDebugTrace_ConstantValues() + { + var expression = "'42'"; + + var input = fixture.PatientExample.ToTypedElement().ToScopedNode(); + var tracer = new TestDebugTracer(); + tracer.TraceNode = (n, expr, contextId, focus, thisValue, index, totalValue, result) => + { + DiagnosticsDebugTracer.DebugTraceCall(expr, contextId, focus, thisValue, index, totalValue, result, null); + }; + var expr = compiler.Compile(expression, true); + Trace.WriteLine("Expression: " + expression + "\r\n"); + var results = expr(input, new FhirEvaluationContext() { DebugTracer = tracer }).ToFhirValues().ToList(); + 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]); + + // Now check the tracer assertions + tracer.Assert(); + } + + [TestMethod] + public void testDebugTrace_GroupedOr() + { + var expression = "id='official' or id='example'"; + + var input = fixture.PatientExample.ToTypedElement().ToScopedNode(); + var tracer = new TestDebugTracer(); + tracer.TraceNode = (n, expr, contextId, focus, thisValue, index, totalValue, result) => + { + DiagnosticsDebugTracer.DebugTraceCall(expr, contextId, focus, thisValue, index, totalValue, result, null); + + // interestingly all the nodes in this expression have the same focus and $this value + var vThis = tracer.DebugTraceValue(thisValue?.FirstOrDefault()); + var vFocus = tracer.DebugTraceValue(focus?.FirstOrDefault()); + Assert.AreEqual("\t(Patient)\tPatient", vThis); + Assert.AreEqual("\t(Patient)\tPatient", vFocus); + + + }; + var expr = compiler.Compile(expression, true); + Trace.WriteLine("Expression: " + expression + "\r\n"); + var results = expr(input, new FhirEvaluationContext() { DebugTracer = tracer }).ToFhirValues().ToList(); + 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]); + + // Now check the tracer assertions + tracer.Assert(); + } + } +} \ 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..0abcb4a138 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,29 @@ 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, true); + + System.Diagnostics.Trace.WriteLine(""); + System.Diagnostics.Trace.WriteLine("------------------------------------"); + System.Diagnostics.Trace.WriteLine(expression); + System.Diagnostics.Trace.WriteLine("------------------------------------"); + + return evaluator.IsBoolean(value, input, ctx ?? new EvaluationContext() { DebugTracer = new DiagnosticsDebugTracer() }); + } public void IsTrue(string expr, Base input) { - Assert.IsTrue(input.IsBoolean(expr, true)); + Assert.IsTrue(IsBoolean(input, expr, true)); } } @@ -479,7 +494,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 +537,7 @@ public void composite_variable_use() } - + [TestMethod] public void use_of_a_variable_outside_context_throws_error() { @@ -554,14 +569,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 +603,7 @@ public void sequence_of_variable_definitions_original() // .toStrictEqual([true, "JimJim"]); } - + [TestMethod] public void multi_tree_vars_valid() { @@ -599,7 +614,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..594f6cdd30 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, true); + Assert.IsTrue(evaluator.IsBoolean(true, dummy, new EvaluationContext() { DebugTracer = new DiagnosticsDebugTracer() })); } 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, true); + return evaluator.Scalar(dummy, new EvaluationContext() { DebugTracer = new DiagnosticsDebugTracer() }); + } + + private static object scalar(ITypedElement dummy, string expr) + { + dummy = dummy.ToScopedNode(); + var compiler = new FhirPathCompiler(); + var evaluator = compiler.Compile(expr, true); + return evaluator.Scalar(dummy, new EvaluationContext() { DebugTracer = new DiagnosticsDebugTracer() }); } [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)