const std = @import("std");
const builtin = @import("builtin");

const interface = @import("host/native/interface.zig");
const StructureType = interface.StructureType;
const StructurePurpose = interface.StructurePurpose;
const StructureFlags = interface.StructureFlags;
const MemberType = interface.MemberType;
const MemberFlags = interface.MemberFlags;
const meta = @import("meta.zig");
pub const options = @import("options.zig");
const js_fn = @import("thunk/js-fn.zig");
const zig_fn = @import("thunk/zig-fn.zig");
const alignment = @import("type/alignment.zig");
const arg_struct = @import("type/arg-struct.zig");
const bit_size = @import("type/bit-size.zig");
const byte_size = @import("type/byte-size.zig");
const ComptimeFree = @import("type/comptime-free.zig").ComptimeFree;
const comptime_only = @import("type/comptime-only.zig");
const content_offset = @import("type/content-offset.zig");
const error_offset = @import("type/error-offset.zig");
const method = @import("type/method.zig");
const object = @import("type/object.zig");
const pointer = @import("type/pointer.zig");
const selector_offset = @import("type/selector-offset.zig");
const selector = @import("type/selector.zig");
const sentinel = @import("type/sentinel.zig");
const signature = @import("type/signature.zig");
const slice = @import("type/slice.zig");
const supported = @import("type/supported.zig");
const target = @import("type/target.zig");
const util = @import("type/util.zig");
const fn_transform = @import("zigft/fn-transform.zig");

pub const Value = *opaque {};
pub const Unsupported = opaque {};

