//! Top-level module // SPDX-FileCopyrightText: 2024-2025 Nguyễn Gia Phong // SPDX-License-Identifier: GPL-3.0-or-later const ArgsTuple = std.meta.ArgsTuple; const HashMap = std.AutoHashMapUnmanaged; const Type = std.builtin.Type; const assert = std.debug.assert; const builtin = @import("builtin"); const c = @cImport(@cInclude("janet.h")); const c_allocator = std.heap.c_allocator; const eql = std.mem.eql; const expectEqualDeep = std.testing.expectEqualDeep; const toLower = std.ascii.toLower; const std = @import("std"); pub const IgnoredDecl = struct { type: type, decl: []const u8, }; const ignored_decls = if (builtin.is_test) [_]IgnoredDecl{ } else @import("root").zsanett_ignored_decls; comptime { assert(@typeInfo(@TypeOf(ignored_decls)).array.child == IgnoredDecl); } pub const Environment = @import("Environment.zig"); test { _ = Environment; } /// Associative list of Zig and wrapped Janet functions. var fn_map: HashMap(*const anyopaque, c.JanetCFunction) = undefined; /// Initializes global Janet state, that is thread local. pub fn init() void { _ = c.janet_init(); // always 0 fn_map = .empty; } /// Frees resources managed by Janet. pub fn deinit() void { fn_map.deinit(c_allocator); c.janet_deinit(); } /// Janet environment Table. pub const Table = c.JanetTable; /// Janet value structure. pub const Value = c.Janet; /// Creates a Janet keyword in kebab-case. fn keyword(comptime name: [:0]const u8) Value { var n: usize = 0; var kebab: [name.len * 2]u8 = undefined; for (name) |char| { switch (char) { 'A'...'Z' => { if (n > 0) { kebab[n] = '-'; n += 1; } kebab[n] = toLower(char); }, '_' => kebab[n] = '-', else => kebab[n] = char, } n += 1; } kebab[n] = 0; return c.janet_keywordv(&kebab, @as(i32, @intCast(n))); } /// Gets function argument. fn getArg(comptime T: type, argv: [*c]Value, n: i32) !T { return switch (@typeInfo(T)) { .bool => c.janet_getboolean(argv, n) != 0, .int => |int| @intCast(switch (int.signedness) { .signed => switch (int.bits) { 0...16 => c.janet_getinteger16, 17...32 => c.janet_getinteger, else => c.janet_getinteger64, }, .unsigned => if (T != usize) switch (int.bits) { 0...16 => c.janet_getuinteger16, 17...32 => c.janet_getuinteger, else => c.janet_getuinteger64, } else c.janet_getsize, }(argv, n)), .float => @floatCast(c.janet_getnumber(argv, n)), else => unwrap(T, argv[@intCast(n)]), }; } /// Wraps native type in Janet value. pub fn wrap(x: anytype) !Value { const T = @TypeOf(x); return switch (@typeInfo(T)) { .void => c.janet_wrap_nil(), .bool => c.janet_wrap_boolean(@intFromBool(x)), .int => c.janet_wrap_integer(@intCast(x)), .comptime_int => c.janet_wrap_integer(x), .float => c.janet_wrap_number(@floatCast(x)), .comptime_float => c.janet_wrap_number(x), .enum_literal => keyword(@tagName(x)), .array => |info| y: { var tuple: [info.len]Value = undefined; for (x, &tuple) |src, *dest| dest.* = try wrap(src); break :y c.janet_wrap_tuple(c.janet_tuple_n(&tuple, tuple.len)); }, .@"enum" => switch (x) { inline else => |tag| keyword(@tagName(tag)), }, .error_set => switch (x) { inline else => |err| keyword("error/" ++ @errorName(err)), }, .error_union => try wrap(x catch |err| err), .@"fn" => |info| y: { assert(!info.is_generic); const entry = try fn_map.getOrPut(c_allocator, &x); if (entry.found_existing) break :y c.janet_wrap_cfunction(entry.value_ptr.*); const n = info.params.len; const y = struct { fn cfun(argc: i32, argv: [*c]Value) callconv(.C) Value { c.janet_fixarity(argc, n); var args: ArgsTuple(@TypeOf(x)) = undefined; inline for (&args, info.params, 0..) |*arg, param, i| { const P = param.type.?; arg.* = getArg(P, argv, i) catch |err| return wrap(err) catch unreachable; } return wrap(@call(.auto, x, args)) catch |err| (wrap(err) catch unreachable); } }.cfun; entry.value_ptr.* = y; break :y c.janet_wrap_cfunction(y); }, .optional => try wrap(x orelse {}), .pointer => |info| switch (info.size) { .one, .many, .c => c.janet_wrap_pointer(@constCast(@ptrCast(x))), .slice => y: { const len: i32 = @intCast(x.len * @sizeOf(info.child)); if (info.is_const) { const string = c.janet_string(@ptrCast(x.ptr), len); break :y c.janet_wrap_string(string); } else { const memory: *anyopaque = @ptrCast(x.ptr); const buffer = c.janet_pointer_buffer_unsafe(memory, len, len); break :y c.janet_wrap_buffer(buffer); } }, }, .@"struct" => |info| y: { const cfuns = fns: { var count: usize = 0; var cfuns: [info.decls.len]struct { Value, Value } = undefined; inline for (info.decls) |decl_info| { if (comptime for (ignored_decls) |ignored| { if (ignored.type == T and eql(u8, ignored.decl, decl_info.name)) break true; } else false) continue; const decl = @field(T, decl_info.name); switch (@typeInfo(@TypeOf(decl))) { .@"fn" => |fn_info| if (!fn_info.is_generic) { cfuns[count] = .{ keyword(decl_info.name), try wrap(decl), }; count += 1; }, else => {}, } } break :fns cfuns[0..count]; }; const count = cfuns.len + info.fields.len; const kv = c.janet_struct_begin(@intCast(count)); for (cfuns) |cfun| { const k, const v = cfun; c.janet_struct_put(kv, k, v); } inline for (info.fields) |field| { const k = keyword(field.name); const v = @field(x, field.name); c.janet_struct_put(kv, k, try wrap(v)); } break :y c.janet_wrap_struct(c.janet_struct_end(kv)); }, .@"union" => |info| if (info.tag_type != null) switch (x) { inline else => |v, tag| c.janet_wrap_tuple(y: { const k = keyword(@tagName(tag)); const tuple = [_]Value{ k, try wrap(v) }; break :y c.janet_tuple_n(&tuple, tuple.len); }), } else @compileError("can't wrap untagged union"), .vector => |info| try wrap(@as([info.len]info.child, x)), else => @compileError(@typeName(@TypeOf(x))), }; } /// Unwraps Janet value to a tuple head. fn tupleHead(x: Value) *const c.JanetTupleHead { return c.janet_tuple_head(c.janet_unwrap_tuple(x)); } /// Accesses Janet tuple data type-safely. fn tupleData(len: comptime_int, head: *const c.JanetTupleHead) *const [len]Value { assert(head.length == len); return @ptrCast(head.data()); } /// Compares given Janet values for equality. fn equal(x: Value, y: Value) bool { return c.janet_equals(x, y) != 0; } /// Checks for Janet nil. fn isNil(x: Value) bool { return c.janet_checktype(x, c.JANET_NIL) != 0; } /// Unwraps to native type. pub fn unwrap(comptime T: type, x: Value) !T { return switch (@typeInfo(T)) { .void => {}, .bool => c.janet_unwrap_boolean(x) != 0, .int => @intCast(c.janet_unwrap_integer(x)), .float => @floatCast(c.janet_unwrap_number(x)), inline .array, .vector => |info| y: { const head = tupleHead(x); var array: [info.len]info.child = undefined; for (tupleData(info.len, head).*, &array) |src, *dest| dest.* = try unwrap(info.child, src); break :y array; }, .@"enum" => |info| inline for (info.fields) |field| { if (equal(keyword(field.name), x)) break @field(T, field.name); } else error.NoCorrespodingEnum, .@"fn" => y: { var iterator = fn_map.iterator(); while (iterator.next()) |entry| if (entry.value_ptr.* == c.janet_unwrap_cfunction(x)) break :y entry.key_ptr.*; break :y error.UnknownFunction; }, .optional => |info| if (isNil(x)) null else unwrap(info.child, x), .pointer => |info| switch (info.size) { .one, .many, .c => @alignCast(@ptrCast(c.janet_unwrap_pointer(x))), .slice => slice: { const ptr = if (info.is_const) c.janet_unwrap_string(x) else c.janet_unwrap_buffer(x).*.data; comptime var many_info = info; many_info.size = .many; const Many = @Type(Type{ .pointer = many_info }); const many: Many = @alignCast(@ptrCast(ptr)); const size = if (info.is_const) c.janet_string_length(ptr) else c.janet_unwrap_buffer(x).*.count; const len = @divExact(size, @sizeOf(info.child)); break :slice if (info.sentinel()) |sentinel| many[0..@intCast(len):sentinel] else many[0..@intCast(len)]; }, }, .@"struct" => |info| y: { const src = c.janet_unwrap_struct(x); var dest: T = undefined; inline for (info.fields) |field| { const k = keyword(field.name); const v = c.janet_struct_get(src, k); @field(dest, field.name) = if (isNil(v)) field.defaultValue() orelse return error.MissingStructField else try unwrap(field.type, v); } break :y dest; }, .@"union" => |info| if (info.tag_type != null) y: { const head = tupleHead(x); const k, const v = tupleData(2, head).*; inline for (info.fields) |field| { if (equal(keyword(field.name), k)) break :y @unionInit(T, field.name, try unwrap(field.type, v)); } else break :y error.UnionTagNotFound; } else @compileError("can't wrap untagged union"), else => @compileError(@typeName(T)), }; } test "isomorphism" { init(); defer deinit(); const Immutable = struct { nothing: void = {}, boolean: bool = true, integer: u16 = 42069, float: f64 = 420.69, enumeration: enum { this, that } = .that, array: [3]u64 = .{ 1, 4, 2 }, string: [:0]const u8 = "foobar", c_string: [*c]const u8 = "baz", slice: []const i32 = &.{ 8, 1, 4 }, tagged_union: union(enum) { integer: i32, float: f32, } = .{ .integer = 123456789 }, vector: @Vector(3, u4) = .{ 5, 6, 7 }, pub fn sum(i: @This()) f64 { return @as(f64, @floatFromInt(i.integer)) + i.float; } }; const i = Immutable{}; try expectEqualDeep(i, try unwrap(Immutable, try wrap(i))); const sum = try unwrap(@TypeOf(&Immutable.sum), try wrap(Immutable.sum)); try expectEqualDeep(i.sum(), sum(i)); var array: [2]bool = undefined; const buffer: []bool = &array; buffer[0] = false; buffer[1] = true; try expectEqualDeep(buffer, try unwrap(@TypeOf(buffer), try wrap(buffer))); }