Skip to content

Commit eb070a2

Browse files
authored
Merge pull request #3210 from FirelyTeam/feature/BP-fhirpath-debugger
Introduce a fhirpath debug tracer
2 parents 6a16c41 + 3489cfc commit eb070a2

File tree

15 files changed

+1109
-133
lines changed

15 files changed

+1109
-133
lines changed
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
* Copyright (c) 2015, Firely (info@fire.ly) and contributors
3+
* See the file CONTRIBUTORS for details.
4+
*
5+
* This file is licensed under the BSD 3-Clause license
6+
* available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE
7+
*/
8+
9+
#nullable enable
10+
11+
using Hl7.Fhir.ElementModel;
12+
using Hl7.FhirPath.Expressions;
13+
using System;
14+
using System.Collections.Generic;
15+
using System.Diagnostics;
16+
using System.Linq;
17+
18+
namespace Hl7.FhirPath
19+
{
20+
21+
public class DiagnosticsDebugTracer : IDebugTracer
22+
{
23+
public void TraceCall(
24+
Expression expr,
25+
int contextId,
26+
IEnumerable<ITypedElement>? focus,
27+
IEnumerable<ITypedElement>? thisValue,
28+
ITypedElement? index,
29+
IEnumerable<ITypedElement> totalValue,
30+
IEnumerable<ITypedElement> result,
31+
IEnumerable<KeyValuePair<string, IEnumerable<ITypedElement>>> variables)
32+
{
33+
DiagnosticsDebugTracer.DebugTraceCall(expr, contextId, focus, thisValue, index, totalValue, result, variables);
34+
}
35+
36+
public static void DebugTraceCall(
37+
Expression expr,
38+
int contextId,
39+
IEnumerable<ITypedElement>? focus,
40+
IEnumerable<ITypedElement>? thisValue,
41+
ITypedElement? index,
42+
IEnumerable<ITypedElement> totalValue,
43+
IEnumerable<ITypedElement> result,
44+
IEnumerable<KeyValuePair<string, IEnumerable<ITypedElement>>> variables)
45+
{
46+
string exprName;
47+
48+
switch (expr)
49+
{
50+
case IdentifierExpression _:
51+
return;
52+
53+
case ConstantExpression ce:
54+
Trace.WriteLine($"{expr.Location.LineNumber},{expr.Location.LinePosition},constant (ctx.id: {contextId})");
55+
exprName = "constant";
56+
break;
57+
58+
case ChildExpression child:
59+
Trace.WriteLine($"{expr.Location.LineNumber},{expr.Location.LinePosition},{child.ChildName} (ctx.id: {contextId})");
60+
exprName = child.ChildName;
61+
break;
62+
63+
case IndexerExpression _:
64+
Trace.WriteLine($"{expr.Location.LineNumber},{expr.Location.LinePosition},[] (ctx.id: {contextId})");
65+
exprName = "[]";
66+
break;
67+
68+
case UnaryExpression ue:
69+
Trace.WriteLine($"{expr.Location.LineNumber},{expr.Location.LinePosition},{ue.Op} (ctx.id: {contextId})");
70+
exprName = ue.Op;
71+
break;
72+
73+
case BinaryExpression be:
74+
Trace.WriteLine($"{expr.Location.LineNumber},{expr.Location.LinePosition},{be.Op} (ctx.id: {contextId})");
75+
exprName = be.Op;
76+
break;
77+
78+
case FunctionCallExpression fe:
79+
Trace.WriteLine($"{expr.Location.LineNumber},{expr.Location.LinePosition},{fe.FunctionName} (ctx.id: {contextId})");
80+
exprName = fe.FunctionName;
81+
break;
82+
83+
case NewNodeListInitExpression _:
84+
Trace.WriteLine($"{expr.Location.LineNumber},{expr.Location.LinePosition},{{}} (empty) (ctx.id: {contextId})");
85+
exprName = "{}";
86+
break;
87+
88+
case AxisExpression ae:
89+
if (ae.AxisName == "that")
90+
return;
91+
Trace.WriteLine($"{expr.Location.LineNumber},{expr.Location.LinePosition},${ae.AxisName} (ctx.id: {contextId})");
92+
exprName = "$" + ae.AxisName;
93+
break;
94+
95+
case VariableRefExpression ve:
96+
Trace.WriteLine($"{expr.Location.LineNumber},{expr.Location.LinePosition},%{ve.Name} (ctx.id: {contextId})");
97+
exprName = "%" + ve.Name;
98+
break;
99+
100+
default:
101+
exprName = expr.GetType().Name;
102+
#if DEBUG
103+
Debugger.Break();
104+
#endif
105+
throw new Exception($"Unknown expression type: {expr.GetType().Name} (ctx.id: {contextId})");
106+
// Trace.WriteLine($"Evaluated: {expr} results: {result.Count()}");
107+
}
108+
109+
if (result != null)
110+
{
111+
foreach (var item in result)
112+
{
113+
DebugTraceValue($"{exprName} »", item);
114+
}
115+
}
116+
117+
if (focus != null)
118+
{
119+
foreach (var item in focus)
120+
{
121+
DebugTraceValue($"$focus", item);
122+
}
123+
}
124+
125+
if (index != null)
126+
{
127+
DebugTraceValue("$index", index);
128+
}
129+
130+
if (thisValue != null)
131+
{
132+
foreach (var item in thisValue)
133+
{
134+
DebugTraceValue("$this", item);
135+
}
136+
}
137+
138+
if (totalValue != null)
139+
{
140+
foreach (var item in totalValue)
141+
{
142+
DebugTraceValue($"{exprName} »", item);
143+
}
144+
}
145+
}
146+
147+
private static void DebugTraceValue(string exprName, ITypedElement? item)
148+
{
149+
if (item == null)
150+
return; // possible with a null focus to kick things off
151+
if (item.Location == "@primitivevalue@" || item.Location == "@QuantityAsPrimitiveValue@")
152+
Trace.WriteLine($" {exprName}:\t{item.Value}\t({item.InstanceType})");
153+
else
154+
Trace.WriteLine($" {exprName}:\t{item.Value}\t({item.InstanceType})\t{item.Location}");
155+
}
156+
}
157+
}

