const std = @import("std"); const List = std.ArrayList; fn Args(comptime T: type, params: []const std.builtin.Type.Fn.Param) type { const method = params.len > 0 and switch (@typeInfo(params[0].type.?)) { .pointer => |p| p.child == T, else => params[0].type == T, }; const argCount = if (method) params.len - 1 else params.len; var argTypes: [argCount]type = undefined; for (0..argCount) |i| { const p = if (method) params[i + 1] else params[i]; argTypes[i] = p.type.?; } return @Tuple(&argTypes); } fn Val(comptime T: type) type { const ti: std.builtin.Type.Struct = switch (@typeInfo(T)) { .@"struct" => |s| s, .pointer => |p| @typeInfo(p.child).@"struct", else => @compileError("only struct types allowed"), }; const fieldCount = ti.fields.len + ti.decls.len + 1; // + 1 for value var names: [fieldCount][]const u8 = undefined; var types: [fieldCount]type = undefined; var attrs: [fieldCount]std.builtin.Type.StructField.Attributes = undefined; // value { names[fieldCount - 1] = "value"; const NT = struct { value: T = undefined, pub fn init(self: *@This(), t: T) void { self.value = t; inline for (ti.fields) |field| { const parent: *Val(T) = @alignCast(@fieldParentPtr("value", self)); @field(parent, field.name).value.init(&@field(self.value, field.name)); } inline for (ti.decls) |decl| { const parent: *Val(T) = @alignCast(@fieldParentPtr("value", self)); @field(parent, decl.name).init(); } } pub fn deinit(self: *@This(), alloc: std.mem.Allocator) void { inline for (ti.fields) |field| { const parent: *Val(T) = @alignCast(@fieldParentPtr("value", self)); @field(parent, field.name).value.deinit(alloc); } inline for (ti.decls) |decl| { const parent: *Val(T) = @alignCast(@fieldParentPtr("value", self)); @field(parent, decl.name).deinit(alloc); } } pub fn get(self: *@This()) if (@typeInfo(T) == .pointer) T else *T { if (@typeInfo(T) == .pointer) { return self.value; } else { return &self.value; } } }; types[fieldCount - 1] = NT; attrs[fieldCount - 1] = .{ .@"comptime" = false, .@"align" = @alignOf(NT), .default_value_ptr = &NT{}, }; } // fields inline for (ti.fields, 0..) |field, idx| { if (field.is_comptime) @compileError("implement skipping comptime fields"); const fi = @typeInfo(field.type); switch (fi) { .@"struct" => { names[idx] = field.name; const NT = Val(*field.type); types[idx] = NT; attrs[idx] = .{}; }, else => { names[idx] = field.name; const NT = struct { const _NT = @This(); value: struct { value: *field.type = undefined, pub fn init(self: *@This(), value: *field.type) void { self.value = value; var parent: *_NT = @fieldParentPtr("value", self); parent.on_get = .empty; parent.on_set = .empty; } pub fn deinit(self: *@This(), alloc: std.mem.Allocator) void { var parent: *_NT = @fieldParentPtr("value", self); parent.on_get.clearAndFree(alloc); parent.on_set.clearAndFree(alloc); } }, on_get: List(*const fn (field.type) void) = .empty, on_set: List(*const fn (field.type) void) = .empty, pub fn get(self: *@This()) field.type { for (self.on_get.items) |cb| cb(self.value.value.*); return self.value.value.*; } pub fn set(self: *@This(), value: field.type) void { for (self.on_set.items) |cb| cb(self.value.value.*); self.value.value.* = value; } }; types[idx] = NT; attrs[idx] = .{}; }, } } // decls inline for (ti.decls, ti.fields.len..) |decl, idx| { names[idx] = decl.name; const T2 = if (@typeInfo(T) == .pointer) @typeInfo(T).pointer.child else T; const F = @TypeOf(@field(T2, decl.name)); const fi = @typeInfo(F).@"fn"; const method = fi.params.len > 0 and switch (@typeInfo(fi.params[0].type.?)) { .pointer => |p| p.child == T, else => fi.params[0].type == T, }; const modifying = method and switch (@typeInfo(fi.params[0].type.?)) { .pointer => |p| !p.is_const, else => false, }; const NT = struct { on_call: List(*const F) = .empty, on_modify: List(*const F) = .empty, pub fn init(self: *@This()) void { self.on_call = .empty; self.on_modify = .empty; } pub fn deinit(self: *@This(), alloc: std.mem.Allocator) void { self.on_call.clearAndFree(alloc); self.on_modify.clearAndFree(alloc); } pub fn call(self: *@This(), args: Args(T, fi.params)) (fi.return_type orelse void) { const allArgs = blk: { if (method) { const parent: *Val(T) = @alignCast(@fieldParentPtr(decl.name, self)); const tiv = @typeInfo(@TypeOf(parent.value.value)); const tip = @typeInfo(fi.params[0].type.?); const arg0 = if (tiv == .pointer and tip != .pointer) parent.value.value.* else if (tiv != .pointer and tip == .pointer) &parent.value.value else parent.value.value; break :blk .{arg0} ++ args; } else { break :blk args; } }; @call(.auto, @field(T2, decl.name), allArgs); for (self.on_call.items) |cb| @call(.auto, cb, allArgs); if (modifying) for (self.on_modify.items) |cb| @call(.auto, cb, allArgs); } }; types[idx] = NT; attrs[idx] = .{ .default_value_ptr = &NT{} }; } return @Struct(.auto, null, &names, &types, &attrs); } pub fn main(init: std.process.Init) !void { const A = struct { i: i32 = 123, j: i32 = 456, s: struct { i: i32 = 789, t: struct { j: i32 = 341, pub fn yo() void { std.debug.print("t.yo!\n", .{}); } } = .{}, pub fn jo() void { std.debug.print("s.jo!\n", .{}); } } = .{}, pub fn b() void { std.debug.print("B!\n", .{}); } pub fn c(self: @This()) void { std.debug.print("C! ({})\n", .{self}); } pub fn d(self: *@This(), i: i32) void { self.i += i; std.debug.print("D! ({})\n", .{self}); } }; var a: Val(A) = undefined; a.value.init(.{}); defer a.value.deinit(init.gpa); const callbacks = struct { fn set1(i: i32) void { std.debug.print("{} was set\n", .{i}); } fn get1(i: i32) void { std.debug.print("{} was get\n", .{i}); } fn f0() void { std.debug.print("called!\n", .{}); } fn f1(self: A) void { std.debug.print("called ({})!\n", .{self}); } fn f2(self: *A, i: i32) void { std.debug.print("called ({}, {})!\n", .{ self, i }); } }; try a.j.on_get.append(init.gpa, callbacks.get1); try a.j.on_set.append(init.gpa, callbacks.set1); try a.b.on_call.append(init.gpa, callbacks.f0); try a.c.on_call.append(init.gpa, callbacks.f1); try a.d.on_call.append(init.gpa, callbacks.f2); try a.s.jo.on_call.append(init.gpa, callbacks.f0); try a.s.t.yo.on_call.append(init.gpa, callbacks.f0); std.debug.print("{}\n{}\n", .{ a, a.value }); a.b.call(.{}); std.debug.print("{}\n", .{a.value.get()}); std.debug.print("{}\n", .{a.s.value.get()}); a.c.call(.{}); a.d.call(.{5}); a.s.jo.call(.{}); a.s.t.yo.call(.{}); std.debug.print("{}\n", .{a.i.get()}); std.debug.print("{}\n", .{a.j.get()}); a.j.set(9); std.debug.print("{}\n", .{a.j.get()}); std.debug.print("{}\n", .{a.j.get()}); std.debug.print("{}\n", .{a.j.get()}); std.debug.print("{}\n", .{a.s.i.get()}); }