544 lines
20 KiB
Zig
544 lines
20 KiB
Zig
const std = @import("std");
|
|
|
|
pub fn main() !void {
|
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
defer _ = gpa.deinit();
|
|
const allocator = gpa.allocator();
|
|
|
|
var input = Signal{ .digital = 1, .analog = 0.5 };
|
|
|
|
var circuit = Circuit.init(allocator);
|
|
defer circuit.deinit();
|
|
|
|
var not1 = try circuit.addComponent(Not);
|
|
not1.invert_output = false;
|
|
not1.component.inputs.items[0] = .{ .signal = &input }; // manually set the input here
|
|
|
|
var battery1 = try circuit.addComponent(Battery);
|
|
var battery2 = try circuit.addComponent(Battery);
|
|
var battery3 = try circuit.addComponent(Battery);
|
|
battery1.value = -0.5;
|
|
battery2.value = 0.5;
|
|
battery3.value = -1.0;
|
|
|
|
var or1 = try circuit.addComponent(Or);
|
|
var or2 = try circuit.addComponent(Or);
|
|
or1.arithmetic_mode = true;
|
|
or2.arithmetic_mode = true;
|
|
try or1.component.resizeInputs(allocator, 3);
|
|
try or2.component.resizeInputs(allocator, 3);
|
|
|
|
var or3 = try circuit.addComponent(Or);
|
|
var or4 = try circuit.addComponent(Or);
|
|
var or5 = try circuit.addComponent(Or);
|
|
|
|
var or6 = try circuit.addComponent(Or);
|
|
var or7 = try circuit.addComponent(Or);
|
|
var or8 = try circuit.addComponent(Or);
|
|
var and1 = try circuit.addComponent(And);
|
|
var and2 = try circuit.addComponent(And);
|
|
or6.arithmetic_mode = true;
|
|
or7.arithmetic_mode = true;
|
|
or8.arithmetic_mode = true;
|
|
and1.arithmetic_mode = true;
|
|
and2.arithmetic_mode = true;
|
|
|
|
try circuit.connectComponents(&battery1.component, 0, &or3.component, 1);
|
|
try circuit.connectComponents(&battery1.component, 0, &or4.component, 1);
|
|
try circuit.connectComponents(&battery1.component, 0, &or5.component, 1);
|
|
|
|
try circuit.connectComponents(&battery2.component, 0, &or6.component, 0);
|
|
try circuit.connectComponents(&battery2.component, 0, &or7.component, 0);
|
|
try circuit.connectComponents(&battery2.component, 0, &or8.component, 0);
|
|
|
|
try circuit.connectComponents(&battery3.component, 0, &and1.component, 0);
|
|
try circuit.connectComponents(&battery3.component, 0, &and2.component, 0);
|
|
|
|
try circuit.connectComponents(¬1.component, 0, &or3.component, 0);
|
|
try circuit.connectComponents(¬1.component, 0, &or1.component, 0);
|
|
try circuit.connectComponents(¬1.component, 0, &or1.component, 1);
|
|
|
|
try circuit.connectComponents(&or1.component, 0, &or4.component, 0);
|
|
try circuit.connectComponents(&or1.component, 0, &or2.component, 0);
|
|
try circuit.connectComponents(&or1.component, 0, &or2.component, 1);
|
|
try circuit.connectComponents(&or2.component, 0, &or5.component, 0);
|
|
|
|
try circuit.connectComponents(&or3.component, 0, &or6.component, 1);
|
|
try circuit.connectComponents(&or4.component, 0, &or7.component, 1);
|
|
try circuit.connectComponents(&or5.component, 0, &or8.component, 1);
|
|
|
|
try circuit.connectComponents(&or6.component, 0, &and1.component, 1);
|
|
try circuit.connectComponents(&and1.component, 0, &or1.component, 2);
|
|
try circuit.connectComponents(&or7.component, 0, &and2.component, 1);
|
|
try circuit.connectComponents(&and2.component, 0, &or2.component, 2);
|
|
|
|
try circuit.tick();
|
|
|
|
// battery1.component.process();
|
|
// battery2.component.process();
|
|
// battery3.component.process();
|
|
// not1.component.process();
|
|
// or3.component.process();
|
|
// or6.component.process();
|
|
// and1.component.process();
|
|
// or1.component.process();
|
|
// or4.component.process();
|
|
// or7.component.process();
|
|
// and2.component.process();
|
|
// or2.component.process();
|
|
// or5.component.process();
|
|
// or8.component.process();
|
|
|
|
// std.debug.print("Input:\n{}\n\n", .{input});
|
|
// std.debug.print("{}\n{}\n\n", .{
|
|
// or1.component.outputs.items[0].signal,
|
|
// or2.component.outputs.items[0].signal,
|
|
// });
|
|
// std.debug.print("{}\n{}\n{}\n\n", .{
|
|
// or3.component.outputs.items[0].signal,
|
|
// or4.component.outputs.items[0].signal,
|
|
// or5.component.outputs.items[0].signal,
|
|
// });
|
|
// std.debug.print("{}\n{}\n{}\n{}\n{}\n\n", .{
|
|
// or6.component.outputs.items[0].signal,
|
|
// and1.component.outputs.items[0].signal,
|
|
// or7.component.outputs.items[0].signal,
|
|
// and2.component.outputs.items[0].signal,
|
|
// or8.component.outputs.items[0].signal,
|
|
// });
|
|
}
|
|
|
|
pub const Circuit = struct {
|
|
allocator: std.mem.Allocator,
|
|
components: Components,
|
|
|
|
process_order: ?ProcessOrder = null,
|
|
|
|
const Components = std.ArrayListUnmanaged(*Component);
|
|
const ComponentIndex = u32;
|
|
|
|
pub fn init(allocator: std.mem.Allocator) Circuit {
|
|
return .{
|
|
.allocator = allocator,
|
|
.components = Components.empty,
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *Circuit) void {
|
|
// std.debug.print("process_order.len {d}\n", .{self.process_order.?.len});
|
|
if (self.process_order) |p| self.allocator.free(p);
|
|
for (0..self.components.items.len) |i| self.components.items[i].deinit(self.allocator);
|
|
self.components.deinit(self.allocator);
|
|
}
|
|
|
|
pub fn addComponent(self: *Circuit, comptime T: type) !*T {
|
|
var c = try T.init(self.allocator);
|
|
errdefer c.component.deinit(self.allocator);
|
|
try self.components.append(self.allocator, &c.component);
|
|
return c;
|
|
}
|
|
|
|
pub fn connectComponents(self: *Circuit, from: *Component, from_idx: usize, to: *Component, to_idx: usize) !void {
|
|
try from.connect(self.allocator, from_idx, to, to_idx);
|
|
}
|
|
|
|
pub fn tick(self: *Circuit) !void {
|
|
if (self.process_order == null) try self.updateProcessOrder();
|
|
for (self.process_order.?) |component| {
|
|
std.log.debug("processing component: {}@{d}", .{ component, self.componentIndex(component).? });
|
|
component.process();
|
|
}
|
|
}
|
|
|
|
fn updateProcessOrder(self: *Circuit) !void {
|
|
var process_order = std.ArrayList(*Component).init(self.allocator);
|
|
errdefer process_order.deinit();
|
|
|
|
const visited = try self.allocator.alloc(bool, self.components.items.len);
|
|
defer self.allocator.free(visited);
|
|
|
|
// find components with nonexistant or disconnected inputs
|
|
var source_components = std.ArrayList(ComponentIndex).init(self.allocator);
|
|
defer source_components.deinit();
|
|
source: for (self.components.items) |component| {
|
|
for (component.inputs.items) |input|
|
|
if (input.connection != null)
|
|
continue :source;
|
|
|
|
const idx = self.componentIndex(component).?;
|
|
try process_order.append(component);
|
|
visited[idx] = true;
|
|
try source_components.append(idx);
|
|
}
|
|
|
|
// do multiple Depth First Searches (with extra steps) once per source component
|
|
for (source_components.items) |source_component_idx| {
|
|
var stack = std.ArrayList(ComponentIndex).init(self.allocator);
|
|
defer stack.deinit();
|
|
|
|
var component_idx = source_component_idx;
|
|
|
|
dfs: while (true) {
|
|
// check each output of the current component
|
|
for (self.components.items[component_idx].outputs.items) |output| {
|
|
// check each connection of the current output
|
|
connections: for (output.connections.items) |connection| {
|
|
const new_idx = self.componentIndex(connection.component).?;
|
|
// std.debug.print("{}@{d}\n", .{ connection, new_idx });
|
|
|
|
// check if component is "ready"
|
|
// i.e. it hasn't already been added to the process list, but all its inputs have.
|
|
if (visited[new_idx]) continue :connections;
|
|
for (connection.component.inputs.items) |input| {
|
|
if (input.connection) |input_component| {
|
|
if (!visited[self.componentIndex(input_component).?]) {
|
|
continue :connections;
|
|
}
|
|
}
|
|
}
|
|
|
|
// component is ready, continue the DFS down that branch
|
|
try process_order.append(connection.component);
|
|
try stack.append(new_idx);
|
|
visited[new_idx] = true;
|
|
component_idx = new_idx;
|
|
continue :dfs;
|
|
}
|
|
}
|
|
|
|
component_idx = stack.pop() orelse break :dfs;
|
|
// std.debug.print("backtracking to {}\n", .{component_idx});
|
|
}
|
|
}
|
|
|
|
// std.debug.print("{any}\n", .{visited});
|
|
|
|
if (self.process_order) |p| self.allocator.free(p);
|
|
self.process_order = try process_order.toOwnedSlice();
|
|
}
|
|
|
|
fn componentIndex(self: *Circuit, component: *Component) ?ComponentIndex {
|
|
const idx = std.mem.indexOfScalar(*Component, self.components.items, component);
|
|
if (idx) |i|
|
|
return @intCast(i)
|
|
else
|
|
return null;
|
|
}
|
|
|
|
const ProcessOrder = []*Component;
|
|
};
|
|
|
|
var null_signal = Signal{};
|
|
|
|
pub const Component = struct {
|
|
name: []const u8,
|
|
|
|
inputs: Inputs,
|
|
outputs: Outputs,
|
|
|
|
processFn: *const fn (*Component) void,
|
|
deinitFn: *const fn (*Component, std.mem.Allocator) void,
|
|
|
|
pub const Input = struct {
|
|
// TODO just allow this to be null and handle that codepath in a user-configurable way
|
|
signal: *Signal = &null_signal,
|
|
// TODO update this to match the Output.Connection interface (without the ArrayList)
|
|
connection: ?*Component = null,
|
|
idx: usize = 0,
|
|
};
|
|
pub const Output = struct {
|
|
signal: Signal = .{},
|
|
connections: Connections = .empty,
|
|
|
|
pub const Connections = std.ArrayListUnmanaged(Connection);
|
|
pub const Connection = struct {
|
|
component: *Component,
|
|
idx: usize,
|
|
};
|
|
};
|
|
const Inputs = std.ArrayListUnmanaged(Input);
|
|
const Outputs = std.ArrayListUnmanaged(Output);
|
|
|
|
pub fn init(
|
|
self: *Component,
|
|
allocator: std.mem.Allocator,
|
|
name: []const u8,
|
|
inputs_len: usize,
|
|
outputs_len: usize,
|
|
processFn: *const fn (*Component) void,
|
|
deinitFn: *const fn (*Component, std.mem.Allocator) void,
|
|
) !void {
|
|
var inputs = Inputs.empty;
|
|
errdefer inputs.deinit(allocator);
|
|
try inputs.resize(allocator, inputs_len);
|
|
for (0..inputs.items.len) |i| inputs.items[i] = .{};
|
|
|
|
var outputs = Outputs.empty;
|
|
errdefer outputs.deinit(allocator);
|
|
try outputs.resize(allocator, outputs_len);
|
|
for (0..outputs.items.len) |i| outputs.items[i] = .{};
|
|
|
|
self.name = name;
|
|
self.inputs = inputs;
|
|
self.outputs = outputs;
|
|
self.processFn = processFn;
|
|
self.deinitFn = deinitFn;
|
|
}
|
|
|
|
pub fn deinit(self: *Component, allocator: std.mem.Allocator) void {
|
|
self.inputs.deinit(allocator);
|
|
for (self.outputs.items) |*output|
|
|
output.connections.deinit(allocator);
|
|
self.outputs.deinit(allocator);
|
|
self.deinitFn(self, allocator);
|
|
}
|
|
|
|
pub fn process(self: *Component) void {
|
|
self.processFn(self);
|
|
}
|
|
|
|
// TODO allow inserting the new elements at an arbitrary index
|
|
// TODO ensure this won't break the opposide side's connections
|
|
pub fn resizeInputs(self: *Component, allocator: std.mem.Allocator, new_len: usize) !void {
|
|
const old_len = self.inputs.items.len;
|
|
try self.inputs.resize(allocator, new_len);
|
|
if (new_len > old_len) for (old_len..new_len) |i| {
|
|
self.inputs.items[i] = .{};
|
|
};
|
|
}
|
|
|
|
// TODO allow inserting the new elements at an arbitrary index
|
|
// TODO ensure this won't break the opposide side's connections
|
|
pub fn resizeOutputs(self: *Component, allocator: std.mem.Allocator, new_len: usize) !void {
|
|
const old_len = self.outputs.items.len;
|
|
try self.outputs.resize(allocator, new_len);
|
|
if (new_len > old_len) for (old_len..new_len) |i| {
|
|
self.outputs.items[i] = .{};
|
|
};
|
|
}
|
|
|
|
fn connect(self: *Component, allocator: std.mem.Allocator, self_idx: usize, to: *Component, to_idx: usize) !void {
|
|
const input = &to.inputs.items[to_idx];
|
|
input.signal = &self.outputs.items[self_idx].signal;
|
|
input.connection = self;
|
|
input.idx = self_idx;
|
|
|
|
const output = &self.outputs.items[self_idx];
|
|
try output.connections.append(allocator, .{
|
|
.component = to,
|
|
.idx = to_idx,
|
|
});
|
|
}
|
|
|
|
pub fn format(
|
|
self: *Component,
|
|
comptime _: []const u8,
|
|
_: std.fmt.FormatOptions,
|
|
writer: anytype,
|
|
) !void {
|
|
try writer.print("Component{{ .name = \"{s}\", .inputs.items.len = {d}, .outputs.items.len = {d} }}", .{
|
|
self.name,
|
|
self.inputs.items.len,
|
|
self.outputs.items.len,
|
|
});
|
|
}
|
|
};
|
|
|
|
pub const Signal = struct {
|
|
digital: i2 = 0,
|
|
analog: f32 = 0.0,
|
|
color: u24 = 0,
|
|
|
|
pub fn format(
|
|
self: Signal,
|
|
comptime fmt: []const u8,
|
|
options: std.fmt.FormatOptions,
|
|
writer: anytype,
|
|
) !void {
|
|
_ = .{ fmt, options };
|
|
try writer.writeAll("Signal(");
|
|
if (self.digital < 0) try writer.writeByte('-') else try writer.writeByte('+');
|
|
try writer.print("{d} / {d:0>1.4})", .{
|
|
@abs(self.digital),
|
|
self.analog,
|
|
});
|
|
}
|
|
};
|
|
|
|
pub const Battery = struct {
|
|
component: Component,
|
|
|
|
value: f32 = 1.0,
|
|
|
|
pub fn init(allocator: std.mem.Allocator) !*Battery {
|
|
var self = try allocator.create(Battery);
|
|
errdefer allocator.destroy(self);
|
|
try Component.init(&self.component, allocator, "Battery", 0, 1, &process, &deinit);
|
|
return self;
|
|
}
|
|
|
|
pub fn deinit(component: *Component, allocator: std.mem.Allocator) void {
|
|
const self: *Battery = @fieldParentPtr("component", component);
|
|
allocator.destroy(self);
|
|
}
|
|
|
|
pub fn process(component: *Component) void {
|
|
const self: *Battery = @fieldParentPtr("component", component);
|
|
component.outputs.items[0].signal.digital = @intFromFloat(std.math.sign(self.value));
|
|
component.outputs.items[0].signal.analog = self.value;
|
|
}
|
|
};
|
|
|
|
pub const Not = struct {
|
|
component: Component,
|
|
|
|
invert_output: bool = true,
|
|
|
|
pub fn init(allocator: std.mem.Allocator) !*Not {
|
|
var self = try allocator.create(Not);
|
|
errdefer allocator.destroy(self);
|
|
try Component.init(&self.component, allocator, "NOT", 1, 1, &process, &deinit);
|
|
return self;
|
|
}
|
|
|
|
pub fn deinit(component: *Component, allocator: std.mem.Allocator) void {
|
|
const self: *Not = @fieldParentPtr("component", component);
|
|
allocator.destroy(self);
|
|
}
|
|
|
|
pub fn process(component: *Component) void {
|
|
const self: *Not = @fieldParentPtr("component", component);
|
|
if (self.invert_output) {
|
|
component.outputs.items[0].signal.digital = 1 - @as(i2, @intCast(@abs(component.inputs.items[0].signal.digital)));
|
|
component.outputs.items[0].signal.analog = 1.0 - @abs(component.inputs.items[0].signal.analog);
|
|
} else {
|
|
component.outputs.items[0].signal.digital = component.inputs.items[0].signal.digital;
|
|
component.outputs.items[0].signal.analog = component.inputs.items[0].signal.analog;
|
|
}
|
|
component.outputs.items[0].signal.analog = std.math.clamp(component.outputs.items[0].signal.analog, -1.0, 1.0);
|
|
}
|
|
};
|
|
|
|
pub const And = struct {
|
|
component: Component,
|
|
|
|
// if false, is in Minimum Input mode
|
|
// if true, is in Multiply Inputs mode
|
|
arithmetic_mode: bool = false,
|
|
|
|
pub fn init(allocator: std.mem.Allocator) !*And {
|
|
var self = try allocator.create(And);
|
|
errdefer allocator.destroy(self);
|
|
try Component.init(&self.component, allocator, "AND", 2, 1, &process, &deinit);
|
|
return self;
|
|
}
|
|
|
|
pub fn deinit(component: *Component, allocator: std.mem.Allocator) void {
|
|
const self: *And = @fieldParentPtr("component", component);
|
|
allocator.destroy(self);
|
|
}
|
|
|
|
// TODO check implementation
|
|
pub fn process(component: *Component) void {
|
|
const self: *And = @fieldParentPtr("component", component);
|
|
if (self.arithmetic_mode) {
|
|
component.outputs.items[0].signal.digital = component.inputs.items[0].signal.digital;
|
|
component.outputs.items[0].signal.analog = component.inputs.items[0].signal.analog;
|
|
for (component.inputs.items[1..]) |input| {
|
|
component.outputs.items[0].signal.digital = 0; // TODO
|
|
component.outputs.items[0].signal.analog *= input.signal.analog;
|
|
}
|
|
} else {
|
|
component.outputs.items[0].signal.digital = component.inputs.items[0].signal.digital;
|
|
component.outputs.items[0].signal.analog = component.inputs.items[0].signal.analog;
|
|
for (component.inputs.items[1..]) |input| {
|
|
component.outputs.items[0].signal.digital = 0; // TODO
|
|
component.outputs.items[0].signal.analog = switch (std.math.order(@abs(component.outputs.items[0].signal.analog), @abs(input.signal.analog))) {
|
|
.lt => component.outputs.items[0].signal.analog,
|
|
.eq => @min(component.outputs.items[0].signal.analog, input.signal.analog), // TODO what does this *actually* do?
|
|
.gt => input.signal.analog,
|
|
};
|
|
}
|
|
}
|
|
component.outputs.items[0].signal.analog = std.math.clamp(component.outputs.items[0].signal.analog, -1.0, 1.0);
|
|
}
|
|
};
|
|
|
|
// TODO update test to use new Component interface
|
|
test "min" {
|
|
var a = Signal{ .analog = 0.0 };
|
|
var b = Signal{ .analog = 1.0 };
|
|
|
|
var inputs = [_]*Signal{ &a, &b };
|
|
var and1 = And{ .inputs = &inputs };
|
|
|
|
and1.process();
|
|
try std.testing.expectEqual(0.0, and1.output.analog);
|
|
|
|
a.analog = -0.5;
|
|
b.analog = -0.2;
|
|
and1.process();
|
|
try std.testing.expectEqual(-0.2, and1.output.analog);
|
|
}
|
|
|
|
pub const Or = struct {
|
|
component: Component,
|
|
|
|
// if false, is in Maximum Input mode
|
|
// if true, is in Add Inputs mode
|
|
arithmetic_mode: bool = false,
|
|
|
|
pub fn init(allocator: std.mem.Allocator) !*Or {
|
|
var self = try allocator.create(Or);
|
|
errdefer allocator.destroy(self);
|
|
try Component.init(&self.component, allocator, "OR", 2, 1, &process, &deinit);
|
|
return self;
|
|
}
|
|
|
|
pub fn deinit(component: *Component, allocator: std.mem.Allocator) void {
|
|
const self: *Or = @fieldParentPtr("component", component);
|
|
allocator.destroy(self);
|
|
}
|
|
|
|
// TODO check implementation
|
|
pub fn process(component: *Component) void {
|
|
const self: *Or = @fieldParentPtr("component", component);
|
|
if (self.arithmetic_mode) {
|
|
component.outputs.items[0].signal.digital = component.inputs.items[0].signal.digital;
|
|
component.outputs.items[0].signal.analog = component.inputs.items[0].signal.analog;
|
|
for (component.inputs.items[1..]) |input| {
|
|
component.outputs.items[0].signal.digital = 0; // TODO
|
|
component.outputs.items[0].signal.analog += input.signal.analog;
|
|
}
|
|
} else {
|
|
component.outputs.items[0].signal.digital = component.inputs.items[0].signal.digital;
|
|
component.outputs.items[0].signal.analog = component.inputs.items[0].signal.analog;
|
|
for (component.inputs.items[1..]) |input| {
|
|
component.outputs.items[0].signal.digital = 0; // TODO
|
|
component.outputs.items[0].signal.analog = switch (std.math.order(@abs(component.outputs.items[0].signal.analog), @abs(input.signal.analog))) {
|
|
.lt => input.signal.analog,
|
|
.eq => @max(component.outputs.items[0].signal.analog, input.signal.analog), // TODO what does this *actually* do?
|
|
.gt => component.outputs.items[0].signal.analog,
|
|
};
|
|
}
|
|
}
|
|
component.outputs.items[0].signal.analog = std.math.clamp(component.outputs.items[0].signal.analog, -1.0, 1.0);
|
|
}
|
|
};
|
|
|
|
// TODO update test to use new Component interface
|
|
test "max" {
|
|
var a = Signal{ .analog = 0.0 };
|
|
var b = Signal{ .analog = 1.0 };
|
|
|
|
var inputs = [_]*Signal{ &a, &b };
|
|
var or1 = Or{ .inputs = &inputs };
|
|
|
|
or1.process();
|
|
try std.testing.expectEqual(1.0, or1.output.analog);
|
|
|
|
a.analog = -0.5;
|
|
b.analog = -0.2;
|
|
or1.process();
|
|
try std.testing.expectEqual(-0.5, or1.output.analog);
|
|
}
|