Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions com.pontoco.lattice/Editor/Lattice/LatticeNodeView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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", _ =>
Expand Down
87 changes: 87 additions & 0 deletions com.pontoco.lattice/Runtime/IR/Diagnostics.cs
Original file line number Diff line number Diff line change
@@ -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*.

/// <summary>A single problem found while compiling a set of graphs. Immutable.</summary>
public readonly struct Diagnostic
{
public readonly DiagnosticSeverity Severity;

/// <summary>Artist-facing text. Null for problems carried entirely by <see cref="Exception" />.</summary>
[CanBeNull]
public readonly string Message;

/// <summary>The graph the problem occurred in, if known. Used as the clickable console context object.</summary>
[CanBeNull]
public readonly LatticeGraph Graph;

/// <summary>The original exception, when one exists. The console replay shows it with its stack trace.</summary>
[CanBeNull]
public readonly Exception Exception;

/// <summary>
/// True when the authored node opted out of console logging (DoNotLogErrors). The diagnostic is still
/// recorded; only the console replay skips it.
/// </summary>
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
}

/// <summary>
/// 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.
/// </summary>
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);
}
}
}
3 changes: 3 additions & 0 deletions com.pontoco.lattice/Runtime/IR/Diagnostics.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

113 changes: 63 additions & 50 deletions com.pontoco.lattice/Runtime/IR/GraphCompilation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ public class GraphCompilation
/// <summary>If true, the graph is invalid enough that we shouldn't try to execute it.</summary>
public bool CannotBeExecuted;

/// <summary>
/// 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).
/// </summary>
public readonly List<Diagnostic> Diagnostics = new();

/// <summary>Set to true after the compiler does its high level analysis like type checking.</summary>
public bool AnalysisFinished;

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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);
}
Expand All @@ -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));
}
}

Expand All @@ -229,19 +234,19 @@ 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;
}

// Attach that to the correct input port on the toNode.
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;
}

Expand All @@ -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));
}
}

Expand All @@ -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);
}
Expand All @@ -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));
}
}
}
Expand Down Expand Up @@ -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."));
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -641,9 +646,10 @@ public void DeduplicatedCodeGen<Key, NodeType>(string typeName, Func<NodeType, K
{
node.SetMethod(t.GetMethod(name));
}
catch (AmbiguousMatchException e)
catch (AmbiguousMatchException)
{
Debug.LogError($"Generated function was duplicated [{t.Name}.{name}]");
Diagnostics.Add(new Diagnostic(DiagnosticSeverity.Error,
$"Generated function was duplicated [{t.Name}.{name}]"));
throw;
}
}
Expand Down Expand Up @@ -848,48 +854,52 @@ private void GetAllDependencies(IRNode node, HashSet<IRNode> 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.
/// </summary>
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
Expand All @@ -899,29 +909,31 @@ 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.
foreach (NodePort input in span.Last.GetLogicalInputs())
{
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)
Expand All @@ -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));
}
}
}
Expand Down
Loading