ink/src/Story/Object.zig
2026-03-30 05:44:35 -06:00

229 lines
6.7 KiB
Zig

//! Runtime Object, whose memory is managed through the virtual machine's
//! garbage collector.
//!
//! This structure is the base object type that is embedded into subtypes.
//! Each subtype has a `.create` method to allocate a new object.
const std = @import("std");
const Story = @import("../Story.zig");
const Object = @This();
tag: Tag,
is_marked: bool = false,
node: std.SinglyLinkedList.Node = .{},
pub const Tag = enum {
string,
code,
knot,
pub fn tagBytes(tag: Tag) []const u8 {
return switch (tag) {
.string => "String",
.code => "Code",
.knot => "Knot",
};
}
pub fn ObjectType(comptime tag: Tag) type {
return switch (tag) {
.string => Object.String,
.code => Object.Code,
.knot => Object.Knot,
};
}
};
pub fn eql(lhs: *Object, rhs: *Object) bool {
if (lhs.tag != rhs.tag) return false;
return switch (lhs.tag) {
.string => blk: {
const lhs_object: *Object.String = @ptrCast(lhs);
const rhs_object: *Object.String = @ptrCast(rhs);
break :blk std.mem.eql(u8, lhs_object.toSlice(), rhs_object.toSlice());
},
.code => |_| false,
.knot => |_| false,
};
}
pub fn destroy(obj: *Object, story: *Story) void {
inline for (comptime std.meta.fields(Tag)) |field| {
const tag = @field(Tag, field.name);
if (obj.tag == tag) {
const typed_obj: *Tag.ObjectType(tag) = @alignCast(@fieldParentPtr("base", obj));
typed_obj.destroy(story);
return;
}
}
unreachable;
}
pub const String = struct {
base: Object,
hash: u32, // TODO: Not implemented.
length: usize, // TODO: This could probably be u32.
bytes: [*]const u8,
const Type = Object.String;
pub const Options = struct {
bytes: []const u8,
};
pub fn create(story: *Story, options: Options) error{OutOfMemory}!*Object.String {
const gpa = story.allocator;
const alloc_len = @sizeOf(Type) + options.bytes.len + 1;
const raw = try gpa.alignedAlloc(u8, .of(Type), alloc_len);
const object: *Type = @ptrCast(raw);
object.* = .{
.base = .{ .tag = .string },
.hash = 0,
.length = options.bytes.len,
.bytes = undefined,
};
// Point bytes slice to the memory *after* the struct
const buf = raw[@sizeOf(Type)..][0 .. options.bytes.len + 1];
object.bytes = buf.ptr;
@memcpy(buf[0..options.bytes.len], options.bytes);
buf[options.bytes.len] = 0;
story.gc_objects.prepend(&object.base.node);
return object;
}
pub fn destroy(obj: *String, story: *Story) void {
const gpa = story.allocator;
const alloc_len = @sizeOf(Type) + obj.length + 1;
const base: [*]align(@alignOf(Type)) u8 = @ptrCast(obj);
gpa.free(base[0..alloc_len]);
}
pub fn toSlice(obj: *const Object.String) []const u8 {
return obj.bytes[0..obj.length];
}
pub fn fromValue(story: *Story, value: Story.Value) !*Object.String {
const print_buffer_len = 64;
var print_buffer: [print_buffer_len]u8 = undefined;
switch (value) {
.nil, .bool, .int, .float => {
const bytes = try std.fmt.bufPrint(&print_buffer, "{f}", .{value});
return .create(story, .{ .bytes = bytes });
},
.object => |object| switch (object.tag) {
.string => return @ptrCast(object),
else => return error.TypeError,
},
}
}
pub fn concat(story: *Story, lhs: *String, rhs: *String) !*Object.String {
const gpa = story.allocator;
const length = lhs.length + rhs.length;
const bytes = try gpa.alloc(u8, length + 1);
defer gpa.free(bytes);
@memcpy(bytes[0..lhs.length], lhs.bytes[0..lhs.length]);
@memcpy(bytes[lhs.length..], rhs.bytes[0..rhs.length]);
//ink_gc_disown(story, INK_OBJ(lhs));
//ink_gc_disown(story, INK_OBJ(rhs));
return .create(story, .{ .bytes = bytes });
}
};
/// Immutable object type for code chunks.
pub const Code = struct {
base: Object,
/// Number of arguments.
args_count: u32,
/// Number of local variables.
locals_count: u32,
/// Stack size required to load.
stack_size: u32,
/// Table of global constant indexes.
constants: []const u8,
/// Raw compiled bytecode.
bytecode: []const u8,
pub const Options = struct {
args_count: u32,
locals_count: u32,
stack_size: u32,
constants: []const u8,
code_bytes: []const u8,
};
const Type = Code;
pub fn create(story: *Story, options: Options) error{OutOfMemory}!*Object.Code {
const gpa = story.allocator;
const raw = try gpa.alignedAlloc(u8, .of(Type), @sizeOf(Type));
const obj: *Type = @ptrCast(raw);
obj.* = .{
.base = .{ .tag = .code },
.args_count = options.args_count,
.locals_count = options.locals_count,
.stack_size = options.stack_size,
.constants = options.constants,
.bytecode = options.code_bytes,
};
story.gc_objects.prepend(&obj.base.node);
return obj;
}
pub fn destroy(obj: *Code, story: *Story) void {
const gpa = story.allocator;
gpa.free(obj.constants);
gpa.free(obj.bytecode);
const base: [*]align(@alignOf(Type)) u8 = @ptrCast(obj);
gpa.free(base[0..@sizeOf(Type)]);
}
};
pub const Knot = struct {
base: Object,
/// Pointer to the name of the knot.
name: *Object.String,
/// Pointer to the code object for the knot.
code: *Object.Code,
members: std.StringHashMapUnmanaged(*Object) = .empty,
pub const Options = struct {
name: []const u8,
code: *Object.Code,
};
const Type = Knot;
pub fn create(story: *Story, options: Options) error{OutOfMemory}!*Object.Knot {
const gpa = story.allocator;
const raw = try gpa.alignedAlloc(u8, .of(Type), @sizeOf(Type));
const obj: *Type = @ptrCast(raw);
obj.* = .{
.base = .{ .tag = .knot },
.name = try .create(story, .{
.bytes = options.name,
}),
.code = options.code,
.members = .empty,
};
story.gc_objects.prepend(&obj.base.node);
return obj;
}
pub fn destroy(obj: *Knot, story: *Story) void {
const gpa = story.allocator;
obj.members.deinit(gpa);
const alloc_len = @sizeOf(Type);
const base: [*]align(@alignOf(Type)) u8 = @ptrCast(obj);
gpa.free(base[0..alloc_len]);
}
};