about summary refs log tree commit diff
path: root/src/root.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/root.zig')
-rw-r--r--src/root.zig416
1 files changed, 270 insertions, 146 deletions
diff --git a/src/root.zig b/src/root.zig
index b944b1b..939549f 100644
--- a/src/root.zig
+++ b/src/root.zig
@@ -2,184 +2,214 @@
 // SPDX-FileCopyrightText: 2024-2025 Nguyễn Gia Phong
 // SPDX-License-Identifier: GPL-3.0-or-later
 
+const Allocator = std.mem.Allocator;
 const ArgsTuple = std.meta.ArgsTuple;
 const HashMap = std.AutoHashMapUnmanaged;
-const Type = std.builtin.Type;
+const SourceLocation = std.builtin.SourceLocation;
 const assert = std.debug.assert;
-const c = @cImport(@cInclude("janet.h"));
 const c_allocator = std.heap.c_allocator;
+const declarations = std.meta.declarations;
+const eql = std.mem.eql;
+const expectEqual = std.testing.expectEqual;
 const expectEqualDeep = std.testing.expectEqualDeep;
-const toLower = std.ascii.toLower;
+const expectError = std.testing.expectError;
 const std = @import("std");
+const toLower = std.ascii.toLower;
+const zeroes = std.mem.zeroes;
+
+pub const janet = rename.janet;
+pub const kebabCase = rename.kebabCase;
+const rename = @import("rename.zig");
+pub const renamespace = rename.space;
 
-pub const Environment = @import("Environment.zig");
 test {
-    _ = Environment;
+    _ = rename;
+}
+
+fn isIntern(T: type) bool {
+    return switch (@typeInfo(T)) {
+        .@"struct", .@"enum", .@"union", .@"opaque" => true,
+        else => false,
+    } and (@import("builtin").is_test or @import("root").zsanettIntern(T));
 }
 
 /// Associative list of Zig and wrapped Janet functions.
-var fn_map: HashMap(*const anyopaque, c.JanetCFunction) = undefined;
+var fn_map: HashMap(*const anyopaque, janet.CFunction) = undefined;
 
 /// Initializes global Janet state, that is thread local.
 pub fn init() void {
-    _ = c.janet_init(); // always 0
+    _ = 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.deinit();
 }
 
-/// Janet environment Table.
-pub const Table = c.JanetTable;
-
 /// Janet value structure.
-pub const Value = c.Janet;
+pub const Value = janet.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);
+    const kebab = kebabCase(name);
+    return janet.keywordv(kebab.ptr, @as(i32, @intCast(kebab.len)));
+}
+
+fn structWithMethods(T: type, fields_len: usize) !*janet.KV {
+    if (!comptime isIntern(T))
+        return janet.structBegin(@intCast(fields_len));
+    const decls = comptime declarations(T);
+    var cfuns: [decls.len]struct { Value, Value } = undefined;
+    var count: usize = 0;
+    inline for (decls) |decl_info| {
+        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;
             },
-            '_' => kebab[n] = '-',
-            else => kebab[n] = char,
+            else => {},
         }
-        n += 1;
     }
-    kebab[n] = 0;
-    return c.janet_keywordv(&kebab, @as(i32, @intCast(n)));
+    const kv = janet.structBegin(@intCast(fields_len + count));
+    for (cfuns[0..count]) |cfun| {
+        const k, const v = cfun;
+        janet.structPut(kv, k, v);
+    }
+    return kv;
 }
 
 /// Gets function argument.
-fn getArg(comptime T: type, argv: [*c]Value, n: i32) !T {
+fn getArg(T: type, argv: [*c]Value, n: i32) !T {
     return switch (@typeInfo(T)) {
-        .bool => c.janet_getboolean(argv, n) != 0,
+        .bool => 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,
+                0...16 => janet.getinteger16,
+                17...32 => janet.getinteger,
+                else => 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,
+                0...16 => janet.getuinteger16,
+                17...32 => janet.getuinteger,
+                else => janet.getuinteger64,
+            } else janet.getsize,
         }(argv, n)),
-        .float => @floatCast(c.janet_getnumber(argv, n)),
+        .float => @floatCast(janet.getnumber(argv, n)),
         else => unwrap(T, argv[@intCast(n)]),
     };
 }
 
