790 lines
22 KiB
Zig
790 lines
22 KiB
Zig
const std = @import("std");
|
|
const mem = std.mem;
|
|
const meta = std.meta;
|
|
const math = std.math;
|
|
const log = std.log.scoped(.system);
|
|
|
|
const bytecode = @import("bytecode.zig");
|
|
const Opcode = bytecode.Opcode;
|
|
const value = @import("value.zig");
|
|
const Value = value.Value;
|
|
const Vm = @import("vm.zig");
|
|
|
|
fn recordBytecodeSize(fields: []const value.TagId) usize {
|
|
var size: usize = 0;
|
|
|
|
size += 1; // Opcode.field
|
|
size += 1; // count: u8
|
|
size += 2 * fields.len; // tags: [count]u16
|
|
size += 1; // Opcode.return
|
|
|
|
return size;
|
|
}
|
|
|
|
fn recordBytecode(comptime fields: []const value.TagId) [recordBytecodeSize(fields)]u8 {
|
|
if (fields.len > 255) @compileError("too many fields");
|
|
|
|
var code = [_]u8{undefined} ** recordBytecodeSize(fields);
|
|
var cursor: usize = 0;
|
|
|
|
code[cursor] = @intFromEnum(Opcode.field);
|
|
cursor += 1;
|
|
code[cursor] = @as(u8, @truncate(fields.len));
|
|
cursor += 1;
|
|
|
|
for (fields) |field| {
|
|
const tag_id = mem.toBytes(field);
|
|
code[cursor] = tag_id[0];
|
|
code[cursor + 1] = tag_id[1];
|
|
cursor += 2;
|
|
}
|
|
|
|
code[cursor] = @intFromEnum(Opcode.ret);
|
|
cursor += 1;
|
|
|
|
return code;
|
|
}
|
|
|
|
const record_dotter_bytecode = recordBytecode(&.{ .From, .To, .Num });
|
|
pub const record_dotter: bytecode.Chunk = .{ .bytecode = &record_dotter_bytecode };
|
|
|
|
pub const Context = struct {
|
|
vm: *Vm,
|
|
allocator: mem.Allocator,
|
|
args: []Value,
|
|
|
|
pub fn arg(cx: *const Context, i: usize) ?Value {
|
|
if (i < cx.args.len) {
|
|
return cx.args[i];
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
};
|
|
|
|
pub const Fn = *const fn (Context) Vm.Error!Value;
|
|
pub const FnTable = [256]Fn;
|
|
|
|
fn invalid(cx: Context) !Value {
|
|
return cx.vm.throw("invalid system function", .{});
|
|
}
|
|
|
|
fn todo(cx: Context) !Value {
|
|
return cx.vm.throw("not yet implemented", .{});
|
|
}
|
|
|
|
fn typeError(vm: *Vm, val: Value, i: usize, expect: []const u8) Vm.Error {
|
|
return vm.throw("argument #{}: {s} expected, but got {s}", .{ i + 1, expect, val.typeName() });
|
|
}
|
|
|
|
// Strongly-typed type tags for Vec4 and Rgba, which are otherwise the same type.
|
|
// Used for differentiating between Vec4 and Rgba arguments.
|
|
const Vec4 = struct { value: value.Vec4 };
|
|
const Rgba = struct { value: value.Rgba };
|
|
|
|
fn fromArgument(cx: Context, comptime T: type, i: usize) Vm.Error!T {
|
|
switch (T) {
|
|
// Context variables
|
|
Context => return cx,
|
|
|
|
// No conversion
|
|
Value => return cx.args[i],
|
|
|
|
// Primitives
|
|
f32 => {
|
|
const val = cx.args[i];
|
|
if (val != .number) return typeError(cx.vm, val, i, "number");
|
|
return val.number;
|
|
},
|
|
Vec4 => {
|
|
const val = cx.args[i];
|
|
if (val != .vec4) return typeError(cx.vm, val, i, "vec4");
|
|
return .{ .value = val.vec4 };
|
|
},
|
|
Rgba => {
|
|
const val = cx.args[i];
|
|
if (val != .rgba) return typeError(cx.vm, val, i, "rgba");
|
|
return .{ .value = val.rgba };
|
|
},
|
|
|
|
// Refs
|
|
value.List => {
|
|
const val = cx.args[i];
|
|
if (val != .ref or val.ref.* != .list) return typeError(cx.vm, val, i, "list");
|
|
return val.ref.list;
|
|
},
|
|
value.Shape => {
|
|
const val = cx.args[i];
|
|
if (toShape(val)) |shape| {
|
|
return shape;
|
|
} else {
|
|
return typeError(cx.vm, val, i, "shape");
|
|
}
|
|
},
|
|
*const value.Closure => {
|
|
const val = cx.args[i];
|
|
if (val != .ref or val.ref.* != .closure) return typeError(cx.vm, val, i, "function");
|
|
return &val.ref.closure;
|
|
},
|
|
else => {},
|
|
}
|
|
@compileError(@typeName(T) ++ " is unsupported as an argument type");
|
|
}
|
|
|
|
fn intoReturn(cx: Context, any: anytype) Vm.Error!Value {
|
|
const T = @TypeOf(any);
|
|
return switch (T) {
|
|
Value => any,
|
|
bool => if (any) .true else .false,
|
|
f32 => .{ .number = any },
|
|
value.Ref => {
|
|
const ref = cx.allocator.create(value.Ref) catch return cx.vm.outOfMemory();
|
|
ref.* = any;
|
|
return .{ .ref = ref };
|
|
},
|
|
else => switch (@typeInfo(T)) {
|
|
.optional => if (any) |v| intoReturn(cx, v) else .nil,
|
|
.error_union => intoReturn(cx, try any),
|
|
else => @compileError(@typeName(T) ++ " is unsupported as a return type"),
|
|
},
|
|
};
|
|
}
|
|
|
|
/// Erase a well-typed function into a function that operates on raw values.
|
|
/// The erased function performs all the necessary conversions automatically.
|
|
///
|
|
/// Note that the argument order is important---function arguments go first, then context (such as
|
|
/// the VM or the allocator.) Otherwise argument indices will not match up.
|
|
fn erase(comptime name: []const u8, comptime func: anytype) Fn {
|
|
return Erased(name, func).call;
|
|
}
|
|
|
|
fn countParams(comptime func: anytype) usize {
|
|
var count: usize = 0;
|
|
inline for (@typeInfo(@TypeOf(func)).@"fn".params) |param| {
|
|
if (param.type != Context) count += 1;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
fn Erased(comptime name: []const u8, comptime func: anytype) type {
|
|
return struct {
|
|
fn call(cx: Context) Vm.Error!Value {
|
|
const param_count = countParams(func);
|
|
if (cx.args.len != param_count) {
|
|
return cx.vm.throw(name ++ " expects {} arguments, but it received {}", .{ param_count, cx.args.len });
|
|
}
|
|
|
|
const Args = meta.ArgsTuple(@TypeOf(func));
|
|
var args: Args = undefined;
|
|
inline for (meta.fields(Args), 0..) |field, i| {
|
|
@field(args, field.name) = try fromArgument(cx, field.type, i);
|
|
}
|
|
|
|
const result = @call(.auto, func, args);
|
|
return intoReturn(cx, result);
|
|
}
|
|
};
|
|
}
|
|
|
|
const SparseFn = struct { u8, Fn };
|
|
const SparseFnTable = []const SparseFn;
|
|
|
|
fn makeFnTable(init: SparseFnTable) FnTable {
|
|
var table = [_]Fn{invalid} ** @typeInfo(FnTable).array.len;
|
|
for (init) |entry| {
|
|
const index, const func = entry;
|
|
table[index] = func;
|
|
}
|
|
return table;
|
|
}
|
|
|
|
pub const fns = makeFnTable(&[_]SparseFn{
|
|
// NOTE: The indices here must match those defined in system.rs.
|
|
|
|
// Once the rest of the compiler is rewritten in Zig, it would be a good idea to rework this
|
|
// system _not_ to hardcode the indices here.
|
|
|
|
.{ 0x00, erase("+", add) },
|
|
.{ 0x01, erase("-", sub) },
|
|
.{ 0x02, erase("*", mul) },
|
|
.{ 0x03, erase("/", div) },
|
|
.{ 0x04, erase("unary -", neg) },
|
|
.{ 0x10, erase("floor", floor) },
|
|
.{ 0x11, erase("ceil", ceil) },
|
|
.{ 0x12, erase("round", round) },
|
|
.{ 0x13, erase("abs", abs) },
|
|
.{ 0x14, erase("mod", mod) },
|
|
.{ 0x15, erase("pow", pow) },
|
|
.{ 0x16, erase("sqrt", sqrt) },
|
|
.{ 0x17, erase("cbrt", cbrt) },
|
|
.{ 0x18, erase("exp", exp) },
|
|
.{ 0x19, erase("exp2", exp2) },
|
|
.{ 0x1a, erase("ln", ln) },
|
|
.{ 0x1b, erase("log2", log2) },
|
|
.{ 0x1c, erase("log10", log10) },
|
|
.{ 0x1d, erase("hypot", hypot) },
|
|
.{ 0x1e, erase("sin", sin) },
|
|
.{ 0x1f, erase("cos", cos) },
|
|
.{ 0x20, erase("tan", tan) },
|
|
.{ 0x21, erase("asin", asin) },
|
|
.{ 0x22, erase("acos", acos) },
|
|
.{ 0x23, erase("atan", atan) },
|
|
.{ 0x24, erase("atan2", atan2) },
|
|
.{ 0x25, erase("expMinus1", expMinus1) },
|
|
.{ 0x26, erase("ln1Plus", ln1Plus) },
|
|
.{ 0x27, erase("sinh", sinh) },
|
|
.{ 0x28, erase("cosh", cosh) },
|
|
.{ 0x29, erase("tanh", tanh) },
|
|
.{ 0x2a, erase("asinh", asinh) },
|
|
.{ 0x2b, erase("acosh", acosh) },
|
|
.{ 0x2c, erase("atanh", atanh) },
|
|
.{ 0x2d, erase("min", min) },
|
|
.{ 0x2e, erase("max", max) },
|
|
.{ 0x30, erase("lerp", lerp) },
|
|
.{ 0x40, erase("!", not) },
|
|
.{ 0x41, erase("==", Value.eql) },
|
|
.{ 0x42, erase("!=", notEql) },
|
|
.{ 0x43, erase("<", less) },
|
|
.{ 0x44, erase("<=", lessOrEqual) },
|
|
.{ 0x45, erase(">", greater) },
|
|
.{ 0x46, erase(">=", greaterOrEqual) },
|
|
.{ 0x80, vec },
|
|
.{ 0x81, erase("vecX", vecX) },
|
|
.{ 0x82, erase("vecY", vecY) },
|
|
.{ 0x83, erase("vecZ", vecZ) },
|
|
.{ 0x84, erase("vecW", vecW) },
|
|
.{ 0x85, rgba },
|
|
.{ 0x86, erase("rgbaR", rgbaR) },
|
|
.{ 0x87, erase("rgbaG", rgbaG) },
|
|
.{ 0x88, erase("rgbaB", rgbaB) },
|
|
.{ 0x89, erase("rgbaA", rgbaA) },
|
|
.{ 0x90, erase("len", listLen) },
|
|
.{ 0x91, erase("index", listIndex) },
|
|
.{ 0x92, erase("range", range) },
|
|
.{ 0x93, erase("map", map) },
|
|
.{ 0x94, erase("filter", filter) },
|
|
.{ 0x95, erase("reduce", reduce) },
|
|
.{ 0x96, erase("flatten", flatten) },
|
|
.{ 0xc0, erase("toShape", valueToShape) },
|
|
.{ 0xc1, erase("line", line) },
|
|
.{ 0xc2, erase("rect", rect) },
|
|
.{ 0xc3, erase("circle", circle) },
|
|
.{ 0xe0, erase("stroke", stroke) },
|
|
.{ 0xe1, erase("fill", fill) },
|
|
.{ 0xf0, erase("withDotter", withDotter) },
|
|
});
|
|
|
|
fn add(a: Value, b: Value, cx: Context) Vm.Error!Value {
|
|
if (meta.activeTag(a) != meta.activeTag(b)) {
|
|
return cx.vm.throw("arguments must be of the same type", .{});
|
|
}
|
|
|
|
return switch (a) {
|
|
.number => .{ .number = a.number + b.number },
|
|
.vec4 => .{ .vec4 = a.vec4 + b.vec4 },
|
|
.rgba => .{ .rgba = a.rgba + b.rgba },
|
|
else => cx.vm.throw("number, vec4, or rgba arguments expected, but got {s}", .{a.typeName()}),
|
|
};
|
|
}
|
|
|
|
fn sub(a: Value, b: Value, cx: Context) Vm.Error!Value {
|
|
if (meta.activeTag(a) != meta.activeTag(b)) {
|
|
return cx.vm.throw("arguments must be of the same type", .{});
|
|
}
|
|
|
|
return switch (a) {
|
|
.number => .{ .number = a.number - b.number },
|
|
.vec4 => .{ .vec4 = a.vec4 - b.vec4 },
|
|
.rgba => .{ .rgba = a.rgba - b.rgba },
|
|
else => cx.vm.throw("number, vec4, or rgba arguments expected, but got {s}", .{a.typeName()}),
|
|
};
|
|
}
|
|
|
|
fn mul(a: Value, b: Value, cx: Context) Vm.Error!Value {
|
|
if (meta.activeTag(a) != meta.activeTag(b)) {
|
|
return cx.vm.throw("arguments must be of the same type", .{});
|
|
}
|
|
|
|
return switch (a) {
|
|
.number => .{ .number = a.number * b.number },
|
|
.vec4 => .{ .vec4 = a.vec4 * b.vec4 },
|
|
.rgba => .{ .rgba = a.rgba * b.rgba },
|
|
else => cx.vm.throw("number, vec4, or rgba arguments expected, but got {s}", .{a.typeName()}),
|
|
};
|
|
}
|
|
|
|
fn div(a: Value, b: Value, cx: Context) Vm.Error!Value {
|
|
if (meta.activeTag(a) != meta.activeTag(b)) {
|
|
return cx.vm.throw("arguments must be of the same type", .{});
|
|
}
|
|
|
|
return switch (a) {
|
|
.number => .{ .number = a.number / b.number },
|
|
.vec4 => .{ .vec4 = a.vec4 / b.vec4 },
|
|
.rgba => .{ .rgba = a.rgba / b.rgba },
|
|
else => cx.vm.throw("number, vec4, or rgba arguments expected, but got {s}", .{a.typeName()}),
|
|
};
|
|
}
|
|
|
|
fn neg(a: Value, cx: Context) Vm.Error!Value {
|
|
return switch (a) {
|
|
.number => .{ .number = -a.number },
|
|
.vec4 => .{ .vec4 = -a.vec4 },
|
|
else => cx.vm.throw("number or vec4 argument expected, but got {s}", .{a.typeName()}),
|
|
};
|
|
}
|
|
|
|
fn floor(a: f32) f32 {
|
|
return @floor(a);
|
|
}
|
|
|
|
fn ceil(a: f32) f32 {
|
|
return @ceil(a);
|
|
}
|
|
|
|
fn round(a: f32) f32 {
|
|
return @round(a);
|
|
}
|
|
|
|
fn abs(a: f32) f32 {
|
|
return @abs(a);
|
|
}
|
|
|
|
fn mod(a: f32, b: f32) f32 {
|
|
return @mod(a, b);
|
|
}
|
|
|
|
fn pow(a: f32, b: f32) f32 {
|
|
return math.pow(f32, a, b);
|
|
}
|
|
|
|
fn sqrt(a: f32) f32 {
|
|
return @sqrt(a);
|
|
}
|
|
|
|
fn cbrt(a: f32) f32 {
|
|
return math.cbrt(a);
|
|
}
|
|
|
|
fn exp(a: f32) f32 {
|
|
return @exp(a);
|
|
}
|
|
|
|
fn exp2(a: f32) f32 {
|
|
return @exp2(a);
|
|
}
|
|
|
|
fn ln(a: f32) f32 {
|
|
return @log(a);
|
|
}
|
|
|
|
fn log2(a: f32) f32 {
|
|
return @log2(a);
|
|
}
|
|
|
|
fn log10(a: f32) f32 {
|
|
return @log10(a);
|
|
}
|
|
|
|
fn hypot(a: f32, b: f32) f32 {
|
|
return math.hypot(a, b);
|
|
}
|
|
|
|
fn sin(a: f32) f32 {
|
|
return @sin(a);
|
|
}
|
|
|
|
fn cos(a: f32) f32 {
|
|
return @cos(a);
|
|
}
|
|
|
|
fn tan(a: f32) f32 {
|
|
return @tan(a);
|
|
}
|
|
|
|
fn asin(a: f32) f32 {
|
|
return math.asin(a);
|
|
}
|
|
|
|
fn acos(a: f32) f32 {
|
|
return math.acos(a);
|
|
}
|
|
|
|
fn atan(a: f32) f32 {
|
|
return math.atan(a);
|
|
}
|
|
|
|
fn atan2(y: f32, x: f32) f32 {
|
|
return math.atan2(y, x);
|
|
}
|
|
|
|
fn expMinus1(a: f32) f32 {
|
|
return math.expm1(a);
|
|
}
|
|
|
|
fn ln1Plus(a: f32) f32 {
|
|
return math.log1p(a);
|
|
}
|
|
|
|
fn sinh(a: f32) f32 {
|
|
return math.sinh(a);
|
|
}
|
|
|
|
fn cosh(a: f32) f32 {
|
|
return math.cosh(a);
|
|
}
|
|
|
|
fn tanh(a: f32) f32 {
|
|
return math.tanh(a);
|
|
}
|
|
|
|
fn asinh(a: f32) f32 {
|
|
return math.asinh(a);
|
|
}
|
|
|
|
fn acosh(a: f32) f32 {
|
|
return math.acosh(a);
|
|
}
|
|
|
|
fn atanh(a: f32) f32 {
|
|
return math.atanh(a);
|
|
}
|
|
|
|
fn min(a: Value, b: Value, cx: Context) Vm.Error!Value {
|
|
if (meta.activeTag(a) != meta.activeTag(b)) {
|
|
return cx.vm.throw("arguments must be of the same type", .{});
|
|
}
|
|
|
|
return switch (a) {
|
|
.number => .{ .number = @min(a.number, b.number) },
|
|
.vec4 => .{ .vec4 = @min(a.vec4, b.vec4) },
|
|
.rgba => .{ .rgba = @min(a.rgba, b.rgba) },
|
|
else => if (a.lt(b).?) a else b,
|
|
};
|
|
}
|
|
|
|
fn max(a: Value, b: Value, cx: Context) Vm.Error!Value {
|
|
if (meta.activeTag(a) != meta.activeTag(b)) {
|
|
return cx.vm.throw("arguments must be of the same type", .{});
|
|
}
|
|
|
|
return switch (a) {
|
|
.number => .{ .number = @max(a.number, b.number) },
|
|
.vec4 => .{ .vec4 = @max(a.vec4, b.vec4) },
|
|
.rgba => .{ .rgba = @max(a.rgba, b.rgba) },
|
|
else => if (a.gt(b).?) a else b,
|
|
};
|
|
}
|
|
|
|
fn lerp(a: Value, b: Value, t: f32, cx: Context) Vm.Error!Value {
|
|
if (meta.activeTag(a) != meta.activeTag(b)) {
|
|
return cx.vm.throw("arguments must be of the same type", .{});
|
|
}
|
|
|
|
return switch (a) {
|
|
.number => .{ .number = math.lerp(a.number, b.number, t) },
|
|
.vec4 => .{ .vec4 = math.lerp(a.vec4, b.vec4, @as(value.Vec4, @splat(t))) },
|
|
.rgba => .{ .rgba = math.lerp(a.rgba, b.rgba, @as(value.Rgba, @splat(t))) },
|
|
else => cx.vm.throw("number, vec4, or rgba expected, but got {s}", .{a.typeName()}),
|
|
};
|
|
}
|
|
|
|
fn not(a: Value) bool {
|
|
return !a.isTruthy();
|
|
}
|
|
|
|
// Value.eql is used as-is
|
|
|
|
fn notEql(a: Value, b: Value) bool {
|
|
return !a.eql(b);
|
|
}
|
|
|
|
fn less(a: Value, b: Value, cx: Context) Vm.Error!bool {
|
|
return a.lt(b) orelse cx.vm.throw("{s} and {s} cannot be compared", .{ a.typeName(), b.typeName() });
|
|
}
|
|
|
|
fn greater(a: Value, b: Value, cx: Context) Vm.Error!bool {
|
|
return a.gt(b) orelse cx.vm.throw("{s} and {s} cannot be compared", .{ a.typeName(), b.typeName() });
|
|
}
|
|
|
|
fn lessOrEqual(a: Value, b: Value, cx: Context) Vm.Error!bool {
|
|
const isGreater = try greater(a, b, cx);
|
|
return !isGreater;
|
|
}
|
|
|
|
fn greaterOrEqual(a: Value, b: Value, cx: Context) Vm.Error!bool {
|
|
const isLess = try less(a, b, cx);
|
|
return !isLess;
|
|
}
|
|
|
|
fn vec(cx: Context) Vm.Error!Value {
|
|
if (cx.args.len > 4) return cx.vm.throw("vec expects 1 to 4 arguments, but it received {}", .{cx.args.len});
|
|
for (cx.args) |arg| {
|
|
if (arg != .number) return cx.vm.throw("number expected, but got {s}", .{arg.typeName()});
|
|
}
|
|
|
|
const zero: Value = .{ .number = 0 };
|
|
return .{ .vec4 = .{
|
|
(cx.arg(0) orelse zero).number,
|
|
(cx.arg(1) orelse zero).number,
|
|
(cx.arg(2) orelse zero).number,
|
|
(cx.arg(3) orelse zero).number,
|
|
} };
|
|
}
|
|
|
|
fn vecX(v: Vec4) f32 {
|
|
return v.value[0];
|
|
}
|
|
|
|
fn vecY(v: Vec4) f32 {
|
|
return v.value[1];
|
|
}
|
|
|
|
fn vecZ(v: Vec4) f32 {
|
|
return v.value[2];
|
|
}
|
|
|
|
fn vecW(v: Vec4) f32 {
|
|
return v.value[3];
|
|
}
|
|
|
|
fn rgba(cx: Context) Vm.Error!Value {
|
|
if (cx.args.len > 4) return cx.vm.throw("rgba expects 1 to 4 arguments, but it received {}", .{cx.args.len});
|
|
for (cx.args) |arg| {
|
|
if (arg != .number) return cx.vm.throw("number expected, but got {s}", .{arg.typeName()});
|
|
}
|
|
|
|
const zero: Value = .{ .number = 0 };
|
|
return .{ .rgba = .{
|
|
(cx.arg(0) orelse zero).number,
|
|
(cx.arg(1) orelse zero).number,
|
|
(cx.arg(2) orelse zero).number,
|
|
(cx.arg(3) orelse zero).number,
|
|
} };
|
|
}
|
|
|
|
fn rgbaR(v: Rgba) f32 {
|
|
return v.value[0];
|
|
}
|
|
|
|
fn rgbaG(v: Rgba) f32 {
|
|
return v.value[1];
|
|
}
|
|
|
|
fn rgbaB(v: Rgba) f32 {
|
|
return v.value[2];
|
|
}
|
|
|
|
fn rgbaA(v: Rgba) f32 {
|
|
return v.value[3];
|
|
}
|
|
|
|
fn listLen(list: value.List) f32 {
|
|
return @floatFromInt(list.len);
|
|
}
|
|
|
|
/// `index`
|
|
fn listIndex(list: value.List, index: f32, cx: Context) Vm.Error!Value {
|
|
const i: usize = @intFromFloat(index);
|
|
if (i >= list.len) return cx.vm.throw("list index out of bounds. length is {}, index is {}", .{ list.len, i });
|
|
return list[i];
|
|
}
|
|
|
|
fn range(fstart: f32, fend: f32, cx: Context) Vm.Error!value.Ref {
|
|
const vm = cx.vm;
|
|
const a = cx.allocator;
|
|
|
|
const start: u32 = @intFromFloat(fstart);
|
|
const end: u32 = @intFromFloat(fend);
|
|
|
|
// Careful here. We don't want someone to generate a list that's so long it DoSes the server.
|
|
// Therefore generating a list consumes fuel, in addition to bulk memory.
|
|
// The cost is still much cheaper than doing it manually.
|
|
const count = @max(start, end) - @min(start, end);
|
|
try vm.consumeFuel(&vm.fuel, count);
|
|
const list = a.alloc(Value, count) catch return vm.outOfMemory();
|
|
|
|
if (start < end) {
|
|
var i = start;
|
|
for (list) |*element| {
|
|
element.* = .{ .number = @floatFromInt(i) };
|
|
i += 1;
|
|
}
|
|
} else {
|
|
var i = end;
|
|
for (list) |*element| {
|
|
element.* = .{ .number = @floatFromInt(i) };
|
|
i -= 1;
|
|
}
|
|
}
|
|
|
|
return .{ .list = list };
|
|
}
|
|
|
|
fn map(list: value.List, f: *const value.Closure, cx: Context) Vm.Error!value.Ref {
|
|
if (f.param_count != 1) {
|
|
return cx.vm.throw("function passed to map must have a single parameter (\\x -> x), but it has {}", .{f.param_count});
|
|
}
|
|
|
|
const vm = cx.vm;
|
|
const a = cx.allocator;
|
|
|
|
const mapped_list = a.dupe(Value, list) catch return vm.outOfMemory();
|
|
for (list, mapped_list) |src, *dst| {
|
|
const bottom = vm.stack_top;
|
|
try vm.push(src);
|
|
try vm.run(a, f, bottom);
|
|
dst.* = try vm.pop();
|
|
}
|
|
|
|
return .{ .list = mapped_list };
|
|
}
|
|
|
|
fn filter(list: value.List, f: *const value.Closure, cx: Context) Vm.Error!value.Ref {
|
|
if (f.param_count != 1) {
|
|
return cx.vm.throw("function passed to filter must have a single parameter (\\x -> True), but it has {}", .{f.param_count});
|
|
}
|
|
|
|
const vm = cx.vm;
|
|
const a = cx.allocator;
|
|
|
|
// Implementing filter is a bit tricky to do without resizable arrays.
|
|
// There are a few paths one could take, but the simplest is to duplicate the list and truncate
|
|
// its length. This wastes a lot of memory, but it probably isn't going to matter in practice.
|
|
//
|
|
// Serves you right for wanting to waste cycles on generating a list and then filtering it down
|
|
// instead of just generating the list you want, I guess.
|
|
//
|
|
// In the future we could try allocating a bit map for the filter's results, but I'm not sure
|
|
// it's worth it.
|
|
const filtered_list = a.alloc(Value, list.len) catch return vm.outOfMemory();
|
|
var len: usize = 0;
|
|
for (list) |val| {
|
|
const bottom = vm.stack_top;
|
|
try vm.push(val);
|
|
try vm.run(a, f, bottom);
|
|
const condition = try vm.pop();
|
|
if (condition.isTruthy()) {
|
|
filtered_list[len] = val;
|
|
len += 1;
|
|
}
|
|
}
|
|
|
|
return .{ .list = filtered_list };
|
|
}
|
|
|
|
fn reduce(list: value.List, init: Value, f: *const value.Closure, cx: Context) Vm.Error!Value {
|
|
if (f.param_count != 2) {
|
|
return cx.vm.throw("function passed to reduce must have two parameters (\\acc, x -> acc), but it has {}", .{f.param_count});
|
|
}
|
|
|
|
const vm = cx.vm;
|
|
const a = cx.allocator;
|
|
|
|
var accumulator = init;
|
|
for (list) |val| {
|
|
const bottom = vm.stack_top;
|
|
try vm.push(accumulator);
|
|
try vm.push(val);
|
|
try vm.run(a, f, bottom);
|
|
accumulator = try vm.pop();
|
|
}
|
|
|
|
return accumulator;
|
|
}
|
|
|
|
fn flatten(list: value.List, cx: Context) Vm.Error!value.Ref {
|
|
var len: usize = 0;
|
|
for (list) |val| {
|
|
if (val == .ref and val.ref.* == .list) {
|
|
len += val.ref.list.len;
|
|
} else {
|
|
len += 1;
|
|
}
|
|
}
|
|
|
|
const vm = cx.vm;
|
|
const a = cx.allocator;
|
|
|
|
const flattened_list = a.alloc(Value, len) catch return vm.outOfMemory();
|
|
var i: usize = 0;
|
|
for (list) |val| {
|
|
if (val == .ref and val.ref.* == .list) {
|
|
@memcpy(flattened_list[i..][0..val.ref.list.len], val.ref.list);
|
|
i += val.ref.list.len;
|
|
} else {
|
|
flattened_list[i] = val;
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
return .{ .list = flattened_list };
|
|
}
|
|
|
|
fn toShape(val: value.Value) ?value.Shape {
|
|
return switch (val) {
|
|
.nil, .false, .true, .tag, .number, .rgba => null,
|
|
.vec4 => |v| .{ .point = value.vec2From4(v) },
|
|
.ref => |r| if (r.* == .shape) r.shape else null,
|
|
};
|
|
}
|
|
|
|
/// `toShape`
|
|
fn valueToShape(val: value.Value) ?value.Ref {
|
|
if (toShape(val)) |shape| {
|
|
return .{ .shape = shape };
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
fn line(start: Vec4, end: Vec4) value.Ref {
|
|
return .{ .shape = .{ .line = .{
|
|
.start = value.vec2From4(start.value),
|
|
.end = value.vec2From4(end.value),
|
|
} } };
|
|
}
|
|
|
|
fn rect(top_left: Vec4, size: Vec4) value.Ref {
|
|
return .{ .shape = .{ .rect = .{
|
|
.top_left = value.vec2From4(top_left.value),
|
|
.size = value.vec2From4(size.value),
|
|
} } };
|
|
}
|
|
|
|
fn circle(center: Vec4, radius: f32) value.Ref {
|
|
return .{ .shape = .{ .circle = .{
|
|
.center = value.vec2From4(center.value),
|
|
.radius = radius,
|
|
} } };
|
|
}
|
|
|
|
fn stroke(thickness: f32, color: Rgba, shape: value.Shape) value.Ref {
|
|
return .{ .scribble = .{
|
|
.shape = shape,
|
|
.action = .{ .stroke = .{
|
|
.thickness = thickness,
|
|
.color = color.value,
|
|
} },
|
|
} };
|
|
}
|
|
|
|
fn fill(color: Rgba, shape: value.Shape) value.Ref {
|
|
return .{ .scribble = .{
|
|
.shape = shape,
|
|
.action = .{ .fill = .{
|
|
.color = color.value,
|
|
} },
|
|
} };
|
|
}
|
|
|
|
fn withDotter(cont: *const value.Closure, cx: Context) Vm.Error!value.Ref {
|
|
log.debug("withDotter({})", .{cont});
|
|
if (cont.param_count != 1) {
|
|
return cx.vm.throw("function passed to withDotter must have a single parameter (\\d -> _), but it has {}", .{cont.param_count});
|
|
}
|
|
return .{ .reticle = .{ .dotter = .{
|
|
.draw = cont,
|
|
} } };
|
|
}
|