ona/source/ona/kym.zig
kayomn 66df9e3a1e
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Add support for subscript operations on objects
2023-08-12 15:10:46 +01:00

1446 lines
41 KiB
Zig

const Ast = @import("./kym/Ast.zig");
const coral = @import("coral");
const file = @import("./file.zig");
pub const Caller = coral.io.Generator(RuntimeError!?*RuntimeRef, *RuntimeEnv);
pub const ErrorHandler = coral.io.Generator(void, ErrorInfo);
pub const ErrorInfo = struct {
message: []const coral.io.Byte,
frames: []const Frame,
};
pub const Fixed = i32;
pub const Float = f64;
pub const Frame = struct {
name: []const coral.io.Byte,
arg_count: u8,
locals_top: usize,
};
pub const RuntimeEnv = struct {
interned_symbols: SymbolSet,
allocator: coral.io.Allocator,
error_handler: ErrorHandler,
system_bindings: RefTable,
locals: RefList,
frames: FrameStack,
const Chunk = struct {
env: *RuntimeEnv,
name: []coral.io.Byte,
opcodes: OpcodeList,
constants: ConstList,
const Constant = union (enum) {
fixed: Fixed,
float: Float,
string: []const coral.io.Byte,
symbol: []const coral.io.Byte,
};
const Opcode = union (enum) {
push_nil,
push_true,
push_false,
push_const: u16,
push_local: u8,
push_table: u32,
push_system: u16,
local_set: u8,
object_get,
object_set,
object_call: u8,
not,
neg,
add,
sub,
mul,
div,
eql,
cgt,
clt,
cge,
cle,
};
const OpcodeList = coral.list.Stack(Opcode);
const CompilationUnit = struct {
local_identifiers_buffer: [255][]const coral.io.Byte = [_][]const coral.io.Byte{""} ** 255,
local_identifiers_count: u8 = 0,
fn compile_expression(self: *CompilationUnit, chunk: *Chunk, expression: Ast.Expression) RuntimeError!void {
const number_format = coral.utf8.DecimalFormat{
.delimiter = "_",
.positive_prefix = .none,
};
switch (expression) {
.nil_literal => try chunk.opcodes.push_one(.push_nil),
.true_literal => try chunk.opcodes.push_one(.push_true),
.false_literal => try chunk.opcodes.push_one(.push_false),
.number_literal => |literal| {
for (literal) |codepoint| {
if (codepoint == '.') {
return chunk.opcodes.push_one(.{
.push_const = try chunk.declare_constant(.{
.float = number_format.parse(literal, Float) orelse unreachable,
}),
});
}
}
try chunk.opcodes.push_one(.{
.push_const = try chunk.declare_constant(.{
.fixed = number_format.parse(literal, Fixed) orelse unreachable,
}),
});
},
.string_literal => |literal| {
try chunk.opcodes.push_one(.{
.push_const = try chunk.declare_constant(.{.string = literal}),
});
},
.symbol_literal => |literal| {
try chunk.opcodes.push_one(.{
.push_const = try chunk.declare_constant(.{.symbol = literal}),
});
},
.table_literal => |fields| {
if (fields.values.len > coral.math.max_int(@typeInfo(u32).Int)) {
return error.OutOfMemory;
}
for (fields.values) |field| {
try self.compile_expression(chunk, field.value_expression);
try self.compile_expression(chunk, field.key_expression);
}
try chunk.opcodes.push_one(.{.push_table = @intCast(fields.values.len)});
},
.binary_operation => |operation| {
try self.compile_expression(chunk, operation.lhs_expression.*);
try self.compile_expression(chunk, operation.rhs_expression.*);
try chunk.opcodes.push_one(switch (operation.operator) {
.addition => .add,
.subtraction => .sub,
.multiplication => .mul,
.divsion => .div,
.greater_equals_comparison => .eql,
.greater_than_comparison => .cgt,
.equals_comparison => .cge,
.less_than_comparison => .clt,
.less_equals_comparison => .cle,
});
},
.unary_operation => |operation| {
try self.compile_expression(chunk, operation.expression.*);
try chunk.opcodes.push_one(switch (operation.operator) {
.boolean_negation => .not,
.numeric_negation => .neg,
});
},
.invoke => |invoke| {
if (invoke.argument_expressions.values.len > coral.math.max_int(@typeInfo(u8).Int)) {
return chunk.env.raise(error.BadSyntax, "lambdas may contain a maximum of 255 arguments");
}
for (invoke.argument_expressions.values) |argument_expression| {
try self.compile_expression(chunk, argument_expression);
}
try self.compile_expression(chunk, invoke.object_expression.*);
try chunk.opcodes.push_one(.{.object_call = @intCast(invoke.argument_expressions.values.len)});
},
.grouped_expression => |grouped_expression| {
try self.compile_expression(chunk, grouped_expression.*);
},
.get_system => |get_system| {
try chunk.opcodes.push_one(.{
.push_system = try chunk.declare_constant(.{.symbol = get_system}),
});
},
.local_get => |local_get| {
try chunk.opcodes.push_one(.{
.push_local = self.resolve_local(local_get) orelse {
return chunk.env.raise(error.OutOfMemory, "undefined local");
},
});
},
.local_set => |local_set| {
if (self.resolve_local(local_set)) |index| {
try chunk.opcodes.push_one(.{.local_set = index});
} else {
if (self.local_identifiers_count == self.local_identifiers_buffer.len) {
return chunk.env.raise(error.BadSyntax, "chunks may have a maximum of 255 locals");
}
self.local_identifiers_buffer[self.local_identifiers_count] = local_set;
self.local_identifiers_count += 1;
}
},
.field_get => |field_get| {
try self.compile_expression(chunk, field_get.object_expression.*);
try chunk.opcodes.push_one(.{
.push_const = try chunk.declare_constant(.{.symbol = field_get.identifier}),
});
try chunk.opcodes.push_one(.object_get);
},
.field_set => |field_set| {
try self.compile_expression(chunk, field_set.object_expression.*);
try chunk.opcodes.push_one(.{
.push_const = try chunk.declare_constant(.{.symbol = field_set.identifier}),
});
try self.compile_expression(chunk, field_set.value_expression.*);
try chunk.opcodes.push_one(.object_set);
},
.subscript_get => |subscript_get| {
try self.compile_expression(chunk, subscript_get.object_expression.*);
try self.compile_expression(chunk, subscript_get.subscript_expression.*);
try chunk.opcodes.push_one(.object_get);
},
.subscript_set => |subscript_set| {
try self.compile_expression(chunk, subscript_set.object_expression.*);
try self.compile_expression(chunk, subscript_set.subscript_expression.*);
try self.compile_expression(chunk, subscript_set.value_expression.*);
try chunk.opcodes.push_one(.object_set);
},
}
}
fn compile_statement(self: *CompilationUnit, chunk: *Chunk, statement: Ast.Statement) RuntimeError!void {
switch (statement) {
.@"return" => |@"return"| {
if (@"return".expression) |expression| {
try self.compile_expression(chunk, expression);
} else {
try chunk.opcodes.push_one(.push_nil);
}
},
.expression => |expression| try self.compile_expression(chunk, expression),
}
}
fn resolve_local(self: *CompilationUnit, local_identifier: []const coral.io.Byte) ?u8 {
if (self.local_identifiers_count == 0) {
return null;
}
var index = @as(u8, self.local_identifiers_count - 1);
while (true) : (index -= 1) {
if (coral.io.equals(local_identifier, self.local_identifiers_buffer[index])) {
return index;
}
if (index == 0) {
return null;
}
}
}
};
fn compile(self: *Chunk, ast: Ast) RuntimeError!void {
var unit = CompilationUnit{};
for (ast.list_statements()) |statement| {
try unit.compile_statement(self, statement);
}
}
fn declare_constant(self: *Chunk, constant: Constant) RuntimeError!u16 {
if (self.constants.values.len == coral.math.max_int(@typeInfo(u16).Int)) {
return self.env.raise(error.BadSyntax, "chunks cannot contain more than 65,535 constants");
}
const constant_index = self.constants.values.len;
try self.constants.push_one(try switch (constant) {
.fixed => |fixed| self.env.new_fixed(fixed),
.float => |float| self.env.new_float(float),
.string => |string| self.env.new_string(string),
.symbol => |symbol| self.env.new_symbol(symbol),
});
return @intCast(constant_index);
}
fn execute(self: *Chunk) RuntimeError!?*RuntimeRef {
try self.env.frames.push_one(.{
.arg_count = 0,
.locals_top = self.env.locals.values.len,
.name = self.name,
});
defer coral.debug.assert(self.env.frames.pop() != null);
for (self.opcodes.values) |opcode| {
switch (opcode) {
.push_nil => try self.env.locals.push_one(null),
.push_true => try self.env.locals.push_one(try self.env.new_boolean(true)),
.push_false => try self.env.locals.push_one(try self.env.new_boolean(false)),
.push_const => |push_const| {
if (push_const >= self.constants.values.len) {
return self.env.raise(error.IllegalState, "invalid const");
}
try self.env.locals.push_one(try self.env.acquire(self.constants.values[push_const]));
},
.push_local => |push_local| {
if (self.env.locals.values[push_local]) |local| {
try self.env.locals.push_one(try self.env.acquire(local));
} else {
try self.env.locals.push_one(null);
}
},
.push_table => |push_table| {
const table = try self.env.new_table();
errdefer self.env.discard(table);
{
const dynamic = table.object().payload.dynamic;
const userdata = dynamic.userdata();
var popped = @as(usize, 0);
while (popped < push_table) : (popped += 1) {
const index = try self.env.expect(try self.pop_local());
defer self.env.discard(index);
const maybe_value = try self.pop_local();
defer {
if (maybe_value) |value| {
self.env.discard(value);
}
}
try dynamic.typeinfo().set(.{
.userdata = userdata,
.env = self.env,
}, index, maybe_value);
}
}
try self.env.locals.push_one(table);
},
.push_system => |push_system| {
if (self.env.system_bindings.lookup(self.constants.values[push_system])) |syscallable| {
try self.env.locals.push_one(try self.env.acquire(syscallable));
} else {
try self.env.locals.push_one(null);
}
},
.local_set => |local_set| {
const local = &self.env.locals.values[local_set];
if (local.*) |previous_local| {
self.env.discard(previous_local);
}
local.* = try self.pop_local();
},
.object_get => {
const index = (try self.pop_local()) orelse {
return self.env.raise(error.TypeMismatch, "nil is not a valid index");
};
defer self.env.discard(index);
const indexable = (try self.pop_local()) orelse {
return self.env.raise(error.TypeMismatch, "nil is not a valid indexable");
};
defer self.env.discard(indexable);
const value = try self.env.get(indexable, index);
errdefer {
if (value) |ref| {
self.env.discard(ref);
}
}
try self.env.locals.push_one(value);
},
.object_set => {
const value = try self.pop_local();
defer {
if (value) |ref| {
self.env.discard(ref);
}
}
const index = try self.pop_local() orelse {
return self.env.raise(error.TypeMismatch, "nil is not a valid index");
};
defer self.env.discard(index);
const indexable = try self.pop_local() orelse {
return self.env.raise(error.TypeMismatch, "nil is not a valid indexable");
};
defer self.env.discard(indexable);
try self.env.set(indexable, index, value);
},
.object_call => |object_call| {
const result = call: {
const callable = try self.env.expect(try self.pop_local());
defer self.env.discard(callable);
try self.env.frames.push_one(.{
.name = "<lambda>",
.arg_count = object_call,
.locals_top = self.env.locals.values.len,
});
defer coral.debug.assert(self.env.frames.pop() != null);
break: call try switch (callable.object().payload) {
.dynamic => |dynamic| dynamic.typeinfo().call(.{
.userdata = dynamic.userdata(),
.env = self.env,
}),
else => self.env.raise(error.TypeMismatch, "object is not callable"),
};
};
for (0 .. object_call) |_| {
if (try self.pop_local()) |popped_arg| {
self.env.discard(popped_arg);
}
}
errdefer {
if (result) |ref| {
self.env.discard(ref);
}
}
try self.env.locals.push_one(result);
},
.not => {
if (try self.pop_local()) |value| {
defer self.env.discard(value);
try self.env.locals.push_one(try self.env.new_boolean(!value.is_truthy()));
} else {
try self.env.locals.push_one(try self.env.new_boolean(true));
}
},
.neg => {
const value = try self.env.expect(try self.pop_local());
defer self.env.discard(value);
try self.env.locals.push_one(try switch (value.object().payload) {
.fixed => |fixed| self.env.new_fixed(-fixed),
.float => |float| self.env.new_float(-float),
else => self.env.raise(error.TypeMismatch, "object is not negatable"),
});
},
.add => {
const rhs = try self.env.expect(try self.pop_local());
defer self.env.discard(rhs);
const lhs = try self.env.expect(try self.pop_local());
defer self.env.discard(lhs);
try self.env.locals.push_one(try switch (lhs.object().payload) {
.fixed => |lhs_fixed| switch (rhs.object().payload) {
.fixed => |rhs_fixed| add: {
if (coral.math.checked_add(lhs_fixed, rhs_fixed)) |fixed| {
break: add self.env.new_fixed(fixed);
}
break: add self.env.new_float(@as(Float,
@floatFromInt(lhs_fixed)) +
@as(Float, @floatFromInt(rhs_fixed)));
},
.float => |rhs_float| self.env.new_float(
@as(Float, @floatFromInt(lhs_fixed)) + rhs_float),
else => self.env.raise(error.TypeMismatch, "right-hand object is not addable"),
},
.float => |lhs_float| switch (rhs.object().payload) {
.float => |rhs_float| self.env.new_float(lhs_float + rhs_float),
.fixed => |rhs_fixed| self.env.new_float(
lhs_float + @as(Float, @floatFromInt(rhs_fixed))),
else => self.env.raise(error.TypeMismatch, "right-hand object is not addable"),
},
else => self.env.raise(error.TypeMismatch, "left-hand object is not addable"),
});
},
.sub => {
const rhs = try self.env.expect(try self.pop_local());
defer self.env.discard(rhs);
const lhs = try self.env.expect(try self.pop_local());
defer self.env.discard(lhs);
try self.env.locals.push_one(try switch (lhs.object().payload) {
.fixed => |lhs_fixed| switch (rhs.object().payload) {
.fixed => |rhs_fixed| sub: {
if (coral.math.checked_sub(lhs_fixed, rhs_fixed)) |fixed| {
break: sub self.env.new_fixed(fixed);
}
break: sub self.env.new_float(@as(Float,
@floatFromInt(lhs_fixed)) -
@as(Float, @floatFromInt(rhs_fixed)));
},
.float => |rhs_float| self.env.new_float(
@as(Float, @floatFromInt(lhs_fixed)) - rhs_float),
else => self.env.raise(error.TypeMismatch, "right-hand object is not subtractable"),
},
.float => |lhs_float| switch (rhs.object().payload) {
.float => |rhs_float| self.env.new_float(lhs_float - rhs_float),
.fixed => |rhs_fixed| self.env.new_float(
lhs_float - @as(Float, @floatFromInt(rhs_fixed))),
else => self.env.raise(error.TypeMismatch, "right-hand object is not subtractable"),
},
else => self.env.raise(error.TypeMismatch, "left-hand object is not subtractable"),
});
},
.mul => {
const rhs = try self.env.expect(try self.pop_local());
defer self.env.discard(rhs);
const lhs = try self.env.expect(try self.pop_local());
defer self.env.discard(lhs);
try self.env.locals.push_one(try switch (lhs.object().payload) {
.fixed => |lhs_fixed| switch (rhs.object().payload) {
.fixed => |rhs_fixed| mul: {
if (coral.math.checked_mul(lhs_fixed, rhs_fixed)) |fixed| {
break: mul self.env.new_fixed(fixed);
}
break: mul self.env.new_float(@as(Float,
@floatFromInt(lhs_fixed)) *
@as(Float, @floatFromInt(rhs_fixed)));
},
.float => |rhs_float| self.env.new_float(
@as(Float, @floatFromInt(lhs_fixed)) * rhs_float),
else => self.env.raise(error.TypeMismatch, "right-hand object is not multiplicable"),
},
.float => |lhs_float| switch (rhs.object().payload) {
.float => |rhs_float| self.env.new_float(lhs_float * rhs_float),
.fixed => |rhs_fixed| self.env.new_float(
lhs_float * @as(Float, @floatFromInt(rhs_fixed))),
else => self.env.raise(error.TypeMismatch, "right-hand object is not multiplicable"),
},
else => self.env.raise(error.TypeMismatch, "left-hand object is not multiplicable"),
});
},
.div => {
const rhs = try self.env.expect(try self.pop_local());
defer self.env.discard(rhs);
const lhs = try self.env.expect(try self.pop_local());
defer self.env.discard(lhs);
try self.env.locals.push_one(try switch (lhs.object().payload) {
.fixed => |lhs_fixed| switch (rhs.object().payload) {
.fixed => |rhs_fixed| self.env.new_float(@as(Float,
@floatFromInt(lhs_fixed)) /
@as(Float, @floatFromInt(rhs_fixed))),
.float => |rhs_float| self.env.new_float(
@as(Float, @floatFromInt(lhs_fixed)) / rhs_float),
else => self.env.raise(error.TypeMismatch, "right-hand object is not divisible"),
},
.float => |lhs_float| switch (rhs.object().payload) {
.float => |rhs_float| self.env.new_float(lhs_float / rhs_float),
.fixed => |rhs_fixed| self.env.new_float(
lhs_float / @as(Float, @floatFromInt(rhs_fixed))),
else => self.env.raise(error.TypeMismatch, "right-hand object is not divisible"),
},
else => self.env.raise(error.TypeMismatch, "left-hand object is not divisible"),
});
},
.eql => {
if (try self.pop_local()) |rhs| {
self.env.discard(rhs);
if (try self.pop_local()) |lhs| {
self.env.discard(lhs);
try self.env.locals.push_one(try self.env.new_boolean(lhs.equals(rhs)));
} else {
try self.env.locals.push_one(try self.env.new_boolean(false));
}
} else {
if (try self.pop_local()) |lhs| {
self.env.discard(lhs);
try self.env.locals.push_one(try self.env.new_boolean(false));
} else {
try self.env.locals.push_one(try self.env.new_boolean(true));
}
}
},
.cgt => {
const rhs = try self.env.expect(try self.pop_local());
defer self.env.discard(rhs);
const lhs = try self.env.expect(try self.pop_local());
defer self.env.discard(lhs);
try self.env.locals.push_one(try self.env.new_boolean(switch (lhs.object().payload) {
.fixed => |lhs_fixed| switch (rhs.object().payload) {
.fixed => |rhs_fixed| lhs_fixed > rhs_fixed,
.float => |rhs_float| @as(Float, @floatFromInt(lhs_fixed)) > rhs_float,
else => return self.env.raise(error.TypeMismatch, "right-hand object is not comparable"),
},
.float => |lhs_float| switch (rhs.object().payload) {
.float => |rhs_float| lhs_float > rhs_float,
.fixed => |rhs_fixed| lhs_float > @as(Float, @floatFromInt(rhs_fixed)),
else => return self.env.raise(error.TypeMismatch, "right-hand object is not comparable"),
},
else => return self.env.raise(error.TypeMismatch, "left-hand object is not comparable"),
}));
},
.clt => {
const rhs = try self.env.expect(try self.pop_local());
defer self.env.discard(rhs);
const lhs = try self.env.expect(try self.pop_local());
defer self.env.discard(lhs);
try self.env.locals.push_one(try self.env.new_boolean(switch (lhs.object().payload) {
.fixed => |lhs_fixed| switch (rhs.object().payload) {
.fixed => |rhs_fixed| lhs_fixed < rhs_fixed,
.float => |rhs_float| @as(Float, @floatFromInt(lhs_fixed)) < rhs_float,
else => return self.env.raise(error.TypeMismatch, "right-hand object is not comparable"),
},
.float => |lhs_float| switch (rhs.object().payload) {
.float => |rhs_float| lhs_float < rhs_float,
.fixed => |rhs_fixed| lhs_float < @as(Float, @floatFromInt(rhs_fixed)),
else => return self.env.raise(error.TypeMismatch, "right-hand object is not comparable"),
},
else => return self.env.raise(error.TypeMismatch, "left-hand object is not comparable"),
}));
},
.cge => {
const rhs = try self.env.expect(try self.pop_local());
defer self.env.discard(rhs);
const lhs = try self.env.expect(try self.pop_local());
defer self.env.discard(lhs);
try self.env.locals.push_one(try self.env.new_boolean(switch (lhs.object().payload) {
.fixed => |lhs_fixed| switch (rhs.object().payload) {
.fixed => |rhs_fixed| lhs_fixed >= rhs_fixed,
.float => |rhs_float| @as(Float, @floatFromInt(lhs_fixed)) >= rhs_float,
else => return self.env.raise(error.TypeMismatch, "right-hand object is not comparable"),
},
.float => |lhs_float| switch (rhs.object().payload) {
.float => |rhs_float| lhs_float >= rhs_float,
.fixed => |rhs_fixed| lhs_float >= @as(Float, @floatFromInt(rhs_fixed)),
else => return self.env.raise(error.TypeMismatch, "right-hand object is not comparable"),
},
else => return self.env.raise(error.TypeMismatch, "left-hand object is not comparable"),
}));
},
.cle => {
const rhs = try self.env.expect(try self.pop_local());
defer self.env.discard(rhs);
const lhs = try self.env.expect(try self.pop_local());
defer self.env.discard(lhs);
try self.env.locals.push_one(try self.env.new_boolean(switch (lhs.object().payload) {
.fixed => |lhs_fixed| switch (rhs.object().payload) {
.fixed => |rhs_fixed| lhs_fixed <= rhs_fixed,
.float => |rhs_float| @as(Float, @floatFromInt(lhs_fixed)) <= rhs_float,
else => return self.env.raise(error.TypeMismatch, "right-hand object is not comparable"),
},
.float => |lhs_float| switch (rhs.object().payload) {
.float => |rhs_float| lhs_float <= rhs_float,
.fixed => |rhs_fixed| lhs_float <= @as(Float, @floatFromInt(rhs_fixed)),
else => return self.env.raise(error.TypeMismatch, "right-hand object is not comparable"),
},
else => return self.env.raise(error.TypeMismatch, "left-hand object is not comparable"),
}));
},
}
}
return self.pop_local();
}
fn free(self: *Chunk) void {
while (self.constants.pop()) |constant| {
self.env.discard(constant);
}
self.constants.free();
self.opcodes.free();
self.env.allocator.deallocate(self.name);
}
fn make(env: *RuntimeEnv, name: []const coral.io.Byte) coral.io.AllocationError!Chunk {
return .{
.name = try coral.io.allocate_copy(env.allocator, name),
.opcodes = OpcodeList.make(env.allocator),
.constants = ConstList.make(env.allocator),
.env = env,
};
}
fn pop_local(self: *Chunk) RuntimeError!?*RuntimeRef {
return self.env.locals.pop() orelse self.env.raise(error.IllegalState, "stack underflow");
}
};
const ConstList = coral.list.Stack(*RuntimeRef);
const FrameStack = coral.list.Stack(Frame);
const RefList = coral.list.Stack(?*RuntimeRef);
const RefTable = coral.map.Table(*RuntimeRef, *RuntimeRef, struct {
pub const hash = RuntimeRef.hash;
pub const equals = RuntimeRef.equals;
});
const SymbolSet = coral.map.StringTable([:0]coral.io.Byte);
const Table = struct {
associative: RefTable,
contiguous: RefList,
fn typeinfo_destruct(method: Typeinfo.Method) void {
const table = @as(*Table, @ptrCast(@alignCast(method.userdata.ptr)));
{
var field_iterable = table.associative.as_iterable();
while (field_iterable.next()) |entry| {
method.env.discard(entry.key);
method.env.discard(entry.value);
}
}
table.associative.free();
while (table.contiguous.pop()) |value| {
if (value) |ref| {
method.env.discard(ref);
}
}
table.contiguous.free();
}
fn typeinfo_get(method: Typeinfo.Method, index: *const RuntimeRef) RuntimeError!?*RuntimeRef {
const table = @as(*Table, @ptrCast(@alignCast(method.userdata.ptr)));
const acquired_index = try method.env.acquire(index);
defer method.env.discard(acquired_index);
if (acquired_index.is_fixed()) |fixed| {
if (fixed < 0) {
// TODO: Negative indexing.
unreachable;
}
if (fixed < table.contiguous.values.len) {
return method.env.acquire(table.contiguous.values[@intCast(fixed)] orelse return null);
}
}
if (table.associative.lookup(acquired_index)) |value_ref| {
return method.env.acquire(value_ref);
}
return null;
}
fn typeinfo_set(method: Typeinfo.Method, index: *const RuntimeRef, value: ?*const RuntimeRef) RuntimeError!void {
const table = @as(*Table, @ptrCast(@alignCast(method.userdata.ptr)));
const acquired_index = try method.env.acquire(index);
errdefer method.env.discard(acquired_index);
if (acquired_index.is_fixed()) |fixed| {
if (fixed < 0) {
// TODO: Negative indexing.
unreachable;
}
if (fixed < table.contiguous.values.len) {
const maybe_replacing = &table.contiguous.values[@intCast(fixed)];
if (maybe_replacing.*) |replacing| {
method.env.discard(replacing);
}
maybe_replacing.* = if (value) |ref| try method.env.acquire(ref) else null;
return;
}
}
const acquired_value = try method.env.acquire(value orelse {
if (table.associative.remove(acquired_index)) |removed| {
method.env.discard(removed.key);
method.env.discard(removed.value);
}
return;
});
errdefer method.env.discard(acquired_value);
if (try table.associative.replace(acquired_index, acquired_value)) |replaced| {
method.env.discard(replaced.key);
method.env.discard(replaced.value);
}
}
};
pub fn acquire(self: *RuntimeEnv, value: *const RuntimeRef) RuntimeError!*RuntimeRef {
const object = value.object();
object.ref_count = coral.math.checked_add(object.ref_count, 1) orelse {
return self.raise(error.IllegalState, "reference overflow");
};
return @ptrCast(object);
}
pub fn arg(self: *RuntimeEnv, index: usize) RuntimeError!?*RuntimeRef {
const frame = self.frames.peek() orelse return self.raise(error.IllegalState, "stack underflow");
if (index < frame.arg_count) {
if (self.locals.values[frame.locals_top - (1 + index)]) |local| {
return self.acquire(local);
}
}
return null;
}
pub fn bind_system(self: *RuntimeEnv, name: []const coral.io.Byte, value: *const RuntimeRef) RuntimeError!void {
const name_symbol = try self.new_symbol(name);
errdefer self.discard(name_symbol);
const acquired_value = try self.acquire(value);
errdefer self.discard(acquired_value);
if (try self.system_bindings.replace(name_symbol, acquired_value)) |replaced| {
self.discard(replaced.key);
self.discard(replaced.value);
}
}
pub fn call(self: *RuntimeEnv, callable: *RuntimeRef, args: []const *RuntimeRef) RuntimeError!?*RuntimeRef {
try self.locals.push_all(args);
defer coral.io.assert(self.locals.drop(args.len));
try self.frames.push_one(.{
.name = "<native>",
.arg_count = args.len,
.locals_top = self.locals.values.len,
});
defer coral.io.assert(self.frames.pop() != null);
return switch (callable.object().payload) {
.dynamic => |dynamic| dynamic.typeinfo.call(.{
.userdata = dynamic.userdata(),
.env = self,
}),
else => self.raise(error.TypeMismatch, "object is not callable"),
};
}
pub fn discard(self: *RuntimeEnv, value: *RuntimeRef) void {
var object = value.object();
coral.debug.assert(object.ref_count != 0);
object.ref_count -= 1;
if (object.ref_count == 0) {
switch (object.payload) {
.false, .true, .float, .fixed, .symbol => {},
.string => |string| {
coral.debug.assert(string.len >= 0);
self.allocator.deallocate(string.ptr[0 .. @intCast(string.len)]);
},
.dynamic => |dynamic| {
if (dynamic.typeinfo().destruct) |destruct| {
destruct(.{
.userdata = dynamic.userdata(),
.env = self,
});
}
self.allocator.deallocate(dynamic.unpack());
},
}
self.allocator.deallocate(object);
}
}
pub fn execute_file(self: *RuntimeEnv, file_access: file.Access, file_path: file.Path) RuntimeError!?*RuntimeRef {
const file_data = (try file.allocate_and_load(self.allocator, file_access, file_path)) orelse {
return self.raise(error.BadOperation, "failed to open or read file specified");
};
defer self.allocator.deallocate(file_data);
const file_name = file_path.to_string() orelse "<script>";
var ast = Ast.make(self.allocator, file_name);
defer ast.free();
ast.parse(file_data) catch |parse_error| return switch (parse_error) {
error.BadSyntax => self.raise(error.BadSyntax, ast.error_message()),
error.OutOfMemory => error.OutOfMemory,
};
var chunk = try Chunk.make(self, file_name);
defer chunk.free();
try chunk.compile(ast);
return chunk.execute();
}
pub fn expect(self: *RuntimeEnv, value: ?*RuntimeRef) RuntimeError!*RuntimeRef {
return value orelse self.raise(error.TypeMismatch, "nil reference");
}
pub fn free(self: *RuntimeEnv) void {
while (self.locals.pop()) |local| {
if (local) |ref| {
self.discard(ref);
}
}
{
var iterable = self.interned_symbols.as_iterable();
while (iterable.next()) |entry| {
self.allocator.deallocate(entry.value);
}
}
{
var iterable = self.system_bindings.as_iterable();
while (iterable.next()) |entry| {
self.discard(entry.key);
self.discard(entry.value);
}
}
self.frames.free();
self.locals.free();
self.system_bindings.free();
self.interned_symbols.free();
}
pub fn get(self: *RuntimeEnv, indexable: *RuntimeRef, index: *const RuntimeRef) RuntimeError!?*RuntimeRef {
return switch (indexable.object().payload) {
.dynamic => |dynamic| dynamic.typeinfo().get(.{
.userdata = dynamic.userdata(),
.env = self,
}, index),
else => self.raise(error.TypeMismatch, "object is not get-indexable"),
};
}
pub fn make(allocator: coral.io.Allocator, error_handler: ErrorHandler) coral.io.AllocationError!RuntimeEnv {
return RuntimeEnv{
.locals = RefList.make(allocator),
.frames = FrameStack.make(allocator),
.system_bindings = RefTable.make(allocator, .{}),
.interned_symbols = SymbolSet.make(allocator, .{}),
.error_handler = error_handler,
.allocator = allocator,
};
}
pub fn new_boolean(self: *RuntimeEnv, value: bool) RuntimeError!*RuntimeRef {
return RuntimeRef.allocate(self.allocator, .{
.ref_count = 1,
.payload = if (value) .true else .false,
});
}
pub fn new_dynamic(
self: *RuntimeEnv,
userdata: [*]const coral.io.Byte,
typeinfo: *const Typeinfo,
) RuntimeError!*RuntimeRef {
const dynamic = try self.allocator.reallocate(null, @sizeOf(usize) + typeinfo.size);
errdefer self.allocator.deallocate(dynamic);
coral.io.copy(dynamic, coral.io.bytes_of(&typeinfo));
coral.io.copy(dynamic[@sizeOf(usize) ..], userdata[0 .. typeinfo.size]);
return RuntimeRef.allocate(self.allocator, .{
.ref_count = 1,
.payload = .{.dynamic = .{.ptr = dynamic.ptr}},
});
}
pub fn new_fixed(self: *RuntimeEnv, value: Fixed) RuntimeError!*RuntimeRef {
return RuntimeRef.allocate(self.allocator, .{
.ref_count = 1,
.payload = .{.fixed = value},
});
}
pub fn new_float(self: *RuntimeEnv, value: Float) RuntimeError!*RuntimeRef {
return RuntimeRef.allocate(self.allocator, .{
.ref_count = 1,
.payload = .{.float = value},
});
}
pub fn new_string(self: *RuntimeEnv, value: []const coral.io.Byte) RuntimeError!*RuntimeRef {
if (value.len > coral.math.max_int(@typeInfo(Fixed).Int)) {
return error.OutOfMemory;
}
const string = try coral.io.allocate_copy(self.allocator, value);
errdefer self.allocator.deallocate(string);
return RuntimeRef.allocate(self.allocator, .{
.ref_count = 1,
.payload = .{
.string = .{
.ptr = string.ptr,
.len = @intCast(string.len),
},
},
});
}
pub fn new_symbol(self: *RuntimeEnv, value: []const coral.io.Byte) RuntimeError!*RuntimeRef {
return RuntimeRef.allocate(self.allocator, .{
.ref_count = 1,
.payload = .{
.symbol = self.interned_symbols.lookup(value) orelse create: {
const symbol_string = try coral.io.allocate_string(self.allocator, value);
errdefer self.allocator.deallocate(symbol_string);
coral.debug.assert(try self.interned_symbols.insert(symbol_string[0 .. value.len], symbol_string));
break: create symbol_string;
},
},
});
}
pub fn new_table(self: *RuntimeEnv) RuntimeError!*RuntimeRef {
var table = Table{
.associative = RefTable.make(self.allocator, .{}),
.contiguous = RefList.make(self.allocator),
};
errdefer {
table.associative.free();
table.contiguous.free();
}
return try self.new_dynamic(coral.io.bytes_of(&table).ptr, &.{
.name = "table",
.size = @sizeOf(Table),
.destruct = Table.typeinfo_destruct,
.get = Table.typeinfo_get,
.set = Table.typeinfo_set,
});
}
pub fn raise(self: *RuntimeEnv, error_value: RuntimeError, message: []const coral.io.Byte) RuntimeError {
self.error_handler.invoke(.{
.message = message,
.frames = self.frames.values,
});
return error_value;
}
pub fn set(self: *RuntimeEnv, indexable: *RuntimeRef, index: *const RuntimeRef, value: ?*const RuntimeRef) RuntimeError!void {
return switch (indexable.object().payload) {
.dynamic => |dynamic| dynamic.typeinfo().set(.{
.userdata = dynamic.userdata(),
.env = self,
}, index, value),
else => self.raise(error.TypeMismatch, "object is not set-indexable"),
};
}
pub fn unbox_dynamic(env: *RuntimeEnv, value: *const RuntimeRef) RuntimeError![]coral.io.Byte {
return switch (value.object().payload) {
.dynamic => |dynamic| dynamic.userdata(),
else => env.raise(error.TypeMismatch, "expected fixed object")
};
}
pub fn unbox_float(env: *RuntimeEnv, value: *const RuntimeRef) RuntimeError!Float {
return switch (value.object().payload) {
.fixed => |fixed| @floatFromInt(fixed),
.float => |float| float,
else => env.raise(error.TypeMismatch, "expected float object")
};
}
pub fn unbox_fixed(env: *RuntimeEnv, value: *const RuntimeRef) RuntimeError!Fixed {
return value.is_fixed() orelse env.raise(error.TypeMismatch, "expected fixed object");
}
pub fn unbox_string(env: *RuntimeEnv, value: *const RuntimeRef) RuntimeError![]coral.io.Byte {
return switch (value.object().payload) {
.string => |string| extract: {
coral.debug.assert(string.len >= 0);
break: extract string.ptr[0 .. @intCast(string.len)];
},
else => env.raise(error.TypeMismatch, "expected string object")
};
}
pub fn unbox_symbol(env: *RuntimeEnv, value: *RuntimeRef) RuntimeError![*:0]const coral.io.Byte {
return switch (value.object().payload) {
.symbol => |symbol| symbol,
else => env.raise(error.TypeMismatch, "expected symbol object")
};
}
};
pub const RuntimeError = coral.io.AllocationError || error {
IllegalState,
TypeMismatch,
BadOperation,
BadSyntax,
};
pub const RuntimeRef = opaque {
const Object = struct {
ref_count: u16,
payload: union (enum) {
false,
true,
float: Float,
fixed: Fixed,
symbol: [*:0]const coral.io.Byte,
string: struct {
ptr: [*]coral.io.Byte,
len: Fixed,
const Self = @This();
fn unpack(self: Self) []coral.io.Byte {
coral.debug.assert(self.len >= 0);
return self.ptr[0 .. @intCast(self.len)];
}
},
dynamic: struct {
ptr: [*]coral.io.Byte,
const Self = @This();
fn typeinfo(self: Self) *const Typeinfo {
return @as(**const Typeinfo, @ptrCast(@alignCast(self.ptr))).*;
}
fn unpack(self: Self) []coral.io.Byte {
return self.ptr[0 .. (@sizeOf(usize) + self.typeinfo().size)];
}
fn userdata(self: Self) []coral.io.Byte {
const unpacked = self.unpack();
const address_size = @sizeOf(usize);
coral.debug.assert(unpacked.len >= address_size);
return unpacked[address_size ..];
}
},
},
};
fn allocate(allocator: coral.io.Allocator, data: Object) coral.io.AllocationError!*RuntimeRef {
return @ptrCast(try coral.io.allocate_one(allocator, data));
}
fn object(self: *const RuntimeRef) *Object {
return @constCast(@ptrCast(@alignCast(self)));
}
pub fn equals(self: *const RuntimeRef, other: *const RuntimeRef) bool {
return switch (self.object().payload) {
.false => other.object().payload == .false,
.true => other.object().payload == .true,
.fixed => |self_fixed| switch (other.object().payload) {
.fixed => |other_fixed| other_fixed == self_fixed,
.float => |other_float| other_float == @as(Float, @floatFromInt(self_fixed)),
else => false,
},
.float => |self_float| switch (other.object().payload) {
.float => |other_float| other_float == self_float,
.fixed => |other_fixed| @as(Float, @floatFromInt(other_fixed)) == self_float,
else => false,
},
.symbol => |self_symbol| switch (other.object().payload) {
.symbol => |other_symbol| self_symbol == other_symbol,
else => false,
},
.string => |self_string| switch (other.object().payload) {
.string => |other_string| coral.io.equals(self_string.unpack(), other_string.unpack()),
else => false,
},
.dynamic => |self_dynamic| switch (other.object().payload) {
.dynamic => |other_dynamic|
self_dynamic.typeinfo() == other_dynamic.typeinfo() and
self_dynamic.userdata().ptr == other_dynamic.userdata().ptr,
else => false,
},
};
}
pub fn hash(self: *const RuntimeRef) usize {
return switch (self.object().payload) {
.false => 1237,
.true => 1231,
.float => |float| @bitCast(float),
.fixed => |fixed| @intCast(@as(u32, @bitCast(fixed))),
.symbol => |symbol| @bitCast(@intFromPtr(symbol)),
.string => |string| coral.io.djb2_hash(@typeInfo(usize).Int, string.unpack()),
.dynamic => |dynamic| @intFromPtr(dynamic.typeinfo()) ^ @intFromPtr(dynamic.userdata().ptr),
};
}
pub fn is_fixed(self: *const RuntimeRef) ?Fixed {
return switch (self.object().payload) {
.fixed => |fixed| @intCast(@as(u32, @bitCast(fixed))),
else => null,
};
}
pub fn is_truthy(self: *const RuntimeRef) bool {
return switch (self.object().payload) {
.false => false,
.true => true,
.float => |float| float != 0,
.fixed => |fixed| fixed != 0,
.symbol => true,
.string => |string| string.len != 0,
.dynamic => true,
};
}
};
pub const Typeinfo = struct {
name: []const coral.io.Byte,
size: usize,
destruct: ?*const fn (method: Method) void = null,
call: *const fn (method: Method) RuntimeError!?*RuntimeRef = default_call,
get: *const fn (method: Method, index: *const RuntimeRef) RuntimeError!?*RuntimeRef = default_get,
set: *const fn (method: Method, value: *const RuntimeRef, value: ?*const RuntimeRef) RuntimeError!void = default_set,
pub const Method = struct {
env: *RuntimeEnv,
userdata: []coral.io.Byte,
};
fn default_call(method: Method) RuntimeError!?*RuntimeRef {
return method.env.raise(error.TypeMismatch, "object is not callable");
}
fn default_get(method: Method, _: *const RuntimeRef) RuntimeError!?*RuntimeRef {
return method.env.raise(error.TypeMismatch, "object is not index-gettable");
}
fn default_set(method: Method, _: *const RuntimeRef, _: ?*const RuntimeRef) RuntimeError!void {
return method.env.raise(error.TypeMismatch, "object is not index-settable");
}
};
pub fn bind_syscaller(env: *RuntimeEnv, name: []const coral.io.Byte, caller: Caller) RuntimeError!void {
const callable = try new_caller(env, caller);
defer env.discard(callable);
try env.bind_system(name, callable);
}
pub fn get_field(env: *RuntimeEnv, indexable: *RuntimeRef, field: []const coral.io.Byte) RuntimeError!?*RuntimeRef {
const field_symbol = try env.new_symbol(field);
defer env.discard(field_symbol);
return env.get(indexable, field_symbol);
}
pub fn get_index(env: *RuntimeEnv, indexable: *RuntimeRef, index: Fixed) RuntimeError!?*RuntimeRef {
const index_number = try env.new_fixed(index);
defer env.discard(index_number);
return env.get(indexable, index_number);
}
pub fn get_key(env: *RuntimeEnv, indexable: *RuntimeRef, key: []const coral.io.Byte) RuntimeError!?*RuntimeRef {
const key_string = try env.new_string(key);
defer env.discard(key_string);
return env.get(indexable, key_string);
}
pub fn new_caller(env: *RuntimeEnv, value: Caller) RuntimeError!*RuntimeRef {
const Callable = struct {
fn call(method: Typeinfo.Method) RuntimeError!?*RuntimeRef {
coral.debug.assert(method.userdata.len == @sizeOf(Caller));
return @as(*Caller, @ptrCast(@alignCast(method.userdata))).invoke(method.env);
}
};
return env.new_dynamic(coral.io.bytes_of(&value).ptr, &.{
.name = "<native>",
.size = @sizeOf(Caller),
.call = Callable.call,
});
}