+fn raise(err: anyerror) Value {
+    const symb = janet.symbol("error", 5);
+    const fun = janet.resolveExt(janet.coreEnv(null), symb).value;
+    const name = @errorName(err);
+    var msg: [*]u8 = @ptrCast(janet.smalloc(name.len * 2));
+    var n: u16 = 0;
+    for (name) |char| {
+        switch (char) {
+            'A'...'Z' => if (n > 0) {
+                msg[n] = ' ';
+                n += 1;
+            },
+            else => {},
+        }
+        msg[n] = toLower(char);
+        n += 1;
+    }
+    msg = @ptrCast(janet.srealloc(msg, n));
+    var argv = [_]Value{ janet.wrapString(janet.string(msg, n)) };
+    return janet.call(janet.unwrapFunction(fun), argv.len, &argv);
+}
+
+pub fn wrapFn(f: anytype) Allocator.Error!janet.CFunction {
+    const F = @TypeOf(f);
+    const info = @typeInfo(F).@"fn";
+    assert(!info.is_generic);
+    const entry = try fn_map.getOrPut(c_allocator, &f);
+    if (!entry.found_existing)
+        entry.value_ptr.* = struct {
+            fn cfun(argc: i32, argv: [*c]Value) callconv(.C) Value {
+                janet.fixarity(argc, info.params.len);
+                var args: ArgsTuple(F) = undefined;
+                inline for (&args, info.params, 0..) |*arg, param, i| {
+                    const P = param.type.?;
+                    arg.* = getArg(P, argv, i) catch |err|
+                        return raise(err);
+                }
+                const result = @call(.auto, f, args);
+                return if (@typeInfo(@TypeOf(result)) != .error_union)
+                    wrap(result) catch unreachable
+                else if (result) |res|
+                    wrap(res) catch unreachable
+                else |err|
+                    raise(err);
+            }
+        }.cfun;
+    return entry.value_ptr.*;
+}
+
 /// 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),
+        .void => janet.wrapNil(),
+        .bool => janet.wrapBoolean(@intFromBool(x)),
+        .int => janet.wrapInteger(@intCast(x)),
+        .comptime_int => janet.wrapInteger(x),
+        .float => janet.wrapNumber(@floatCast(x)),
+        .comptime_float  => janet.wrapNumber(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));
+            break :y janet.wrapTuple(janet.tupleN(&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 {}),
+        .@"fn" => janet.wrapCfunction(try wrapFn(x)),
+        .optional => if (x) |child| try wrap(child) else try wrap({}),
         .pointer => |info| switch (info.size) {
-            .one, .many, .c => c.janet_wrap_pointer(@constCast(@ptrCast(x))),
+            .one => if (isIntern(info.child)) y: {
+                const kv = try structWithMethods(info.child, 1);
+                const ptr = janet.wrapPointer(@constCast(@ptrCast(x)));
+                janet.structPut(kv, keyword(@typeName(info.child)), ptr);
+                break :y janet.wrapStruct(janet.structEnd(kv));
+            } else janet.wrapPointer(@constCast(@ptrCast(x))),
+            .many, .c => janet.wrapPointer(@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);
+                    const string = janet.string(@ptrCast(x.ptr), len);
+                    break :y janet.wrapString(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);
+                    const buffer = janet.pointerBufferUnsafe(memory, len, len);
+                    break :y janet.wrapBuffer(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| {
-                    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);
-            }
+            const kv = try structWithMethods(T, info.fields.len);
             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));
+                janet.structPut(kv, k, try wrap(v));
             }
