ExprGraph lists are read-only views over private arrays; test short evaluator inputs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-14 07:54:31 +02:00
commit 646cc59426
4 changed files with 86 additions and 12 deletions

View file

@ -19,13 +19,14 @@ public static class ExprEvaluator
var points = outputs.Length / outputCount;
if (inputs.Length != points * graph.InputNames.Count) throw new ExprGraphException("column-shape");
var nodes = graph.Nodes;
var nodes = graph.NodeArray;
var outputNodes = graph.OutputNodeArray;
var start = graph.SlotStart;
var slots = new float[graph.SlotCount];
Span<float> operands = stackalloc float[ExprOps.MaxArity];
for (var point = 0; point < points; point++)
{
for (var i = 0; i < nodes.Count; i++)
for (var i = 0; i < nodes.Length; i++)
{
var node = nodes[i];
switch (node.Kind)
@ -40,17 +41,17 @@ public static class ExprEvaluator
slots[start[i]] = BitConverter.Int32BitsToSingle(node.Payload);
break;
case ExprNodeKind.Pick:
slots[start[i]] = slots[start[node.Operands[0]] + node.Payload];
slots[start[i]] = slots[start[node.OperandArray[0]] + node.Payload];
break;
default:
var op = node.Op!;
for (var k = 0; k < op.Arity; k++) operands[k] = slots[start[node.Operands[k]]];
for (var k = 0; k < op.Arity; k++) operands[k] = slots[start[node.OperandArray[k]]];
op.Evaluate(operands.Slice(0, op.Arity), new Span<float>(slots, start[i], op.ResultCount));
break;
}
}
for (var o = 0; o < outputCount; o++) outputs[o * points + point] = slots[start[graph.OutputNodes[o]]];
for (var o = 0; o < outputCount; o++) outputs[o * points + point] = slots[start[outputNodes[o]]];
}
}
}

View file

@ -26,6 +26,12 @@ public sealed class ExprGraph
this.outputNames = outputNames;
this.outputNodes = outputNodes;
this.nodes = nodes;
InputNames = Array.AsReadOnly(inputNames);
ParamNames = Array.AsReadOnly(paramNames);
ParamDefaultBits = Array.AsReadOnly(paramDefaultBits);
OutputNames = Array.AsReadOnly(outputNames);
OutputNodes = Array.AsReadOnly(outputNodes);
Nodes = Array.AsReadOnly(nodes);
SlotStart = new int[nodes.Length];
var slots = 0;
for (var i = 0; i < nodes.Length; i++)
@ -37,19 +43,25 @@ public sealed class ExprGraph
SlotCount = slots;
}
public IReadOnlyList<string> InputNames => inputNames;
public IReadOnlyList<string> InputNames { get; }
public IReadOnlyList<string> ParamNames => paramNames;
public IReadOnlyList<string> ParamNames { get; }
/// <summary>The default of each param as float bits, so a NaN payload or a signed zero survives.</summary>
public IReadOnlyList<int> ParamDefaultBits => paramDefaultBits;
public IReadOnlyList<int> ParamDefaultBits { get; }
public IReadOnlyList<string> OutputNames => outputNames;
public IReadOnlyList<string> OutputNames { get; }
/// <summary>The node each output reads.</summary>
public IReadOnlyList<int> OutputNodes => outputNodes;
public IReadOnlyList<int> OutputNodes { get; }
public IReadOnlyList<ExprNode> Nodes => nodes;
public IReadOnlyList<ExprNode> Nodes { get; }
/// <summary>The evaluator's view of the nodes: the private array, never handed out.</summary>
internal ExprNode[] NodeArray => nodes;
/// <summary>The evaluator's view of the output nodes: the private array, never handed out.</summary>
internal int[] OutputNodeArray => outputNodes;
/// <summary>Where each node's results start in an evaluator's value array.</summary>
internal int[] SlotStart { get; }

View file

@ -31,6 +31,7 @@ public sealed class ExprNode
Kind = kind;
WireName = wireName;
operandArray = operands;
Operands = Array.AsReadOnly(operands);
Payload = payload;
Op = op;
ResultCount = op?.ResultCount ?? 1;
@ -41,7 +42,10 @@ public sealed class ExprNode
/// <summary>The persisted name: an op name for a call, otherwise <c>input</c>, <c>param</c>, <c>const</c> or <c>pick</c>.</summary>
public string WireName { get; }
public IReadOnlyList<int> Operands => operandArray;
public IReadOnlyList<int> Operands { get; }
/// <summary>The evaluator's view of the operands: the private array, never handed out.</summary>
internal int[] OperandArray => operandArray;
public int Payload { get; }

