summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/libzisp.zig131
-rw-r--r--src/libzisp/value.zig34
-rw-r--r--src/libzisp/value/boole.zig28
-rw-r--r--src/libzisp/value/char.zig4
-rw-r--r--src/libzisp/value/eof.zig27
-rw-r--r--src/libzisp/value/fixnum.zig2
-rw-r--r--src/libzisp/value/misc.zig8
-rw-r--r--src/libzisp/value/nil.zig27
-rw-r--r--src/libzisp/value/ptr.zig34
-rw-r--r--src/libzisp/value/sstr.zig64
10 files changed, 302 insertions, 57 deletions
diff --git a/src/libzisp.zig b/src/libzisp.zig
index 542b84c..c7ead3b 100644
--- a/src/libzisp.zig
+++ b/src/libzisp.zig
@@ -5,7 +5,7 @@ const std = @import("std");
const builtin = @import("builtin");
const testing = std.testing;
-const value = @import("libzisp/value.zig");
+pub const value = @import("libzisp/value.zig");
test "double" {
const d1: f64 = 0.123456789;
@@ -18,7 +18,7 @@ test "double" {
try std.testing.expect(value.double.check(v1));
try std.testing.expect(value.double.check(v2));
try std.testing.expect(value.double.check(v3));
- try std.testing.expect(result == d1 + d2);
+ try std.testing.expectEqual(d1 + d2, result);
}
test "fixnum" {
@@ -32,20 +32,121 @@ test "fixnum" {
try std.testing.expect(value.fixnum.check(v1));
try std.testing.expect(value.fixnum.check(v2));
try std.testing.expect(value.fixnum.check(v3));
- try std.testing.expect(result == int1 + int2);
+ try std.testing.expectEqual(int1 + int2, result);
}
test "ptr" {
- const ptr1 = value.ptr.pack(@ptrFromInt(256), value.ptr.Tag.string);
- try std.testing.expect(value.ptr.check(ptr1));
- try std.testing.expect(value.ptr.checkZisp(ptr1));
- try std.testing.expect(value.ptr.checkNormal(ptr1));
-
- const ptr2 = value.ptr.makeWeak(ptr1);
- try std.testing.expect(value.ptr.check(ptr2));
- try std.testing.expect(value.ptr.checkZisp(ptr2));
- try std.testing.expect(value.ptr.checkWeak(ptr2));
-
- // Make sure ptr1 wasn't modified
- try std.testing.expect(value.ptr.checkNormal(ptr1));
+ const p: *anyopaque = @ptrFromInt(256);
+
+ const p1 = value.ptr.pack(p, value.ptr.Tag.string);
+ try std.testing.expect(value.ptr.check(p1));
+ try std.testing.expect(value.ptr.checkZisp(p1));
+ try std.testing.expect(value.ptr.checkNormal(p1));
+ const p1v, const p1t = value.ptr.unpack(p1);
+ try std.testing.expectEqual(p, p1v);
+ try std.testing.expectEqual(value.ptr.Tag.string, p1t);
+
+ const p2 = value.ptr.makeWeak(p1);
+ try std.testing.expect(value.ptr.check(p2));
+ try std.testing.expect(value.ptr.checkZisp(p2));
+ try std.testing.expect(value.ptr.checkWeak(p2));
+ const p2v, const p2t = value.ptr.unpack(p1);
+ try std.testing.expectEqual(p, p2v);
+ try std.testing.expectEqual(value.ptr.Tag.string, p2t);
+}
+
+test "sstr" {
+ const impls = .{
+ .{ value.sstr.pack, value.sstr.unpack },
+ // .{ value.sstr.pack1, value.sstr.unpack1 },
+ // .{ value.sstr.pack2, value.sstr.unpack2 },
+ // .{ value.sstr.pack3, value.sstr.unpack3 },
+ // .{ value.sstr.pack4, value.sstr.unpack4 },
+ };
+
+ inline for (impls, 0..) |impl, i| {
+ const pack, const unpack = impl;
+
+ const ss1 = pack("1");
+ const ss2 = pack("123");
+ const ss3 = pack("123456");
+
+ const s1, const l1 = unpack(ss1);
+ const s2, const l2 = unpack(ss2);
+ const s3, const l3 = unpack(ss3);
+
+ try std.testing.expect(value.sstr.check(ss1));
+ try std.testing.expect(value.sstr.check(ss2));
+ try std.testing.expect(value.sstr.check(ss3));
+
+ try std.testing.expectEqual(1, l1);
+ try std.testing.expectEqualStrings("1", s1[0..l1]);
+
+ try std.testing.expectEqual(3, l2);
+ try std.testing.expectEqualStrings("123", s2[0..l2]);
+
+ try std.testing.expectEqual(6, l3);
+ try std.testing.expectEqualStrings("123456", s3[0..l3]);
+
+ var timer = try std.time.Timer.start();
+ var ns: f64 = undefined;
+ var secs: f64 = undefined;
+
+ const iters = 1;
+ if (iters > 1) {
+ for (0..iters) |_i| {
+ _ = _i;
+ std.mem.doNotOptimizeAway(pack("1"));
+ std.mem.doNotOptimizeAway(pack("123"));
+ std.mem.doNotOptimizeAway(pack("123456"));
+ }
+
+ ns = @floatFromInt(timer.lap());
+ secs = ns / 1_000_000_000;
+
+ std.debug.print("pack{}: {d:.3}s\t", .{ i, secs });
+
+ for (0..iters) |_i| {
+ _ = _i;
+ std.mem.doNotOptimizeAway(unpack(ss1));
+ std.mem.doNotOptimizeAway(unpack(ss2));
+ std.mem.doNotOptimizeAway(unpack(ss3));
+ }
+
+ ns = @floatFromInt(timer.lap());
+ secs = ns / 1_000_000_000;
+
+ std.debug.print("unpack{}: {d:.3}s\n", .{ i, secs });
+ }
+ }
+}
+
+test "char" {
+ const c1 = value.char.pack('\x00');
+ try std.testing.expect(value.char.check(c1));
+ try std.testing.expectEqual('\x00', value.char.unpack(c1));
+
+ const c2 = value.char.pack('😀');
+ try std.testing.expect(value.char.check(c2));
+ try std.testing.expectEqual('😀', value.char.unpack(c2));
+}
+
+test "misc" {
+ const f = value.boole.pack(false);
+ try std.testing.expect(value.boole.check(f));
+ try std.testing.expectEqual(false, value.boole.unpack(f));
+ try std.testing.expect(value.boole.unpack(value.boole.pred(f)));
+
+ const t = value.boole.pack(true);
+ try std.testing.expect(value.boole.check(t));
+ try std.testing.expectEqual(true, value.boole.unpack(t));
+ try std.testing.expect(value.boole.unpack(value.boole.pred(t)));
+
+ const nil = value.nil.get();
+ try std.testing.expect(value.nil.check(nil));
+ try std.testing.expect(value.boole.unpack(value.nil.pred(nil)));
+
+ const eof = value.eof.get();
+ try std.testing.expect(value.eof.check(eof));
+ try std.testing.expect(value.boole.unpack(value.eof.pred(eof)));
}
diff --git a/src/libzisp/value.zig b/src/libzisp/value.zig
index 62807be..da67af7 100644
--- a/src/libzisp/value.zig
+++ b/src/libzisp/value.zig
@@ -100,8 +100,13 @@
// into 6 bytes or less will be stored as an immediate value, not requiring any
// heap allocation or interning. (It's implicitly interned.)
//
-// There may still be uninterned strings on the heap that are just as short.
-// Calling intern on them will return the equivalent small string.
+// The null byte serves as a terminator and cannot appear in these strings; a
+// string that short but actually containing a null byte will need to be heap
+// allocated like other strings.
+//
+// There may also be uninterned strings on the heap that are also as short but
+// ended up on the heap due to being uninterned. Calling intern on them will
+// return the equivalent small string.
//
// Unicode code points need a maximum of 21 bits, yet we have 48 available.
// This may be exploited for a future extension.
@@ -110,10 +115,10 @@
// dozen singleton values (false, true, null, and so on). As such, this range
// of bit patterns may be subdivided further in the future.
//
-// And on top of all that we still have two 50-bit ranges left!
+// And on top of all that we still have a 48-bit and a 50-bit range left!
//
-// The forbidden value 4, Positive Infinity, is in one of the two undefined
-// value ranges.
+// The forbidden value 4, Positive Infinity, is in the 48-bit undefined value
+// range starting with the 000 tag.
//
// Here's the original article explaining the strategy:
@@ -133,8 +138,9 @@ pub const ptr = @import("value/ptr.zig");
pub const sstr = @import("value/sstr.zig");
pub const char = @import("value/char.zig");
-pub const misc = @import("value/misc.zig");
pub const boole = @import("value/boole.zig");
+pub const nil = @import("value/nil.zig");
+pub const eof = @import("value/eof.zig");
/// To fill up the u11 exponent part of a NaN.
const FILL = 0x7ff;
@@ -142,6 +148,7 @@ const FILL = 0x7ff;
/// Represents a Zisp value/object.
pub const Value = packed union {
double: f64,
+ bits: u64,
nan: packed struct {
rest: u51,
@@ -178,7 +185,7 @@ pub const Value = packed union {
sstr: packed struct {
// packed struct cannot contain array
value: u48,
- tag: Tag = .str,
+ tag: Tag = .sstr,
ptr: bool = false,
_: u11 = FILL,
fixnum: bool = false,
@@ -186,7 +193,7 @@ pub const Value = packed union {
char: packed struct {
value: u48,
- tag: u3 = 2,
+ tag: Tag = .char,
ptr: bool = false,
_: u11 = FILL,
fixnum: bool = false,
@@ -194,13 +201,13 @@ pub const Value = packed union {
misc: packed struct {
value: u48,
- tag: u3 = 3,
+ tag: Tag = .misc,
ptr: bool = false,
_: u11 = FILL,
fixnum: bool = false,
},
- const Tag = enum(u3) { str = 1, char = 2, misc = 3 };
+ const Tag = enum(u3) { sstr = 1, char = 2, misc = 3 };
const Self = @This();
@@ -209,13 +216,8 @@ pub const Value = packed union {
std.debug.dumpHex(std.mem.asBytes(&self));
}
- /// Checks for any IEEE 754 NaN.
- pub fn isNan(self: Self) bool {
- return self.nan.exp == FILL;
- }
-
/// Checks for a Zisp value (non-double) packed into a NaN.
pub fn isPacked(self: Self) bool {
- return self.isNan() and self.nan.rest != 0;
+ return self.nan.exp == FILL and self.nan.rest != 0;
}
};
diff --git a/src/libzisp/value/boole.zig b/src/libzisp/value/boole.zig
index d4fbd28..0af7e22 100644
--- a/src/libzisp/value/boole.zig
+++ b/src/libzisp/value/boole.zig
@@ -1,10 +1,34 @@
const Value = @import("../value.zig").Value;
const misc = @import("misc.zig");
-// These can be accessed from either namespace.
pub const f = misc.f;
pub const t = misc.t;
+// Zig API
+
+/// Checks if the value is a boole.
+pub fn check(v: Value) bool {
+ return v.bits == f.bits or v.bits == t.bits;
+}
+
+pub fn assert(v: Value) void {
+ if (!check(v)) {
+ v.dump();
+ @panic("not bool");
+ }
+}
+
pub fn pack(b: bool) Value {
- return if (b) f else t;
+ return if (b) t else f;
+}
+
+pub fn unpack(v: Value) bool {
+ assert(v);
+ return v.bits == t.bits;
+}
+
+// Zisp API
+
+pub fn pred(v: Value) Value {
+ return if (check(v)) t else f;
}
diff --git a/src/libzisp/value/char.zig b/src/libzisp/value/char.zig
index 7034128..6a38f0d 100644
--- a/src/libzisp/value/char.zig
+++ b/src/libzisp/value/char.zig
@@ -15,10 +15,10 @@ pub fn assert(v: Value) void {
}
pub fn pack(c: u21) Value {
- return .{ .char = .{c} };
+ return .{ .char = .{ .value = c } };
}
pub fn unpack(v: Value) u21 {
assert(v);
- return v.char.value;
+ return @truncate(v.char.value);
}
diff --git a/src/libzisp/value/eof.zig b/src/libzisp/value/eof.zig
new file mode 100644
index 0000000..34ab35d
--- /dev/null
+++ b/src/libzisp/value/eof.zig
@@ -0,0 +1,27 @@
+const Value = @import("../value.zig").Value;
+const misc = @import("misc.zig");
+
+pub const eof = misc.eof;
+
+// Zig API
+
+pub fn check(v: Value) bool {
+ return v.bits == eof.bits;
+}
+
+pub fn assert(v: Value) void {
+ if (!check(v)) {
+ v.dump();
+ @panic("not bool");
+ }
+}
+
+// Zisp API
+
+pub fn get() Value {
+ return eof;
+}
+
+pub fn pred(v: Value) Value {
+ return if (check(v)) misc.t else misc.f;
+}
diff --git a/src/libzisp/value/fixnum.zig b/src/libzisp/value/fixnum.zig
index 60b4239..6d26a9c 100644
--- a/src/libzisp/value/fixnum.zig
+++ b/src/libzisp/value/fixnum.zig
@@ -21,7 +21,7 @@ pub fn assert(v: Value) void {
const fixnum_min = std.math.minInt(i52) + 1;
const fixnum_max = std.math.maxInt(i52) - 1;
-fn isValidRange(int: i64) bool {
+pub fn isValidRange(int: i64) bool {
return fixnum_min < int and int < fixnum_max;
}
diff --git a/src/libzisp/value/misc.zig b/src/libzisp/value/misc.zig
index 2570644..793c60e 100644
--- a/src/libzisp/value/misc.zig
+++ b/src/libzisp/value/misc.zig
@@ -1,6 +1,6 @@
const Value = @import("../value.zig").Value;
-pub const f = Value{ .misc = .{0} };
-pub const t = Value{ .misc = .{1} };
-pub const nil = Value{ .misc = .{2} };
-pub const eof = Value{ .misc = .{3} };
+pub const f = Value{ .misc = .{ .value = 0 } };
+pub const t = Value{ .misc = .{ .value = 1 } };
+pub const nil = Value{ .misc = .{ .value = 2 } };
+pub const eof = Value{ .misc = .{ .value = 3 } };
diff --git a/src/libzisp/value/nil.zig b/src/libzisp/value/nil.zig
new file mode 100644
index 0000000..1b1a51e
--- /dev/null
+++ b/src/libzisp/value/nil.zig
@@ -0,0 +1,27 @@
+const Value = @import("../value.zig").Value;
+const misc = @import("misc.zig");
+
+pub const nil = misc.nil;
+
+// Zig API
+
+pub fn check(v: Value) bool {
+ return v.bits == nil.bits;
+}
+
+pub fn assert(v: Value) void {
+ if (!check(v)) {
+ v.dump();
+ @panic("not bool");
+ }
+}
+
+// Zisp API
+
+pub fn get() Value {
+ return nil;
+}
+
+pub fn pred(v: Value) Value {
+ return if (check(v)) misc.t else misc.f;
+}
diff --git a/src/libzisp/value/ptr.zig b/src/libzisp/value/ptr.zig
index 4bf92b6..6a3a6c4 100644
--- a/src/libzisp/value/ptr.zig
+++ b/src/libzisp/value/ptr.zig
@@ -88,15 +88,15 @@ pub fn packWeak(ptr: *anyopaque, tag: Tag) Value {
}
// Unpacks weak as well; no need for a separate fn.
-pub fn unpack(v: Value) struct { ptr: *anyopaque, tag: Tag } {
+pub fn unpack(v: Value) PtrAndTag {
assertZisp(v);
- return untagPtr(v.ptr.value.tagged);
+ return untagPtr(v.ptr.value);
}
// Weak pointers may be null.
pub fn isNull(v: Value) bool {
assertWeak(v);
- const ptr, _ = untagPtr(v.ptr.value.tagged);
+ const ptr, _ = untagPtr(v.ptr.value);
return @intFromPtr(ptr) == 0;
}
@@ -106,36 +106,38 @@ pub fn tagPtr(ptr: *anyopaque, tag: Tag) u49 {
return untagged << 1 | @intFromEnum(tag);
}
-pub fn untagPtr(tagged: 49) struct { ptr: *anyopaque, tag: Tag } {
+pub const PtrAndTag = struct { *anyopaque, Tag };
+
+pub fn untagPtr(tagged: u49) PtrAndTag {
const untagged: u49 = tagged >> 1 & 0xfffffffffff0;
const ptr: *anyopaque = @ptrFromInt(untagged);
const int: u4 = @truncate(tagged);
const tag: Tag = @enumFromInt(int);
- return .{ .ptr = ptr, .tag = tag };
+ return .{ ptr, tag };
}
pub const Tag = enum(u4) {
- /// 1. Strings / Symbols
+ /// 0. Strings / Symbols
string,
- /// 2. Bignums / Ratnums
+ /// 1. Bignums / Ratnums
number,
- /// 3. Pairs ([2]Value)
+ /// 2. Pairs ([2]Value)
pair,
- /// 4. Vector, bytevector, etc.
+ /// 3. Vector, bytevector, etc.
array,
- /// 5. Ordered hash table
+ /// 4. Ordered hash table
table,
- /// 6. String buffer
+ /// 5. String buffer
text,
- /// 7. Class, interface, etc.
+ /// 6. Class, interface, etc.
role,
- /// 8. Instance, basically
+ /// 7. Instance, basically
actor,
- /// 9. I/O Port
+ /// 8. I/O Port
port,
- /// 10. Procedure
+ /// 9. Procedure
proc,
- /// 11. Continuation
+ /// 10. Continuation
cont,
/// Other
other = 15,
diff --git a/src/libzisp/value/sstr.zig b/src/libzisp/value/sstr.zig
index 3c0755d..c2b2859 100644
--- a/src/libzisp/value/sstr.zig
+++ b/src/libzisp/value/sstr.zig
@@ -1,3 +1,65 @@
+const std = @import("std");
+
const Value = @import("../value.zig").Value;
-// stub
+// Zig API
+
+pub fn check(v: Value) bool {
+ return v.isPacked() and
+ !v.sstr.fixnum and
+ !v.sstr.ptr and
+ v.sstr.tag == .sstr;
+}
+
+pub fn assert(v: Value) void {
+ if (!check(v)) {
+ v.dump();
+ @panic("not sstr");
+ }
+}
+
+// For now, ignore encoding, just treat it as []u8.
+
+pub fn isValidSstr(s: []const u8) bool {
+ if (s.len > 6) {
+ return false;
+ }
+ for (s) |c| {
+ if (c == 0) {
+ return false;
+ }
+ }
+ return true;
+}
+
+fn assertValidSstr(s: []const u8) void {
+ if (!isValidSstr(s)) {
+ std.debug.print("invalid sstr: {s}", .{s});
+ @panic("invalid sstr");
+ }
+}
+
+// Different ways of doing the following have been tested, including manual
+// shifting and bit masking, but memcpy always wins easily according to our
+// micro-benchmarks, both under ReleaseSafe and under ReleaseFast.
+
+pub fn pack(s: []const u8) Value {
+ assertValidSstr(s);
+ var v = Value{ .sstr = .{ .value = 0 } };
+ const dest: [*]u8 = @ptrCast(&v.sstr.value);
+ @memcpy(dest, s);
+ return v;
+}
+
+// It's tempting to inline for here to eliminate the if statement or prevent
+// need of @truncate but all alternatives were a little slower.
+
+pub fn unpack(v: Value) struct { [6]u8, u3 } {
+ var s: [6]u8 = undefined;
+ const src: *const [6]u8 = @ptrCast(&v.sstr.value);
+ @memcpy(&s, src);
+ for (0..6) |i| {
+ if (s[i] == 0) return .{ s, @truncate(i) };
+ }
+ return .{ s, 6 };
+}