Thanks for that info!
Currently, there is no simulation. I’m still working on the interactions for modification of circuits (this is way more work! Svg rendering, cues for editing, coming up with the datastructures and operations for editing, etc.). I’m already pretty confident I know how simulation is going to work out, at least for the start.
There are some questions that come up eventually when designing a circuit simulator:
What do you do about loose gate ends?
Option 1: Loose ends are low (off), there only exist 2 states of wires: high and low (on and off)
So the output of a single NAND gate that’s not connected to anything would be high.
How do you handle splitting the wire into two? That’s easy: Just duplicate whatever signal is at the input end (is there an ‘input’ end to a wire? In this way of simulation, there has to be!).
How do you handle joining two wires? The answer to this is hard. I guess the only option is to disallow this. See how this directly has an influence on the design, as wires now need directionality: If wires weren’t directional, there’s no way to observe a difference between a wire split and a wire join. Both would just be 3-way wires.
Option 2: Loose ends are another wire state ‘undefined’.
Now we start to move into multi-state logic. Normal gates with any input connection being undefined output undefined, too. At wire joins and splits undefined on any end causes an undefined on other ends.
If there is no mechanism of getting rid of this undefined, then it’ll quickly just ‘infest’ your whole logic circuit, if you have loops in your circuit going back to the input, so you might want to incorporate elements like ‘pull up’ or ‘pull down’ which can be connected to wires to causes them being ‘high’ or ‘low’, respectively, if they became undefined.
Often gates like a MUX also allow undefined input values without an undefined output, if the undefined wires aren’t chosen.
How do you handle feedback loops?
Sometimes feedback loops behave very well, as for example the NOR RS-Latch from the screenshot of my last post: No matter the inputs, there are always well-defined outputs (If you choose a valid starting state).
But if you allow feedback loops (which you should! It’s the only way to implement memory components), then you need to decide about what happens with a NOT gate that has its output connected to its input.
Option 1: Let it alternate.
Like @jxxcarlson wrote, you’ll likely end up writing a stepping simulator: Each step you look at the inputs of all gates and set the output values accordingly.
So in this case you’d look at the input value of your NOT gate: Let’s say it’s off. Then you’ll turn on the output value, and for now you’ll stop looking at the input value of this NOT gate. So for now your wire shows it’s ‘high’.
In the next time step you’ll look at the input of this NOT gate and see it’s ‘high’, so you set the output value of it to ‘low’.
What results is a circuit that keeps going on and off as quickly as you simulate your circuit: Hey’ you’ve built yourself a clock! This can be cool, but in reality people try to not build such circuits and they use different mechanisms for clocks.
Option 2: Burn out.
Have you played Minecraft? If you played around with redstone quite a bit, you might have noticed that turning a redstone torch on and off very fast repeatedly causes it to ‘burn out’. It just doesn’t turn back on again. (This is the exact same situation: You probably know that redstone torches are basically NOT (/NOR) gates).
In other words: You could observe and keep track of your circuits in a way to predict whether it ‘converges’ or whether it ‘alternates’. If it alternates, you could set the output simply to ‘low’, but that might be confusing to users. ‘undefined’ is kind of not true as well, so let’s go for another value: ‘error’.
And now you need to find semantics for propagating ‘error’ through your gates and circuits and ‘pull up’ and 'pull down’s too.
My choice for question 1 right now is Option 1, as I find it is simpler. I don’t worry about wires having directionality. I feel this is quite natural as we think of gates to have direction, too. So why wouldn’t splits and joins be like gates too? In fact, an ‘or’ gate is kind of a join between to wires, isn’t it? My application keeps track of the direction of wires and disallows any input pins to have more than 1 connection. Everything that’s not connected is considered to be low.
My choice for question 2 is Option 2 as well, for now.
All in all, I think my way is just the simpler way of doing things for now, and I recommend you do the same. Just keep in mind you have to carefully handle the directionality of wires (every wire connects from one gate output to another gate’s input!).
The other alternatives are very valid, too though! And there are even more I didn’t even mention. Logisim, that I mentioned earlier both has ‘undefined’ and ‘error’ wires. I have also read about some simulators having even more values, for example when simulating ‘MOSFET’ transistors you might want ‘strong high’, ‘weak high’, ‘weak low’, ‘strong low’.
So now I didn’t talk about implementation, yet. @jxxcarlson has got you covered pretty nicely for the start. Looking at your initial post, I’d suggest you try implementing it not in terms of ‘Composite Gate’ and ‘BuiltInGate’, but just let it be a list of wires and a list of gates. The wires connect two gates by index. This is not ideal from a list-index-lookup-guaruntee way, but it’s the simplest for now. So essentially, you’d have a graph datastructure with nodes that are gates and edges that are wires.
If you simulate, you step through all your gates and determine the new output state by looking up their inputs and computing whatever they compute usually.
Hope this all got you inspired and thinking about concrete solutions. I know this is a low of text, I’m just really passionate about logic circuit simulation. I’ve spend countless hours in Logisim, when I should’ve payed attention in class