CultMath.Expr: scalar SSA expression graph, builder with substitution, one validator, evaluator, MessagePack shape
ExprGraph (immutable class, one validator in Create), ExprBuilder (hash-consing, dead-node removal, Substitute), ExprOps (the one op table), ExprEvaluator, ExprBindings, ExprGraphException; CultMathExprResolver appended to the default chain after CultMathResolver. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9ec66d77f7
commit
dc302a96a7
17 changed files with 782 additions and 1 deletions
8
packages/cultmath/src/CultMath/Expr.meta
Normal file
8
packages/cultmath/src/CultMath/Expr.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a99b96b837a3454781b426cec6bfe837
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
39
packages/cultmath/src/CultMath/Expr/ExprBindings.cs
Normal file
39
packages/cultmath/src/CultMath/Expr/ExprBindings.cs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
namespace CultMath.Expr;
|
||||
|
||||
/// <summary>
|
||||
/// The param values of one graph for an evaluation batch: every param at its default unless overridden by name.
|
||||
/// </summary>
|
||||
public sealed class ExprBindings
|
||||
{
|
||||
private ExprBindings(ExprGraph graph, float[] values)
|
||||
{
|
||||
Graph = graph;
|
||||
Values = values;
|
||||
}
|
||||
|
||||
public ExprGraph Graph { get; }
|
||||
|
||||
/// <summary>One value per param, in the graph's param order.</summary>
|
||||
internal float[] Values { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Binds <paramref name="graph"/>'s params: a name in <paramref name="overrides"/> replaces the param's default
|
||||
/// and every other param keeps it. A name the graph has no param for is refused.
|
||||
/// </summary>
|
||||
public static ExprBindings Bind(ExprGraph graph, IReadOnlyDictionary<string, float>? overrides = null)
|
||||
{
|
||||
var values = new float[graph.ParamNames.Count];
|
||||
for (var i = 0; i < values.Length; i++) values[i] = BitConverter.Int32BitsToSingle(graph.ParamDefaultBits[i]);
|
||||
if (overrides is not null)
|
||||
{
|
||||
foreach (var pair in overrides)
|
||||
{
|
||||
var index = graph.ParamIndex(pair.Key);
|
||||
if (index < 0) throw new ExprGraphException("unknown-param");
|
||||
values[index] = pair.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return new ExprBindings(graph, values);
|
||||
}
|
||||
}
|
||||
2
packages/cultmath/src/CultMath/Expr/ExprBindings.cs.meta
Normal file
2
packages/cultmath/src/CultMath/Expr/ExprBindings.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c9dea1bb6c854e8f80a5e0f05902fd17
|
||||
184
packages/cultmath/src/CultMath/Expr/ExprBuilder.cs
Normal file
184
packages/cultmath/src/CultMath/Expr/ExprBuilder.cs
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
namespace CultMath.Expr;
|
||||
|
||||
/// <summary>A node of an <see cref="ExprBuilder"/> under construction. Only the builder that made it accepts it.</summary>
|
||||
public sealed class ExprValue
|
||||
{
|
||||
internal ExprValue(ExprBuilder owner, int index, int resultCount)
|
||||
{
|
||||
Owner = owner;
|
||||
Index = index;
|
||||
ResultCount = resultCount;
|
||||
}
|
||||
|
||||
internal ExprBuilder Owner { get; }
|
||||
|
||||
internal int Index { get; }
|
||||
|
||||
/// <summary>More than one for a multi-result op, whose results are read only through <see cref="ExprBuilder.Pick"/>.</summary>
|
||||
public int ResultCount { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds an <see cref="ExprGraph"/>. Identical (op, operands, constant bits) nodes are one node, and
|
||||
/// <see cref="Build"/> drops nodes no output reaches and renumbers densely, so the same call sequence builds
|
||||
/// byte-identical graphs. Every call runs the validator's rules and refuses at the call.
|
||||
/// </summary>
|
||||
public sealed class ExprBuilder
|
||||
{
|
||||
private readonly List<ExprNode> nodes = new();
|
||||
private readonly List<ExprValue> handles = new();
|
||||
private readonly Dictionary<string, int> interned = new();
|
||||
private readonly List<string> inputNames = new();
|
||||
private readonly List<string> paramNames = new();
|
||||
private readonly List<int> paramDefaultBits = new();
|
||||
private readonly List<string> outputNames = new();
|
||||
private readonly List<int> outputNodes = new();
|
||||
|
||||
/// <summary>The graph input of this name, declared on first use.</summary>
|
||||
public ExprValue Input(string name)
|
||||
{
|
||||
var port = inputNames.IndexOf(name);
|
||||
if (port < 0)
|
||||
{
|
||||
ExprGraph.CheckPortName(name);
|
||||
port = inputNames.Count;
|
||||
inputNames.Add(name);
|
||||
}
|
||||
|
||||
return Intern(ExprNode.Input(port));
|
||||
}
|
||||
|
||||
/// <summary>The graph param of this name, declared on first use. Declaring it again with other default bits is refused.</summary>
|
||||
public ExprValue Param(string name, float defaultValue) => ParamBits(name, BitConverter.SingleToInt32Bits(defaultValue));
|
||||
|
||||
/// <summary>A constant; its bits (a signed zero, a NaN payload, a subnormal) are kept.</summary>
|
||||
public ExprValue Const(float value) => ConstBits(BitConverter.SingleToInt32Bits(value));
|
||||
|
||||
public ExprValue Call(string op, params ExprValue[] operands)
|
||||
{
|
||||
var indices = new int[operands.Length];
|
||||
for (var i = 0; i < operands.Length; i++) indices[i] = Own(operands[i]);
|
||||
return Intern(ExprNode.Call(op, indices));
|
||||
}
|
||||
|
||||
/// <summary>Result <paramref name="k"/> of a multi-result node.</summary>
|
||||
public ExprValue Pick(ExprValue node, int k) => Intern(ExprNode.Pick(Own(node), k));
|
||||
|
||||
/// <summary>Names a single-result node as an output.</summary>
|
||||
public void Output(string name, ExprValue value)
|
||||
{
|
||||
ExprGraph.CheckPortName(name);
|
||||
if (outputNames.Contains(name)) throw new ExprGraphException("duplicate-port-name");
|
||||
var node = Own(value);
|
||||
ExprGraph.CheckOutput(node, nodes);
|
||||
outputNames.Add(name);
|
||||
outputNodes.Add(node);
|
||||
}
|
||||
|
||||
/// <summary>The graph of the outputs declared so far: only nodes an output reaches, renumbered densely in build order.</summary>
|
||||
public ExprGraph Build()
|
||||
{
|
||||
var live = new bool[nodes.Count];
|
||||
foreach (var node in outputNodes) live[node] = true;
|
||||
for (var i = nodes.Count - 1; i >= 0; i--)
|
||||
if (live[i])
|
||||
foreach (var operand in nodes[i].Operands) live[operand] = true;
|
||||
|
||||
var renumbered = new int[nodes.Count];
|
||||
var kept = new List<ExprNode>();
|
||||
for (var i = 0; i < nodes.Count; i++)
|
||||
{
|
||||
if (!live[i]) continue;
|
||||
renumbered[i] = kept.Count;
|
||||
kept.Add(nodes[i].WithOperands(nodes[i].Operands.Select(operand => renumbered[operand]).ToArray()));
|
||||
}
|
||||
|
||||
return ExprGraph.Create(
|
||||
ExprGraph.FormatVersion, inputNames, paramNames, paramDefaultBits, outputNames, outputNodes.Select(node => renumbered[node]).ToList(), kept);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds <paramref name="graph"/> inside this builder with the named inputs replaced by the given values
|
||||
/// (Fidget's remap_xyz) and returns each output's value by name. An input not named is this builder's input
|
||||
/// of that name; every param is this builder's param of that name. A stencil is N substitutions combined by
|
||||
/// ops: an ordinary flat graph, with no loop construct.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, ExprValue> Substitute(ExprGraph graph, IReadOnlyDictionary<string, ExprValue> replacements)
|
||||
{
|
||||
foreach (var pair in replacements)
|
||||
{
|
||||
if (!graph.InputNames.Contains(pair.Key)) throw new ExprGraphException("unknown-input");
|
||||
Own(pair.Value);
|
||||
if (pair.Value.ResultCount != 1) throw new ExprGraphException("bare-multi-result");
|
||||
}
|
||||
|
||||
var mapped = new ExprValue[graph.Nodes.Count];
|
||||
for (var i = 0; i < mapped.Length; i++)
|
||||
{
|
||||
var node = graph.Nodes[i];
|
||||
switch (node.Kind)
|
||||
{
|
||||
case ExprNodeKind.Input:
|
||||
var input = graph.InputNames[node.Payload];
|
||||
mapped[i] = replacements.TryGetValue(input, out var replacement) ? replacement : Input(input);
|
||||
break;
|
||||
case ExprNodeKind.Param:
|
||||
mapped[i] = ParamBits(graph.ParamNames[node.Payload], graph.ParamDefaultBits[node.Payload]);
|
||||
break;
|
||||
case ExprNodeKind.Const:
|
||||
mapped[i] = ConstBits(node.Payload);
|
||||
break;
|
||||
case ExprNodeKind.Pick:
|
||||
mapped[i] = Pick(mapped[node.Operands[0]], node.Payload);
|
||||
break;
|
||||
default:
|
||||
mapped[i] = Call(node.WireName, node.Operands.Select(operand => mapped[operand]).ToArray());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var outputs = new Dictionary<string, ExprValue>();
|
||||
for (var o = 0; o < graph.OutputNames.Count; o++) outputs[graph.OutputNames[o]] = mapped[graph.OutputNodes[o]];
|
||||
return outputs;
|
||||
}
|
||||
|
||||
private ExprValue ParamBits(string name, int bits)
|
||||
{
|
||||
var port = paramNames.IndexOf(name);
|
||||
if (port < 0)
|
||||
{
|
||||
ExprGraph.CheckPortName(name);
|
||||
port = paramNames.Count;
|
||||
paramNames.Add(name);
|
||||
paramDefaultBits.Add(bits);
|
||||
}
|
||||
else if (paramDefaultBits[port] != bits)
|
||||
{
|
||||
throw new ExprGraphException("port-conflict");
|
||||
}
|
||||
|
||||
return Intern(ExprNode.Param(port));
|
||||
}
|
||||
|
||||
private ExprValue ConstBits(int bits) => Intern(ExprNode.Const(bits));
|
||||
|
||||
private int Own(ExprValue value)
|
||||
{
|
||||
if (value.Owner != this) throw new ExprGraphException("foreign-operand", nodes.Count);
|
||||
return value.Index;
|
||||
}
|
||||
|
||||
private ExprValue Intern(ExprNode node)
|
||||
{
|
||||
ExprGraph.CheckNode(nodes.Count, node, nodes, inputNames.Count, paramNames.Count);
|
||||
if (!interned.TryGetValue(node.Key, out var index))
|
||||
{
|
||||
index = nodes.Count;
|
||||
nodes.Add(node);
|
||||
handles.Add(new ExprValue(this, index, node.ResultCount));
|
||||
interned[node.Key] = index;
|
||||
}
|
||||
|
||||
return handles[index];
|
||||
}
|
||||
}
|
||||
2
packages/cultmath/src/CultMath/Expr/ExprBuilder.cs.meta
Normal file
2
packages/cultmath/src/CultMath/Expr/ExprBuilder.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7326eeabc2d04a43b3d0a1fe6e45b018
|
||||
56
packages/cultmath/src/CultMath/Expr/ExprEvaluator.cs
Normal file
56
packages/cultmath/src/CultMath/Expr/ExprEvaluator.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
namespace CultMath.Expr;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates an <see cref="ExprGraph"/> per point: nodes in order through <see cref="ExprOps"/>, IEEE single per op,
|
||||
/// so a result equals the direct composition of the same math calls bit for bit. Allocates once per call, never per point.
|
||||
/// </summary>
|
||||
public static class ExprEvaluator
|
||||
{
|
||||
/// <summary>
|
||||
/// Columns are contiguous: input <c>i</c> of point <c>p</c> is <c>inputs[i * n + p]</c>, and output <c>o</c> is
|
||||
/// <c>outputs[o * n + p]</c>, where n = <c>outputs.Length / graph.OutputNames.Count</c>.
|
||||
/// </summary>
|
||||
public static void Evaluate(ExprGraph graph, ExprBindings bindings, ReadOnlySpan<float> inputs, Span<float> outputs)
|
||||
{
|
||||
if (bindings.Graph != graph) throw new ExprGraphException("foreign-bindings");
|
||||
var outputCount = graph.OutputNames.Count;
|
||||
if (outputCount == 0) return;
|
||||
if (outputs.Length % outputCount != 0) throw new ExprGraphException("column-shape");
|
||||
var points = outputs.Length / outputCount;
|
||||
if (inputs.Length != points * graph.InputNames.Count) throw new ExprGraphException("column-shape");
|
||||
|
||||
var nodes = graph.Nodes;
|
||||
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++)
|
||||
{
|
||||
var node = nodes[i];
|
||||
switch (node.Kind)
|
||||
{
|
||||
case ExprNodeKind.Input:
|
||||
slots[start[i]] = inputs[node.Payload * points + point];
|
||||
break;
|
||||
case ExprNodeKind.Param:
|
||||
slots[start[i]] = bindings.Values[node.Payload];
|
||||
break;
|
||||
case ExprNodeKind.Const:
|
||||
slots[start[i]] = BitConverter.Int32BitsToSingle(node.Payload);
|
||||
break;
|
||||
case ExprNodeKind.Pick:
|
||||
slots[start[i]] = slots[start[node.Operands[0]] + node.Payload];
|
||||
break;
|
||||
default:
|
||||
var op = node.Op!;
|
||||
for (var k = 0; k < op.Arity; k++) operands[k] = slots[start[node.Operands[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]]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 38108254b5b4430b8f87094e6905dc59
|
||||
168
packages/cultmath/src/CultMath/Expr/ExprGraph.cs
Normal file
168
packages/cultmath/src/CultMath/Expr/ExprGraph.cs
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
namespace CultMath.Expr;
|
||||
|
||||
/// <summary>
|
||||
/// An immutable scalar SSA expression graph: nodes in topological order (an operand names a lower node), named
|
||||
/// inputs that vary per point, named params that are uniform per batch and carry a default, and named outputs.
|
||||
/// A graph is a value; an edit builds a new one. <see cref="Create"/> is the one validator: the builder runs
|
||||
/// it on <see cref="ExprBuilder.Build"/> and a decoder runs it on persisted bytes, so no partial graph is ever returned.
|
||||
/// </summary>
|
||||
public sealed class ExprGraph
|
||||
{
|
||||
/// <summary>The persisted format version this build reads and writes.</summary>
|
||||
public const int FormatVersion = 1;
|
||||
|
||||
private readonly string[] inputNames;
|
||||
private readonly string[] paramNames;
|
||||
private readonly int[] paramDefaultBits;
|
||||
private readonly string[] outputNames;
|
||||
private readonly int[] outputNodes;
|
||||
private readonly ExprNode[] nodes;
|
||||
|
||||
private ExprGraph(string[] inputNames, string[] paramNames, int[] paramDefaultBits, string[] outputNames, int[] outputNodes, ExprNode[] nodes)
|
||||
{
|
||||
this.inputNames = inputNames;
|
||||
this.paramNames = paramNames;
|
||||
this.paramDefaultBits = paramDefaultBits;
|
||||
this.outputNames = outputNames;
|
||||
this.outputNodes = outputNodes;
|
||||
this.nodes = nodes;
|
||||
SlotStart = new int[nodes.Length];
|
||||
var slots = 0;
|
||||
for (var i = 0; i < nodes.Length; i++)
|
||||
{
|
||||
SlotStart[i] = slots;
|
||||
slots += nodes[i].ResultCount;
|
||||
}
|
||||
|
||||
SlotCount = slots;
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> InputNames => inputNames;
|
||||
|
||||
public IReadOnlyList<string> ParamNames => paramNames;
|
||||
|
||||
/// <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<string> OutputNames => outputNames;
|
||||
|
||||
/// <summary>The node each output reads.</summary>
|
||||
public IReadOnlyList<int> OutputNodes => outputNodes;
|
||||
|
||||
public IReadOnlyList<ExprNode> Nodes => nodes;
|
||||
|
||||
/// <summary>Where each node's results start in an evaluator's value array.</summary>
|
||||
internal int[] SlotStart { get; }
|
||||
|
||||
internal int SlotCount { get; }
|
||||
|
||||
/// <summary>The index of the param of this name, or -1.</summary>
|
||||
internal int ParamIndex(string name) => Array.IndexOf(paramNames, name);
|
||||
|
||||
/// <summary>
|
||||
/// Validates and returns a graph, or throws <see cref="ExprGraphException"/> naming the rule and the node.
|
||||
/// The rules: format version, port counts, port names (syntax, unique per kind), and per node its kind's
|
||||
/// operand count, a known op and its arity, operands below the node and never a bare multi-result node,
|
||||
/// pick range, port index and payload; and per output a node in range that is not a bare multi-result node.
|
||||
/// </summary>
|
||||
public static ExprGraph Create(
|
||||
int version,
|
||||
IReadOnlyList<string> inputNames,
|
||||
IReadOnlyList<string> paramNames,
|
||||
IReadOnlyList<int> paramDefaultBits,
|
||||
IReadOnlyList<string> outputNames,
|
||||
IReadOnlyList<int> outputNodes,
|
||||
IReadOnlyList<ExprNode> nodes)
|
||||
{
|
||||
if (version != FormatVersion) throw new ExprGraphException("version");
|
||||
if (paramNames.Count != paramDefaultBits.Count || outputNames.Count != outputNodes.Count) throw new ExprGraphException("port-count");
|
||||
CheckPortNames(inputNames);
|
||||
CheckPortNames(paramNames);
|
||||
CheckPortNames(outputNames);
|
||||
|
||||
var before = new List<ExprNode>(nodes.Count);
|
||||
for (var i = 0; i < nodes.Count; i++)
|
||||
{
|
||||
CheckNode(i, nodes[i], before, inputNames.Count, paramNames.Count);
|
||||
before.Add(nodes[i]);
|
||||
}
|
||||
|
||||
foreach (var node in outputNodes) CheckOutput(node, before);
|
||||
|
||||
return new ExprGraph(
|
||||
inputNames.ToArray(), paramNames.ToArray(), paramDefaultBits.ToArray(), outputNames.ToArray(), outputNodes.ToArray(), nodes.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>A port name is <c>[A-Za-z_][A-Za-z0-9_]*</c>.</summary>
|
||||
internal static void CheckPortName(string name)
|
||||
{
|
||||
if (name.Length == 0 || !IsHead(name[0])) throw new ExprGraphException("port-name");
|
||||
for (var i = 1; i < name.Length; i++)
|
||||
if (!IsHead(name[i]) && !(name[i] >= '0' && name[i] <= '9')) throw new ExprGraphException("port-name");
|
||||
}
|
||||
|
||||
private static bool IsHead(char c) => c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
|
||||
|
||||
private static void CheckPortNames(IReadOnlyList<string> names)
|
||||
{
|
||||
var seen = new HashSet<string>();
|
||||
foreach (var name in names)
|
||||
{
|
||||
CheckPortName(name);
|
||||
if (!seen.Add(name)) throw new ExprGraphException("duplicate-port-name");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The rules for one node given the nodes before it. The builder runs this per call, the validator per node.</summary>
|
||||
internal static void CheckNode(int index, ExprNode node, IReadOnlyList<ExprNode> before, int inputCount, int paramCount)
|
||||
{
|
||||
var operands = node.Operands;
|
||||
switch (node.Kind)
|
||||
{
|
||||
case ExprNodeKind.Input:
|
||||
CheckLeaf(index, node, inputCount);
|
||||
break;
|
||||
case ExprNodeKind.Param:
|
||||
CheckLeaf(index, node, paramCount);
|
||||
break;
|
||||
case ExprNodeKind.Const:
|
||||
if (operands.Count != 0) throw new ExprGraphException("arity", index);
|
||||
break;
|
||||
case ExprNodeKind.Pick:
|
||||
if (operands.Count != 1) throw new ExprGraphException("arity", index);
|
||||
CheckOperandOrder(index, operands[0]);
|
||||
var results = before[operands[0]].ResultCount;
|
||||
if (results <= 1 || node.Payload < 0 || node.Payload >= results) throw new ExprGraphException("pick-range", index);
|
||||
break;
|
||||
default:
|
||||
if (node.Op is null) throw new ExprGraphException("unknown-op", index);
|
||||
if (operands.Count != node.Op.Arity) throw new ExprGraphException("arity", index);
|
||||
if (node.Payload != 0) throw new ExprGraphException("payload", index);
|
||||
foreach (var operand in operands)
|
||||
{
|
||||
CheckOperandOrder(index, operand);
|
||||
if (before[operand].ResultCount != 1) throw new ExprGraphException("bare-multi-result", index);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckLeaf(int index, ExprNode node, int portCount)
|
||||
{
|
||||
if (node.Operands.Count != 0) throw new ExprGraphException("arity", index);
|
||||
if (node.Payload < 0 || node.Payload >= portCount) throw new ExprGraphException("port-index", index);
|
||||
}
|
||||
|
||||
private static void CheckOperandOrder(int index, int operand)
|
||||
{
|
||||
if (operand < 0 || operand >= index) throw new ExprGraphException("operand-order", index);
|
||||
}
|
||||
|
||||
/// <summary>An output reads a node in range that is single-result.</summary>
|
||||
internal static void CheckOutput(int node, IReadOnlyList<ExprNode> nodes)
|
||||
{
|
||||
if (node < 0 || node >= nodes.Count) throw new ExprGraphException("output-node");
|
||||
if (nodes[node].ResultCount != 1) throw new ExprGraphException("bare-multi-result", node);
|
||||
}
|
||||
}
|
||||
2
packages/cultmath/src/CultMath/Expr/ExprGraph.cs.meta
Normal file
2
packages/cultmath/src/CultMath/Expr/ExprGraph.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 04d707ad2a4e4e0299407ce1d86afea3
|
||||
22
packages/cultmath/src/CultMath/Expr/ExprGraphException.cs
Normal file
22
packages/cultmath/src/CultMath/Expr/ExprGraphException.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
namespace CultMath.Expr;
|
||||
|
||||
/// <summary>
|
||||
/// The one refusal of the expression graph: a build call, a decoded encoding, a binding or an evaluation
|
||||
/// that breaks a rule. <see cref="Rule"/> names the rule and <see cref="NodeIndex"/> the node it broke at
|
||||
/// (-1 when no node is involved). The message names the rule and never echoes a value it was given.
|
||||
/// </summary>
|
||||
public sealed class ExprGraphException : Exception
|
||||
{
|
||||
public ExprGraphException(string rule, int nodeIndex = -1)
|
||||
: base(nodeIndex < 0 ? rule : rule + " at node " + nodeIndex)
|
||||
{
|
||||
Rule = rule;
|
||||
NodeIndex = nodeIndex;
|
||||
}
|
||||
|
||||
/// <summary>The rule broken, a short stable name such as <c>unknown-op</c> or <c>operand-order</c>.</summary>
|
||||
public string Rule { get; }
|
||||
|
||||
/// <summary>The index of the node the rule broke at, or -1.</summary>
|
||||
public int NodeIndex { get; }
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 88e355a7be3f44ada7f1d9464c60428e
|
||||
79
packages/cultmath/src/CultMath/Expr/ExprNode.cs
Normal file
79
packages/cultmath/src/CultMath/Expr/ExprNode.cs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
namespace CultMath.Expr;
|
||||
|
||||
/// <summary>What a node is: a graph input, a graph param, a constant, a pick of one result of a multi-result node, or a call of an op.</summary>
|
||||
public enum ExprNodeKind
|
||||
{
|
||||
Input,
|
||||
Param,
|
||||
Const,
|
||||
Pick,
|
||||
Call,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One node of an <see cref="ExprGraph"/>: a kind, the operand node indices (always lower than its own),
|
||||
/// and one integer payload: the port index of an input or param, the float bits of a constant, the result
|
||||
/// index of a pick, and zero for a call. Immutable.
|
||||
/// </summary>
|
||||
public sealed class ExprNode
|
||||
{
|
||||
private const string InputName = "input";
|
||||
private const string ParamName = "param";
|
||||
private const string ConstName = "const";
|
||||
private const string PickName = "pick";
|
||||
|
||||
private static readonly int[] NoOperands = Array.Empty<int>();
|
||||
|
||||
private readonly int[] operandArray;
|
||||
|
||||
private ExprNode(ExprNodeKind kind, string wireName, int[] operands, int payload, ExprOp? op)
|
||||
{
|
||||
Kind = kind;
|
||||
WireName = wireName;
|
||||
operandArray = operands;
|
||||
Payload = payload;
|
||||
Op = op;
|
||||
ResultCount = op?.ResultCount ?? 1;
|
||||
}
|
||||
|
||||
public ExprNodeKind Kind { get; }
|
||||
|
||||
/// <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 int Payload { get; }
|
||||
|
||||
/// <summary>How many floats the node produces: the op's result count for a call, otherwise one.</summary>
|
||||
public int ResultCount { get; }
|
||||
|
||||
/// <summary>The op of a known call; null for every other node and for a call naming an unknown op.</summary>
|
||||
internal ExprOp? Op { get; }
|
||||
|
||||
public static ExprNode Input(int port) => new(ExprNodeKind.Input, InputName, NoOperands, port, null);
|
||||
|
||||
public static ExprNode Param(int port) => new(ExprNodeKind.Param, ParamName, NoOperands, port, null);
|
||||
|
||||
public static ExprNode Const(int bits) => new(ExprNodeKind.Const, ConstName, NoOperands, bits, null);
|
||||
|
||||
public static ExprNode Pick(int node, int result) => new(ExprNodeKind.Pick, PickName, new[] { node }, result, null);
|
||||
|
||||
public static ExprNode Call(string op, IReadOnlyList<int> operands) =>
|
||||
new(ExprNodeKind.Call, op, operands.ToArray(), 0, ExprOps.Find(op));
|
||||
|
||||
/// <summary>The node a persisted <paramref name="wireName"/> names. A name that is none of the four kinds is a call, which the validator checks against the op table.</summary>
|
||||
public static ExprNode FromWire(string wireName, IReadOnlyList<int> operands, int payload) => wireName switch
|
||||
{
|
||||
InputName => new ExprNode(ExprNodeKind.Input, wireName, operands.ToArray(), payload, null),
|
||||
ParamName => new ExprNode(ExprNodeKind.Param, wireName, operands.ToArray(), payload, null),
|
||||
ConstName => new ExprNode(ExprNodeKind.Const, wireName, operands.ToArray(), payload, null),
|
||||
PickName => new ExprNode(ExprNodeKind.Pick, wireName, operands.ToArray(), payload, null),
|
||||
_ => new ExprNode(ExprNodeKind.Call, wireName, operands.ToArray(), payload, ExprOps.Find(wireName)),
|
||||
};
|
||||
|
||||
internal ExprNode WithOperands(int[] operands) => new(Kind, WireName, operands, Payload, Op);
|
||||
|
||||
/// <summary>Equal keys are the same expression: the builder's hash-consing identity.</summary>
|
||||
internal string Key => WireName + "|" + string.Join(",", operandArray) + "|" + Payload;
|
||||
}
|
||||
2
packages/cultmath/src/CultMath/Expr/ExprNode.cs.meta
Normal file
2
packages/cultmath/src/CultMath/Expr/ExprNode.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: fc3cb4abaa27482aa98a76cd876b8805
|
||||
86
packages/cultmath/src/CultMath/Expr/ExprOps.cs
Normal file
86
packages/cultmath/src/CultMath/Expr/ExprOps.cs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
using static CultMath.math;
|
||||
|
||||
namespace CultMath.Expr;
|
||||
|
||||
/// <summary>One entry of the op table: a stable name, its arity, its result count and its C# implementation.</summary>
|
||||
public sealed class ExprOp
|
||||
{
|
||||
/// <summary>Writes <paramref name="results"/> (<see cref="ResultCount"/> floats) from <paramref name="operands"/>
|
||||
/// (<see cref="Arity"/> floats).</summary>
|
||||
public delegate void Function(ReadOnlySpan<float> operands, Span<float> results);
|
||||
|
||||
private readonly Function function;
|
||||
|
||||
internal ExprOp(string name, int arity, int resultCount, Function function)
|
||||
{
|
||||
Name = name;
|
||||
Arity = arity;
|
||||
ResultCount = resultCount;
|
||||
this.function = function;
|
||||
}
|
||||
|
||||
/// <summary>The persisted name. Append-only: a released name keeps its meaning.</summary>
|
||||
public string Name { get; }
|
||||
|
||||
public int Arity { get; }
|
||||
|
||||
/// <summary>One for a scalar op; more for an op read only through <see cref="ExprBuilder.Pick"/>.</summary>
|
||||
public int ResultCount { get; }
|
||||
|
||||
public void Evaluate(ReadOnlySpan<float> operands, Span<float> results) => function(operands, results);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The one op table of the expression graph: each op names a CultMath function and calls it, wrapping no type
|
||||
/// and copying no body. The C# evaluator reads it, and so does any later lowering.
|
||||
/// </summary>
|
||||
public static class ExprOps
|
||||
{
|
||||
/// <summary>The largest arity in the table.</summary>
|
||||
internal const int MaxArity = 4;
|
||||
|
||||
private static ExprOp Op(string name, int arity, ExprOp.Function function) => new(name, arity, 1, function);
|
||||
|
||||
private static readonly ExprOp[] Table =
|
||||
{
|
||||
Op("add", 2, (a, r) => r[0] = a[0] + a[1]),
|
||||
Op("sub", 2, (a, r) => r[0] = a[0] - a[1]),
|
||||
Op("mul", 2, (a, r) => r[0] = a[0] * a[1]),
|
||||
Op("div", 2, (a, r) => r[0] = a[0] / a[1]),
|
||||
Op("neg", 1, (a, r) => r[0] = -a[0]),
|
||||
Op("min", 2, (a, r) => r[0] = min(a[0], a[1])),
|
||||
Op("max", 2, (a, r) => r[0] = max(a[0], a[1])),
|
||||
Op("abs", 1, (a, r) => r[0] = abs(a[0])),
|
||||
Op("floor", 1, (a, r) => r[0] = floor(a[0])),
|
||||
Op("frac", 1, (a, r) => r[0] = frac(a[0])),
|
||||
Op("sqrt", 1, (a, r) => r[0] = sqrt(a[0])),
|
||||
Op("exp", 1, (a, r) => r[0] = exp(a[0])),
|
||||
Op("log", 1, (a, r) => r[0] = log(a[0])),
|
||||
Op("pow", 2, (a, r) => r[0] = pow(a[0], a[1])),
|
||||
Op("sin", 1, (a, r) => r[0] = sin(a[0])),
|
||||
Op("cos", 1, (a, r) => r[0] = cos(a[0])),
|
||||
Op("clamp", 3, (a, r) => r[0] = clamp(a[0], a[1], a[2])),
|
||||
Op("saturate", 1, (a, r) => r[0] = saturate(a[0])),
|
||||
Op("lerp", 3, (a, r) => r[0] = lerp(a[0], a[1], a[2])),
|
||||
Op("step", 2, (a, r) => r[0] = step(a[0], a[1])),
|
||||
Op("smoothstep", 3, (a, r) => r[0] = smoothstep(a[0], a[1], a[2])),
|
||||
Op("select", 4, (a, r) => r[0] = select(a[0] > a[1], a[2], a[3])),
|
||||
Op("snoise2", 2, (a, r) => r[0] = snoise(new float2(a[0], a[1]))),
|
||||
Op("snoise3", 3, (a, r) => r[0] = snoise(new float3(a[0], a[1], a[2]))),
|
||||
new ExprOp("cellular3", 3, 9, (a, r) =>
|
||||
{
|
||||
var c = cellular(new float3(a[0], a[1], a[2]));
|
||||
r[0] = c.nearest.x; r[1] = c.nearest.y; r[2] = c.nearest.z; r[3] = c.nearest.w;
|
||||
r[4] = c.edge.x; r[5] = c.edge.y; r[6] = c.edge.z; r[7] = c.edge.w;
|
||||
r[8] = c.id;
|
||||
}),
|
||||
};
|
||||
|
||||
private static readonly Dictionary<string, ExprOp> ByName = Table.ToDictionary(op => op.Name);
|
||||
|
||||
/// <summary>Every op, in declaration order.</summary>
|
||||
public static IReadOnlyList<ExprOp> All => Table;
|
||||
|
||||
/// <summary>The op with this persisted name, or null.</summary>
|
||||
public static ExprOp? Find(string name) => ByName.TryGetValue(name, out var op) ? op : null;
|
||||
}
|
||||
2
packages/cultmath/src/CultMath/Expr/ExprOps.cs.meta
Normal file
2
packages/cultmath/src/CultMath/Expr/ExprOps.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f2cb6ebc7a4f43d59009e613a1b75888
|
||||
|
|
@ -58,7 +58,7 @@ public static class CultDocumentMessagePackSerialization
|
|||
// The guard goes first and wraps whatever formatter the rest of the composite would hand out, so a consumer
|
||||
// resolver still encodes an arm while the key check runs.
|
||||
var inner = CompositeResolver.Create(
|
||||
consumerResolvers.Append(CultDocumentResolver.Instance).Append(CultMathResolver.Instance).Append(StandardResolver.Instance).ToArray());
|
||||
consumerResolvers.Append(CultDocumentResolver.Instance).Append(CultMathResolver.Instance).Append(CultMathExprResolver.Instance).Append(StandardResolver.Instance).ToArray());
|
||||
return MessagePackSerializerOptions.Standard
|
||||
.WithResolver(CompositeResolver.Create(new CultUnionGuard(inner), inner))
|
||||
.WithSecurity(CultMessagePackSecurity.Instance);
|
||||
|
|
|
|||
125
src/GameCult.Caching.MessagePack/CultMathExprResolver.cs
Normal file
125
src/GameCult.Caching.MessagePack/CultMathExprResolver.cs
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CultMath.Expr;
|
||||
using MessagePack;
|
||||
using MessagePack.Formatters;
|
||||
|
||||
namespace GameCult.Caching.MessagePack;
|
||||
|
||||
/// <summary>
|
||||
/// The canonical MessagePack encoding of <see cref="ExprGraph"/>, beside <see cref="CultMathResolver"/> in the
|
||||
/// default chain. A graph is <c>[version, inputs, params, outputs, nodes]</c>: <c>inputs</c> are names; a param is
|
||||
/// <c>[name, default float bits]</c>; an output is <c>[name, node]</c>; a node is <c>[wire name, operands, payload]</c>
|
||||
/// (the payload is the port index of an input or param, the float bits of a constant, the result index of a pick,
|
||||
/// zero for a call). Decoding runs <see cref="ExprGraph.Create"/>, the one validator, so a graph that breaks a rule
|
||||
/// (unknown op, newer version, forward operand) is refused whole with an <see cref="ExprGraphException"/> and never
|
||||
/// partly loaded. A nil graph is refused both ways.
|
||||
/// </summary>
|
||||
public sealed class CultMathExprResolver : IFormatterResolver
|
||||
{
|
||||
public static readonly CultMathExprResolver Instance = new();
|
||||
private CultMathExprResolver() { }
|
||||
|
||||
public IMessagePackFormatter<T>? GetFormatter<T>() => Cache<T>.Formatter;
|
||||
|
||||
private static class Cache<T>
|
||||
{
|
||||
public static readonly IMessagePackFormatter<T>? Formatter =
|
||||
typeof(T) == typeof(ExprGraph) ? (IMessagePackFormatter<T>)(object)new GraphFormatter() : null;
|
||||
}
|
||||
|
||||
private sealed class GraphFormatter : IMessagePackFormatter<ExprGraph>
|
||||
{
|
||||
public void Serialize(ref MessagePackWriter writer, ExprGraph value, MessagePackSerializerOptions options)
|
||||
{
|
||||
if (value is null) throw new ExprGraphException("shape");
|
||||
writer.WriteArrayHeader(5);
|
||||
writer.Write(ExprGraph.FormatVersion);
|
||||
|
||||
writer.WriteArrayHeader(value.InputNames.Count);
|
||||
foreach (var name in value.InputNames) writer.Write(name);
|
||||
|
||||
writer.WriteArrayHeader(value.ParamNames.Count);
|
||||
for (var i = 0; i < value.ParamNames.Count; i++)
|
||||
{
|
||||
writer.WriteArrayHeader(2);
|
||||
writer.Write(value.ParamNames[i]);
|
||||
writer.Write(value.ParamDefaultBits[i]);
|
||||
}
|
||||
|
||||
writer.WriteArrayHeader(value.OutputNames.Count);
|
||||
for (var i = 0; i < value.OutputNames.Count; i++)
|
||||
{
|
||||
writer.WriteArrayHeader(2);
|
||||
writer.Write(value.OutputNames[i]);
|
||||
writer.Write(value.OutputNodes[i]);
|
||||
}
|
||||
|
||||
writer.WriteArrayHeader(value.Nodes.Count);
|
||||
foreach (var node in value.Nodes)
|
||||
{
|
||||
writer.WriteArrayHeader(3);
|
||||
writer.Write(node.WireName);
|
||||
writer.WriteArrayHeader(node.Operands.Count);
|
||||
foreach (var operand in node.Operands) writer.Write(operand);
|
||||
writer.Write(node.Payload);
|
||||
}
|
||||
}
|
||||
|
||||
public ExprGraph Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
|
||||
{
|
||||
if (reader.TryReadNil()) throw new ExprGraphException("shape");
|
||||
options.Security.DepthStep(ref reader);
|
||||
try
|
||||
{
|
||||
Expect(ref reader, 5);
|
||||
var version = reader.ReadInt32();
|
||||
|
||||
var inputs = new List<string>();
|
||||
for (int i = 0, n = reader.ReadArrayHeader(); i < n; i++) inputs.Add(ReadName(ref reader));
|
||||
|
||||
var paramNames = new List<string>();
|
||||
var paramBits = new List<int>();
|
||||
for (int i = 0, n = reader.ReadArrayHeader(); i < n; i++)
|
||||
{
|
||||
Expect(ref reader, 2);
|
||||
paramNames.Add(ReadName(ref reader));
|
||||
paramBits.Add(reader.ReadInt32());
|
||||
}
|
||||
|
||||
var outputNames = new List<string>();
|
||||
var outputNodes = new List<int>();
|
||||
for (int i = 0, n = reader.ReadArrayHeader(); i < n; i++)
|
||||
{
|
||||
Expect(ref reader, 2);
|
||||
outputNames.Add(ReadName(ref reader));
|
||||
outputNodes.Add(reader.ReadInt32());
|
||||
}
|
||||
|
||||
var nodes = new List<ExprNode>();
|
||||
for (int i = 0, n = reader.ReadArrayHeader(); i < n; i++)
|
||||
{
|
||||
Expect(ref reader, 3);
|
||||
var wireName = ReadName(ref reader);
|
||||
var operands = new List<int>();
|
||||
for (int k = 0, m = reader.ReadArrayHeader(); k < m; k++) operands.Add(reader.ReadInt32());
|
||||
nodes.Add(ExprNode.FromWire(wireName, operands, reader.ReadInt32()));
|
||||
}
|
||||
|
||||
return ExprGraph.Create(version, inputs, paramNames, paramBits, outputNames, outputNodes, nodes);
|
||||
}
|
||||
finally
|
||||
{
|
||||
reader.Depth--;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Expect(ref MessagePackReader reader, int count)
|
||||
{
|
||||
if (reader.ReadArrayHeader() != count) throw new ExprGraphException("shape");
|
||||
}
|
||||
|
||||
private static string ReadName(ref MessagePackReader reader) =>
|
||||
reader.ReadString() ?? throw new ExprGraphException("shape");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue