diff --git a/StoneRed.LogicSimulator.Test/CircuitDefinition.cs b/StoneRed.LogicSimulator.Test/CircuitDefinition.cs new file mode 100644 index 0000000..dcb2b36 --- /dev/null +++ b/StoneRed.LogicSimulator.Test/CircuitDefinition.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StoneRed.LogicSimulator.Test; + +public sealed class CircuitDefinition +{ + private readonly List gateKinds = new(); + private readonly List<(int FromGate, int ToGate, byte ToInputBit)> connections = new(); + private readonly List inputPins = new(); + private readonly List outputPins = new(); + private readonly List macroInstances = new(); + + public IReadOnlyList GateKinds => gateKinds; + public IReadOnlyList<(int FromGate, int ToGate, byte ToInputBit)> Connections => connections; + public IReadOnlyList InputPins => inputPins; + public IReadOnlyList OutputPins => outputPins; + public IReadOnlyList MacroInstances => macroInstances; + + public sealed record MacroInstanceDef(string Name, int[] Inputs, int[] Outputs); + + public int AddGate(GateKind kind) + { + int id = gateKinds.Count; + gateKinds.Add(kind); + return id; + } + + public int AddInputPin() + { + int id = AddGate(GateKind.Source); + inputPins.Add(id); + return id; + } + + public int AddOutputPin() + { + int id = AddGate(GateKind.Sink); + outputPins.Add(id); + return id; + } + + public MacroInstanceDef AddMacroInstance(string name, int inputCount, int outputCount) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Macro name must not be empty.", nameof(name)); + } + + if (inputCount < 0) + { + throw new ArgumentOutOfRangeException(nameof(inputCount)); + } + + if (outputCount < 0) + { + throw new ArgumentOutOfRangeException(nameof(outputCount)); + } + + int[] inputs = new int[inputCount]; + for (int i = 0; i < inputCount; i++) + { + inputs[i] = AddGate(GateKind.Buffer); + } + + int[] outputs = new int[outputCount]; + for (int i = 0; i < outputCount; i++) + { + outputs[i] = AddGate(GateKind.Sink); + } + + var instance = new MacroInstanceDef(name, inputs, outputs); + macroInstances.Add(instance); + return instance; + } + + public void Connect(int fromGate, int toGate, int toInputBit) + { + if ((uint)fromGate >= (uint)gateKinds.Count) throw new ArgumentOutOfRangeException(nameof(fromGate)); + if ((uint)toGate >= (uint)gateKinds.Count) throw new ArgumentOutOfRangeException(nameof(toGate)); + if ((uint)toInputBit >= 32u) throw new ArgumentOutOfRangeException(nameof(toInputBit)); + + connections.Add((fromGate, toGate, (byte)toInputBit)); + } + + public void Validate() + { + // Minimal validation to prevent ambiguous semantics. + // - Input pins must be Source gates and must not have incoming connections. + // - Output pins must be Sink gates. + // - All Source gates must be listed as InputPins. + // - LUT gates are not allowed in definitions (they are generated by the simulator). + var hasIncoming = new bool[gateKinds.Count]; + for (int i = 0; i < connections.Count; i++) + { + (_, int to, _) = connections[i]; + hasIncoming[to] = true; + } + + var inputPinSet = new HashSet(inputPins); + for (int i = 0; i < gateKinds.Count; i++) + { + if (gateKinds[i] == GateKind.Source && !inputPinSet.Contains(i)) + { + throw new InvalidOperationException("Only Source gates marked as InputPins are allowed inside a circuit definition."); + } + + if (gateKinds[i] == GateKind.Lut) + { + throw new InvalidOperationException("Circuit definitions must not contain LUT gates."); + } + } + + var macroPinGates = new HashSet(); + foreach (MacroInstanceDef instance in macroInstances) + { + if (string.IsNullOrWhiteSpace(instance.Name)) + { + throw new InvalidOperationException("Macro instance name must not be empty."); + } + + foreach (int pin in instance.Inputs) + { + if ((uint)pin >= (uint)gateKinds.Count) throw new InvalidOperationException("Macro instance input pin is out of range."); + if (gateKinds[pin] != GateKind.Buffer) throw new InvalidOperationException("Macro instance inputs must be Buffer gates."); + if (!macroPinGates.Add(pin)) throw new InvalidOperationException("Macro instance pin is used more than once."); + } + + foreach (int pin in instance.Outputs) + { + if ((uint)pin >= (uint)gateKinds.Count) throw new InvalidOperationException("Macro instance output pin is out of range."); + if (gateKinds[pin] != GateKind.Sink) throw new InvalidOperationException("Macro instance outputs must be Sink gates."); + if (!macroPinGates.Add(pin)) throw new InvalidOperationException("Macro instance pin is used more than once."); + } + } + + foreach (int pin in macroPinGates) + { + if (inputPinSet.Contains(pin) || outputPins.Contains(pin)) + { + throw new InvalidOperationException("Macro instance pins must not be listed as InputPins/OutputPins."); + } + } + + foreach (int input in inputPins) + { + if (gateKinds[input] != GateKind.Source) + { + throw new InvalidOperationException("Input pins must be Source gates."); + } + + if (hasIncoming[input]) + { + throw new InvalidOperationException("Input pins must not have incoming connections."); + } + } + + foreach (int output in outputPins) + { + if (gateKinds[output] != GateKind.Sink) + { + throw new InvalidOperationException("Output pins must be Sink gates."); + } + } + + if (inputPins.Distinct().Count() != inputPins.Count) + { + throw new InvalidOperationException("Input pins contain duplicates."); + } + + if (outputPins.Distinct().Count() != outputPins.Count) + { + throw new InvalidOperationException("Output pins contain duplicates."); + } + } +} diff --git a/StoneRed.LogicSimulator.Test/ExprCircuitSimulator.cs b/StoneRed.LogicSimulator.Test/ExprCircuitSimulator.cs new file mode 100644 index 0000000..10e6af3 --- /dev/null +++ b/StoneRed.LogicSimulator.Test/ExprCircuitSimulator.cs @@ -0,0 +1,618 @@ +using System.Linq.Expressions; + +namespace StoneRed.LogicSimulator.Test; + +public enum GateKind : byte +{ + Source, + Not, + And2, + Or2, + Buffer, + Sink, + Lut, +} + +public sealed class ExprCircuitSimulator +{ + private readonly List gateKinds = []; + private readonly List<(int FromGate, int ToGate, byte ToInputBit)> connections = []; + private readonly List lutTableByGate = []; + private readonly Dictionary macroGates = new(StringComparer.Ordinal); + + private int[] inputMasks = Array.Empty(); + private int[] nextInputMasks = Array.Empty(); + private int[] outputMasks = Array.Empty(); + private int[] sourceStates = Array.Empty(); + + private int[] edgeStart = Array.Empty(); + private int[] edgeToGate = Array.Empty(); + private byte[] edgeToInputBit = Array.Empty(); + + private int[] lutOffsets = Array.Empty(); + private int[] lutMasks = Array.Empty(); + private int[] lutData = Array.Empty(); + + private Action? computeOutputs; + private bool compiled; + + public int GateCount => gateKinds.Count; + + public sealed record MacroInstance(string Name, int[] Inputs, int[] Outputs); + + private sealed record MacroLut(int InputCount, int OutputCount, int[][] OutputTables); + + private sealed record MacroInfo(CircuitDefinition Definition, MacroLut? Lut); + + public int AddGate(GateKind kind) + { + if (kind == GateKind.Lut) + { + throw new InvalidOperationException("Use AddLutGate() to create LUT gates."); + } + + int id = gateKinds.Count; + gateKinds.Add(kind); + lutTableByGate.Add(null); + compiled = false; + return id; + } + + public int AddLutGate(int inputCount, int[] table) + { + if (inputCount is < 0 or > 30) + { + throw new ArgumentOutOfRangeException(nameof(inputCount), "LUT input count must be between 0 and 30."); + } + + if (table is null) + { + throw new ArgumentNullException(nameof(table)); + } + + int expected = 1 << inputCount; + if (table.Length != expected) + { + throw new ArgumentException($"LUT table length must be {expected} for {inputCount} inputs.", nameof(table)); + } + + int id = gateKinds.Count; + gateKinds.Add(GateKind.Lut); + lutTableByGate.Add(table); + compiled = false; + return id; + } + + public void ConnectGates(int fromGate, int toGate, int toInputBit) + { + if ((uint)fromGate >= (uint)gateKinds.Count) + { + throw new ArgumentOutOfRangeException(nameof(fromGate)); + } + + if ((uint)toGate >= (uint)gateKinds.Count) + { + throw new ArgumentOutOfRangeException(nameof(toGate)); + } + + if ((uint)toInputBit >= 32u) + { + throw new ArgumentOutOfRangeException(nameof(toInputBit)); + } + + connections.Add((fromGate, toGate, (byte)toInputBit)); + compiled = false; + } + + public void RegisterMacroGate(string name, CircuitDefinition definition) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Macro name must not be empty.", nameof(name)); + } + + definition.Validate(); + macroGates[name] = new MacroInfo(definition, Lut: null); + compiled = false; + } + + public bool ComputeLut(string name, int maxSteps = 4096) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Macro name must not be empty.", nameof(name)); + } + + if (maxSteps <= 0) + { + throw new ArgumentOutOfRangeException(nameof(maxSteps)); + } + + if (!macroGates.TryGetValue(name, out MacroInfo? macro)) + { + throw new KeyNotFoundException($"Macro gate '{name}' is not registered."); + } + + MacroLut? lut = TryBuildMacroLut(macro.Definition, maxSteps: maxSteps); + macroGates[name] = macro with { Lut = lut }; + compiled = false; + return lut is not null; + } + + public MacroInstance AddMacroGate(string name) + { + if (!macroGates.TryGetValue(name, out MacroInfo? macro)) + { + throw new KeyNotFoundException($"Macro gate '{name}' is not registered."); + } + + CircuitDefinition definition = macro.Definition; + + if (macro.Lut is not null) + { + return AddMacroGateFromLut(name, macro.Lut); + } + + // Inline (flatten) definition into this simulator by copying its gates and connections. + // Input pins are represented as Source gates in the definition, but Source gates cannot be driven by wires. + // For each input pin we substitute a Buffer gate, which can be driven externally and fans out internally. + int[] map = CopyDefinitionGatesAndConnections( + destination: this, + definition: definition, + mapKind: (gateId, kind) => + kind == GateKind.Source + ? (definition.InputPins.Contains(gateId) ? GateKind.Buffer : throw new InvalidOperationException("Only Source gates marked as InputPins are allowed inside a macro definition.")) + : kind); + + int[] inputs = MapPins(definition.InputPins, map); + int[] outputs = MapPins(definition.OutputPins, map); + + return new MacroInstance(name, inputs, outputs); + } + + public void ClearSignals() + { + EnsureStorage(); + Array.Clear(inputMasks); + Array.Clear(nextInputMasks); + Array.Clear(outputMasks); + } + + public void SetSource(int gateId, bool value) + { + EnsureStorage(); + + if ((uint)gateId >= (uint)gateKinds.Count) + { + throw new ArgumentOutOfRangeException(nameof(gateId)); + } + + if (gateKinds[gateId] != GateKind.Source) + { + throw new InvalidOperationException("Gate is not a source."); + } + + sourceStates[gateId] = value ? 1 : 0; + } + + public bool GetOutput(int gateId) + { + EnsureStorage(); + if ((uint)gateId >= (uint)gateKinds.Count) + { + throw new ArgumentOutOfRangeException(nameof(gateId)); + } + + return (outputMasks[gateId] & 1) != 0; + } + + public void Step() + { + EnsureCompiled(); + computeOutputs!(inputMasks, outputMasks, sourceStates); + Propagate(); + } + + public int RunUntilStable(int maxSteps = 1024) + { + if (!TryRunUntilStable(maxSteps, out int steps)) + { + throw new InvalidOperationException($"Circuit did not stabilize within {maxSteps} steps."); + } + + return steps; + } + + private void Propagate() + { + Array.Clear(nextInputMasks); + + for (int fromGate = 0; fromGate < gateKinds.Count; fromGate++) + { + if ((outputMasks[fromGate] & 1) == 0) + { + continue; + } + + int start = edgeStart[fromGate]; + int end = edgeStart[fromGate + 1]; + + for (int e = start; e < end; e++) + { + int toGate = edgeToGate[e]; + nextInputMasks[toGate] |= 1 << edgeToInputBit[e]; + } + } + + (inputMasks, nextInputMasks) = (nextInputMasks, inputMasks); + } + + private bool PropagateAndSwapDetectChange() + { + Array.Clear(nextInputMasks); + + for (int fromGate = 0; fromGate < gateKinds.Count; fromGate++) + { + if ((outputMasks[fromGate] & 1) == 0) + { + continue; + } + + int start = edgeStart[fromGate]; + int end = edgeStart[fromGate + 1]; + + for (int e = start; e < end; e++) + { + int toGate = edgeToGate[e]; + nextInputMasks[toGate] |= 1 << edgeToInputBit[e]; + } + } + + bool changed = false; + for (int i = 0; i < inputMasks.Length; i++) + { + if (inputMasks[i] != nextInputMasks[i]) + { + changed = true; + break; + } + } + + (inputMasks, nextInputMasks) = (nextInputMasks, inputMasks); + return changed; + } + + private void EnsureStorage() + { + int n = gateKinds.Count; + if (inputMasks.Length == n) + { + return; + } + + inputMasks = new int[n]; + nextInputMasks = new int[n]; + outputMasks = new int[n]; + sourceStates = new int[n]; + } + + private void EnsureCompiled() + { + EnsureStorage(); + + if (compiled) + { + return; + } + + CompileNetlist(); + CompileLuts(); + computeOutputs = CompileComputeOutputsExpr(); + compiled = true; + } + + private void CompileNetlist() + { + int n = gateKinds.Count; + edgeStart = new int[n + 1]; + + for (int i = 0; i < connections.Count; i++) + { + (int from, _, _) = connections[i]; + edgeStart[from + 1]++; + } + + for (int i = 1; i < edgeStart.Length; i++) + { + edgeStart[i] += edgeStart[i - 1]; + } + + edgeToGate = new int[connections.Count]; + edgeToInputBit = new byte[connections.Count]; + + int[] cursor = (int[])edgeStart.Clone(); + for (int i = 0; i < connections.Count; i++) + { + (int from, int to, byte bit) = connections[i]; + int at = cursor[from]++; + edgeToGate[at] = to; + edgeToInputBit[at] = bit; + } + } + + private void CompileLuts() + { + int n = gateKinds.Count; + lutOffsets = new int[n]; + lutMasks = new int[n]; + + int total = 0; + for (int i = 0; i < n; i++) + { + if (gateKinds[i] != GateKind.Lut) + { + continue; + } + + int[]? table = lutTableByGate[i]; + if (table is null) + { + throw new InvalidOperationException("LUT gate is missing its truth table."); + } + + if (!IsPowerOfTwo(table.Length)) + { + throw new InvalidOperationException("LUT table length must be a power of two."); + } + + lutOffsets[i] = total; + lutMasks[i] = table.Length - 1; + total += table.Length; + } + + lutData = new int[total]; + int cursor = 0; + for (int i = 0; i < n; i++) + { + if (gateKinds[i] != GateKind.Lut) + { + continue; + } + + int[] table = lutTableByGate[i]!; + Array.Copy(table, 0, lutData, cursor, table.Length); + cursor += table.Length; + } + } + + private Action CompileComputeOutputsExpr() + { + ParameterExpression inputsParam = Expression.Parameter(typeof(int[]), "inputs"); + ParameterExpression outputsParam = Expression.Parameter(typeof(int[]), "outputs"); + ParameterExpression sourcesParam = Expression.Parameter(typeof(int[]), "sources"); + + ConstantExpression lutDataConst = Expression.Constant(lutData); + + Expression[] block = new Expression[gateKinds.Count]; + + for (int i = 0; i < gateKinds.Count; i++) + { + ConstantExpression idx = Expression.Constant(i); + Expression inMask = Expression.ArrayIndex(inputsParam, idx); + + Expression outExpr = gateKinds[i] switch + { + GateKind.Source => Expression.And(Expression.ArrayIndex(sourcesParam, idx), Expression.Constant(1)), + + GateKind.Not => Expression.Condition( + Expression.Equal(Expression.And(inMask, Expression.Constant(1)), Expression.Constant(0)), + Expression.Constant(1), + Expression.Constant(0)), + + GateKind.And2 => Expression.Condition( + Expression.Equal(Expression.And(inMask, Expression.Constant(0b11)), Expression.Constant(0b11)), + Expression.Constant(1), + Expression.Constant(0)), + + GateKind.Or2 => Expression.Condition( + Expression.NotEqual(Expression.And(inMask, Expression.Constant(0b11)), Expression.Constant(0)), + Expression.Constant(1), + Expression.Constant(0)), + + GateKind.Buffer => Expression.Condition( + Expression.NotEqual(Expression.And(inMask, Expression.Constant(1)), Expression.Constant(0)), + Expression.Constant(1), + Expression.Constant(0)), + + GateKind.Sink => Expression.Condition( + Expression.NotEqual(Expression.And(inMask, Expression.Constant(1)), Expression.Constant(0)), + Expression.Constant(1), + Expression.Constant(0)), + + GateKind.Lut => Expression.ArrayIndex( + lutDataConst, + Expression.Add( + Expression.Constant(lutOffsets[i]), + Expression.And(inMask, Expression.Constant(lutMasks[i])))), + + _ => throw new ArgumentOutOfRangeException(), + }; + + block[i] = Expression.Assign(Expression.ArrayAccess(outputsParam, idx), outExpr); + } + + BlockExpression body = Expression.Block(block); + return Expression.Lambda>(body, inputsParam, outputsParam, sourcesParam).Compile(); + } + + private static bool IsPowerOfTwo(int value) => value > 0 && (value & (value - 1)) == 0; + + private MacroLut? TryBuildMacroLut(CircuitDefinition definition, int maxSteps) + { + int inputCount = definition.InputPins.Count; + int outputCount = definition.OutputPins.Count; + + if (inputCount < 0 || outputCount <= 0) + { + return null; + } + + if (inputCount > 30) + { + throw new InvalidOperationException("Cannot build a LUT for macros with more than 30 inputs (would overflow 32-bit indexing)."); + } + + int patterns = 1 << inputCount; + + var sim = new ExprCircuitSimulator(); + foreach ((string macroName, MacroInfo macroInfo) in macroGates) + { + sim.macroGates[macroName] = macroInfo; + } + int[] map = CopyDefinitionGatesAndConnections(sim, definition, static (_, kind) => kind); + int[] inGates = MapPins(definition.InputPins, map); + int[] outGates = MapPins(definition.OutputPins, map); + + int[][] outputTables = new int[outputCount][]; + for (int o = 0; o < outputCount; o++) + { + outputTables[o] = new int[patterns]; + } + + for (int pattern = 0; pattern < patterns; pattern++) + { + sim.ClearSignals(); + + for (int i = 0; i < inputCount; i++) + { + bool bit = ((pattern >> i) & 1) != 0; + sim.SetSource(inGates[i], bit); + } + + if (!sim.TryRunUntilStable(maxSteps: maxSteps, out _)) + { + return null; + } + + for (int o = 0; o < outputCount; o++) + { + outputTables[o][pattern] = sim.GetOutput(outGates[o]) ? 1 : 0; + } + } + + return new MacroLut(inputCount, outputCount, outputTables); + } + + private MacroInstance AddMacroGateFromLut(string name, MacroLut lut) + { + int[] inputs = new int[lut.InputCount]; + for (int i = 0; i < inputs.Length; i++) + { + inputs[i] = AddGate(GateKind.Buffer); + } + + int[] outputs = new int[lut.OutputCount]; + for (int o = 0; o < outputs.Length; o++) + { + int lutGate = AddLutGate(lut.InputCount, lut.OutputTables[o]); + for (int i = 0; i < inputs.Length; i++) + { + ConnectGates(inputs[i], lutGate, toInputBit: i); + } + + int sink = AddGate(GateKind.Sink); + ConnectGates(lutGate, sink, toInputBit: 0); + outputs[o] = sink; + } + + return new MacroInstance(name, inputs, outputs); + } + + public bool TryRunUntilStable(int maxSteps, out int steps) + { + steps = 0; + if (maxSteps <= 0) + { + return false; + } + + EnsureCompiled(); + + while (steps < maxSteps) + { + steps++; + computeOutputs!(inputMasks, outputMasks, sourceStates); + if (!PropagateAndSwapDetectChange()) + { + return true; + } + } + + return false; + } + + private static int[] CopyDefinitionGatesAndConnections( + ExprCircuitSimulator destination, + CircuitDefinition definition, + Func mapKind) + { + int gateCount = definition.GateKinds.Count; + int[] map = new int[gateCount]; + Array.Fill(map, -1); + + // Expand nested macro instances by mapping their placeholder pin gates directly to the instantiated sub-macro pins. + for (int i = 0; i < definition.MacroInstances.Count; i++) + { + CircuitDefinition.MacroInstanceDef instanceDef = definition.MacroInstances[i]; + MacroInstance instance = destination.AddMacroGate(instanceDef.Name); + + if (instance.Inputs.Length != instanceDef.Inputs.Length || instance.Outputs.Length != instanceDef.Outputs.Length) + { + throw new InvalidOperationException($"Macro instance '{instanceDef.Name}' pin counts do not match the referenced macro definition."); + } + + for (int p = 0; p < instanceDef.Inputs.Length; p++) + { + int gateId = instanceDef.Inputs[p]; + if (map[gateId] != -1) throw new InvalidOperationException("Macro pin gate was mapped more than once."); + map[gateId] = instance.Inputs[p]; + } + + for (int p = 0; p < instanceDef.Outputs.Length; p++) + { + int gateId = instanceDef.Outputs[p]; + if (map[gateId] != -1) throw new InvalidOperationException("Macro pin gate was mapped more than once."); + map[gateId] = instance.Outputs[p]; + } + } + + for (int i = 0; i < gateCount; i++) + { + if (map[i] != -1) + { + continue; + } + + map[i] = destination.AddGate(mapKind(i, definition.GateKinds[i])); + } + + for (int i = 0; i < definition.Connections.Count; i++) + { + (int from, int to, byte bit) = definition.Connections[i]; + destination.ConnectGates(map[from], map[to], bit); + } + + return map; + } + + private static int[] MapPins(IReadOnlyList pins, int[] map) + { + int[] result = new int[pins.Count]; + for (int i = 0; i < result.Length; i++) + { + result[i] = map[pins[i]]; + } + + return result; + } +} diff --git a/StoneRed.LogicSimulator.Test/Program.cs b/StoneRed.LogicSimulator.Test/Program.cs new file mode 100644 index 0000000..d9f4979 --- /dev/null +++ b/StoneRed.LogicSimulator.Test/Program.cs @@ -0,0 +1,46 @@ +namespace StoneRed.LogicSimulator.Test; + +internal static class Program +{ + public static void Main() + { + var sim = new ExprCircuitSimulator(); + + var inverter = new CircuitDefinition(); + int invIn = inverter.AddInputPin(); // Source inside definition + int invNot = inverter.AddGate(GateKind.Not); + int invOut = inverter.AddOutputPin(); // Sink inside definition + inverter.Connect(invIn, invNot, toInputBit: 0); + inverter.Connect(invNot, invOut, toInputBit: 0); + + sim.RegisterMacroGate("INV", inverter); + sim.ComputeLut("INV"); + + var doubleInverter = new CircuitDefinition(); + int inv2In = doubleInverter.AddInputPin(); + CircuitDefinition.MacroInstanceDef invA = doubleInverter.AddMacroInstance("INV", inputCount: 1, outputCount: 1); + CircuitDefinition.MacroInstanceDef invB = doubleInverter.AddMacroInstance("INV", inputCount: 1, outputCount: 1); + int inv2Out = doubleInverter.AddOutputPin(); + doubleInverter.Connect(inv2In, invA.Inputs[0], toInputBit: 0); + doubleInverter.Connect(invA.Outputs[0], invB.Inputs[0], toInputBit: 0); + doubleInverter.Connect(invB.Outputs[0], inv2Out, toInputBit: 0); + + sim.RegisterMacroGate("INV2", doubleInverter); + sim.ComputeLut("INV2"); + + int a = sim.AddGate(GateKind.Source); + ExprCircuitSimulator.MacroInstance inv2 = sim.AddMacroGate("INV2"); + int lamp = sim.AddGate(GateKind.Sink); + + sim.ConnectGates(a, inv2.Inputs[0], toInputBit: 0); + sim.ConnectGates(inv2.Outputs[0], lamp, toInputBit: 0); + + sim.SetSource(a, value: false); + sim.RunUntilStable(); + Console.WriteLine($"A=0 => Lamp={sim.GetOutput(lamp)} (expected False)"); + + sim.SetSource(a, value: true); + sim.RunUntilStable(); + Console.WriteLine($"A=1 => Lamp={sim.GetOutput(lamp)} (expected True)"); + } +} diff --git a/StoneRed.LogicSimulator.Test/StoneRed.LogicSimulator.Test.csproj b/StoneRed.LogicSimulator.Test/StoneRed.LogicSimulator.Test.csproj new file mode 100644 index 0000000..129061d --- /dev/null +++ b/StoneRed.LogicSimulator.Test/StoneRed.LogicSimulator.Test.csproj @@ -0,0 +1,13 @@ + + + + Exe + net8.0 + enable + enable + + + + + +