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]