From 646cc59426bd8cf0aca44403619ffb3ea72d27ac Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 07:54:31 +0200 Subject: [PATCH] ExprGraph lists are read-only views over private arrays; test short evaluator inputs Co-Authored-By: Claude Opus 5.5 --- .../src/CultMath/Expr/ExprEvaluator.cs | 11 ++-- .../cultmath/src/CultMath/Expr/ExprGraph.cs | 24 ++++++-- .../cultmath/src/CultMath/Expr/ExprNode.cs | 6 +- .../tests/CultMath.Tests/ExprTests.cs | 57 +++++++++++++++++++ 4 files changed, 86 insertions(+), 12 deletions(-) diff --git a/packages/cultmath/src/CultMath/Expr/ExprEvaluator.cs b/packages/cultmath/src/CultMath/Expr/ExprEvaluator.cs index 3a7496af..2912f1ee 100644 --- a/packages/cultmath/src/CultMath/Expr/ExprEvaluator.cs +++ b/packages/cultmath/src/CultMath/Expr/ExprEvaluator.cs @@ -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 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(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]]]; } } } diff --git a/packages/cultmath/src/CultMath/Expr/ExprGraph.cs b/packages/cultmath/src/CultMath/Expr/ExprGraph.cs index a75bb941..d0edc488 100644 --- a/packages/cultmath/src/CultMath/Expr/ExprGraph.cs +++ b/packages/cultmath/src/CultMath/Expr/ExprGraph.cs @@ -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 InputNames => inputNames; + public IReadOnlyList InputNames { get; } - public IReadOnlyList ParamNames => paramNames; + public IReadOnlyList ParamNames { get; } /// The default of each param as float bits, so a NaN payload or a signed zero survives. - public IReadOnlyList ParamDefaultBits => paramDefaultBits; + public IReadOnlyList ParamDefaultBits { get; } - public IReadOnlyList OutputNames => outputNames; + public IReadOnlyList OutputNames { get; } /// The node each output reads. - public IReadOnlyList OutputNodes => outputNodes; + public IReadOnlyList OutputNodes { get; } - public IReadOnlyList Nodes => nodes; + public IReadOnlyList Nodes { get; } + + /// The evaluator's view of the nodes: the private array, never handed out. + internal ExprNode[] NodeArray => nodes; + + /// The evaluator's view of the output nodes: the private array, never handed out. + internal int[] OutputNodeArray => outputNodes; /// Where each node's results start in an evaluator's value array. internal int[] SlotStart { get; } diff --git a/packages/cultmath/src/CultMath/Expr/ExprNode.cs b/packages/cultmath/src/CultMath/Expr/ExprNode.cs index 719eac66..d0bd7d39 100644 --- a/packages/cultmath/src/CultMath/Expr/ExprNode.cs +++ b/packages/cultmath/src/CultMath/Expr/ExprNode.cs @@ -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 /// The persisted name: an op name for a call, otherwise input, param, const or pick. public string WireName { get; } - public IReadOnlyList Operands => operandArray; + public IReadOnlyList Operands { get; } + + /// The evaluator's view of the operands: the private array, never handed out. + internal int[] OperandArray => operandArray; public int Payload { get; } diff --git a/packages/cultmath/tests/CultMath.Tests/ExprTests.cs b/packages/cultmath/tests/CultMath.Tests/ExprTests.cs index c4f3a2c2..491e7537 100644 --- a/packages/cultmath/tests/CultMath.Tests/ExprTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/ExprTests.cs @@ -414,6 +414,10 @@ public sealed class ExprTests var bindings = ExprBindings.Bind(graph); Assert.Equal("column-shape", Assert.Throws(() => ExprEvaluator.Evaluate(graph, bindings, new float[2], new float[5])).Rule); Assert.Equal("column-shape", Assert.Throws(() => ExprEvaluator.Evaluate(graph, bindings, new float[3], new float[4])).Rule); + var short_ = new float[4]; + Assert.Equal("column-shape", Assert.Throws(() => ExprEvaluator.Evaluate(graph, bindings, new float[1], short_)).Rule); + Assert.Equal("column-shape", Assert.Throws(() => ExprEvaluator.Evaluate(graph, bindings, ReadOnlySpan.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(() => ((IList)operands)[0] = 7); + Assert.Throws(() => ((IList)graph.OutputNodes)[0] = 99); + Assert.Throws(() => ((IList)graph.ParamDefaultBits)[0] = 0); + Assert.Throws(() => ((IList)graph.InputNames)[0] = "bad name"); + Assert.Throws(() => ((IList)graph.ParamNames)[0] = "bad name"); + Assert.Throws(() => ((IList)graph.OutputNames)[0] = "bad name"); + Assert.Throws(() => ((IList)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(), Array.Empty(), 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(action); [Fact]