-            break :y c.janet_wrap_struct(c.janet_struct_end(kv));
+            break :y janet.wrapStruct(janet.structEnd(kv));
         },
         .@"union" => |info| if (info.tag_type != null) switch (x) {
-            inline else => |v, tag| c.janet_wrap_tuple(y: {
+            inline else => |v, tag| janet.wrapTuple(y: {
                 const k = keyword(@tagName(tag));
                 const tuple = [_]Value{ k, try wrap(v) };
-                break :y c.janet_tuple_n(&tuple, tuple.len);
+                break :y janet.tupleN(&tuple, tuple.len);
             }),
         } else @compileError("can't wrap untagged union"),
         .vector => |info| try wrap(@as([info.len]info.child, x)),
@@ -187,37 +217,32 @@ pub fn wrap(x: anytype) !Value {
     };
 }
 
-/// 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 {
+             head: *const janet.TupleHead) *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;
+    return janet.equals(x, y) != 0;
 }
 
 /// Checks for Janet nil.
 fn isNil(x: Value) bool {
-    return c.janet_checktype(x, c.JANET_NIL) != 0;
+    return janet.checktype(x, janet.NIL) != 0;
 }
 
 /// Unwraps to native type.
-pub fn unwrap(comptime T: type, x: Value) !T {
+pub fn unwrap(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)),
+        .bool => janet.unwrapBoolean(x) != 0,
+        .int => @intCast(janet.unwrapInteger(x)),
+        .float => @floatCast(janet.unwrapNumber(x)),
         inline .array, .vector => |info| y: {
-            const head = tupleHead(x);
+            const head = janet.tupleHead(janet.unwrapTuple(x));
             var array: [info.len]info.child = undefined;
             for (tupleData(info.len, head).*, &array) |src, *dest|
                 dest.* = try unwrap(info.child, src);
@@ -227,29 +252,33 @@ pub fn unwrap(comptime T: type, x: Value) !T {
             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),
+        .optional => |info| if (isNil(x)) null else try unwrap(info.child, x),
         .pointer => |info| switch (info.size) {
-            .one, .many, .c => @alignCast(@ptrCast(c.janet_unwrap_pointer(x))),
+            .one => if (@typeInfo(info.child) == .@"fn") y: {
+                var iterator = fn_map.iterator();
+                while (iterator.next()) |entry|
+                    if (entry.value_ptr.* == janet.unwrapCfunction(x))
+                        break :y @ptrCast(entry.key_ptr.*);
+                break :y error.UnknownFunction;
+            } else if (isIntern(info.child)) y: {
+                const ptr = janet.structGet(janet.unwrapStruct(x),
+                                            keyword(@typeName(info.child)));
+                break :y @alignCast(@ptrCast(janet.unwrapPointer(ptr)));
+            } else @alignCast(@ptrCast(janet.unwrapPointer(x))),
+            .many, .c => @alignCast(@ptrCast(janet.unwrapPointer(x))),
             .slice => slice: {
                 const ptr = if (info.is_const)
-                    c.janet_unwrap_string(x)
+                    janet.unwrapString(x)
                 else
-                    c.janet_unwrap_buffer(x).*.data;
+                    janet.unwrapBuffer(x).*.data;
                 comptime var many_info = info;
                 many_info.size = .many;
-                const Many = @Type(Type{ .pointer = many_info });
+                const Many = @Type(.{ .pointer = many_info });
                 const many: Many = @alignCast(@ptrCast(ptr));
                 const size = if (info.is_const)
-                    c.janet_string_length(ptr)
+                    janet.stringLength(ptr)
                 else
-                    c.janet_unwrap_buffer(x).*.count;
+                    janet.unwrapBuffer(x).*.count;
                 const len = @divExact(size, @sizeOf(info.child));
                 break :slice if (info.sentinel()) |sentinel|
                     many[0..@intCast(len):sentinel]
@@ -258,26 +287,24 @@ pub fn unwrap(comptime T: type, x: Value) !T {
             },
         },
         .@"struct" => |info| y: {
-            const src = c.janet_unwrap_struct(x);
+            const src = janet.unwrapStruct(x);
             var dest: T = undefined;
-            inline for (info.fields) |field| {
+            break :y inline for (info.fields) |field| {
                 const k = keyword(field.name);
-                const v = c.janet_struct_get(src, k);
+                const v = janet.structGet(src, k);
                 @field(dest, field.name) = if (isNil(v))
-                    field.defaultValue() orelse return error.MissingStructField
+                    field.defaultValue() orelse break error.MissingStructField
                 else
                     try unwrap(field.type, v);
-            }
-            break :y dest;
+            } else dest;
         },
         .@"union" => |info| if (info.tag_type != null) y: {
-            const head = tupleHead(x);
+            const head = janet.tupleHead(janet.unwrapTuple(x));
             const k, const v = tupleData(2, head).*;
-            inline for (info.fields) |field| {
+            break :y 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;
+                    break @unionInit(T, field.name, try unwrap(field.type, v));
+            } else error.UnionTagNotFound;
         } else @compileError("can't wrap untagged union"),
         else => @compileError(@typeName(T)),
     };
@@ -302,18 +329,115 @@ test "isomorphism" {
         } = .{ .integer = 123456789 },
         vector: @Vector(3, u4) = .{ 5, 6, 7 },
 
-        pub fn sum(i: @This()) f64 {
+        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));
+    try expectEqual(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)));
+    try expectEqual(buffer, try unwrap(@TypeOf(buffer), try wrap(buffer)));
+}
+
+/// Evaluates the Janet program given in str.
+pub fn eval(comptime T: type, str: []const u8, src: [*:0]const u8) !T {
+    var ret: Value = undefined;
+    const errflags = janet.dobytes(janet.coreEnv(null),
+                                   str.ptr, @intCast(str.len), src, &ret);
+    // Errors are already logged by Janet,
+    // among them only one is returned and not machine-readable.
+    return if (errflags == 0) unwrap(T, ret) else error.JanetError;
+}
+
+test eval {
+    init();
+    defer deinit();
+    try expectEqual(try eval(i32, "(+ 3 7)", @src().fn_name), 10);
+}
+
+/// Binds val to name.
+pub fn def(name: [*:0]const u8, val: anytype,
+           documentation: [*:0]const u8) !void {
+    janet.def(janet.coreEnv(null), name, try wrap(val), documentation);
+}
+
+test def {
+    const Number = struct {
+        const Number = @This();
+        n: i8,
+        pub fn nextN(self: Number) Number {
+            return Number{ .n = self.n + 1 };
+        }
+    };
+    init();
+    defer deinit();
+    try def("seven", Number{ .n = 7 }, "number 7 in a struct");
+    const nine = try eval(Number, "(:next-n (:next-n seven))",
+                              @src().fn_name);
+    try expectEqual(Number{ .n = 9 }, nine);
+}
+
+/// Gets value.
+pub fn resolve(T: type, name: []const u8) !T {
+    const symbol = janet.symbol(name.ptr, @intCast(name.len));
+    return try unwrap(T, janet.resolveExt(janet.coreEnv(null), symbol).value);
+}
+
+test resolve {
+    init();
+    defer deinit();
+    try def("pi", 3, "pi for engineers");
+    try expectEqual(try resolve(u8, "pi"), 3);
+}
+
+/// Exposes given function in Janet.
+pub fn defn(comptime fun: anytype, reg: struct {
+    prefix: ?[*:0]const u8 = null,
+    name: [*:0]const u8,
+    documentation: [*:0]const u8,
+    source: SourceLocation,
+}) !void {
+    const cfuns = [_]janet.RegExt{
+        .{
+            .name = reg.name,
+            .cfun = try wrapFn(fun),
+            .documentation = reg.documentation,
+            .source_file = reg.source.file,
+            .source_line = @bitCast(reg.source.line),
+        },
+        zeroes(janet.RegExt),
+    };
+    const env = janet.coreEnv(null);
+    if (reg.prefix) |prefix|
+        janet.cfunsExtPrefix(env, prefix, &cfuns)
+    else
+        janet.cfunsExt(env, null, &cfuns);
+}
+
+test defn {
+    init();
+    defer deinit();
+    try defn(struct {
+        fn add(a: i32, b: i32) i32 { return a + b; }
+    }.add, .{
+        .name = "add",
+        .documentation = "(add a b)\n\nReturn a + b.",
+        .source = @src(),
+    });
+    try expectEqual(try eval(i32, "(add 3 7)", @src().fn_name), 10);
+
+    try defn(struct {
+        fn raise() !void { return error.ThisIsFine; }
+    }.raise, .{
+        .name = "raise",
+        .documentation = "(raise)\n\nRaise an error.",
+        .source = @src(),
+    });
+    try expectError(error.JanetError, eval(void, "(raise)", @src().fn_name));
 }