fn Factory(comptime host: type, comptime module: type) type {
    return struct {
        pub fn getStructureType(comptime T: type) StructureType {
            return if (arg_struct.is(T, false))
                .arg_struct
            else if (arg_struct.is(T, true))
                .variadic_struct
            else if (slice.is(T))
                .slice
            else switch (@typeInfo(T)) {
                .bool, .int, .float, .void => .primitive,
                .comptime_int, .comptime_float, .enum_literal, .type, .null, .undefined => .@"comptime",
                .@"struct" => .@"struct",
                .@"union" => .@"union",
                .error_union => .error_union,
                .error_set => .error_set,
                .optional => .optional,
                .@"enum" => .@"enum",
                .array => .array,
                .pointer => .pointer,
                .vector => .vector,
                .@"opaque" => .@"opaque",
                .@"fn" => .function,
                else => @compileError("Unsupported type: " ++ @typeName(T)),
            };
        }

        pub fn getStructurePurpose(comptime T: type) StructurePurpose {
            return switch (T) {
                std.mem.Allocator => .allocator,
                std.Io.File => .file,
                std.Io.Dir => .directory,
                else => get: {
                    if (util.IteratorReturnValue(T) != null) break :get .iterator;
                    if (util.getInternalType(T)) |it| break :get switch (it) {
                        inline else => |t| @field(StructurePurpose, @tagName(t)),
                    };
                    break :get .unknown;
                },
            };
        }

        pub fn getStructureFlags(comptime T: type) switch (@typeInfo(T)) {
            .bool, .int, .float, .void => StructureFlags.Primitive,
            .comptime_int, .comptime_float, .enum_literal, .type, .null, .undefined => StructureFlags.Comptime,
            .@"struct" => if (arg_struct.is(T, null))
                StructureFlags.ArgStruct
            else if (slice.is(T))
                StructureFlags.Slice
            else
                StructureFlags.Struct,
            .@"union" => StructureFlags.Union,
            .error_union => StructureFlags.ErrorUnion,
            .optional => StructureFlags.Optional,
            .@"enum" => StructureFlags.Enum,
            .error_set => StructureFlags.ErrorSet,
            .array => StructureFlags.Array,
            .vector => StructureFlags.Vector,
            .pointer => StructureFlags.Pointer,
            .@"opaque" => StructureFlags.Opaque,
            .@"fn" => StructureFlags.Function,
            else => @compileError("Unknown structure: " ++ @typeName(T)),
        } {
            return switch (@typeInfo(T)) {
                .bool, .int, .float, .void => .{
                    .is_size = T == usize or T == isize,
                },
                .comptime_int, .comptime_float, .enum_literal, .type, .null, .undefined => .{
                    .has_slot = true,
                },
                .@"struct" => |st| init: {
                    const has_object = inline for (st.field_types) |FieldType| {
                        if (object.is(FieldType)) break true;
                    } else false;
                    const has_slot = inline for (st.field_types, 0..) |FieldType, i| {
                        if (object.is(FieldType) or comptime_only.is(FieldType) or st.field_attrs[i].@"comptime") break true;
                    } else false;
                    break :init if (comptime arg_struct.is(T, null)) .{
                        .has_object = has_object,
                        .has_slot = has_slot,
                        .has_pointer = pointer.has(T),
                        .has_options = inline for (st.field_types) |FieldType| {
                            if (getStructurePurpose(FieldType).isOptional()) break true;
                        } else false,
                        .is_throwing = inline for (@typeInfo(T).@"struct".field_types, 0..) |FieldType, i| {
                            if (i == 0) {
                                // retval
                                if (@typeInfo(FieldType) == .error_union) break true;
                            } else {
                                if (comptime util.getInternalType(FieldType)) |it| {
                                    if (it == .promise or it == .generator) {
                                        switch (@typeInfo(FieldType.payload)) {
                                            .error_union => break true,
                                            .optional => |op| switch (@typeInfo(op.child)) {
                                                .error_union => break true,
                                                else => {},
                                            },
                                            else => {},
                                        }
                                    }
                                }
                            }
                        } else false,
                        .is_async = inline for (st.field_types) |FieldType| {
                            switch (getStructurePurpose(FieldType)) {
                                .promise, .generator => break true,
                                else => {},
                            }
                        } else false,
                    } else if (comptime slice.is(T)) .{
                        .has_object = has_object,
                        .has_slot = has_slot,
                        .has_pointer = pointer.has(T),
                        .has_sentinel = T.sentinel != null,
                        .is_string = T.ElementType == u8 or T.ElementType == u16,
                        .is_typed_array = canBeTypedArray(T),
                        .is_clamped_array = canBeClampedArray(T),
                        .is_opaque = T.is_opaque,
                    } else .{
                        .has_object = has_object,
                        .has_slot = has_slot,
                        .has_pointer = pointer.has(T),
                        .is_extern = st.layout == .@"extern",
                        .is_packed = st.layout == .@"packed",
                        .is_tuple = st.is_tuple,
                        .is_optional = util.hasDefaultFields(T),
                    };
                },
                .@"union" => |un| init: {
                    const has_object = inline for (un.field_types) |FieldType| {
                        if (object.is(FieldType)) break true;
                    } else false;
                    const has_slot = inline for (un.field_types) |FieldType| {
                        if (object.is(FieldType) or comptime_only.is(FieldType)) break true;
                    } else false;
                    break :init .{
                        .has_object = has_object,
                        .has_slot = has_slot,
                        .has_pointer = pointer.has(T),
                        .has_tag = un.tag_type != null,
                        .has_inaccessible = un.tag_type == null and pointer.has(T),
                        .has_selector = selector.has(T),
                        .is_extern = un.layout == .@"extern",
                        .is_packed = un.layout == .@"packed",
                    };
                },
                .error_union => |eu| init: {
                    break :init .{
                        .has_object = object.is(eu.payload),
                        .has_slot = object.is(eu.payload) or comptime_only.is(eu.payload),
                        .has_pointer = pointer.has(T),
                    };
                },
                .optional => |op| init: {
                    break :init .{
                        .has_object = object.is(op.child),
                        .has_slot = object.is(op.child) or comptime_only.is(op.child),
                        .has_pointer = pointer.has(T),
                        .has_selector = selector.has(T),
                    };
                },
                .@"enum" => |en| .{
                    .is_open_ended = en.mode == .nonexhaustive,
                },
                .error_set => |es| .{
                    .is_global = es.error_names == null,
                },
                .array => |ar| init: {
                    break :init .{
                        .has_object = object.is(ar.child),
                        .has_slot = object.is(ar.child) or comptime_only.is(ar.child),
                        .has_pointer = pointer.has(ar.child),
                        .has_sentinel = sentinel.get(T) != null,
                        .is_string = ar.child == u8 or ar.child == u16,
                        .is_typed_array = canBeTypedArray(T),
                        .is_clamped_array = canBeClampedArray(T),
                    };
                },
                .vector => |ve| init: {
                    break :init .{
                        .has_object = object.is(ve.child),
                        .has_slot = object.is(ve.child),
                        .has_pointer = pointer.has(ve.child),
                        .is_typed_array = canBeTypedArray(T),
                    };
                },
                .pointer => |pt| .{
                    .has_slot = true,
                    .has_length = pt.size == .slice,
                    .has_proxy = switch (@typeInfo(pt.child)) {
                        .pointer => pt.size != .one, // .one pointer doesn't need a proxy
                        .@"fn" => false,
                        else => true,
                    },
                    .is_const = pt.attrs.@"const",
                    .is_single = pt.size == .one or pt.size == .c,
                    .is_multiple = pt.size != .one,
                    .is_nullable = pt.attrs.@"allowzero" or pt.child == anyopaque,
                },
                .@"opaque" => .{},
                .@"fn" => .{},
                else => @compileError("Unknown structure: " ++ @typeName(T)),
            };
        }

        pub fn getStructureLength(comptime T: type) ?usize {
            return switch (@typeInfo(T)) {
                .array => |ar| ar.len,
                .vector => |ve| ve.len,
                .@"struct" => |st| switch (arg_struct.is(T, null)) {
                    true => comptime req_arg_count: {
                        var len = 0;
                        for (st.field_types, 0..) |FieldType, index| {
                            // first field is retval
                            if (index > 0) {
                                const purpose = getStructurePurpose(FieldType);
                                if (!purpose.isOptional()) len += 1;
                            }
                        }
                        break :req_arg_count len;
                    },
                    false => switch (st.is_tuple) {
                        true => st.field_types.len,
                        false => null,
                    },
                },
                .@"fn" => getStructureLength(arg_struct.ArgStruct(T)),
                else => null,
            };
        }

        pub fn getStructureName(comptime T: type) ?[]const u8 {
            return switch (@typeInfo(T)) {
                // names of these can be inferred on the JS side
                .bool,
                .int,
                .comptime_int,
                .float,
                .comptime_float,
                .void,
                .type,
                .error_union,
                .optional,
                .array,
                .pointer,
                .vector,
                .@"fn",
                .null,
                .undefined,
                .enum_literal,
                .noreturn,
                => null,
                // return the name without ns qualifier if it's alphanumeric
                .@"struct", .@"union", .@"enum" => result: {
                    var name: []const u8 = @typeName(T);
                    if (std.mem.lastIndexOfScalar(u8, name, '.')) |index| {
                        name = name[index + 1 .. name.len];
                    }
                    if (T == module) {
                        break :result name;
                    }
                    return for (name) |c| {
                        if (!std.ascii.isAlphanumeric(c)) {
                            break :result null;
                        }
                    } else name;
                },
                .error_set => switch (T) {
                    anyerror => "anyerror",
                    else => null,
                },
                .@"opaque" => switch (T) {
                    anyopaque => "anyopaque",
                    else => null,
                },
                else => null,
            };
        }

        pub fn getMemberType(comptime T: type, comptime is_comptime: bool) MemberType {
            return switch (supported.is(T)) {
                false => .unsupported,
                true => switch (is_comptime) {
                    true => .object,
                    false => switch (@typeInfo(T)) {
                        .bool => .bool,
                        .int => |int| if (int.signedness == .signed) .int else .uint,
                        .float => .float,
                        .@"enum" => |en| if (@typeInfo(en.tag_type).int.signedness == .signed) .int else .uint,
                        .error_set => .uint,
                        .@"struct",
                        .@"union",
                        .array,
                        .error_union,
                        .optional,
                        .pointer,
                        .vector,
                        .@"fn",
                        .comptime_int,
                        .comptime_float,
                        => .object,
                        .type => .type,
                        .enum_literal => .literal,
                        .void => .void,
                        .null => .null,
                        else => .undefined,
                    },
                },
            };
        }

        // NOTE: anyerror has to be used here since the function is called recursively
        // and https://github.com/ziglang/zig/issues/2971 has not been fully resolved yet
        fn getStructure(self: @This(), comptime T: type) anyerror!Value {
            const name = @typeName(T);
            return host.getStructure(name) catch result: {
                const instance = try createObject(.{});
                const static = try createObject(.{});
                const structure = try createObject(.{
                    .name = getStructureName(T),
                    .type = getStructureType(T),
                    .purpose = getStructurePurpose(T),
                    .flags = getStructureFlags(T),
                    .signature = signature.get(T),
                    .length = getStructureLength(T),
                    .byteSize = byte_size.get(T),
                    .@"align" = alignment.get(T),
                    .instance = instance,
                    .static = static,
                });
                // set the structure immediately so that recursive definition works correctly
                try host.setStructure(name, structure);
                // define members and add template if applicable
                try setProperties(instance, .{
                    .members = try self.getMembers(T),
                    .template = try self.getTemplate(T),
                });
                // define the shape so that static members can be instances of the structure
                try host.beginStructure(structure);
                // add static variables and functions, excluding internal util and problematic namespaces
                const type_name = @typeName(T);
                const ignore = comptime if (std.mem.startsWith(u8, type_name, "Io."))
                    true
                else if (std.mem.eql(u8, type_name, "Io"))
                    true
                else
                    util.getInternalType(T) != null;
                try setProperties(static, .{
                    .members = try self.getStaticMembers(T, ignore),
                    .template = try self.getStaticTemplate(T, ignore),
                });
                // indicate that structure is complete
                try host.finishStructure(structure);
                break :result structure;
            };
        }

        fn getMembers(self: @This(), comptime T: type) !Value {
            const list = try createList(.{});
            switch (comptime getStructureType(T)) {
                .primitive, .error_set, .@"enum" => try self.addPrimitiveMember(list, T),
                .arg_struct, .variadic_struct => try self.addArgStructMembers(list, T),
                .@"struct" => try self.addStructMembers(list, T),
                .@"union" => try self.addUnionMembers(list, T),
                .pointer => try self.addPointerMember(list, T),
                .array => try self.addArrayMember(list, T),
                .slice => try self.addSliceMember(list, T),
                .error_union => try self.addErrorUnionMembers(list, T),
                .optional => try self.addOptionalMembers(list, T),
                .vector => try self.addVectorMember(list, T),
                .function => try self.addFunctionMember(list, T),
                .@"comptime" => try self.addComptimeMember(list, T),
                else => {},
            }
            return list;
        }

        fn addPrimitiveMember(self: @This(), list: Value, comptime T: type) !void {
            try appendList(list, .{
                .type = getMemberType(T, false),
                .bitSize = bit_size.get(T),
                .byteSize = byte_size.get(T),
                .bitOffset = 0,
                .structure = try self.getStructure(T),
            });
        }

        fn addArrayMember(self: @This(), list: Value, comptime T: type) !void {
            const CT = @typeInfo(T).array.child;
            try appendList(list, .{
                .type = getMemberType(CT, false),
                .bitSize = bit_size.get(CT),
                .byteSize = byte_size.get(CT),
                .structure = try self.getStructure(CT),
            });
            try self.addSentinelMember(list, T, CT);
        }

        fn addSliceMember(self: @This(), list: Value, comptime T: type) !void {
            const CT = T.ElementType;
            try appendList(list, .{
                .type = getMemberType(CT, false),
                .bitSize = bit_size.get(CT),
                .byteSize = byte_size.get(CT),
                .structure = try self.getStructure(CT),
            });
            try self.addSentinelMember(list, T, CT);
        }

        fn addSentinelMember(self: @This(), list: Value, comptime T: type, comptime CT: type) !void {
            if (comptime sentinel.get(T)) |s| {
                try appendList(list, .{
                    .type = getMemberType(CT, false),
                    .flags = MemberFlags{
                        .is_sentinel = true,
                        .is_required = s.is_required,
                    },
                    .bitSize = bit_size.get(CT),
                    .byteSize = byte_size.get(CT),
                    .structure = try self.getStructure(CT),
                });
            }
        }

        fn addVectorMember(self: @This(), list: Value, comptime T: type) !void {
            const ve = @typeInfo(T).vector;
            const is_bit_vector = @sizeOf(ve.child) * ve.len > @sizeOf(T);
            try appendList(list, .{
                .type = getMemberType(ve.child, false),
                .bitSize = bit_size.get(ve.child),
                .byteSize = if (is_bit_vector) null else byte_size.get(ve.child),
                .structure = try self.getStructure(ve.child),
            });
        }

        fn addPointerMember(self: @This(), list: Value, comptime T: type) !void {
            const TT = target.get(T);
            const target_structure = try self.getStructure(TT);
            try appendList(list, .{
                .type = getMemberType(T, false),
                .bitSize = bit_size.get(T),
                .byteSize = byte_size.get(T),
                .slot = 0,
                .structure = target_structure,
            });
            // JavaScript code allows usize to be number instead of bigint, so we use
            // u32 and u64 for the address instead
            const Address = @Int(.unsigned, @bitSizeOf(*anyopaque));
            const address_structure = try self.getStructure(Address);
            try appendList(list, .{
                .type = getMemberType(Address, false),
                .bitOffset = 0,
                .bitSize = bit_size.get(Address),
                .byteSize = byte_size.get(Address),
                .structure = address_structure,
            });
            const usize_structure = try self.getStructure(usize);
            if (@typeInfo(T).pointer.size == .slice) {
                try appendList(list, .{
                    .type = getMemberType(usize, false),
                    .bitOffset = @bitSizeOf(usize),
                    .bitSize = bit_size.get(usize),
                    .byteSize = byte_size.get(usize),
                    .structure = usize_structure,
                });
            }
            if (@typeInfo(TT) == .@"fn" and !@typeInfo(TT).@"fn".attrs.varargs) {
                // add thunk controller to enable callback
                const FT = TT;
                const controller = comptime js_fn.createThunkController(host, FT);
                const memory = try self.exportPointerTarget(controller, false);
                const template = try host.createTemplate(memory, null);
                const AT = arg_struct.ArgStruct(FT);
                const member_flags = try createObject(.{});
                const arg_info = @typeInfo(AT).@"struct";
                inline for (arg_info.field_types, 0..) |FieldType, index| {
                    const can_be_string = comptime (index > 0) and canBeString(FieldType);
                    // first field is retval, hence the subtraction
                    const is_string = comptime can_be_string and meta.call("isArgumentString", .{ FT, index - 1 });
                    const can_be_clamped_array = comptime (index > 0) and !is_string and canBeClampedArray(FieldType);
                    const is_clamped_array = comptime can_be_clamped_array and meta.call("isArgumentClampedArray", .{ FT, index - 1 });
                    const can_be_typed_array = comptime (index > 0) and !is_string and !is_clamped_array and canBeTypedArray(FieldType);
                    const is_typed_array = comptime can_be_typed_array and meta.call("isArgumentTypedArray", .{ FT, index - 1 });
                    const can_be_plain = comptime (index > 0) and !is_string and !is_typed_array and !is_clamped_array and canBePlain(FieldType);
                    const is_plain = comptime can_be_plain and meta.call("isArgumentPlain", .{ FT, index - 1 });
                    const flags = try host.createInteger(@bitCast(MemberFlags{
                        .is_required = true,
                        .is_string = is_string,
                        .is_plain = is_plain,
                        .is_typed_array = is_typed_array,
                        .is_clamped_array = is_clamped_array,
                    }), false);
                    try host.setSlotValue(member_flags, index, flags);
                }
                try host.enableCallback(target_structure, template, member_flags);
            }
        }

        fn addArgStructMembers(self: @This(), list: Value, comptime T: type) !void {
            const info = @typeInfo(T).@"struct";
            inline for (info.field_names, 0..) |field_name, index| {
                const FieldType = info.field_types[index];
                const field_attrs = info.field_attrs[index];
                try appendList(list, .{
                    .name = field_name,
                    .type = getMemberType(FieldType, field_attrs.@"comptime"),
                    .flags = MemberFlags{ .is_required = true },
                    .bitOffset = @bitOffsetOf(T, field_name),
                    .bitSize = bit_size.get(FieldType),
                    .byteSize = byte_size.get(FieldType),
                    .slot = index,
                    .structure = try self.getStructure(FieldType),
                });
            }
        }

        fn addStructMembers(self: @This(), list: Value, comptime T: type) !void {
            const FieldEnum = std.meta.FieldEnum(T);
            const info = @typeInfo(T).@"struct";
            inline for (info.field_names, 0..) |field_name, index| {
                const FieldType = info.field_types[index];
                const field_attrs = info.field_attrs[index];
                const field_enum = comptime std.meta.stringToEnum(FieldEnum, field_name).?;
                // comptime fields are not actually stored in the struct
                // fields of comptime util in comptime structs are handled in the same manner
                const is_actual = comptime !field_attrs.@"comptime" and !comptime_only.is(FieldType);
                // check meta function to see if field should be handled in a special manner
                const can_be_string = comptime canBeString(FieldType);
                const is_string = comptime can_be_string and meta.call("isFieldString", .{ T, field_enum });
                const can_be_clamped_array = comptime !is_string and canBeClampedArray(FieldType);
                const is_clamped_array = comptime can_be_clamped_array and meta.call("isFieldClampedArray", .{ T, field_enum });
                const can_be_typed_array = comptime !is_string and !is_clamped_array and canBeTypedArray(FieldType);
                const is_typed_array = comptime can_be_typed_array and meta.call("isFieldTypedArray", .{ T, field_enum });
                const can_be_plain = comptime !is_string and !is_typed_array and !is_clamped_array and canBePlain(FieldType);
                const is_plain = comptime can_be_plain and meta.call("isFieldPlain", .{ T, field_enum });
                const is_packed = @typeInfo(T).@"struct".layout == .@"packed";
                const FT = if (comptime supported.is(FieldType)) FieldType else Unsupported;
                try appendList(list, .{
                    .name = field_name,
                    .type = getMemberType(FieldType, field_attrs.@"comptime"),
                    .flags = MemberFlags{
                        .is_read_only = !is_actual,
                        .is_required = is_actual and field_attrs.default_value_ptr == null,
                        .is_string = is_string,
                        .is_plain = is_plain,
                        .is_typed_array = is_typed_array,
                        .is_clamped_array = is_clamped_array,
                    },
                    .bitOffset = if (is_actual) @bitOffsetOf(T, field_name) else null,
                    .bitSize = if (is_actual) bit_size.get(FieldType) else null,
                    .byteSize = if (is_actual and !is_packed) byte_size.get(FieldType) else null,
                    .slot = index,
                    .structure = try self.getStructure(FT),
                });
            }
            if (info.backing_integer) |IT| {
                // add member for backing int
                try appendList(list, .{
                    .type = getMemberType(IT, false),
                    .flags = MemberFlags{ .is_backing_int = true },
                    .bitSize = bit_size.get(IT),
                    .byteSize = byte_size.get(IT),
                    .bitOffset = 0,
                    .structure = try self.getStructure(IT),
                });
            }
        }

        fn addUnionMembers(self: @This(), list: Value, comptime T: type) !void {
            const FieldEnum = std.meta.FieldEnum(T);
            const info = @typeInfo(T).@"union";
            inline for (info.field_names, 0..) |field_name, index| {
                const FieldType = info.field_types[index];
                const field_enum = comptime std.meta.stringToEnum(FieldEnum, field_name).?;
                const can_be_string = comptime canBeString(FieldType);
                const is_string = comptime can_be_string and meta.call("isFieldString", .{ T, field_enum });
                const can_be_clamped_array = comptime !is_string and canBeClampedArray(FieldType);
                const is_clamped_array = comptime can_be_clamped_array and meta.call("isFieldClampedArray", .{ T, field_enum });
                const can_be_typed_array = comptime !is_string and !is_clamped_array and canBeTypedArray(FieldType);
                const is_typed_array = comptime can_be_typed_array and meta.call("isFieldTypedArray", .{ T, field_enum });
                const can_be_plain = comptime !is_string and !is_typed_array and !is_clamped_array and canBePlain(FieldType);
                const is_plain = comptime can_be_plain and meta.call("isFieldPlain", .{ T, field_enum });
                const FT = if (comptime supported.is(FieldType)) FieldType else Unsupported;
                try appendList(list, .{
                    .name = field_name,
                    .type = getMemberType(FieldType, false),
                    .flags = MemberFlags{
                        .is_read_only = comptime_only.is(FieldType),
                        .is_string = is_string,
                        .is_plain = is_plain,
                        .is_typed_array = is_typed_array,
                        .is_clamped_array = is_clamped_array,
                    },
                    .bitOffset = content_offset.get(T),
                    .bitSize = bit_size.get(FieldType),
                    .byteSize = byte_size.get(FieldType),
                    .slot = index,
                    .structure = try self.getStructure(FT),
                });
            }
            if (selector.get(T)) |ST| {
                try appendList(list, .{
                    .type = getMemberType(ST, false),
                    .flags = MemberFlags{ .is_selector = true },
                    .bitOffset = selector_offset.get(T),
                    .bitSize = bit_size.get(ST),
                    .byteSize = byte_size.get(ST),
                    .structure = try self.getStructure(ST),
                });
            }
        }

        fn addOptionalMembers(self: @This(), list: Value, comptime T: type) !void {
            // value always comes first
            const CT = @typeInfo(T).optional.child;
            try appendList(list, .{
                .type = getMemberType(CT, false),
                .bitSize = bit_size.get(CT),
                .byteSize = byte_size.get(CT),
                .bitOffset = 0,
                .slot = 0,
                .structure = try self.getStructure(CT),
            });
            const ST = selector.get(T).?;
            try appendList(list, .{
                .type = getMemberType(ST, false),
                .flags = MemberFlags{ .is_selector = true },
                .bitOffset = selector_offset.get(T),
                .bitSize = bit_size.get(ST),
                .byteSize = byte_size.get(ST),
                .structure = try self.getStructure(ST),
            });
        }

        fn addErrorUnionMembers(self: @This(), list: Value, comptime T: type) !void {
            const PT = @typeInfo(T).error_union.payload;
            try appendList(list, .{
                .type = getMemberType(PT, false),
                .bitOffset = content_offset.get(T),
                .bitSize = bit_size.get(PT),
                .byteSize = byte_size.get(PT),
                .slot = 0,
                .structure = try self.getStructure(PT),
            });
            // don't export inferred error sets that are essentially anyerror as separate sets
            comptime var ES = @typeInfo(T).error_union.error_set;
            if (@typeInfo(ES).error_set.error_names == null) {
                ES = anyerror;
            }
            try appendList(list, .{
                .type = getMemberType(ES, false),
                .flags = MemberFlags{ .is_selector = true },
                .bitOffset = error_offset.get(T),
                .bitSize = bit_size.get(ES),
                .byteSize = byte_size.get(ES),
                .structure = try self.getStructure(ES),
            });
        }

        fn addFunctionMember(self: @This(), list: Value, comptime T: type) !void {
            const FT = fn_transform.Uninlined(T);
            const AT = arg_struct.ArgStruct(FT);
            try appendList(list, .{
                .type = getMemberType(AT, false),
                .structure = try self.getStructure(AT),
            });
        }

        fn addComptimeMember(self: @This(), list: Value, comptime T: type) !void {
            try appendList(list, .{
                .type = getMemberType(T, false),
                .slot = @as(usize, 0),
                .structure = try self.getStructure(T),
            });
        }

        fn getTemplate(self: @This(), comptime T: type) !?Value {
            var memory: ?Value = null;
            var slots: ?Value = null;
            switch (@typeInfo(T)) {
                .@"struct" => |st| if (comptime slice.is(T)) {
                    if (comptime sentinel.get(T)) |s| {
                        memory = try self.exportPointerTarget(&s.value, false);
                    }
                } else if (!comptime arg_struct.is(T, null)) {
                    if (comptime !comptime_only.is(T) and @sizeOf(T) > 0) {
                        const default_values = comptime init: {
                            var values: T = undefined;
                            for (st.field_names, 0..) |field_name, index| {
                                const FieldType = st.field_types[index];
                                const field_attrs = st.field_attrs[index];
                                if (!field_attrs.@"comptime") {
                                    if (field_attrs.default_value_ptr) |opaque_ptr| {
                                        const default_value_ptr: *const FieldType = @ptrCast(@alignCast(opaque_ptr));
                                        @field(values, field_name) = default_value_ptr.*;
                                    }
                                }
                            }
                            break :init values;
                        };
                        memory = try self.exportPointerTarget(&default_values, false);
                    }
                    inline for (st.field_attrs, 0..) |field_attrs, index| {
                        const FieldType = st.field_types[index];
                        if (field_attrs.default_value_ptr) |opaque_ptr| {
                            if ((field_attrs.@"comptime" or comptime_only.is(FieldType)) and comptime supported.is(FieldType)) {
                                // comptime members aren't stored in the struct's memory
                                // they're separate objects in the slots of the static template
                                const default_value_ptr: *const FieldType = @ptrCast(@alignCast(opaque_ptr));
                                const value_obj = try self.exportPointerTarget(default_value_ptr, true);
                                if (slots == null) slots = try host.createObject();
                                try host.setSlotValue(slots.?, index, value_obj);
                            }
                        }
                    }
                },
                .@"fn" => {
                    const FT = fn_transform.Uninlined(T);
                    const thunk = comptime zig_fn.createThunk(FT);
                    memory = try self.exportPointerTarget(thunk, false);
                },
                .array => if (comptime sentinel.get(T)) |s| {
                    memory = try self.exportPointerTarget(&s.value, false);
                },
                .pointer => if (comptime slice.is(T)) {
                    if (comptime sentinel.get(T)) |s| {
                        memory = try self.exportPointerTarget(&s.value, false);
                    }
                },
                else => {},
            }
            const flags = getStructureFlags(T);
            // add slots to template if the structure is using them
            if (slots == null and flags.has_slot) slots = try host.createObject();
            if (memory == null and slots == null) return null;
            return try host.createTemplate(memory, slots);
        }

        fn getStaticMembers(self: @This(), comptime T: type, comptime ignore_decl: bool) !?Value {
            comptime var offset: usize = 0;
            const list = try createList(.{});
            if (!ignore_decl) {
                switch (@typeInfo(T)) {
                    .@"struct", .@"union", .@"enum", .@"opaque" => if (comptime !arg_struct.is(T, null) and !slice.is(T)) {
                        const DeclEnum = std.meta.DeclEnum(T);
                        inline for (comptime std.meta.declarations(T), 0..) |decl_name, index| {
                            if (comptime std.mem.startsWith(u8, decl_name, "meta(")) continue;
                            const decl_enum = comptime std.meta.stringToEnum(DeclEnum, decl_name).?;
                            const decl_ptr = &@field(T, decl_name);
                            const PT = @TypeOf(decl_ptr);
                            if (comptime supported.is(PT)) {
                                const decl_value = decl_ptr.*;
                                const DT = @TypeOf(decl_value);
                                // export type only if it's supported
                                const is_value_supported = switch (DT) {
                                    type => supported.is(decl_value),
                                    else => true,
                                };
                                const should_export = if (is_value_supported) switch (@typeInfo(DT)) {
                                    .@"fn" => !options.omit_functions,
                                    else => !options.omit_variables or @typeInfo(PT).pointer.attrs.@"const",
                                } else false;
                                if (should_export) {
                                    checkStaticMember(DT);
                                    const can_be_string = comptime canBeString(DT);
                                    const is_string = comptime can_be_string and meta.call("isDeclString", .{ T, decl_enum });
                                    const can_be_clamped_array = comptime !is_string and canBeClampedArray(DT);
                                    const is_clamped_array = comptime can_be_clamped_array and meta.call("isDeclClampedArray", .{ T, decl_enum });
                                    const can_be_typed_array = comptime !is_string and !is_clamped_array and canBeTypedArray(DT);
                                    const is_typed_array = comptime can_be_typed_array and meta.call("isDeclTypedArray", .{ T, decl_enum });
                                    const can_be_plain = comptime !is_string and !is_typed_array and !is_clamped_array and canBePlain(DT);
                                    const is_plain = comptime can_be_plain and meta.call("isDeclPlain", .{ T, decl_enum });
                                    try appendList(list, .{
                                        .name = decl_name,
                                        .type = MemberType.object,
                                        .flags = MemberFlags{
                                            .is_read_only = @typeInfo(PT).pointer.attrs.@"const",
                                            .is_method = method.is(T, DT, false),
                                            .is_expecting_instance = method.is(T, DT, true),
                                            .is_string = is_string,
                                            .is_plain = is_plain,
                                            .is_typed_array = is_typed_array,
                                            .is_clamped_array = is_clamped_array,
                                        },
                                        .slot = index,
                                        .structure = try self.getStructure(DT),
                                    });
                                }
                            }
                            offset += 1;
                        }
                    },
                    else => {},
                }
            }
            // add implicit static members
            switch (@typeInfo(T)) {
                .@"enum" => |en| {
                    // add fields as static members
                    inline for (en.field_names, 0..) |field_name, index| {
                        try appendList(list, .{
                            .name = field_name,
                            .type = MemberType.object,
                            .flags = MemberFlags{ .is_part_of_set = true },
                            .slot = offset + index,
                            .structure = try self.getStructure(T),
                        });
                    }
                },
                .error_set => |es| if (es.error_names) |error_names| {
                    inline for (error_names, 0..) |error_name, index| {
                        try appendList(list, .{
                            .name = error_name,
                            .type = MemberType.object,
                            .flags = MemberFlags{ .is_part_of_set = true },
                            .slot = index,
                            .structure = try self.getStructure(T),
                        });
                    }
                },
                else => {},
            }
            return list;
        }

        fn getStaticTemplate(self: @This(), comptime T: type, comptime ignore_decl: bool) !?Value {
            comptime var offset: usize = 0;
            var slots: ?Value = null;
            if (!ignore_decl) {
                switch (@typeInfo(T)) {
                    .@"struct", .@"union", .@"enum", .@"opaque" => if (comptime !arg_struct.is(T, null)) {
                        inline for (comptime std.meta.declarations(T), 0..) |decl_name, index| {
                            if (comptime std.mem.startsWith(u8, decl_name, "meta(")) continue;
                            const decl_ptr = &@field(T, decl_name);
                            const PT = @TypeOf(decl_ptr);
                            if (comptime supported.is(PT)) {
                                const decl_value = decl_ptr.*;
                                const DT = @TypeOf(decl_value);
                                const is_value_supported = switch (DT) {
                                    type => supported.is(decl_value),
                                    else => true,
                                };
                                const should_export = if (is_value_supported) switch (@typeInfo(DT)) {
                                    .@"fn" => !options.omit_functions,
                                    else => !options.omit_variables or @typeInfo(PT).pointer.attrs.@"const",
                                } else false;
                                if (should_export) {
                                    const target_ptr = comptime switch (@typeInfo(DT)) {
                                        .@"fn" => |f| switch (f.attrs.@"callconv") {
                                            .@"inline" => &fn_transform.uninline(decl_value),
                                            else => decl_ptr,
                                        },
                                        else => decl_ptr,
                                    };
                                    const value_obj = try self.exportPointerTarget(target_ptr, true);
                                    if (slots == null) slots = try host.createObject();
                                    try host.setSlotValue(slots.?, index, value_obj);
                                }
                            }
                            offset += 1;
                        }
                    },
                    else => {},
                }
            }
            switch (@typeInfo(T)) {
                .@"enum" => |en| {
                    inline for (en.field_names, 0..) |field_name, index| {
                        const value = @field(T, field_name);
                        const value_obj = try self.exportPointerTarget(&value, true);
                        if (slots == null) slots = try host.createObject();
                        try host.setSlotValue(slots.?, offset + index, value_obj);
                    }
                },
                .error_set => |es| if (es.error_names) |error_names| {
                    inline for (error_names, 0..) |error_name, index| {
                        const err = @field(anyerror, error_name);
                        const value_obj = try self.exportError(err, T);
                        if (slots == null) slots = try host.createObject();
                        try host.setSlotValue(slots.?, offset + index, value_obj);
                    }
                },
                else => {},
            }
            if (slots == null) return null;
            return host.createTemplate(null, slots);
        }

        fn checkStaticMember(comptime T: anytype) void {
            comptime {
                switch (@typeInfo(T)) {
                    .@"fn" => |f| {
                        var has_abort_signal = false;
                        var has_promise = false;
                        for (f.param_types) |param_type| {
                            switch (getStructurePurpose(param_type.?)) {
                                .abort_signal => has_abort_signal = true,
                                .promise => has_promise = true,
                                else => {},
                            }
                        }
                        if (has_abort_signal and !has_promise) {
                            @compileError("Function accepting AbortSignal as an argument must accept a Promise as well: " ++ @typeName(T));
                        }
                    },
                    else => {},
                }
            }
        }

        fn getTypedArrayType(comptime T: type) ?std.lang.Type.Int {
            return switch (@typeInfo(T)) {
                .int => |int| inline for (.{ 8, 16, 32, 64 }) |bits| {
                    if (int.bits == bits) break .{ .bits = bits, .signedness = int.signedness };
                } else null,
                .float => |float| inline for (.{ 16, 32, 64 }) |bits| {
                    if (float.bits == bits) break .{ .bits = bits, .signedness = .signed };
                } else null,
                .array => |ar| getTypedArrayType(ar.child),
                .vector => |ve| getTypedArrayType(ve.child),
                .@"struct" => switch (comptime @hasDecl(T, "ElementType") and slice.is(T)) {
                    true => getTypedArrayType(T.ElementType),
                    false => null,
                },
                else => null,
            };
        }

        fn canBeString(comptime T: type) bool {
            return switch (@typeInfo(T)) {
                .pointer => |pt| switch (pt.size) {
                    .one => canBeString(pt.child),
                    .many, .c => if (pt.sentinel_ptr != null) pt.child == u8 or pt.child == u16 else false,
                    .slice => pt.child == u8 or pt.child == u16 or canBeString(pt.child),
                },
                .array => |ar| ar.child == u8 or ar.child == u16 or canBeString(ar.child),
                .optional => |op| canBeString(op.child),
                .error_union => |eu| canBeString(eu.payload),
                .@"fn" => |f| inline for (f.param_types) |param_type| {
                    if (param_type) |PT| {
                        if (comptime util.getInternalType(PT)) |internal_type| {
                            if (internal_type == .promise or internal_type == .generator) {
                                if (canBeString(PT.payload)) break true;
                            }
                        }
                    }
                } else if (f.return_type) |RT| canBeString(RT) else false,
                else => false,
            };
        }

        fn canBePlain(comptime T: type) bool {
            return switch (@typeInfo(T)) {
                .pointer => |pt| switch (pt.size) {
                    .one, .c => canBePlain(pt.child),
                    .slice => true,
                    else => false,
                },
                .@"struct", .@"union", .array, .vector, .@"enum" => true,
                .optional => |op| canBePlain(op.child),
                .error_union => |eu| canBePlain(eu.payload),
                .@"fn" => |f| inline for (f.param_types) |param_type| {
                    if (param_type) |PT| {
                        if (comptime util.getInternalType(PT)) |internal_type| {
                            if (internal_type == .promise or internal_type == .generator) {
                                if (canBePlain(PT.payload)) break true;
                            }
                        }
                    }
                } else if (f.return_type) |RT| canBePlain(RT) else false,
                else => false,
            };
        }

        fn canBeTypedArray(comptime T: type) bool {
            return switch (@typeInfo(T)) {
                .pointer => canBeTypedArray(target.get(T)),
                .@"fn" => |f| inline for (f.param_types) |param_type| {
                    if (param_type) |PT| {
                        if (comptime util.getInternalType(PT)) |internal_type| {
                            if (internal_type == .promise or internal_type == .generator) {
                                if (canBeTypedArray(PT.payload)) break true;
                            }
                        }
                    }
                } else if (f.return_type) |RT| canBeTypedArray(RT) else false,
                .int, .float => false,
                else => getTypedArrayType(T) != null,
            };
        }

        fn canBeClampedArray(comptime T: type) bool {
            return switch (@typeInfo(T)) {
                .pointer => canBeClampedArray(target.get(T)),
                .@"fn" => |f| inline for (f.param_types) |param_type| {
                    if (param_type) |PT| {
                        if (comptime util.getInternalType(PT)) |internal_type| {
                            if (internal_type == .promise or internal_type == .generator) {
                                if (canBeClampedArray(PT.payload)) break true;
                            }
                        }
                    }
                } else if (f.return_type) |RT| canBeClampedArray(RT) else false,
                .int, .float => false,
                else => if (getTypedArrayType(T)) |i| i.bits == 8 and i.signedness == .unsigned else false,
            };
        }

        fn exportPointerTarget(self: @This(), comptime ptr: anytype, comptime casting: bool) !Value {
            const pt = @typeInfo(@TypeOf(ptr)).pointer;
            const value_ptr = ptr: {
                // values that only exist at comptime need to have their comptime part replaced with void
                // (comptime keyword needed here since expression evaluates to different pointer util)
                if (comptime comptime_only.is(pt.child)) {
                    var runtime_value: ComptimeFree(pt.child) = removeComptimeValues(ptr.*);
                    break :ptr &runtime_value;
                } else {
                    break :ptr ptr;
                }
            };
            const is_comptime = comptime pt.attrs.@"const" and @typeInfo(pt.child) != .@"fn" and !pointer.has(pt.child);
            const export_handle = if (!is_comptime) host.getExportHandle(ptr) else null;
            if (casting) {
                const structure = try self.getStructure(pt.child);
                const comptime_values = switch (comptime comptime_only.is(pt.child)) {
                    true => try self.getComptimeValues(ptr.*),
                    false => null,
                };
                const obj = try createInstance(structure, value_ptr, is_comptime, pt.attrs.@"const", export_handle, comptime_values);
                return obj;
            } else {
                return createView(value_ptr, is_comptime, pt.attrs.@"const", export_handle);
            }
        }

        fn exportError(self: @This(), err: anyerror, comptime T: type) !Value {
            const structure = try self.getStructure(T);
            return try createInstance(structure, &err, true, true, null, null);
        }

        fn exportComptimeValue(self: @This(), comptime value: anytype) !Value {
            return switch (@typeInfo(@TypeOf(value))) {
                .comptime_int => self.exportPointerTarget(&@as(util.IntFor(value), value), true),
                .comptime_float => self.exportPointerTarget(&@as(f64, value), true),
                .enum_literal => self.exportPointerTarget(sentinel.remove(@tagName(value)), true),
                .type => self.getStructure(value),
                else => return self.exportPointerTarget(&value, true),
            };
        }

        fn getComptimeValues(self: @This(), comptime value: anytype) !Value {
            const T = @TypeOf(value);
            const slots = try host.createObject();
            switch (@typeInfo(T)) {
                .type => {
                    const obj = try self.getStructure(value);
                    try host.setSlotValue(slots, 0, obj);
                },
                .comptime_int, .comptime_float, .enum_literal => {
                    const obj = try self.exportComptimeValue(value);
                    try host.setSlotValue(slots, 0, obj);
                },
                .optional => if (value) |v| {
                    const obj = try self.exportComptimeValue(v);
                    try host.setSlotValue(slots, 0, obj);
                },
                .error_union => if (value) |v| {
                    const obj = try self.exportComptimeValue(v);
                    try host.setSlotValue(slots, 0, obj);
                } else |_| {},
                .array => inline for (value, 0..) |element, index| {
                    const obj = try self.exportComptimeValue(element);
                    try host.setSlotValue(slots, index, obj);
                },
                .@"struct" => |st| inline for (st.field_names, 0..) |field_name, index| {
                    const FieldType = st.field_types[index];
                    if (comptime_only.is(FieldType)) {
                        const field_value = @field(value, field_name);
                        const obj = try self.exportComptimeValue(field_value);
                        try host.setSlotValue(slots, index, obj);
                    }
                },
                .@"union" => |un| if (un.tag_type) |Tag| {
                    const tag: Tag = value;
                    inline for (un.field_names, 0..) |field_name, index| {
                        if (@field(Tag, field_name) == tag) {
                            const FieldType = un.field_types[index];
                            if (comptime_only.is(FieldType)) {
                                const field_value = @field(value, field_name);
                                const obj = try self.exportComptimeValue(field_value);
                                try host.setSlotValue(slots, index, obj);
                            }
                        }
                    }
                } else {
                    @compileError("Unable to handle comptime value in bare union");
                },
                else => {},
            }
            return slots;
        }

        fn createObject(initializers: anytype) !Value {
            const obj = try host.createObject();
            try setProperties(obj, initializers);
            return obj;
        }

        fn setProperties(obj: Value, initializers: anytype) !void {
            inline for (comptime std.meta.fieldNames(@TypeOf(initializers))) |field_name| {
                if (try createValue(@field(initializers, field_name))) |value| {
                    try host.setProperty(obj, field_name, value);
                }
            }
        }

        fn createList(initializers: anytype) !Value {
            const list = try host.createList();
            inline for (initializers) |initializer| {
                try appendList(list, initializer);
            }
            return list;
        }

        fn appendList(list: Value, initializer: anytype) !void {
            if (try createValue(initializer)) |value| {
                try host.appendList(list, value);
            }
        }

        fn createValue(initializer: anytype) !?Value {
            const T = @TypeOf(initializer);
            return switch (@typeInfo(T)) {
                .bool => try host.createBool(initializer),
                .int => |int| if (T == usize)
                    createValue(@as(u32, @intCast(initializer)))
                else switch (int.bits) {
                    32 => try host.createInteger(@bitCast(initializer), int.signedness == .unsigned),
                    64 => try host.createBigInteger(@bitCast(initializer), int.signedness == .unsigned),
                    else => try host.createInteger(@intCast(initializer), int.signedness == .unsigned),
                },
                .comptime_int => try createValue(@as(util.IntFor(initializer), initializer)),
                .@"enum" => try createValue(@intFromEnum(initializer)),
                .pointer => switch (T) {
                    Value => initializer,
                    []const u8, [:0]const u8 => try host.createString(initializer),
                    else => {
                        @compileLog(T);
                        @compileError("Unhandled pointer type");
                    },
                },
                .optional => if (initializer) |v| try createValue(v) else null,
                .@"struct" => |st| switch (st.layout) {
                    .@"packed" => try createValue(@as(st.backing_integer.?, @bitCast(initializer))),
                    else => try createObject(initializer),
                },
                .null => null,
                else => {
                    @compileLog(T);
                    @compileError("Unhandled type");
                },
            };
        }

        fn createView(ptr: anytype, copying: bool, read_only: bool, export_handle: anytype) !Value {
            const PtrT = @TypeOf(ptr);
            const pt = @typeInfo(PtrT).pointer;
            const child_size = switch (@typeInfo(pt.child)) {
                .@"fn" => 0,
                else => @sizeOf(pt.child),
            };
            const child_align = switch (@typeInfo(pt.child)) {
                .@"fn" => 1,
                else => @alignOf(pt.child),
            };
            const address = switch (pt.size) {
                .slice => @intFromPtr(ptr.ptr),
                else => @intFromPtr(ptr),
            };
            const invalid_address = create: {
                var invalid_ptr: *u8 = undefined;
                _ = &invalid_ptr;
                break :create @intFromPtr(invalid_ptr);
            };
            if (address == invalid_address) {
                return host.createView(null, 0, copying, read_only, export_handle, child_align);
            }
            const len: usize = switch (pt.size) {
                .one => child_size,
                .slice => child_size * ptr.len,
                .many, .c => get: {
                    if (address != 0) {
                        if (pt.sentinel_ptr) |opaque_ptr| {
                            const sentinel_ptr: *const pt.child = @ptrCast(@alignCast(opaque_ptr));
                            var len: usize = 0;
                            while (ptr[len] != sentinel_ptr.*) {
                                len += 1;
                            }
                            break :get (len + 1) * child_size;
                        } else {
                            break :get 1;
                        }
                    } else {
                        break :get 0;
                    }
                },
            };
            const bytes: [*]const u8 = @ptrFromInt(address);
            return host.createView(bytes, len, copying, read_only, export_handle, child_align);
        }

        fn createInstance(structure: Value, ptr: anytype, copying: bool, read_only: bool, export_handle: anytype, slots: ?Value) !Value {
            // export_handle is  null for WebAssembly, since addresses don't change there
            const dv = try createView(ptr, copying, read_only, export_handle);
            return host.createInstance(structure, dv, slots);
        }
    };
}