View file

@ -414,6 +414,10 @@ public sealed class ExprTests
var bindings = ExprBindings.Bind(graph);
Assert.Equal("column-shape", Assert.Throws<ExprGraphException>(() => ExprEvaluator.Evaluate(graph, bindings, new float[2], new float[5])).Rule);
Assert.Equal("column-shape", Assert.Throws<ExprGraphException>(() => ExprEvaluator.Evaluate(graph, bindings, new float[3], new float[4])).Rule);
var short_ = new float[4];
Assert.Equal("column-shape", Assert.Throws<ExprGraphException>(() => ExprEvaluator.Evaluate(graph, bindings, new float[1], short_)).Rule);
Assert.Equal("column-shape", Assert.Throws<ExprGraphException>(() => ExprEvaluator.Evaluate(graph, bindings, ReadOnlySpan<float>.Empty, short_)).Rule);
Assert.Equal(new float[4], short_);
var outputs = new float[6];
ExprEvaluator.Evaluate(graph, bindings, new[] { 1f, 2f, 3f }, outputs);
var none = new ExprBuilder().Build();
@ -421,6 +425,59 @@ public sealed class ExprTests
Assert.Equal(new[] { 1f, 2f, 3f, -1f, -2f, -3f }, outputs);
}
[Fact]
public void AValidatedGraphCannotBeChangedThroughItsLists()
{
var b = new ExprBuilder();
var x = b.Input("x");
var k = b.Param("k", 2f);
b.Output("o", b.Call("mul", x, k));
var graph = b.Build();
var input = new[] { 1f, 2f, 3f };
var before = new float[3];
ExprEvaluator.Evaluate(graph, ExprBindings.Bind(graph), input, before);
var operands = graph.Nodes[2].Operands;
Assert.False(graph.InputNames is string[] || graph.ParamNames is string[] || graph.OutputNames is string[]);
Assert.False(graph.ParamDefaultBits is int[] || graph.OutputNodes is int[] || operands is int[] || graph.Nodes is ExprNode[]);
Assert.Throws<NotSupportedException>(() => ((IList<int>)operands)[0] = 7);
Assert.Throws<NotSupportedException>(() => ((IList<int>)graph.OutputNodes)[0] = 99);
Assert.Throws<NotSupportedException>(() => ((IList<int>)graph.ParamDefaultBits)[0] = 0);
Assert.Throws<NotSupportedException>(() => ((IList<string>)graph.InputNames)[0] = "bad name");
Assert.Throws<NotSupportedException>(() => ((IList<string>)graph.ParamNames)[0] = "bad name");
Assert.Throws<NotSupportedException>(() => ((IList<string>)graph.OutputNames)[0] = "bad name");
Assert.Throws<NotSupportedException>(() => ((IList<ExprNode>)graph.Nodes)[0] = graph.Nodes[1]);
var after = new float[3];
ExprEvaluator.Evaluate(graph, ExprBindings.Bind(graph), input, after);
Assert.Equal(before, after);
Assert.Equal(new[] { 2f, 4f, 6f }, after);
}
[Fact]
public void CreateCopiesWhatItIsGiven()
{
var inputs = new[] { "x" };
var outputs = new[] { "o" };
var outputNodes = new[] { 0 };
var nodes = new[] { ExprNode.Input(0) };
var graph = ExprGraph.Create(ExprGraph.FormatVersion, inputs, Array.Empty<string>(), Array.Empty<int>(), outputs, outputNodes, nodes);
inputs[0] = "bad name";
outputs[0] = "bad name";
outputNodes[0] = 99;
nodes[0] = ExprNode.Const(0);
Assert.Equal(new[] { "x" }, graph.InputNames);
Assert.Equal(new[] { "o" }, graph.OutputNames);
Assert.Equal(new[] { 0 }, graph.OutputNodes);
Assert.Equal(ExprNodeKind.Input, graph.Nodes[0].Kind);
var operands = new[] { 0 };
var call = ExprNode.Call("neg", operands);
operands[0] = 5;
Assert.Equal(new[] { 0 }, call.Operands);
}
private static ExprGraphException Refusal(Action action) => Assert.Throws<ExprGraphException>(action);
[Fact]