diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5fa644b7..91b16ab8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,8 +1,9 @@ on: push: - branches: [main] + branches: [time-feature] pull_request: branches: [main] + workflow_dispatch: jobs: tests: diff --git a/src/GC.zig b/src/GC.zig index 2b4b1ee1..54009aa8 100644 --- a/src/GC.zig +++ b/src/GC.zig @@ -463,12 +463,6 @@ fn freeObj(self: *GC, obj: *o.Obj) (std.mem.Allocator.Error || std.fmt.BufPrintE }, .UpValue => { const obj_upvalue = o.ObjUpValue.cast(obj).?; - if (obj_upvalue.closed) |value| { - if (value.isObj()) { - try freeObj(self, value.obj()); - } - } - free(self, o.ObjUpValue, obj_upvalue); }, .Closure => { diff --git a/src/buzz_api.zig b/src/buzz_api.zig index 9b856686..08c894f3 100644 --- a/src/buzz_api.zig +++ b/src/buzz_api.zig @@ -775,6 +775,9 @@ export fn bz_newQualifiedObjectInstance(self: *VM, qualified_name: [*]const u8, @panic("Could not create error"); }; + // Keep the new instance rooted while cloning defaults may allocate. + self.push(instance.toValue()); + // Set instance fields with default values for (object.defaults, 0..) |default, idx| { if (default) |udefault| { @@ -786,7 +789,7 @@ export fn bz_newQualifiedObjectInstance(self: *VM, qualified_name: [*]const u8, } } - return instance.toValue(); + return self.pop(); } fn instanciateError( @@ -890,6 +893,9 @@ export fn bz_newObjectInstance(vm: *VM, object_value: v.Value, typedef_value: v. ) catch @panic("Out of memory"), ) catch @panic("Could not instanciate object"); + // Keep the new instance rooted while cloning defaults may allocate. + vm.push(instance.toValue()); + // If not anonymous, set default fields if (object) |uobject| { for (uobject.defaults, 0..) |default, idx| { @@ -903,7 +909,7 @@ export fn bz_newObjectInstance(vm: *VM, object_value: v.Value, typedef_value: v. } } - return instance.toValue(); + return vm.pop(); } export fn bz_getObjectField(object_value: v.Value, field_idx: usize) callconv(.c) v.Value { diff --git a/src/lib/buzz_time.zig b/src/lib/buzz_time.zig new file mode 100644 index 00000000..8497af90 --- /dev/null +++ b/src/lib/buzz_time.zig @@ -0,0 +1,32 @@ +const api = @import("buzz_api.zig"); +const std = @import("std"); + +pub export fn timeNow(ctx: *api.NativeCtx) callconv(.c) c_int { + const ts = std.Io.Timestamp.now(ctx.getIo(), .real); + const ms = ts.toMilliseconds(); + ctx.vm.bz_push(.fromDouble(@floatFromInt(ms))); + return 1; +} + +pub export fn timeMonotonic(ctx: *api.NativeCtx) callconv(.c) c_int { + const ts = std.Io.Timestamp.now(ctx.getIo(), .awake); + const ms = ts.toMilliseconds(); + ctx.vm.bz_push(.fromDouble(@floatFromInt(ms))); + return 1; +} + +pub export fn timeCpu(ctx: *api.NativeCtx) callconv(.c) c_int { + const ts = std.Io.Timestamp.now(ctx.getIo(), .cpu_process); + const ms = ts.toMilliseconds(); + ctx.vm.bz_push(.fromDouble(@floatFromInt(ms))); + return 1; +} + +pub const library = api.BuzzApi( + "time", + &.{ + &.{ "now", timeNow }, + &.{ "monotonic", timeMonotonic }, + &.{ "cpu", timeCpu }, + }, +){}; diff --git a/src/lib/static_headers.zig b/src/lib/static_headers.zig index 4ece9fa0..905bba89 100644 --- a/src/lib/static_headers.zig +++ b/src/lib/static_headers.zig @@ -22,6 +22,7 @@ pub const math = Header{ .name = "math", .path = "math.buzz" }; pub const os = Header{ .name = "os", .path = "os.buzz" }; pub const serialize = Header{ .name = "serialize", .path = "serialize.buzz" }; pub const std = Header{ .name = "std", .path = "std.buzz" }; +pub const time = Header{ .name = "time", .path = "time.buzz" }; pub const testing = Header{ .name = "test", .path = "testing.buzz" }; /// Buzz headers bundled with the compiler/runtime and installed for tooling. @@ -41,4 +42,5 @@ pub const all = [_]Header{ serialize, std, testing, + time, }; diff --git a/src/lib/static_libraries.zig b/src/lib/static_libraries.zig index 6463a3c5..d64f59bb 100644 --- a/src/lib/static_libraries.zig +++ b/src/lib/static_libraries.zig @@ -30,6 +30,7 @@ pub const all = [_]Library{ .{ .header = static_headers.os, .zig_path = "buzz_os.zig", .wasm_native = false }, .{ .header = static_headers.serialize, .zig_path = "buzz_serialize.zig", .wasm_native = true }, .{ .header = static_headers.std, .zig_path = "buzz_std.zig", .wasm_native = true }, + .{ .header = static_headers.time, .zig_path = "buzz_time.zig", .wasm_native = false }, .{ .header = static_headers.testing, .zig_path = null, .wasm_native = false }, }; @@ -98,6 +99,7 @@ fn nativeMethods(comptime library: Library) std.StaticStringMap(buzz_api.NativeF if (std.mem.eql(u8, zig_path, "buzz_os.zig")) return checkedNativeMethods(library, @import("buzz_os.zig").library); if (std.mem.eql(u8, zig_path, "buzz_serialize.zig")) return checkedNativeMethods(library, @import("buzz_serialize.zig").library); if (std.mem.eql(u8, zig_path, "buzz_std.zig")) return checkedNativeMethods(library, @import("buzz_std.zig").library); + if (std.mem.eql(u8, zig_path, "buzz_time.zig")) return checkedNativeMethods(library, @import("buzz_time.zig").library); @compileError("unknown native library path: " ++ zig_path); } diff --git a/src/lib/time.buzz b/src/lib/time.buzz new file mode 100644 index 00000000..15d7f98f --- /dev/null +++ b/src/lib/time.buzz @@ -0,0 +1,12 @@ +namespace time; + +import "buzz:errors"; + +/// Real-time (wall-clock) in milliseconds since Unix epoch +export extern fun now() > double; + +/// Monotonic clock in milliseconds (never goes backwards for intervals) +export extern fun monotonic() > double; + +/// Cpu time used by the current process in milliseconds +export extern fun cpu() > double !> errors\UnexpectedError; diff --git a/src/obj.zig b/src/obj.zig index 4a216b0d..c453956c 100644 --- a/src/obj.zig +++ b/src/obj.zig @@ -1571,16 +1571,20 @@ pub const ObjObjectInstance = struct { type_def: *ObjTypeDef, gc: *GC, ) !Self { + const fields = try gc.allocateMany( + Value, + type_def.resolved_type.?.ObjectInstance.of + .resolved_type.?.Object + .propertiesCount(), + ); + + @memset(fields, Value.Null); + return .{ .vm = vm, .object = object, .type_def = type_def, - .fields = try gc.allocateMany( - Value, - type_def.resolved_type.?.ObjectInstance.of - .resolved_type.?.Object - .propertiesCount(), - ), + .fields = fields, }; } @@ -1759,15 +1763,23 @@ pub const ObjObject = struct { property_count: ?usize = 0, pub fn init(allocator: Allocator, type_def: *ObjTypeDef) !Self { + const fields = try allocator.alloc( + Value, + type_def.resolved_type.?.Object.fields.count(), + ); + errdefer allocator.free(fields); + + const defaults = try allocator.alloc( + ?Value, + type_def.resolved_type.?.Object.propertiesCount(), + ); + + @memset(fields, Value.Void); + @memset(defaults, null); + const self = Self{ - .fields = try allocator.alloc( - Value, - type_def.resolved_type.?.Object.fields.count(), - ), - .defaults = try allocator.alloc( - ?Value, - type_def.resolved_type.?.Object.propertiesCount(), - ), + .fields = fields, + .defaults = defaults, .type_def = type_def, }; @@ -5925,24 +5937,38 @@ pub fn cloneObject(obj: *Obj, vm: *VM) !Value { .List => { const list = ObjList.cast(obj).?; + var items = try list.items.clone(vm.gc.allocator); + errdefer items.deinit(vm.gc.allocator); + + const methods = try vm.gc.allocator.alloc(?*ObjNative, list.methods.len); + errdefer vm.gc.allocator.free(methods); + + @memcpy(methods, list.methods); return (try vm.gc.allocateObject( ObjList{ .type_def = list.type_def, - .items = try list.items.clone(vm.gc.allocator), - .methods = list.methods, + .items = items, + .methods = methods, }, )).toValue(); }, .Map => { const map = ObjMap.cast(obj).?; + var cloned_map = try map.map.clone(vm.gc.allocator); + errdefer cloned_map.deinit(vm.gc.allocator); + + const methods = try vm.gc.allocator.alloc(?*ObjNative, map.methods.len); + errdefer vm.gc.allocator.free(methods); + + @memcpy(methods, map.methods); return (try vm.gc.allocateObject( ObjMap{ .type_def = map.type_def, - .map = try map.map.clone(vm.gc.allocator), - .methods = map.methods, + .map = cloned_map, + .methods = methods, }, )).toValue(); }, diff --git a/src/vm.zig b/src/vm.zig index 23bf3a2b..451fc45f 100644 --- a/src/vm.zig +++ b/src/vm.zig @@ -3084,6 +3084,9 @@ pub const VM = struct { unreachable; }; + // Keep the new instance rooted while cloning defaults may allocate. + self.push(obj_instance.toValue()); + // If not anonymous, set default fields if (object) |uobject| { // Set instance fields with default values @@ -3104,8 +3107,6 @@ pub const VM = struct { } } - self.push(obj_instance.toValue()); - const frame = self.currentFrame().?; const next_full_instruction = self.readInstruction(frame); @call( diff --git a/tests/behavior/c-buzz-api.buzz b/tests/behavior/c-buzz-api.buzz index 28d23891..f6e11987 100644 --- a/tests/behavior/c-buzz-api.buzz +++ b/tests/behavior/c-buzz-api.buzz @@ -1,6 +1,43 @@ import "buzz:std"; import "../utils/buzz_c_api"; +object ApiDefaultClone { + tokens: [int] = [ + 1, 2, 3, 4, 5, 6, 7, 8, + 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, + 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, + 49, 50, 51, 52, 53, 54, 55, 56, + 57, 58, 59, 60, 61, 62, 63, 64, + 65, 66, 67, 68, 69, 70, 71, 72, + 73, 74, 75, 76, 77, 78, 79, 80, + 81, 82, 83, 84, 85, 86, 87, 88, + 89, 90, 91, 92, 93, 94, 95, 96, + 97, 98, 99, 100, 101, 102, 103, 104, + 105, 106, 107, 108, 109, 110, 111, 112, + 113, 114, 115, 116, 117, 118, 119, 120, + 121, 122, 123, 124, 125, 126, 127, 128, + 129, 130, 131, 132, 133, 134, 135, 136, + 137, 138, 139, 140, 141, 142, 143, 144, + 145, 146, 147, 148, 149, 150, 151, 152, + 153, 154, 155, 156, 157, 158, 159, 160, + 161, 162, 163, 164, 165, 166, 167, 168, + 169, 170, 171, 172, 173, 174, 175, 176, + 177, 178, 179, 180, 181, 182, 183, 184, + 185, 186, 187, 188, 189, 190, 191, 192, + 193, 194, 195, 196, 197, 198, 199, 200, + 201, 202, 203, 204, 205, 206, 207, 208, + 209, 210, 211, 212, 213, 214, 215, 216, + 217, 218, 219, 220, 221, 222, 223, 224, + 225, 226, 227, 228, 229, 230, 231, 232, + 233, 234, 235, 236, 237, 238, 239, 240, + 241, 242, 243, 244, 245, 246, 247, 248, + 249, 250, 251, 252, 253, 254, 255, 256, + ], +} + test "C buzz api library" { std\assert( buzz_c_api\add(40, rhs: 2) == 42, @@ -22,4 +59,11 @@ test "C buzz api library" { buzz_c_api\argCount(1, b: 2, c: 3) == 3, message: "C library can read NativeCtx arg_count" ); + std\assert( + buzz_c_api\instantiateDefaultedObject( + ApiDefaultClone, + typedef_value: typeof ApiDefaultClone{} + ) == 5120000, + message: "C library can instantiate objects with cloneable defaults under GC pressure" + ); } diff --git a/tests/behavior/objects.buzz b/tests/behavior/objects.buzz index dc8df491..250aee44 100644 --- a/tests/behavior/objects.buzz +++ b/tests/behavior/objects.buzz @@ -41,3 +41,52 @@ test "Object with static fields" { std\assert(second.id == Second.nextId, message: "could use static fields"); } + +object CloneDefault { + tokens: [int] = [ + 1, 2, 3, 4, 5, 6, 7, 8, + 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, + 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, + 49, 50, 51, 52, 53, 54, 55, 56, + 57, 58, 59, 60, 61, 62, 63, 64, + 65, 66, 67, 68, 69, 70, 71, 72, + 73, 74, 75, 76, 77, 78, 79, 80, + 81, 82, 83, 84, 85, 86, 87, 88, + 89, 90, 91, 92, 93, 94, 95, 96, + 97, 98, 99, 100, 101, 102, 103, 104, + 105, 106, 107, 108, 109, 110, 111, 112, + 113, 114, 115, 116, 117, 118, 119, 120, + 121, 122, 123, 124, 125, 126, 127, 128, + 129, 130, 131, 132, 133, 134, 135, 136, + 137, 138, 139, 140, 141, 142, 143, 144, + 145, 146, 147, 148, 149, 150, 151, 152, + 153, 154, 155, 156, 157, 158, 159, 160, + 161, 162, 163, 164, 165, 166, 167, 168, + 169, 170, 171, 172, 173, 174, 175, 176, + 177, 178, 179, 180, 181, 182, 183, 184, + 185, 186, 187, 188, 189, 190, 191, 192, + 193, 194, 195, 196, 197, 198, 199, 200, + 201, 202, 203, 204, 205, 206, 207, 208, + 209, 210, 211, 212, 213, 214, 215, 216, + 217, 218, 219, 220, 221, 222, 223, 224, + 225, 226, 227, 228, 229, 230, 231, 232, + 233, 234, 235, 236, 237, 238, 239, 240, + 241, 242, 243, 244, 245, 246, 247, 248, + 249, 250, 251, 252, 253, 254, 255, 256, + ], +} + +test "Objects keep cloneable defaults alive during instantiation" { + var i = 0; + var total = 0; + + while (i < 20000) { + total = total + CloneDefault{}.tokens[0]; + i = i + 1; + } + + std\assert(total == 20000, message: "object instances survive GC while cloning defaults"); +} diff --git a/tests/behavior/time.buzz b/tests/behavior/time.buzz new file mode 100644 index 00000000..a62c3b60 --- /dev/null +++ b/tests/behavior/time.buzz @@ -0,0 +1,31 @@ +import "buzz:std"; +import "buzz:time"; + +test "now returns a positive number" { + final now = time\now(); + std\assert( + now > 0, + message: "now should be greater than 0" + ); +} + +test "monotonic increases" { + final start = time\monotonic(); + os\sleep(10.0); + final end = time\monotonic(); + std\assert( + end > start, + message: "monotonic should increase after sleep" + ); +} + +test "cpu returns a positive number" { + final cpu = time\cpu() catch { + // CPU clock might not be available on all platforms + return; + }; + std\assert( + cpu >= 0, + message: "cpu time should be >= 0" + ); +} diff --git a/tests/behavior/upvalues.buzz b/tests/behavior/upvalues.buzz index aed8bcaf..4cd49700 100644 --- a/tests/behavior/upvalues.buzz +++ b/tests/behavior/upvalues.buzz @@ -1,4 +1,10 @@ import "buzz:std"; +import "buzz:gc"; + +enum CapturedEnum { + one, + two, +} fun upvals() > fun () { final upvalue = 12; @@ -10,3 +16,17 @@ fun upvals() > fun () { test "Upvalues" { upvals()(); } + +fun captureEnumCase() > fun () > CapturedEnum { + final captured = CapturedEnum.two; + + return fun () > CapturedEnum => captured; +} + +test "Closed upvalues can capture enum cases through GC" { + _ = captureEnumCase(); + + gc\collect(); + + std\assert(true, message: "collecting discarded closures with enum captures does not corrupt GC"); +} diff --git a/tests/utils/buzz_c_api.buzz b/tests/utils/buzz_c_api.buzz index c8133c56..12d8ccda 100644 --- a/tests/utils/buzz_c_api.buzz +++ b/tests/utils/buzz_c_api.buzz @@ -5,3 +5,5 @@ export extern fun add(lhs: int, rhs: int) > int; export extern fun isNumber(value: any) > bool; export extern fun argCount(a: int, b: int, c: int) > int; + +export extern fun instantiateDefaultedObject(object_value: any, typedef_value: type) > int; diff --git a/tests/utils/buzz_c_api.c b/tests/utils/buzz_c_api.c index 388b8a4c..3b6d55ca 100644 --- a/tests/utils/buzz_c_api.c +++ b/tests/utils/buzz_c_api.c @@ -23,6 +23,27 @@ static int argCount(NativeCtx *ctx) { return 1; } +static int instantiateDefaultedObject(NativeCtx *ctx) { + Value object_value = bz_peek(ctx->vm, 1); + Value typedef_value = bz_peek(ctx->vm, 0); + Integer total = 0; + + for (int i = 0; i < 20000; i++) { + Value instance = bz_newObjectInstance( + ctx->vm, + object_value, + typedef_value + ); + Value tokens = bz_getObjectInstanceProperty(instance, 0); + + total += (Integer)bz_listLen(tokens); + } + + bz_push(ctx->vm, bz_valueFromInteger(total)); + + return 1; +} + NativeFn buzz_c_api(const char *symbol) { if (strcmp(symbol, "add") == 0) { return add; @@ -36,5 +57,9 @@ NativeFn buzz_c_api(const char *symbol) { return argCount; } + if (strcmp(symbol, "instantiateDefaultedObject") == 0) { + return instantiateDefaultedObject; + } + return 0; }