src/Hl7.Fhir.Base/FhirPath/EvaluationContext.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ public class EvaluationContext
1111
[Obsolete("This method does not initialize any members and will be removed in a future version. Use the empty constructor instead.")]
1212
public static EvaluationContext CreateDefault() => new();
1313

14-
14+
private int ClosuresCreated { get; set; } = 0;
15+
internal int IncrementClosuresCreatedCount() => ClosuresCreated++;
16+
1517
public EvaluationContext()
1618
{
1719
// no defaults yet
@@ -35,13 +37,13 @@ public EvaluationContext(ITypedElement? resource, ITypedElement? rootResource)
3537
Resource = resource;
3638
RootResource = rootResource ?? resource;
3739
}
38-
40+
3941
[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")]
4042
public EvaluationContext(ITypedElement? resource, ITypedElement? rootResource, IDictionary<string, IEnumerable<ITypedElement>> environment) : this(resource, rootResource)
4143
{
4244
Environment = environment;
4345
}
44-
46+
4547
/// <summary>
4648
/// The data represented by <c>%rootResource</c>.
4749
/// </summary>
@@ -61,6 +63,11 @@ public EvaluationContext(ITypedElement? resource, ITypedElement? rootResource, I
6163
/// A delegate that handles the output for the <c>trace()</c> function.
6264
/// </summary>
6365
public Action<string?, IEnumerable<ITypedElement>>? Tracer { get; set; }
66+
67+
/// <summary>
68+
/// Gets or sets the tracer used for capturing debug information during evaluation
69+
/// </summary>
70+
public IDebugTracer? DebugTracer { get; set; }
6471
}
6572

6673
public static class EvaluationContextExtensions

src/Hl7.Fhir.Base/FhirPath/Expressions/Closure.cs

Lines changed: 63 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
/*
1+
/*
22
* Copyright (c) 2015, Firely (info@fire.ly) and contributors
33
* See the file CONTRIBUTORS for details.
4-
*
4+
*
55
* This file is licensed under the BSD 3-Clause license
66
* available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE
77
*/
@@ -17,44 +17,86 @@ namespace Hl7.FhirPath.Expressions
1717
{
1818
internal class Closure
1919
{
20-
public Closure()
20+
internal int Id { get; private set; }
21+
22+
public Closure(EvaluationContext ctx)
23+
{
24+
EvaluationContext = ctx;
25+
Id = ctx.IncrementClosuresCreatedCount();
26+
_debugTracerActive = ctx.DebugTracer != null;
27+
}
28+
29+
public Closure(Closure parent, EvaluationContext ctx)
30+
{
31+
Parent = parent;
32+
EvaluationContext = ctx;
33+
Id = ctx.IncrementClosuresCreatedCount();
34+
_debugTracerActive = ctx.DebugTracer != null;
35+
}
36+
37+
/// <summary>
38+
/// When the debug/trace is enabled this property is used to record the focus of the closure.
39+
/// <br/>VALUE IS NOT USED OUTSIDE DEBUG - without debug/tracer, the value is not consistent.
40+
/// </summary>
41+
/// <remarks>
42+
/// It is set in the delegate produced for each node by the evaluator visitor.
43+
/// The debug tracer will reset the focus in the closure after calling the delegate it's wrapping.
44+
/// ensuring that argument evaluation doesn't impact the focus logged in the debug trace in other
45+
/// calls.
46+
/// </remarks>
47+
public IEnumerable<ITypedElement> focus
2148
{
49+
get
50+
{
51+
if (!_debugTracerActive)
52+
return ElementNode.EmptyList;
53+
return _focus;
54+
}
55+
set
56+
{
57+
if (!_debugTracerActive)
58+
return;
59+
_focus = value;
60+
}
2261
}
2362

63+
private IEnumerable<ITypedElement> _focus;
64+
private bool _debugTracerActive = false;
65+
2466
public EvaluationContext EvaluationContext { get; private set; }
2567

2668
public static Closure Root(ITypedElement root, EvaluationContext ctx = null)
2769
{
2870
var newContext = ctx ?? new EvaluationContext();
29-
71+
3072
var node = root as ScopedNode;
31-
73+
3274
newContext.Resource ??= node != null // if the value has been manually set, we do nothing. Otherwise, if the root is a scoped node:
3375
? getResourceFromNode(node) // we infer the resource from the scoped node
3476
: (root?.Definition?.IsResource is true // if we do not have a scoped node, we see if this is even a resource to begin with
3577
? root // if it is, we use the root as the resource
3678
: null // if not, this breaks the spec in every way (but we will still continue, hopefully we do not need %resource or %rootResource)
37-
);
38-
79+
);
80+
3981
// Same thing, but we copy the resource into the root resource if we cannot infer it from the node.
40-
newContext.RootResource ??= node != null
41-
? getRootResourceFromNode(node)
42-
: newContext.Resource;
43-
44-
var newClosure = new Closure() { EvaluationContext = ctx ?? new EvaluationContext() };
82+
newContext.RootResource ??= node != null
83+
? getRootResourceFromNode(node)
84+
: newContext.Resource;
85+
86+
var newClosure = new Closure(ctx ?? new EvaluationContext());
4587

4688
var input = new[] { root };
4789

4890
foreach (var assignment in newClosure.EvaluationContext.Environment)
4991
{
5092
newClosure.SetValue(assignment.Key, assignment.Value);
5193
}
52-
94+
5395
newClosure.SetThis(input);
5496
newClosure.SetThat(input);
5597
newClosure.SetIndex(ElementNode.CreateList(0));
5698
newClosure.SetOriginalContext(input);
57-
99+
58100
if (newContext.Resource != null) newClosure.SetResource(new[] { newContext.Resource });
59101
if (newContext.RootResource != null) newClosure.SetRootResource(new[] { newContext.RootResource });
60102

@@ -63,6 +105,11 @@ public static Closure Root(ITypedElement root, EvaluationContext ctx = null)
63105

64106
private Dictionary<string, IEnumerable<ITypedElement>> _namedValues = new Dictionary<string, IEnumerable<ITypedElement>>();
65107

108+
internal IEnumerable<KeyValuePair<string, IEnumerable<ITypedElement>>> Variables()
109+
{
110+
return _namedValues;
111+
}
112+
66113
public virtual void SetValue(string name, IEnumerable<ITypedElement> value)
67114
{
68115
_namedValues.Remove(name);
@@ -74,11 +121,7 @@ public virtual void SetValue(string name, IEnumerable<ITypedElement> value)
74121

75122
public virtual Closure Nest()
76123
{
77-
return new Closure()
78-
{
79-
Parent = this,
80-
EvaluationContext = this.EvaluationContext
81-
};
124+
return new Closure(this, EvaluationContext);
82125
}
83126

84127

@@ -100,7 +143,7 @@ public virtual IEnumerable<ITypedElement> ResolveValue(string name)
100143
}
101144

102145
private static ScopedNode getResourceFromNode(ScopedNode node) => node.AtResource ? node : node.ParentResource;
103-
146+
104147
private static ScopedNode getRootResourceFromNode(ScopedNode node)
105148
{
106149
var resource = getResourceFromNode(node);

src/Hl7.Fhir.Base/FhirPath/Expressions/DynaDispatcher.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
/*
1+
/*
22
* Copyright (c) 2015, Firely (info@fire.ly) and contributors
33
* See the file CONTRIBUTORS for details.
4-
*
4+
*
55
* This file is licensed under the BSD 3-Clause license
66
* available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE
77
*/
@@ -11,6 +11,7 @@
1111
using System.Collections.Generic;
1212
using System.Linq;
1313
using System.Reflection;
14+
using FocusCollection = System.Collections.Generic.IEnumerable<Hl7.Fhir.ElementModel.ITypedElement>;
1415

1516
namespace Hl7.FhirPath.Expressions
1617
{
@@ -25,11 +26,12 @@ public DynaDispatcher(string name, SymbolTable scope)
2526
private readonly string _name;
2627
private readonly SymbolTable _scope;
2728

28-
public IEnumerable<ITypedElement> Dispatcher(Closure context, IEnumerable<Invokee> args)
29+
public FocusCollection Dispatcher(Closure context, IEnumerable<Invokee> args)
2930
{
30-
var actualArgs = new List<IEnumerable<ITypedElement>>();
31+
var actualArgs = new List<FocusCollection>();
3132

3233
var focus = args.First()(context, InvokeeFactory.EmptyArgs);
34+
context.focus = focus;
3335
if (!focus.Any()) return ElementNode.EmptyList;
3436

3537
actualArgs.Add(focus);
@@ -46,9 +48,13 @@ public IEnumerable<ITypedElement> Dispatcher(Closure context, IEnumerable<Invoke
4648
{
4749
// The Get() here should never fail, since we already know there's a (dynamic) matching candidate
4850
// Need to clean up this duplicate logic later
49-
5051
var argFuncs = actualArgs.Select(InvokeeFactory.Return);
51-
return entry(context, argFuncs);
52+
var result = entry(context, argFuncs);
53+
54+
// Dynamically dispatched function arguments aren't wrapped
55+
// for the debug/trace, so need to manually put the focus back
56+
context.focus = focus;
57+
return result;
5258
}
5359
catch (TargetInvocationException tie)
5460
{

0 commit comments

Comments
 (0)