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); }