pub fn getFactoryThunk(comptime host: type, comptime module: type) zig_fn.Thunk {
    const ns = struct {
        fn exportStructures(_: *const anyopaque, _: *anyopaque) anyerror!void {
            @setEvalBranchQuota(options.eval_branch_quota);
            const factory: Factory(host, module) = .{};
            _ = factory.getStructure(module) catch |err| {
                std.debug.print("Error ecountered during export processs: {s}\n", .{@errorName(err)});
                return err;
            };
        }
    };
    return ns.exportStructures;
}

fn removeComptimeValues(comptime value: anytype) ComptimeFree(@TypeOf(value)) {
    const T = @TypeOf(value);
    const RT = ComptimeFree(T);
    if (comptime T == RT) {
        return value;
    }
    var result: RT = undefined;
    switch (@typeInfo(T)) {
        .comptime_float,
        .comptime_int,
        .enum_literal,
        .type,
        .null,
        .undefined,
        => result = {},
        .array => {
            inline for (value, 0..) |element, index| {
                result[index] = removeComptimeValues(element);
            }
        },
        .@"struct" => |st| {
            inline for (st.field_names) |field_name| {
                @field(result, field_name) = removeComptimeValues(@field(value, field_name));
            }
        },
        .@"union" => |un| {
            if (un.tag_type) |Tag| {
                const tag: Tag = value;
                const field_name = @tagName(tag);
                const field_value = @field(value, field_name);
                result = @unionInit(RT, field_name, removeComptimeValues(field_value));
            } else {
                @compileError("Unable to handle comptime value in bare union");
            }
        },
        .optional => result = if (value) |v| removeComptimeValues(v) else null,
        .error_union => result = if (value) |v| removeComptimeValues(v) else |e| e,
        else => result = value,
    }
    return result;
}

fn readModifier(comptime modifier: ?type, comptime key: []const u8, comptime def: anytype) @TypeOf(def) {
    if (modifier) |T| {
        if (@hasField(T, key)) {
            const m: T = .{};
            return @field(m, key);
        }
    }
    return def;
}
