diff --git a/com.pontoco.lattice/Editor/Lattice/LatticeNodeView.cs b/com.pontoco.lattice/Editor/Lattice/LatticeNodeView.cs
index 4ca1876..4c95dea 100644
--- a/com.pontoco.lattice/Editor/Lattice/LatticeNodeView.cs
+++ b/com.pontoco.lattice/Editor/Lattice/LatticeNodeView.cs
@@ -434,6 +434,12 @@ public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
_ =>
{
Target.Last.DoNotLogErrors = !Target.Last.DoNotLogErrors;
+
+ // The flag is baked into the compilation, so it only takes effect through a recompile. This
+ // refreshes the editor's view; a graph executing in play mode keeps its old logging until the
+ // Runtime graph itself recompiles.
+ EditorUtility.SetDirty(Owner.Graph);
+ GlobalGraph.LanguageServer.RecompileIfNeeded(force: true);
});
evt.menu.AppendAction("Show Selected In GraphViz", _ =>
diff --git a/com.pontoco.lattice/Runtime/IR/Diagnostics.cs b/com.pontoco.lattice/Runtime/IR/Diagnostics.cs
new file mode 100644
index 0000000..9607f03
--- /dev/null
+++ b/com.pontoco.lattice/Runtime/IR/Diagnostics.cs
@@ -0,0 +1,87 @@
+using System;
+using JetBrains.Annotations;
+
+namespace Lattice.IR
+{
+ // The compiler reports problems as Diagnostic values accumulated on GraphCompilation.Diagnostics,
+ // and never writes to the console itself. Callers choose policy: CompileFixedSet replays the stream
+ // to the console at the end of a compile (unless suppressed), and tests can read the list directly.
+ // NodeCompilationError keeps its role as the value that *propagates* an error through node metadata;
+ // a Diagnostic is how that error *surfaces*.
+
+ /// A single problem found while compiling a set of graphs. Immutable.
+ public readonly struct Diagnostic
+ {
+ public readonly DiagnosticSeverity Severity;
+
+ /// Artist-facing text. Null for problems carried entirely by .
+ [CanBeNull]
+ public readonly string Message;
+
+ /// The graph the problem occurred in, if known. Used as the clickable console context object.
+ [CanBeNull]
+ public readonly LatticeGraph Graph;
+
+ /// The original exception, when one exists. The console replay shows it with its stack trace.
+ [CanBeNull]
+ public readonly Exception Exception;
+
+ ///
+ /// True when the authored node opted out of console logging (DoNotLogErrors). The diagnostic is still
+ /// recorded; only the console replay skips it.
+ ///
+ public readonly bool Muted;
+
+ public Diagnostic(DiagnosticSeverity severity, [CanBeNull] string message, LatticeGraph graph = null,
+ Exception exception = null, bool muted = false)
+ {
+ Severity = severity;
+ Message = message;
+ Graph = graph;
+ Exception = exception;
+ Muted = muted;
+ }
+ }
+
+ public enum DiagnosticSeverity
+ {
+ Info,
+ Warning,
+ Error
+ }
+
+ ///
+ /// The artist-facing diagnostic messages, each composing compiler values into a sentence, so their wording
+ /// sits in one place to read and police: they name nodes, ports, systems, and types the way the editor shows
+ /// them, say what went wrong, and say what to do next. ICE messages -- addressed to us, not the artist --
+ /// stay inline at their site. An artist has never heard of inference or a qualifier, so that vocabulary
+ /// doesn't belong in here.
+ ///
+ public static class DiagnosticMessages
+ {
+ private const string MixedEntityScopesTemplate =
+ "This node mixes values from two different entities ({0} and {1}). A node can only combine values from one entity.";
+
+ private const string PhasesNotOrderedTemplate =
+ "This node reads from {0} and {1}, but Lattice can't tell which runs first. Add an ordering between those systems.\n" +
+ "(Missing UpdateAfter/UpdateBefore between [{0}] and [{1}].)";
+
+ private const string GraphNameNotUniqueTemplate =
+ "Two Lattice graphs are both named [{0}]. Give each graph a unique name.";
+
+ public static string MixedEntityScopes(Qualifier first, Qualifier second)
+ {
+ return string.Format(MixedEntityScopesTemplate, first.ShortName(), second.ShortName());
+ }
+
+ public static string PhasesNotOrdered(Type firstGroup, Type secondGroup)
+ {
+ return string.Format(PhasesNotOrderedTemplate, firstGroup.Name, secondGroup.Name);
+ }
+
+ public static string GraphNameNotUnique(string graphName)
+ {
+ return string.Format(GraphNameNotUniqueTemplate, graphName);
+ }
+ }
+}
diff --git a/com.pontoco.lattice/Runtime/IR/Diagnostics.cs.meta b/com.pontoco.lattice/Runtime/IR/Diagnostics.cs.meta
new file mode 100644
index 0000000..bad9961
--- /dev/null
+++ b/com.pontoco.lattice/Runtime/IR/Diagnostics.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 31753c727cea64e0e8452f2623f39dfc
+timeCreated: 1781498018
diff --git a/com.pontoco.lattice/Runtime/IR/GraphCompilation.cs b/com.pontoco.lattice/Runtime/IR/GraphCompilation.cs
index 4d81289..7e98751 100644
--- a/com.pontoco.lattice/Runtime/IR/GraphCompilation.cs
+++ b/com.pontoco.lattice/Runtime/IR/GraphCompilation.cs
@@ -61,6 +61,13 @@ public class GraphCompilation
/// If true, the graph is invalid enough that we shouldn't try to execute it.
public bool CannotBeExecuted;
+ ///
+ /// Every problem found while building and analyzing the graphs, in the order found. The compiler only
+ /// accumulates these; whether they reach the console is the caller's choice (the logToConsole flag on
+ /// the compile entry points, defaulting on).
+ ///
+ public readonly List Diagnostics = new();
+
/// Set to true after the compiler does its high level analysis like type checking.
public bool AnalysisFinished;
@@ -145,7 +152,8 @@ public void AddToplevelGraph(LatticeGraph graphAsset)
{
if (TopLevelGraphs.Contains(graphAsset))
{
- Debug.LogError($"(Lattice) ICE: Graph [{graphAsset.name}] was added to compilation twice.");
+ Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Error,
+ $"(Lattice) ICE: Graph [{graphAsset.name}] was added to compilation twice.", graphAsset));
return;
}
@@ -197,12 +205,8 @@ private IRGraph BuildGraphBody(LatticeGraph graphAsset)
}
catch (Exception e)
{
- if (!node.DoNotLogErrors)
- {
- Debug.LogError(
- $"Syntax Error at [{node}]: {e.Message}\n\n StackTrace:\n" + e.StackTrace + "\n\n",
- graphAsset);
- }
+ Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Error,
+ $"Syntax Error at [{node}]: {e.Message}", graphAsset, e, node.DoNotLogErrors));
graph.ReplaceNodeWithMalformed(node.ToRootPath(), e);
}
@@ -215,7 +219,8 @@ private IRGraph BuildGraphBody(LatticeGraph graphAsset)
}
catch (Exception e)
{
- Debug.LogException(e, graphAsset);
+ Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Error,
+ $"ICE: Setting up port defaults failed for node [{node}].", graphAsset, e));
}
}
@@ -229,9 +234,9 @@ private IRGraph BuildGraphBody(LatticeGraph graphAsset)
if (!graph.GetOutputMap(((LatticeNode)edge.fromNode).ToRootPath())
.TryGetValue(edge.fromPortIdentifier, out IRNodeRef inputNode))
{
- Debug.LogError(
+ Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Error,
$"ICE: No IRNode was mapped for output port [{edge.fromPortIdentifier}] on node [{edge.fromNode}].",
- graphAsset);
+ graphAsset));
continue;
}
@@ -239,9 +244,9 @@ private IRGraph BuildGraphBody(LatticeGraph graphAsset)
if (!graph.GetInputMap(((LatticeNode)edge.toNode).ToRootPath()).TryGetValue(edge.toPortIdentifier,
out (IRNode node, string irPort)? portMap))
{
- Debug.LogError(
+ Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Error,
$"ICE: No IRNode was mapped for input port [{edge.toPortIdentifier}] on node [{edge.toNode}].",
- graphAsset);
+ graphAsset));
continue;
}
@@ -257,7 +262,8 @@ private IRGraph BuildGraphBody(LatticeGraph graphAsset)
else
{
// Stitch this node into the mutation pipeline.
- Debug.LogError($"ICE: Ref edges are not currently supported. Edge: [{edge}]");
+ Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Error,
+ $"ICE: Ref edges are not currently supported. Edge: [{edge}]", graphAsset));
}
}
@@ -268,7 +274,8 @@ private IRGraph BuildGraphBody(LatticeGraph graphAsset)
{
if (ids.TryGetValue(node.Id, out IRNode n))
{
- Debug.LogError($"ICE: Two IRNodes have the same Id. Node [{node}] and [{n}]. Id:[{node.Id}]");
+ Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Error,
+ $"ICE: Two IRNodes have the same Id. Node [{node}] and [{n}]. Id:[{node.Id}]", graphAsset));
}
ids.Add(node.Id, node);
}
@@ -277,8 +284,8 @@ private IRGraph BuildGraphBody(LatticeGraph graphAsset)
{
if (n.Node.Ports.Count != 1)
{
- Debug.LogError(
- $"ICE: Input node in graph does not have 1 port. [{n.Node}][{n.Node.Ports.Count}]");
+ Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Error,
+ $"ICE: Input node in graph does not have 1 port. [{n.Node}][{n.Node.Ports.Count}]", graphAsset));
}
}
}
@@ -328,8 +335,9 @@ public Metadata CompileNode(IRNode node)
}
catch (Exception e)
{
- Debug.LogWarning($"ICE: Node failed to compile. [{node}]", Graph.GetOwner(node)?.Last.Graph);
- Debug.LogException(e, Graph.GetOwner(node)?.Last.Graph);
+ CodePath? owner = Graph.GetOwner(node);
+ Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Warning,
+ $"ICE: Node failed to compile. [{node}]", owner?.Last.Graph, e));
metadata = new Metadata(null, typeof(ITypeUnknown), LatticePhases.GetLatticeDefaultPhase(),
new NodeCompilationError(node, "ICE: Node failed to compile."));
}
@@ -445,7 +453,7 @@ public Metadata CalculateMetadata(IRNode node, IEnumerable<(string portId, Metad
)
{
error = new NodeCompilationError(node,
- $"Inputs with different entity qualifiers. Input qualifiers: [{inputMetadata.Qualifier}][{qualifier}]");
+ DiagnosticMessages.MixedEntityScopes(inputMetadata.Qualifier.Value, qualifier.Value));
qualifier = null; // Null out the qualifier, because we'll just be returning an exception.
}
else
@@ -522,15 +530,12 @@ private void TryMergePhase(IRNode node, ref Type existingPhase, Type inputPhase,
if (!orderedAfter.HasValue)
{
+ // Set the error for propagation and veto execution. Surfacing is left to EmitCompilationErrors,
+ // which reports it once, cleanly, at this node. The veto is unconditional -- DoNotLogErrors only
+ // silences the console, never changes whether the graph runs.
error = new NodeCompilationError(node,
- "Cannot compile: Lattice Phases for input nodes were not fully ordered (UpdateAfter/UpdateBefore). Info:\n" +
- $"Phases not fully ordered: [{inputPhase}] and [{existingPhase}]\n" +
- $"Because: missing system UpdateAfter/UpdateBefore between systems: [{inputGroup}] and [{existingGroup}]");
- if (!node.DoNotLogErrors)
- {
- Debug.LogException(error);
- CannotBeExecuted = true;
- }
+ DiagnosticMessages.PhasesNotOrdered(inputGroup, existingGroup));
+ CannotBeExecuted = true;
return;
}
@@ -641,9 +646,10 @@ public void DeduplicatedCodeGen(string typeName, Func output)
/// Makes sure nodes have an output port mapping and some other default mappings. For single output port nodes,
/// just maps them to the added node.
///
- private static void SetupDefaults(IRGraph compilation, CodePath span)
+ private void SetupDefaults(IRGraph graph, CodePath span)
{
// Add primary node marker if there's only one ir node.
- if (compilation.GetPrimaryNode(span) == null)
+ if (graph.GetPrimaryNode(span) == null)
{
- var nodes = compilation.GetNodesUnderPath(span);
+ var nodes = graph.GetNodesUnderPath(span);
if (nodes.Count > 1)
{
- Debug.LogError($"No primary node specified for node [{span}].");
+ Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Error,
+ $"No primary node specified for node [{span}].", span.Root));
}
else if (nodes.Count == 1)
{
- compilation.SetPrimaryNode(span, nodes[0]);
+ graph.SetPrimaryNode(span, nodes[0]);
}
else
{
- Debug.LogError($"No nodes created for node [{span}].");
+ Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Error,
+ $"No nodes created for node [{span}].", span.Root));
}
}
// (Doesn't overwrite manually specified output mapping.)
// If the user didn't specify an output port map, attempt to generate it.
- var outputMap = compilation.GetOutputMap(span);
+ var outputMap = graph.GetOutputMap(span);
if (!outputMap.Any())
{
// Add default mapping for single output port nodes.
if (span.Last.GetValueOutputs().Count() == 1)
{
- var nodes = compilation.GetNodesUnderPath(span);
+ var nodes = graph.GetNodesUnderPath(span);
if (nodes.Count != 1)
{
- Debug.LogError(
- $"Couldn't add default output port mapping to node [{span}]. Node count: {nodes.Count}.");
+ Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Error,
+ $"Couldn't add default output port mapping to node [{span}]. Node count: {nodes.Count}.",
+ span.Root));
return;
}
- compilation.MapOutputPort(span, span.Last.OutputPorts[0].portData.identifier, nodes[0]);
+ graph.MapOutputPort(span, span.Last.OutputPorts[0].portData.identifier, nodes[0]);
}
else if (span.Last.GetValueOutputs().Count() > 1)
{
- Debug.LogError($"No output port mapping found for compiled node [{span}].");
+ Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Error,
+ $"No output port mapping found for compiled node [{span}].", span.Root));
}
}
else
@@ -899,13 +909,14 @@ private static void SetupDefaults(IRGraph compilation, CodePath span)
{
if (!outputMap.ContainsKey(output.portData.identifier))
{
- Debug.LogError(
- $"Incomplete OutputPortMap for compiled node [{span}]. Missing port [{output.portData.identifier}]");
+ Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Error,
+ $"Incomplete OutputPortMap for compiled node [{span}]. Missing port [{output.portData.identifier}]",
+ span.Root));
}
}
}
- var inputMap = compilation.GetInputMap(span);
+ var inputMap = graph.GetInputMap(span);
if (inputMap.Any())
{
// If the node specified an input port mapping, verify it.
@@ -913,15 +924,16 @@ private static void SetupDefaults(IRGraph compilation, CodePath span)
{
if (!inputMap.ContainsKey(input.portData.identifier))
{
- Debug.LogError(
- $"Incomplete InputPortMap for compiled node [{span}]. Missing port [{input.portData.identifier}]");
+ Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Error,
+ $"Incomplete InputPortMap for compiled node [{span}]. Missing port [{input.portData.identifier}]",
+ span.Root));
}
}
}
else
{
// If no input port map is provided, and there's only a single node, map inputs there.
- var nodes = compilation.GetNodesUnderPath(span);
+ var nodes = graph.GetNodesUnderPath(span);
if (nodes.Count == 1)
{
if (nodes[0] is MalformedIRNode)
@@ -931,17 +943,18 @@ private static void SetupDefaults(IRGraph compilation, CodePath span)
// the type information for the editor.
foreach (NodePort port in span.Last.GetLogicalInputs())
{
- compilation.MapInputPort(span, port.portData.identifier, null);
+ graph.MapInputPort(span, port.portData.identifier, null);
}
}
else
{
- compilation.MapInputPorts(span, nodes[0]);
+ graph.MapInputPorts(span, nodes[0]);
}
}
else if (span.Last.GetLogicalInputs().Any())
{
- Debug.LogError($"No input port mapping found for compiled node [{span}].");
+ Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Error,
+ $"No input port mapping found for compiled node [{span}].", span.Root));
}
}
}
diff --git a/com.pontoco.lattice/Runtime/IR/GraphCompiler.cs b/com.pontoco.lattice/Runtime/IR/GraphCompiler.cs
index 8c115ef..f0b196b 100644
--- a/com.pontoco.lattice/Runtime/IR/GraphCompiler.cs
+++ b/com.pontoco.lattice/Runtime/IR/GraphCompiler.cs
@@ -53,15 +53,17 @@ public override int GetHashCode()
}
/// Compiles a standalone compilation including just this graph and its dependencies.
- public static GraphCompilation CompileStandalone(LatticeGraph graph, Settings settings = default)
+ public static GraphCompilation CompileStandalone(LatticeGraph graph, Settings settings = default,
+ bool logToConsole = true)
{
- return CompileFixedSet(GetGraphDependencies(graph), settings);
+ return CompileFixedSet(GetGraphDependencies(graph), settings, logToConsole);
}
/// Compiles a standalone compilation including all the given graphs and their dependencies.
- public static GraphCompilation CompileStandalone(IEnumerable graphs, Settings settings = default)
+ public static GraphCompilation CompileStandalone(IEnumerable graphs, Settings settings = default,
+ bool logToConsole = true)
{
- return CompileFixedSet(graphs.SelectMany(GetGraphDependencies).ToHashSet(), settings);
+ return CompileFixedSet(graphs.SelectMany(GetGraphDependencies).ToHashSet(), settings, logToConsole);
}
internal static HashSet GetGraphDependencies(LatticeGraph startingGraph)
@@ -93,7 +95,7 @@ void AddDownstreamRecursive(LatticeGraph graph)
/// IR graph for all nodes.
///
internal static GraphCompilation CompileFixedSet(IEnumerable topLevelGraphs,
- Settings settings = default)
+ Settings settings = default, bool logToConsole = true)
{
using ProfilerMarker.AutoScope marker = new ProfilerMarker("Lattice Compile").Auto();
@@ -114,6 +116,60 @@ internal static GraphCompilation CompileFixedSet(IEnumerable topLe
// ===============================
GraphCompilation compilation = new(settings);
+ // The compiler accumulates problems as data; whether to echo them to the console is the caller's
+ // choice, defaulting on. The finally flushes whatever was found even if a pass throws partway, so a
+ // fatal compile still leaves breadcrumbs. A caller that compiled quietly can walk Diagnostics itself.
+ try
+ {
+ return CompileFixedSetInner(compilation, topLevelGraphs, settings);
+ }
+ finally
+ {
+ if (logToConsole)
+ {
+ LogDiagnosticMessages(compilation);
+ }
+ }
+ }
+
+ private static void LogDiagnosticMessages(GraphCompilation compilation)
+ {
+ foreach (Diagnostic d in compilation.Diagnostics)
+ {
+ if (d.Muted)
+ {
+ continue;
+ }
+
+ if (d.Message != null)
+ {
+ switch (d.Severity)
+ {
+ case DiagnosticSeverity.Error:
+ Debug.LogError(d.Message, d.Graph);
+ break;
+ case DiagnosticSeverity.Warning:
+ Debug.LogWarning(d.Message, d.Graph);
+ break;
+ case DiagnosticSeverity.Info:
+ Debug.Log(d.Message, d.Graph);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ if (d.Exception != null)
+ {
+ Debug.LogException(d.Exception, d.Graph);
+ }
+ }
+ }
+
+ private static GraphCompilation CompileFixedSetInner(GraphCompilation compilation,
+ IEnumerable topLevelGraphs,
+ Settings settings)
+ {
// All graphs in the compilation must have a unique name. This is because we use names for GUIDs, until we have a better solution.
HashSet names = new HashSet();
var graphsWithUniqueNames = new List();
@@ -121,7 +177,8 @@ internal static GraphCompilation CompileFixedSet(IEnumerable topLe
{
if (!names.Add(g.name))
{
- Debug.LogError($"Lattice graph named [{g.name}] was defined twice. Graphs must have unique names.");
+ compilation.Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Error,
+ DiagnosticMessages.GraphNameNotUnique(g.name), g));
continue;
}
graphsWithUniqueNames.Add(g);
@@ -170,7 +227,8 @@ internal static GraphCompilation CompileFixedSet(IEnumerable topLe
{
if (ids.TryGetValue(node.Id, out IRNode n))
{
- Debug.LogError($"ICE: Two IRNodes have the same Id. Node [{node}] and [{n}]. Id:[{node.Id}]");
+ compilation.Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Error,
+ $"ICE: Two IRNodes have the same Id. Node [{node}] and [{n}]. Id:[{node.Id}]"));
}
ids.Add(node.Id, node);
}
@@ -229,7 +287,8 @@ internal static GraphCompilation CompileFixedSet(IEnumerable topLe
{
if (p.BackRef == null)
{
- Debug.LogError("ICE: PreviousNode with null BackRef.");
+ compilation.Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Error,
+ "ICE: PreviousNode with null BackRef."));
continue;
}
compilation.Graph.ReferencedByPreviousNode.Add(p.BackRef.Node);
@@ -248,21 +307,28 @@ internal static GraphCompilation CompileFixedSet(IEnumerable topLe
}
- // Emit compilation errors for nodes, but keep going (malformed nodes are valid runtime nodes)
+ // Surface propagated node errors as diagnostics, once each, at the node that caused them.
+ // Compilation keeps going -- malformed nodes are valid runtime nodes.
private static void EmitCompilationErrors(GraphCompilation compilation)
{
foreach (var n in compilation.Graph.Nodes)
{
- if (n.DoNotLogErrors)
- {
- continue;
- }
var error = compilation.CompileNode(n).CompilationError;
if (error != null && error.Node == n)
{
- Debug.LogError(
- $"Syntax Error at [{error.Node}]: {error}\n\n",
- compilation.Graph.GetOwner(error.Node)?.Last.Graph);
+ // The artist-facing text lives on the inner exception -- NodeCompilationError stashes its
+ // message there. We name the authored node, since these messages ("this node...") otherwise
+ // can't be located on a big graph. No exception is attached: a real throw already logged its
+ // stack where it was caught (BuildGraphBody), and a logic error has no useful one.
+ CodePath? owner = compilation.Graph.GetOwner(error.Node);
+ string message = error.InnerException?.Message ?? error.Message;
+ if (owner.HasValue)
+ {
+ message = $"{owner.Value.Last.GetPath()}: {message}";
+ }
+
+ compilation.Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Error, message,
+ owner?.Last.Graph, muted: n.DoNotLogErrors));
}
}
}
@@ -463,8 +529,10 @@ bool TypesAreCompatible(Type portType, Type inputType)
{
if (compilation.CompileNode(node).OutputType.IsGenericParameter)
{
- Debug.LogError(
- $"ICE: Generic type was found in node [{node}]. All generics must resolve into concrete types after node compilation.");
+ CodePath? owner = compilation.Graph.GetOwner(node);
+ compilation.Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Error,
+ $"ICE: Generic type was found in node [{node}]. All generics must resolve into concrete types after node compilation.",
+ owner?.Last.Graph));
compilation.CannotBeExecuted = true;
}
}
@@ -476,23 +544,26 @@ bool TypesAreCompatible(Type portType, Type inputType)
if (port.Inputs.Count == 0 && !port.IsOptional)
{
// FunctionIRNodes must be connected on all ports.
- Debug.LogError($"ICE: Port [{id}] must be connected on node [{node}]. No inputs connected.");
+ CodePath? owner = compilation.Graph.GetOwner(node);
+ compilation.Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Error,
+ $"ICE: Port [{id}] must be connected on node [{node}]. No inputs connected.",
+ owner?.Last.Graph));
compilation.CannotBeExecuted = true;
}
var compileData = compilation.CompileNode(node);
if (compileData.OutputType.IsByRef)
{
- Debug.LogError(
- $"$ICE: Node has output of type 'ref'. [{node}] ByRef types are not allowed because pointers can't be stored across frames.");
+ compilation.Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Error,
+ $"$ICE: Node has output of type 'ref'. [{node}] ByRef types are not allowed because pointers can't be stored across frames."));
compilation.CannotBeExecuted = true;
}
if (compileData.OutputType.IsByRef)
{
// Managed types aren't supported yet, as codegen assumes all types can be boxed/nullable-wrapped.
- Debug.LogError(
- $"$ICE: Node has a managed output type. [{node}] Managed types are not allowed, yet.");
+ compilation.Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Error,
+ $"$ICE: Node has a managed output type. [{node}] Managed types are not allowed, yet."));
compilation.CannotBeExecuted = true;
}
@@ -501,8 +572,10 @@ bool TypesAreCompatible(Type portType, Type inputType)
Type inputType = compilation.CompileNode(input).OutputType;
if (!TypesAreCompatible(port.Type, inputType))
{
- Debug.LogError(
- $"ICE TypeError: Port [{id}][{port.Type}] cannot be assigned input of type [{inputType}]. Node: [{node}] Input: [{input}]");
+ CodePath? owner = compilation.Graph.GetOwner(node);
+ compilation.Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Error,
+ $"ICE TypeError: Port [{id}][{port.Type}] cannot be assigned input of type [{inputType}]. Node: [{node}] Input: [{input}]",
+ owner?.Last.Graph));
compilation.CannotBeExecuted = true;
}
}
@@ -751,8 +824,8 @@ public static void Pass_ConvertPreviousNodesToPointers(GraphCompilation compilat
}
catch (Exception e)
{
- Debug.LogError("Replacing MutatorIRNodes failed.");
- Debug.LogException(e);
+ compilation.Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Error,
+ "Replacing MutatorIRNodes failed.", exception: e));
}
}
diff --git a/com.pontoco.lattice/Runtime/IR/ILGeneration.cs b/com.pontoco.lattice/Runtime/IR/ILGeneration.cs
index a7113ba..ff52d9f 100644
--- a/com.pontoco.lattice/Runtime/IR/ILGeneration.cs
+++ b/com.pontoco.lattice/Runtime/IR/ILGeneration.cs
@@ -19,7 +19,6 @@
using Label = GrEmit.GroboIL.Label;
// Performance improvements:
-// - Move DoNotLogErrors check to compile-time.
// - Can we emit more efficient code for the big function calls at the end of a node's execution?
namespace Lattice.IR
@@ -799,12 +798,16 @@ void EmitReadNodeValue(IRNode node, [CanBeNull] Local entityIdx, Local outNodeVa
emit.Ldloc(exceptionValue); // Load the exceptionValue (or null) as arg5.
emit.Call(typeof(ILGeneration).GetMethod(nameof(DebugNodeExecution)).MakeGenericMethod(executionOutputType));
- // Check the node's output for errors and log them.
- emit.Ldarg(argumentExecutionIdx); // Load the IRExecution as arg1.
- emit.Ldc_I4(nodeIdx); // Load the node index as arg 2.
- emit.Ldloc(latticeEntity); // Load the entity (or Entity.Null) as arg3.
- emit.Ldloc(exceptionValue); // Load the exceptionValue (or null) as arg4.
- emit.Call(typeof(ILGeneration).GetMethod(nameof(LogNodeErrors)));
+ // Check the node's output for errors and log them. Nodes that opted out of error logging
+ // simply don't get the call emitted; the choice is baked in at compile time.
+ if (!node.DoNotLogErrors)
+ {
+ emit.Ldarg(argumentExecutionIdx); // Load the IRExecution as arg1.
+ emit.Ldc_I4(nodeIdx); // Load the node index as arg 2.
+ emit.Ldloc(latticeEntity); // Load the entity (or Entity.Null) as arg3.
+ emit.Ldloc(exceptionValue); // Load the exceptionValue (or null) as arg4.
+ emit.Call(typeof(ILGeneration).GetMethod(nameof(LogNodeErrors)));
+ }
// If the node is needed in the next frame, write it to the entity's state.
if (compilation.Graph.ReferencedByPreviousNode.Contains(node))
@@ -1148,20 +1151,16 @@ public static void DebugNodeExecution(IRExecution execution, int nodeIdx, Ent
// Debug.Log($"Executed [{node}] [{exceptionValue ?? outputValue}]");
}
- // Logs a node's errors if it returned an exception.
+ // Logs a node's errors if it returned an exception. Muted nodes (DoNotLogErrors) never emit a call to this.
public static void LogNodeErrors(IRExecution execution, int nodeIdx, Entity qualifier, Exception exceptionValue)
{
- IRNode node = execution.Compilation.Graph.Nodes[nodeIdx];
-
- if (!node.DoNotLogErrors)
+ // Only log an error if the exception originated from this node. If the exception is InputNodeException
+ // it originated from an earlier node in this node's input graph.
+ if (exceptionValue != null && exceptionValue is not InputNodeException)
{
- // Only log an error if the exception originated from this node. If the exception is InputNodeException
- // it originated from an earlier node in this node's input graph.
- if (exceptionValue != null && exceptionValue is not InputNodeException)
- {
- Debug.LogError($"Lattice node error: [{node}:{qualifier}]", execution.Compilation.Graph.GetOwner(node)?.Root);
- Debug.LogException(exceptionValue, execution.Compilation.Graph.GetOwner(node)?.Root);
- }
+ IRNode node = execution.Compilation.Graph.Nodes[nodeIdx];
+ Debug.LogError($"Lattice node error: [{node}:{qualifier}]", execution.Compilation.Graph.GetOwner(node)?.Root);
+ Debug.LogException(exceptionValue, execution.Compilation.Graph.GetOwner(node)?.Root);
}
}
diff --git a/com.pontoco.lattice/Runtime/IR/Nodes/IRNode.cs b/com.pontoco.lattice/Runtime/IR/Nodes/IRNode.cs
index 3acde49..29511d9 100644
--- a/com.pontoco.lattice/Runtime/IR/Nodes/IRNode.cs
+++ b/com.pontoco.lattice/Runtime/IR/Nodes/IRNode.cs
@@ -125,10 +125,11 @@ public abstract class IRNode
public string DebugName;
///
- /// If set to true, the compilation and execution of graphs will not log errors to the Unity console for syntax
- /// and runtime errors. Everything else will act like normal, just no logging. ICE's will still log. This is useful for
- /// unit tests which would fail on console messages, but where we still want to test to make sure errors propagate
- /// correctly.
+ /// If set to true, errors on this node stay out of the Unity console. Compile errors are still recorded as
+ /// muted s, and the generated code simply omits the runtime error logging call --
+ /// the flag is baked in at IL emission, so flipping it after compilation does nothing. Everything else acts
+ /// like normal: errors still propagate, and ICEs still log. Useful for graphs with intentional errors, like
+ /// tests.
///
internal bool DoNotLogErrors = false;
diff --git a/com.pontoco.lattice/Runtime/Nodes/LatticeNode.cs b/com.pontoco.lattice/Runtime/Nodes/LatticeNode.cs
index 212f10c..a42234b 100644
--- a/com.pontoco.lattice/Runtime/Nodes/LatticeNode.cs
+++ b/com.pontoco.lattice/Runtime/Nodes/LatticeNode.cs
@@ -43,9 +43,10 @@ public abstract class LatticeNode : BaseNode
public List ActionPorts = new();
///
- /// If set to true, the compilation and execution of graphs will not log errors to the Unity console for syntax and runtime errors.
- /// Everything else will act like normal, just no logging. ICE's will still log. This is useful for unit tests which would fail on console
- /// messages, but where we still want to test to make sure errors propagate correctly.
+ /// If set to true, errors on this node stay out of the Unity console: compile errors become muted
+ /// s and the compiled graph omits runtime error logging for it.
+ /// Errors still propagate like normal, and ICEs still log. Toggled per node from the editor's context
+ /// menu; useful for graphs with intentional errors, like tests.
///
[SerializeField]
[HideInInspector]