diff --git a/build.zig b/build.zig index 0fe87b1..b7c5fdd 100644 --- a/build.zig +++ b/build.zig @@ -1,5 +1,26 @@ const std = @import("std"); +/// Creates a new test suite, makes a test step, and returns the test step. +pub fn add_tests(b: *std.Build, name: []const u8, desc: []const u8, path: []const u8, target: std.zig.CrossTarget, optimize: std.builtin.OptimizeMode) *std.Build.Step { + const new_tests = b.addTest(.{ + .root_source_file = .{ .path = path }, + .target = target, + .optimize = optimize, + }); + + const run_new_tests = b.addRunArtifact(new_tests); + + const new_tests_step = b.step(name, desc); + new_tests_step.dependOn(&run_new_tests.step); + + return new_tests_step; +} + +// TODO: Continue splitting the project into multiple sub-modules and implement this function. +// pub fn add_module(b: *std.Build, name: []const u8, test_desc: []const u8, path: []const u8, target: std.zig.CrossTarget, optimize: std.builtin.OptimizeMode) *std.Build.Step { +// const new_module = b.addModule // ??? +// } + pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); @@ -12,14 +33,8 @@ pub fn build(b: *std.Build) void { }); b.installArtifact(lib); - const main_tests = b.addTest(.{ - .root_source_file = .{ .path = "src/tests.zig" }, - .target = target, - .optimize = optimize, - }); - - const run_main_tests = b.addRunArtifact(main_tests); - const test_step = b.step("test", "Run library tests"); - test_step.dependOn(&run_main_tests.step); + + test_step.dependOn(add_tests(b, "common_tests", "Run the unit tests for the Common library.", "src/common_tests.zig", target, optimize)); + test_step.dependOn(add_tests(b, "rule_tests", "Run the unit tests for the Rule type.", "src/rule_tests.zig", target, optimize)); } diff --git a/src/common.zig b/src/common.zig new file mode 100644 index 0000000..c07df68 --- /dev/null +++ b/src/common.zig @@ -0,0 +1,278 @@ +/// Metaplasia Common Utils +/// ©2023 Cristian Vasilache (NNCV / Nylvon) +/// +/// Common functionality shared across the project. +pub const Common = @This(); +const std = @import("std"); + +/// Small container for look-ups. +/// TODO: Refactor using this type. +pub const LookupData = struct { + lookup_name: []const u8, + lookup_mode: LookupMode, +}; + +/// Restricts look-up to only fields or declarations, or does not restrict at all. +/// If Any is selected, fields are prioritised over declarations in the look-up order. +pub const LookupMode = enum { Field, Declaration, Any }; + +// +// Convenience wrappers +// + +/// Generates lookup info for a field given a name. +pub fn FindField(name: []const u8) LookupData { + return LookupData{ + .lookup_name = name, + .lookup_mode = .Field, + }; +} + +/// Generates lookup info for a declaration given a name. +pub fn FindDeclaration(name: []const u8) LookupData { + return LookupData{ + .lookup_name = name, + .lookup_mode = .Declaration, + }; +} + +/// Generates lookup info for a member given a name. +pub fn FindAny(name: []const u8) LookupData { + return LookupData{ + .lookup_name = name, + .lookup_mode = .Any, + }; +} + +/// Used by FindInType to detail why the function call failed. +pub const LookupError = error{ + InvalidType, + NotFound, + TypeHasNoFields, + TypeHasNoDeclarations, + TypeHasNoFieldsOrDeclarations, +}; + +/// Contains the type that the declaration was gotten from as well as the declaration. +pub const FullDeclaration = struct { + decl: std.builtin.Type.Declaration, + type: type, + is_const: bool, +}; + +/// Contains the type that the enum field was gotten from as well as the enum field. +pub const FullEnumField = struct { + field: std.builtin.Type.EnumField, + type: type, +}; + +/// This enum is used to identify the active part of the +/// TypeItem union wrapper. +pub const TypeItemKind = enum { + Declaration, + StructField, + EnumField, + UnionField, +}; + +/// The mutability of a TypeItem entity. +pub const TypeItemMutability = enum { Variable, Constant }; + +/// A Type Item is a part of a type, it is a wrapper over +/// fields and declarations of all types. +pub const TypeItem = union(TypeItemKind) { + // NOTE: Should be kept up to date with the language specification. + Declaration: FullDeclaration, + StructField: std.builtin.Type.StructField, + EnumField: FullEnumField, + UnionField: std.builtin.Type.UnionField, + + /// The error set that can be returned by the functions of the TypeItem wrapper. + pub const Error = error{ + InvalidType, + }; + + /// Returns the type of the entity described. + pub fn GetType(comptime self: @This()) type { + switch (self) { + .Declaration => |d| return @TypeOf(@field(d.type, d.decl.name)), + .StructField => |sf| return sf.type, + .EnumField => |ef| return ef.type, + .UnionField => |uf| return uf.type, + } + } + + /// Returns the name of the entity described. + pub fn GetName(comptime self: @This()) []const u8 { + switch (self) { + .Declaration => |d| return d.decl.name, + .StructField => |sf| return sf.name, + .EnumField => |ef| return ef.field.name, + .UnionField => |uf| return uf.name, + } + } + + /// Returns the default value of the entity described. + /// If the entity doesn't have a default value, null will be returned. + /// If the entity cannot have a default value, an error will be returned. + pub fn GetDefaultValue(comptime self: @This()) switch (self) { + .StructField => ?*const anyopaque, + else => TypeItem.Error, + } { + switch (self) { + .StructField => |sf| return sf.default_value, + else => return TypeItem.Error.InvalidType, + } + } + + /// Returns whether the entity described has a constant value. + pub fn GetIsConstant(comptime self: @This()) bool { + switch (self) { + .Declaration => |d| return d.is_const, + else => return false, + } + } + + /// Returns whether the entity described has a non-constant value. + pub fn GetIsVariable(comptime self: @This()) bool { + return !self.GetIsConstant(); + } +}; + +/// Internal use only. Looks up a field from a type-info struct. +inline fn UnsafeFieldLookup(comptime target: type, comptime lookup_name: []const u8) LookupError!TypeItem { + comptime { + const type_info = switch (@typeInfo(target)) { + .Struct => |s| s, + .Enum => |e| e, + .Union => |u| u, + else => unreachable, + }; + + if (type_info.fields.len == 0) return LookupError.TypeHasNoFields; + + for (type_info.fields) |field| { + if (std.mem.eql(u8, field.name, lookup_name)) { + switch (@typeInfo(target)) { + .Struct => return TypeItem{ .StructField = field }, + .Enum => return TypeItem{ .EnumField = FullEnumField{ .field = field, .type = target } }, + .Union => return TypeItem{ .UnionField = field }, + else => unreachable, + } + } + } + + // If we're here, we haven't found it. + return LookupError.NotFound; + } +} + +/// Internal use only. Looks up a declaration from a type-info struct. +inline fn UnsafeDeclarationLookup(comptime target: type, comptime lookup_name: []const u8) LookupError!TypeItem { + comptime { + const type_info = switch (@typeInfo(target)) { + .Struct => |s| s, + .Enum => |e| e, + .Union => |u| u, + else => unreachable, + }; + + if (type_info.decls.len == 0) return LookupError.TypeHasNoDeclarations; + + for (type_info.decls) |decl| { + if (std.mem.eql(u8, decl.name, lookup_name)) + return TypeItem{ .Declaration = FullDeclaration{ + .decl = decl, + .type = target, + .is_const = @typeInfo(@TypeOf(&@field(target, decl.name))).Pointer.is_const, + } }; + } + + // If we're here, we haven't found it. + return LookupError.NotFound; + } +} + +/// Internal use only. Looks up a field or declaraton from a type-info struct. +inline fn UnsafeAnyLookup(comptime target: type, comptime lookup_name: []const u8) LookupError!TypeItem { + comptime { + const type_info = switch (@typeInfo(target)) { + .Struct => |s| s, + .Enum => |e| e, + .Union => |u| u, + else => unreachable, + }; + + if (type_info.fields.len == 0 and type_info.decls.len == 0) return LookupError.TypeHasNoFieldsOrDeclarations; + + for (type_info.fields) |field| { + if (std.mem.eql(u8, field.name, lookup_name)) { + switch (@typeInfo(target)) { + .Struct => return TypeItem{ .StructField = field }, + .Enum => return TypeItem{ .EnumField = FullEnumField{ .field = field, .type = target } }, + .Union => return TypeItem{ .UnionField = field }, + else => @compileError("Please do not use this function on its own, as this function does not do any checks on the inputs!"), + } + } + } + + for (type_info.decls) |decl| { + if (std.mem.eql(u8, decl.name, lookup_name)) + return TypeItem{ .Declaration = FullDeclaration{ + .decl = decl, + .type = target, + .is_const = @typeInfo(@TypeOf(&@field(target, decl.name))).Pointer.is_const, + } }; + } + + // If we're here, we haven't found it. + return LookupError.NotFound; + } +} + +/// Internal use only. Looks up parameters from structs and unions. +inline fn UnsafeLookup(comptime target: type, comptime lookup_name: []const u8, comptime lookup_mode: LookupMode) LookupError!TypeItem { + comptime { + switch (lookup_mode) { + // Search for a field + .Field => { + return UnsafeFieldLookup(target, lookup_name); + }, + // Search for a declaration + .Declaration => { + return UnsafeDeclarationLookup(target, lookup_name); + }, + // Search for either + .Any => { + return UnsafeAnyLookup(target, lookup_name); + }, + } + } +} + +/// Tries to find a field or declaration given a name and a specified look-up mode. +/// Returns the type of the field/declaration if found, otherwise errors with details. +pub fn FindInType(comptime from: type, comptime lookup_name: []const u8, comptime lookup_mode: LookupMode) LookupError!TypeItem { + switch (@typeInfo(from)) { + // Structs can have both fields and declarations + .Struct, .Union, .Enum => { + return UnsafeLookup(from, lookup_name, lookup_mode); + }, + .Pointer, .Vector, .Optional, .Array, .ErrorUnion => { + return FindInType(GetBaseType(from), lookup_name, lookup_mode); + }, + else => return LookupError.InvalidType, + } +} + +/// Gets the base type of any pointer/vector/array/etc type. +pub fn GetBaseType(comptime from: type) type { + switch (@typeInfo(from)) { + .Pointer => |ptr| return GetBaseType(ptr.child), + .Vector => |vec| return GetBaseType(vec.child), + .Optional => |opt| return GetBaseType(opt.child), + .Array => |arr| return GetBaseType(arr.child), + .ErrorUnion => |eru| return GetBaseType(eru.payload), + else => return from, + } +} diff --git a/src/common_tests.zig b/src/common_tests.zig new file mode 100644 index 0000000..acdaed0 --- /dev/null +++ b/src/common_tests.zig @@ -0,0 +1,194 @@ +/// Metaplasia Common Tests +/// ©2023 Cristian Vasilache (NNCV / Nylvon) +/// +/// Provides an exhaustive test suite for the common +/// functionality of the Metaplasia library. +/// +/// Each test name is formatted as follows: +/// "[FeatureName] [Scenario]" +pub const CommonTests = @This(); +const Common = @import("common.zig").Common; +const TypeItemKind = Common.TypeItemKind; +const TypeItemMutability = Common.TypeItemMutability; +const LookupMode = Common.LookupMode; +const LookupError = Common.LookupError; +const std = @import("std"); +const testing = std.testing; +const expect = testing.expect; +const expectEqual = testing.expectEqual; +const expectError = testing.expectError; +const activeTag = std.meta.activeTag; +const eql = std.mem.eql; + +// FindInType tests + +/// Convenience wrapper for testing the +/// FindInType function over types. +pub fn test_findintype( + // L = Look-up + // E = Expected + comptime target: type, + comptime l_name: []const u8, + comptime l_mode: LookupMode, + comptime e_type: ?type, + comptime e_kind: ?TypeItemKind, + comptime e_default: ?*const anyopaque, + comptime e_decl_const: ?bool, + comptime e_outcome: enum { Success, Failure }, + comptime e_error: ?anyerror, +) !void { + switch (e_outcome) { + .Success => { + const info = try Common.FindInType(target, l_name, l_mode); + + if (e_kind) |kind| { + try expect(activeTag(info) == kind); + } else return error.NullKind; + + if (e_type) |t| { + try expect(info.GetType() == t); + } else return error.NullType; + + try expect(eql(u8, info.GetName(), l_name)); + switch (info) { + .StructField => { + try expect(info.GetDefaultValue() == e_default); + }, + .Declaration => { + try expect(info.GetIsConstant() == e_decl_const.?); + }, + else => {}, + } + }, + .Failure => { + const info = Common.FindInType(target, l_name, l_mode); + if (e_error) |err| { + try expectError(err, info); + } else { + return error.NullError; + } + }, + } +} + +test "FindInType" { + const test_struct = struct { + a: i32, + pub const b: i64 = 10; + pub var c: i64 = 20; + }; + + try test_findintype(test_struct, "a", .Field, i32, .StructField, null, null, .Success, null); + try test_findintype(test_struct, "a", .Any, i32, .StructField, null, null, .Success, null); + try test_findintype(test_struct, "b", .Declaration, i64, .Declaration, null, true, .Success, null); + try test_findintype(test_struct, "b", .Any, i64, .Declaration, null, true, .Success, null); + try test_findintype(test_struct, "c", .Declaration, i64, .Declaration, null, false, .Success, null); + try test_findintype(test_struct, "c", .Any, i64, .Declaration, null, false, .Success, null); + + try test_findintype(test_struct, "a", .Declaration, null, null, null, null, .Failure, LookupError.NotFound); + try test_findintype(test_struct, "b", .Field, null, null, null, null, .Failure, LookupError.NotFound); + + const test_struct_empty = struct {}; + + try test_findintype(test_struct_empty, "", .Field, null, null, null, null, .Failure, LookupError.TypeHasNoFields); + try test_findintype(test_struct_empty, "", .Declaration, null, null, null, null, .Failure, LookupError.TypeHasNoDeclarations); + try test_findintype(test_struct_empty, "", .Any, null, null, null, null, .Failure, LookupError.TypeHasNoFieldsOrDeclarations); + + const test_union = union { + a: i32, + pub const b: i64 = 10; + }; + + try test_findintype(test_union, "a", .Field, i32, .UnionField, null, null, .Success, null); + try test_findintype(test_union, "a", .Any, i32, .UnionField, null, null, .Success, null); + try test_findintype(test_union, "b", .Declaration, i64, .Declaration, null, true, .Success, null); + try test_findintype(test_union, "b", .Any, i64, .Declaration, null, true, .Success, null); + + try test_findintype(test_union, "a", .Declaration, null, null, null, null, .Failure, LookupError.NotFound); + try test_findintype(test_union, "b", .Field, null, null, null, null, .Failure, LookupError.NotFound); + + const test_union_empty = union {}; + + try test_findintype(test_union_empty, "", .Field, null, null, null, null, .Failure, LookupError.TypeHasNoFields); + try test_findintype(test_union_empty, "", .Declaration, null, null, null, null, .Failure, LookupError.TypeHasNoDeclarations); + try test_findintype(test_union_empty, "", .Any, null, null, null, null, .Failure, LookupError.TypeHasNoFieldsOrDeclarations); + + const test_enum = enum { + a, + pub const b: i64 = 10; + }; + + // NOTE: Rethink the expected type on an enum. Maybe the enum should return the actual enum selector? + // Maybe a different function would be of use. + try test_findintype(test_enum, "a", .Field, test_enum, .EnumField, null, null, .Success, null); + try test_findintype(test_enum, "a", .Any, test_enum, .EnumField, null, null, .Success, null); + try test_findintype(test_enum, "b", .Declaration, i64, .Declaration, null, true, .Success, null); + try test_findintype(test_enum, "b", .Any, i64, .Declaration, null, true, .Success, null); + + try test_findintype(test_enum, "a", .Declaration, null, null, null, null, .Failure, LookupError.NotFound); + try test_findintype(test_enum, "b", .Field, null, null, null, null, .Failure, LookupError.NotFound); + + const test_enum_empty = enum {}; + + try test_findintype(test_enum_empty, "", .Field, null, null, null, null, .Failure, LookupError.TypeHasNoFields); + try test_findintype(test_enum_empty, "", .Declaration, null, null, null, null, .Failure, LookupError.TypeHasNoDeclarations); + try test_findintype(test_enum_empty, "", .Any, null, null, null, null, .Failure, LookupError.TypeHasNoFieldsOrDeclarations); + + // + // For pointers, optionals, error unions, vectors and arrays + // the test_struct type was used as the base. + // Any other could have been chosen, but I have chosen this as it isolates + // the checking for the base type from the pointer, optional, error union, vector + // and array code-paths for checking (even though they eventually converge). + // + + const test_pointer = *test_struct; + + try test_findintype(test_pointer, "a", .Field, i32, .StructField, null, null, .Success, null); + try test_findintype(test_pointer, "a", .Any, i32, .StructField, null, null, .Success, null); + try test_findintype(test_pointer, "b", .Declaration, i64, .Declaration, null, true, .Success, null); + try test_findintype(test_pointer, "b", .Any, i64, .Declaration, null, true, .Success, null); + + try test_findintype(test_pointer, "a", .Declaration, null, null, null, null, .Failure, LookupError.NotFound); + try test_findintype(test_pointer, "b", .Field, null, null, null, null, .Failure, LookupError.NotFound); + + const test_optional = ?test_struct; + + try test_findintype(test_optional, "a", .Field, i32, .StructField, null, null, .Success, null); + try test_findintype(test_optional, "a", .Any, i32, .StructField, null, null, .Success, null); + try test_findintype(test_optional, "b", .Declaration, i64, .Declaration, null, true, .Success, null); + try test_findintype(test_optional, "b", .Any, i64, .Declaration, null, true, .Success, null); + + try test_findintype(test_optional, "a", .Declaration, null, null, null, null, .Failure, LookupError.NotFound); + try test_findintype(test_optional, "b", .Field, null, null, null, null, .Failure, LookupError.NotFound); + + const test_error_union = anyerror!test_struct; + + try test_findintype(test_error_union, "a", .Field, i32, .StructField, null, null, .Success, null); + try test_findintype(test_error_union, "a", .Any, i32, .StructField, null, null, .Success, null); + try test_findintype(test_error_union, "b", .Declaration, i64, .Declaration, null, true, .Success, null); + try test_findintype(test_error_union, "b", .Any, i64, .Declaration, null, true, .Success, null); + + try test_findintype(test_error_union, "a", .Declaration, null, null, null, null, .Failure, LookupError.NotFound); + try test_findintype(test_error_union, "b", .Field, null, null, null, null, .Failure, LookupError.NotFound); + + const test_vector = @Vector(4, *test_struct); + + try test_findintype(test_vector, "a", .Field, i32, .StructField, null, null, .Success, null); + try test_findintype(test_vector, "a", .Any, i32, .StructField, null, null, .Success, null); + try test_findintype(test_vector, "b", .Declaration, i64, .Declaration, null, true, .Success, null); + try test_findintype(test_vector, "b", .Any, i64, .Declaration, null, true, .Success, null); + + try test_findintype(test_vector, "a", .Declaration, null, null, null, null, .Failure, LookupError.NotFound); + try test_findintype(test_vector, "b", .Field, null, null, null, null, .Failure, LookupError.NotFound); + + const test_array = [4]test_struct; + + try test_findintype(test_array, "a", .Field, i32, .StructField, null, null, .Success, null); + try test_findintype(test_array, "a", .Any, i32, .StructField, null, null, .Success, null); + try test_findintype(test_array, "b", .Declaration, i64, .Declaration, null, true, .Success, null); + try test_findintype(test_array, "b", .Any, i64, .Declaration, null, true, .Success, null); + + try test_findintype(test_array, "a", .Declaration, null, null, null, null, .Failure, LookupError.NotFound); + try test_findintype(test_array, "b", .Field, null, null, null, null, .Failure, LookupError.NotFound); +} diff --git a/src/interface.zig b/src/interface.zig new file mode 100644 index 0000000..026bd5a --- /dev/null +++ b/src/interface.zig @@ -0,0 +1,24 @@ +/// Metaplasia Interface +/// ©2023 Cristian Vasilache (NNCV / Nylvon) +/// +/// An interface is a set of independent rules that +/// must all be matched in order for a type to validate it. +pub const Interface = @This(); +const Common = @import("common.zig"); +const std = @import("std"); +const RuleSet = @import("ruleset.zig").RuleSet; +const Rule = @import("rule.zig").Rule; + +/// Creates a set of independent rules based off of the array of rules +/// that have been passed as a parameter. +pub fn Make(rules: []const Rule) RuleSet { + var interface = RuleSet{ + .rules = [1]RuleSet.RuleNode{undefined} ** rules.len, + }; + + for (rules, 0..) |r, i| { + interface.rules[i].children = null; + interface.rules[i].mode = .Independent; + interface.rules[i].rule = r; + } +} diff --git a/src/metaplasia.zig b/src/metaplasia.zig index 67dc642..94e4f39 100644 --- a/src/metaplasia.zig +++ b/src/metaplasia.zig @@ -5,185 +5,6 @@ /// easy reflection, type merging and other techniques of /// type mangling. pub const Metaplasia = @This(); +pub const Common = @import("common.zig"); +pub const Interface = @import("interface.zig"); const std = @import("std"); - -/// Restricts look-up to only fields or declarations, or does not restrict at all. -/// If Any is selected, fields are prioritised over declarations in the look-up order. -pub const LookupMode = enum { Field, Declaration, Any }; - -pub const LookupError = error{ - InvalidType, - NotFound, - TypeHasNoFields, - TypeHasNoDeclarations, - TypeHasNoFieldsOrDeclarations, -}; - -/// Internal use only. Looks up a field from a type-info struct. -inline fn UnsafeFieldLookup(comptime type_info: anytype, comptime lookup_name: []const u8) LookupError!type { - comptime { - if (type_info.fields.len == 0) return LookupError.TypeHasNoFields; - - for (type_info.fields) |field| { - if (std.mem.eql(u8, field.name, lookup_name)) return field.type; - } - - // If we're here, we haven't found it - return LookupError.NotFound; - } -} - -/// Internal use only. Looks up a declaration from a type-info struct. -inline fn UnsafeDeclarationLookup(comptime from: type, comptime type_info: anytype, comptime lookup_name: []const u8) LookupError!type { - comptime { - if (type_info.decls.len == 0) return LookupError.TypeHasNoDeclarations; - - for (type_info.decls) |decl| { - if (std.mem.eql(u8, decl.name, lookup_name)) return @TypeOf(@field(from, lookup_name)); - } - - // If we're here, we haven't found it - return LookupError.NotFound; - } -} - -/// Internal use only. Looks up a field or declaraton from a type-info struct. -inline fn UnsafeAnyLookup(comptime from: type, comptime type_info: anytype, comptime lookup_name: []const u8) LookupError!type { - comptime { - if (type_info.fields.len == 0 and type_info.decls.len == 0) return LookupError.TypeHasNoFieldsOrDeclarations; - - for (type_info.fields) |field| { - if (std.mem.eql(u8, field.name, lookup_name)) return field.type; - } - - for (type_info.decls) |decl| { - if (std.mem.eql(u8, decl.name, lookup_name)) return @TypeOf(@field(from, lookup_name)); - } - - // If we're here, we haven't found it - return LookupError.NotFound; - } -} - -// Internal use only. Creates a tuple type for the enum look-up result (used for .Field and .Any lookups on enums) -pub fn EnumLookupResult(comptime from: type) type { - return struct { field: ?from, decl: ?type }; -} - -/// Internal use only. Looks up a field from a type-info struct. Used for enums -inline fn UnsafeEnumFieldLookup(comptime from: type, comptime type_info: anytype, comptime lookup_name: []const u8) LookupError!from { - comptime { - if (type_info.fields.len == 0) return LookupError.TypeHasNoFields; - - for (type_info.fields) |field| { - if (std.mem.eql(u8, field.name, lookup_name)) return @enumFromInt(field.value); - } - - // If we're here, we haven't found it - return LookupError.NotFound; - } -} - -/// Internal use only. Looks up a field or declaraton from a type-info struct. Used for enums. -inline fn UnsafeEnumAnyLookup(comptime from: type, comptime type_info: anytype, comptime lookup_name: []const u8) LookupError!EnumLookupResult(from) { - comptime { - const ResultType = EnumLookupResult(from); - - if (type_info.fields.len == 0 and type_info.decls.len == 0) return LookupError.TypeHasNoFieldsOrDeclarations; - - for (type_info.fields) |field| { - if (std.mem.eql(u8, field.name, lookup_name)) return ResultType{ .field = @enumFromInt(field.value), .decl = null }; - } - - for (type_info.decls) |decl| { - if (std.mem.eql(u8, decl.name, lookup_name)) return ResultType{ .field = null, .decl = @TypeOf(@field(from, lookup_name)) }; - } - - // If we're here, we haven't found it - return LookupError.NotFound; - } -} - -/// Internal use only. Looks up parameters from structs and unions. -inline fn UnsafeLookup(comptime from: type, comptime type_info: anytype, comptime lookup_name: []const u8, comptime lookup_mode: LookupMode) LookupError!type { - comptime { - switch (lookup_mode) { - // Search for a field - .Field => { - return UnsafeFieldLookup(type_info, lookup_name); - }, - // Search for a declaration - .Declaration => { - return UnsafeDeclarationLookup(from, type_info, lookup_name); - }, - // Search for either - .Any => { - return UnsafeAnyLookup(from, type_info, lookup_name); - }, - } - } -} - -/// Internal use only. Looks up parameters from enums. -inline fn UnsafeEnumLookup(comptime from: type, comptime type_info: anytype, comptime lookup_name: []const u8, comptime lookup_mode: LookupMode) switch (lookup_mode) { - .Field => LookupError!from, - .Declaration => LookupError!type, - .Any => LookupError!EnumLookupResult(from), -} { - comptime { - switch (lookup_mode) { - // Search for a field - .Field => { - return UnsafeEnumFieldLookup(from, type_info, lookup_name); - }, - // Search for a declaration - .Declaration => { - return UnsafeDeclarationLookup(from, type_info, lookup_name); - }, - // Search for either - .Any => { - return UnsafeEnumAnyLookup(from, type_info, lookup_name); - }, - } - } -} - -/// Tries to find a field or declaration given a name and a specified look-up mode -/// Returns the type of the field/declaration if found, otherwise errors with details. -pub fn FindInType(comptime from: type, comptime lookup_name: []const u8, comptime lookup_mode: LookupMode) switch (@typeInfo(from)) { - .Enum => switch (lookup_mode) { - .Field => LookupError!from, - .Declaration => LookupError!type, - .Any => LookupError!EnumLookupResult(from), - }, - else => LookupError!type, -} { - switch (@typeInfo(from)) { - // Structs can have both fields and declarations - .Struct => |struct_info| { - return UnsafeLookup(from, struct_info, lookup_name, lookup_mode); - }, - .Union => |uni_info| { - return UnsafeLookup(from, uni_info, lookup_name, lookup_mode); - }, - .Enum => |enu_info| { - return UnsafeEnumLookup(from, enu_info, lookup_name, lookup_mode); - }, - .Pointer => |ptr_info| { - return FindInType(ptr_info.child, lookup_name, lookup_mode); - }, - .Vector => |vec_info| { - return FindInType(vec_info.child, lookup_name, lookup_mode); - }, - .Optional => |opt_info| { - return FindInType(opt_info.child, lookup_name, lookup_mode); - }, - .Array => |arr_info| { - return FindInType(arr_info.child, lookup_name, lookup_mode); - }, - .ErrorUnion => |eru_info| { - return FindInType(eru_info.payload, lookup_name, lookup_mode); - }, - else => return LookupError.InvalidType, - } -} diff --git a/src/rule.zig b/src/rule.zig new file mode 100644 index 0000000..de97c50 --- /dev/null +++ b/src/rule.zig @@ -0,0 +1,228 @@ +/// Metaplasia Rule +/// ©2023 Cristian Vasilache (NNCV / Nylvon) +/// +/// A rule is a function that validates a constraint on a type. +/// A ruleset is a set of rules that validate multiple constraints in any manner. +pub const Rule = @This(); +const Common = @import("common.zig"); +const std = @import("std"); + +/// Similar to std.meta.trait's TraitFn, but it can also return errors. +/// Useful for debugging why something doesn't validate a rule. +pub const RuleFn = fn (comptime type) anyerror!bool; + +/// A rule set is a decision tree that upon traversal determines whether +/// a type meets certain criteria, and if not, returns all the errors. +pub const RuleSet = struct { + rule: ?RuleFn, + children: ?[]const RuleSet, + link: Link, + outcome: ?anyerror!bool, + + /// Determines how this rule is linked with the previous rule. + pub const Link = enum { + Root, // Reserved for the root rule-set. + And, // Previous rule is valid if this one is also valid. + Or, // Previous rule may be valid if this one isn't. + // TODO: Add more links. + // NOTE: Is it necessary? + }; + + pub fn CheckChildren(comptime self: *const RuleSet, comptime target: type) anyerror!bool { + if (self.children) |children| { + var ok = true; + inline for (children) |*child| { + const child_outcome = try child.Check(target); + switch (child.link) { + .And => { + ok = ok and child_outcome; + }, + .Or => { + ok = ok or child_outcome; + }, + .Root => { + return error.ChildIsRoot; + }, + } + } + return ok; + } else return error.NoChildren; + } + + pub fn Check(comptime self: *const RuleSet, comptime target: type) anyerror!bool { + // If there is no rule, it passes the check, otherwise evaluate. + const ok_rule = if (self.rule) |r| try r(target) else true; + const ok_children = self.CheckChildren(target) catch |err| { + if (err == error.NoChildren) { + // If there's no children, just return the rule's result. + return ok_rule; + } else return err; + }; + // If we're here, ok_children is a bool. + // A root node is an "and" node with children. + // self.outcome = ok_children and ok_rule; + return ok_children and ok_rule; + } + + // + // Rule-set generation utilities + // + + /// Returns a blank rule-set. + /// Used to return a root rule-set node. + pub fn Blank() RuleSet { + return RuleSet{ + .rule = null, + .children = null, + .link = .Root, + .outcome = null, + }; + } + + /// Returns an insular rule-set from a rule function. + pub fn From(comptime rule: RuleFn, comptime link: RuleSet.Link) RuleSet { + return RuleSet{ + .rule = rule, + .children = null, + .link = link, + .outcome = null, + }; + } + + /// Adds the specified rule-set to the current one as a child of it. + /// Returns a pointer to the current rule-set after modifying it. + pub fn Inject(comptime self: *const RuleSet, comptime ruleset: RuleSet) *const RuleSet { + if (self.children != null) { + const new_ruleset = RuleSet{ + // + .rule = self.rule, + .link = self.link, + .outcome = self.outcome, + .children = self.children.? ++ &[1]RuleSet{ruleset}, + }; + return &new_ruleset; + } else { + const new_ruleset = RuleSet{ + // + .rule = self.rule, + .link = self.link, + .outcome = self.outcome, + .children = &[1]RuleSet{ruleset}, + }; + return &new_ruleset; + // self.children = [1]RuleSet{ruleset}; + // var new_ruleset = RuleSet{ .rule = self.rule, .link = self.link, .outcome = self.outcome, .children = @constCast(&[1]RuleSet{ruleset}) }; + // return &new_ruleset; + } + } + + /// Adds another rule with an "and" relationship to the current rule-set. + /// Returns a pointer to the current rule-set after modifying it. + pub fn And(comptime self: *const RuleSet, comptime rule: RuleFn) *const RuleSet { + const new_ruleset = From(rule, .And); + return self.Inject(new_ruleset); + } + + /// Adds another rule with an "or" relationship to the current rule-set. + /// Returns a pointer to the current rule-set after modifying it. + pub fn Or(comptime self: *const RuleSet, comptime rule: RuleFn) *const RuleSet { + const new_ruleset = From(rule, .Or); + return self.Inject(new_ruleset); + } + + /// Prints the ruleset as a string + pub fn Print(comptime self: *const RuleSet, comptime index: usize) []const u8 { + comptime var pad: []const u8 = ""; + comptime { + for (0..index) |i| { + _ = i; + pad = pad ++ "\t"; + } + switch (self.link) { + .And => pad = pad ++ "AND", + .Or => pad = pad ++ "OR", + .Root => pad = pad ++ "ROOT", + } + pad = pad ++ "\n"; + if (self.children) |children| { + for (children) |c| { + pad = pad ++ c.Print(index + 1); + } + } + } + return pad; + } +}; + +// +// Basic rules +// + +/// Checks whether a looked-up member is inside a type. +/// eg: Whether a struct has a member "check" that is a declaration. +pub fn IsInType(comptime lookup_data: Common.LookupData) RuleFn { + return struct { + pub fn rule(comptime target: type) anyerror!bool { + _ = Common.FindInType(target, lookup_data.lookup_name, lookup_data.lookup_mode) catch |err| { + if (err == Common.LookupError.NotFound) { + return false; + } else return err; + }; + return true; + } + }.rule; +} + +/// Checks whether a looked-up member is of a certain type. +/// eg: Whether a struct has a member "check" that is of "fn(void) void" type. +pub fn IsType(comptime lookup_data: Common.LookupData, comptime wanted_type: type) RuleFn { + return struct { + pub fn rule(comptime target: type) anyerror!bool { + const lookup = try Common.FindInType(target, lookup_data.lookup_name, lookup_data.lookup_mode); + return lookup.GetType() == wanted_type; + } + }.rule; +} + +/// Checks whether a looked-up member is of a certain archetype. +/// eg: Whether a struct has a member "check" that is a function (.Fn archetype) +pub fn IsArchetype(comptime lookup_data: Common.LookupData, comptime archetype: std.meta.tags(std.builtin.Type)) RuleFn { + return struct { + pub fn rule(comptime target: type) anyerror!bool { + const lookup = try Common.FindInType(target, lookup_data.lookup_name, lookup_data.lookup_mode); + return std.meta.activeTag(@typeInfo(lookup.GetType())) == archetype; + } + }.rule; +} + +/// Checks whether a looked-up member is of a function archetype +/// This is a specialised case of "IsArchetype". +pub fn IsFunction(comptime lookup_data: Common.LookupData) RuleFn { + return struct { + pub fn rule(comptime target: type) anyerror!bool { + return IsArchetype(lookup_data, .Fn)(target); + } + }.rule; +} + +/// Checks whether a looked-up member is variable. +/// eg: Whether a struct has a declaration "check" whose value can be changed. +pub fn IsVariable(comptime lookup_data: Common.LookupData) RuleFn { + return struct { + pub fn rule(comptime target: type) anyerror!bool { + const lookup = try Common.FindInType(target, lookup_data.lookup_name, lookup_data.lookup_mode); + return lookup.GetIsVariable(); + } + }.rule; +} + +/// Checks whether a looked-up member is constant. +/// eg: Whether a struct has a declaration "check" whose value can't be changed. +pub fn IsConstant(comptime lookup_data: Common.LookupData) RuleFn { + return struct { + pub fn rule(comptime target: type) anyerror!bool { + const lookup = try Common.FindInType(target, lookup_data.lookup_name, lookup_data.lookup_mode); + return lookup.GetIsConstant(); + } + }.rule; +} diff --git a/src/rule_tests.zig b/src/rule_tests.zig new file mode 100644 index 0000000..e98ce6c --- /dev/null +++ b/src/rule_tests.zig @@ -0,0 +1,69 @@ +/// Metaplasia Rule Tests +/// ©2023 Cristian Vasilache (NNCV / Nylvon) +/// +/// Provides an exhaustive test suite for the +/// Rule part of the Metaplasia library. +/// +/// Each test name is formatted as follows: +/// "[FeatureName] [Scenario]" +pub const RuleTests = @This(); +const Common = @import("common.zig"); +const FindField = Common.FindField; +const FindDeclaration = Common.FindDeclaration; +const FindAny = Common.FindAny; +const Rule = @import("rule.zig"); +const RuleSet = Rule.RuleSet; +const Blank = RuleSet.Blank; +const From = RuleSet.From; +const Inject = RuleSet.Inject; +const And = RuleSet.And; +const Or = RuleSet.Or; +const IsInType = Rule.IsInType; +const IsType = Rule.IsType; +const IsArchetype = Rule.IsArchetype; +const IsFunction = Rule.IsFunction; +const IsVariable = Rule.IsVariable; +const IsConstant = Rule.IsConstant; +const std = @import("std"); +const testing = std.testing; +const expect = testing.expect; +const expectEqual = testing.expectEqual; +const expectError = testing.expectError; +const activeTag = std.meta.activeTag; +const eql = std.mem.eql; + +test "Interfaces at last" { + const test_type = struct { + a: i32, + b: i32, + }; + + // A simple interface definition + const HasBoth = + Blank() + .And(IsInType(FindField("a"))) + .And(IsInType(FindField("b"))); + + const HasEither = + Blank() + .Or(IsInType(FindField("a"))) + .Or(IsInType(FindField("b"))); + + const checkBoth = try HasBoth.Check(test_type); + const checkEither = try HasEither.Check(test_type); + try expect(checkBoth); + try expect(checkEither); + + const test_type_2 = struct { + a: i32, + }; + + const checkBoth2 = try HasBoth.Check(test_type_2); + const checkEither2 = try HasEither.Check(test_type_2); + try expect(!checkBoth2); + try expect(checkEither2); + + const test_type_3 = struct {}; + + try expectError(Common.LookupError.TypeHasNoFields, HasBoth.Check(test_type_3)); +} diff --git a/src/ruleset.zig b/src/ruleset.zig new file mode 100644 index 0000000..3151287 --- /dev/null +++ b/src/ruleset.zig @@ -0,0 +1,30 @@ +/// Metaplasia RuleSet +/// ©2023 Cristian Vasilache (NNCV / Nylvon) +/// +/// A ruleset is a tree of rules with a specifier dictating +/// how the rules ought to be interpreted as a whole. +/// (i.e.: Some rules may depend on some other rules, whilst other may not) +pub const RuleSet = @This(); +const Common = @import("common.zig"); +const Rule = @import("rule.zig").Rule; +const std = @import("std"); + +/// How two rules can relate to eachother inside of a ruleset +pub const Relationship = enum { + And, + Or, + Xor, + Independent, +}; + +/// A rule node is a rule and a specifier. +/// The specifier marks that +pub const RuleNode = struct { + rule: Rule, + mode: Relationship, + children: ?[]const Rule, +}; + +/// A ruleset has one or more rules, +/// Each rule can be a tree of rules. +rules: []const RuleNode, diff --git a/src/tests.zig b/src/tests.zig deleted file mode 100644 index b094dc9..0000000 --- a/src/tests.zig +++ /dev/null @@ -1,256 +0,0 @@ -/// Metaplasia Tests -/// ©2023 Cristian Vasilache (NNCV / Nylvon) -/// -/// Provides an exhaustive test suite for the Metaplasia library. -/// Each test name is formatted as follows: -/// "[FeatureName] [Scenario]" -pub const MetaplasiaTests = @This(); -pub const Metaplasia = @import("metaplasia.zig"); -const LookupError = Metaplasia.LookupError; -const std = @import("std"); -const testing = std.testing; -const expect = testing.expect; -const expectEqual = testing.expectEqual; -const expectError = testing.expectError; - -// FindInType tests - -test "FindInType Struct" { - const sample_type = struct { - a: i32, - pub const b: i64 = 10; - }; - - // Try to find the field - // First two should not error, the third should error - const field_type = try Metaplasia.FindInType(sample_type, "a", .Field); - try expect(field_type == i32); - const field_type_any = try Metaplasia.FindInType(sample_type, "a", .Any); - try expect(field_type_any == i32); - const field_error = Metaplasia.FindInType(sample_type, "a", .Declaration); - try expectError(LookupError.NotFound, field_error); - - // Try to find the declaration - // First two should not error, the third should error - const decl_type = try Metaplasia.FindInType(sample_type, "b", .Declaration); - try expect(decl_type == i64); - const decl_type_any = try Metaplasia.FindInType(sample_type, "b", .Any); - try expect(decl_type_any == i64); - const decl_error = Metaplasia.FindInType(sample_type, "b", .Field); - try expectError(LookupError.NotFound, decl_error); -} - -test "FindInType Struct Empty" { - const sample_type = struct {}; - - const field_error = Metaplasia.FindInType(sample_type, "a", .Field); - try expectError(LookupError.TypeHasNoFields, field_error); - const decl_error = Metaplasia.FindInType(sample_type, "a", .Declaration); - try expectError(LookupError.TypeHasNoDeclarations, decl_error); - const any_error = Metaplasia.FindInType(sample_type, "a", .Any); - try expectError(LookupError.TypeHasNoFieldsOrDeclarations, any_error); -} - -test "FindInType Union" { - const sample_type = union { - a: i32, - pub const b: i64 = 10; - }; - - // Try to find the field - // First two should not error, the third should error - const field_type = try Metaplasia.FindInType(sample_type, "a", .Field); - try expect(field_type == i32); - const field_type_any = try Metaplasia.FindInType(sample_type, "a", .Any); - try expect(field_type_any == i32); - const field_error = Metaplasia.FindInType(sample_type, "a", .Declaration); - try expectError(LookupError.NotFound, field_error); - - // Try to find the declaration - // First two should not error, the third should error - const decl_type = try Metaplasia.FindInType(sample_type, "b", .Declaration); - try expect(decl_type == i64); - const decl_type_any = try Metaplasia.FindInType(sample_type, "b", .Any); - try expect(decl_type_any == i64); - const decl_error = Metaplasia.FindInType(sample_type, "b", .Field); - try expectError(LookupError.NotFound, decl_error); -} - -test "FindInType Union Empty" { - const sample_type = union {}; - - const field_error = Metaplasia.FindInType(sample_type, "a", .Field); - try expectError(LookupError.TypeHasNoFields, field_error); - const decl_error = Metaplasia.FindInType(sample_type, "a", .Declaration); - try expectError(LookupError.TypeHasNoDeclarations, decl_error); - const any_error = Metaplasia.FindInType(sample_type, "a", .Any); - try expectError(LookupError.TypeHasNoFieldsOrDeclarations, any_error); -} - -test "FindInType Enum" { - const sample_type = enum { - a, - pub const b: i64 = 10; - }; - - // Try to find the field - // First two should not error, the third should error - const field_type = try Metaplasia.FindInType(sample_type, "a", .Field); - try expect(field_type == sample_type.a); - const field_type_any = try Metaplasia.FindInType(sample_type, "a", .Any); - try expect(field_type_any.field == sample_type.a); - const field_error = Metaplasia.FindInType(sample_type, "a", .Declaration); - try expectError(LookupError.NotFound, field_error); - - // Try to find the declaration - // First two should not error, the third should error - const decl_type = try Metaplasia.FindInType(sample_type, "b", .Declaration); - try expect(decl_type == i64); - const decl_type_any = try Metaplasia.FindInType(sample_type, "b", .Any); - try expect(decl_type_any.decl == i64); - const decl_error = Metaplasia.FindInType(sample_type, "b", .Field); - try expectError(LookupError.NotFound, decl_error); -} - -test "FindInType Enum Empty" { - const sample_type_a = enum { a }; - - // Enums cannot be empty (for now; due to an error in std.fmt:527:46 (@tagName of empty enum is impossible)) - const decl_error = Metaplasia.FindInType(sample_type_a, "a", .Declaration); - try expectError(LookupError.TypeHasNoDeclarations, decl_error); -} - -test "FindInType Pointer" { - const sample_type = struct { - a: i32, - pub const b: i64 = 10; - }; - - const target_type = *sample_type; - - // Try to find the field - // First two should not error, the third should error - const field_type = try Metaplasia.FindInType(target_type, "a", .Field); - try expect(field_type == i32); - const field_type_any = try Metaplasia.FindInType(target_type, "a", .Any); - try expect(field_type_any == i32); - const field_error = Metaplasia.FindInType(target_type, "a", .Declaration); - try expectError(LookupError.NotFound, field_error); - - // Try to find the declaration - // First two should not error, the third should error - const decl_type = try Metaplasia.FindInType(target_type, "b", .Declaration); - try expect(decl_type == i64); - const decl_type_any = try Metaplasia.FindInType(target_type, "b", .Any); - try expect(decl_type_any == i64); - const decl_error = Metaplasia.FindInType(target_type, "b", .Field); - try expectError(LookupError.NotFound, decl_error); -} - -test "FindInType Optional" { - const sample_type = struct { - a: i32, - pub const b: i64 = 10; - }; - - const target_type = ?sample_type; - - // Try to find the field - // First two should not error, the third should error - const field_type = try Metaplasia.FindInType(target_type, "a", .Field); - try expect(field_type == i32); - const field_type_any = try Metaplasia.FindInType(target_type, "a", .Any); - try expect(field_type_any == i32); - const field_error = Metaplasia.FindInType(target_type, "a", .Declaration); - try expectError(LookupError.NotFound, field_error); - - // Try to find the declaration - // First two should not error, the third should error - const decl_type = try Metaplasia.FindInType(target_type, "b", .Declaration); - try expect(decl_type == i64); - const decl_type_any = try Metaplasia.FindInType(target_type, "b", .Any); - try expect(decl_type_any == i64); - const decl_error = Metaplasia.FindInType(target_type, "b", .Field); - try expectError(LookupError.NotFound, decl_error); -} - -test "FindInType Error Union" { - const sample_type = struct { - a: i32, - pub const b: i64 = 10; - }; - - const target_type = anyerror!sample_type; - - // Try to find the field - // First two should not error, the third should error - const field_type = try Metaplasia.FindInType(target_type, "a", .Field); - try expect(field_type == i32); - const field_type_any = try Metaplasia.FindInType(target_type, "a", .Any); - try expect(field_type_any == i32); - const field_error = Metaplasia.FindInType(target_type, "a", .Declaration); - try expectError(LookupError.NotFound, field_error); - - // Try to find the declaration - // First two should not error, the third should error - const decl_type = try Metaplasia.FindInType(target_type, "b", .Declaration); - try expect(decl_type == i64); - const decl_type_any = try Metaplasia.FindInType(target_type, "b", .Any); - try expect(decl_type_any == i64); - const decl_error = Metaplasia.FindInType(target_type, "b", .Field); - try expectError(LookupError.NotFound, decl_error); -} - -test "FindInType Vector" { - const sample_type = struct { - a: i32, - pub const b: i64 = 10; - }; - - const target_type = @Vector(4, *sample_type); - - // Try to find the field - // First two should not error, the third should error - const field_type = try Metaplasia.FindInType(target_type, "a", .Field); - try expect(field_type == i32); - const field_type_any = try Metaplasia.FindInType(target_type, "a", .Any); - try expect(field_type_any == i32); - const field_error = Metaplasia.FindInType(target_type, "a", .Declaration); - try expectError(LookupError.NotFound, field_error); - - // Try to find the declaration - // First two should not error, the third should error - const decl_type = try Metaplasia.FindInType(target_type, "b", .Declaration); - try expect(decl_type == i64); - const decl_type_any = try Metaplasia.FindInType(target_type, "b", .Any); - try expect(decl_type_any == i64); - const decl_error = Metaplasia.FindInType(target_type, "b", .Field); - try expectError(LookupError.NotFound, decl_error); -} - -test "FindInType Array" { - const sample_type = struct { - a: i32, - pub const b: i64 = 10; - }; - - const target_type = [4]sample_type; - - // Try to find the field - // First two should not error, the third should error - const field_type = try Metaplasia.FindInType(target_type, "a", .Field); - try expect(field_type == i32); - const field_type_any = try Metaplasia.FindInType(target_type, "a", .Any); - try expect(field_type_any == i32); - const field_error = Metaplasia.FindInType(target_type, "a", .Declaration); - try expectError(LookupError.NotFound, field_error); - - // Try to find the declaration - // First two should not error, the third should error - const decl_type = try Metaplasia.FindInType(target_type, "b", .Declaration); - try expect(decl_type == i64); - const decl_type_any = try Metaplasia.FindInType(target_type, "b", .Any); - try expect(decl_type_any == i64); - const decl_error = Metaplasia.FindInType(target_type, "b", .Field); - try expectError(LookupError.NotFound, decl_error); -}