diff --git a/CHANGELOG.md b/CHANGELOG.md index ea6f5e07..c84b99e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ var value = <{ out result; } ``` +- `recursive_call_limit` build option limit recursive calls (default to 200) ## Changed - Map type notation has changed from `{K, V}` to `{K: V}`. Similarly map expression with specified typed went from `{, ...}` to `{, ...}` (https://github.com/buzz-language/buzz/issues/253) diff --git a/build.zig b/build.zig index f430f7ec..bb248788 100644 --- a/build.zig +++ b/build.zig @@ -65,6 +65,7 @@ const BuzzBuildOptions = struct { jit: BuzzJITOptions, target: Build.ResolvedTarget, cycle_limit: ?u128, + recursive_call_limit: u32, pub fn step(self: @This(), b: *Build) *Build.Module { var options = b.addOptions(); @@ -72,6 +73,7 @@ const BuzzBuildOptions = struct { options.addOption(@TypeOf(self.sha), "sha", self.sha); options.addOption(@TypeOf(self.mimalloc), "mimalloc", self.mimalloc); options.addOption(@TypeOf(self.cycle_limit), "cycle_limit", self.cycle_limit); + options.addOption(@TypeOf(self.recursive_call_limit), "recursive_call_limit", self.recursive_call_limit); self.debug.step(options); self.gc.step(options); @@ -147,6 +149,11 @@ pub fn build(b: *Build) !void { "cycle_limit", "Amount of bytecode (x 1000) the script is allowed to run (WARNING: this disables JIT compilation)", ) orelse null, + .recursive_call_limit = b.option( + u32, + "recursive_call_limit", + "Maximum depth for recursive calls", + ) orelse 200, .mimalloc = b.option( bool, "mimalloc", diff --git a/src/buzz_api.zig b/src/buzz_api.zig index 44c346b3..53a02809 100644 --- a/src/buzz_api.zig +++ b/src/buzz_api.zig @@ -1175,6 +1175,21 @@ export fn bz_context(ctx: *NativeCtx, closure_value: Value, new_ctx: *NativeCtx, else null; + // If recursive call, update counter + ctx.vm.current_fiber.recursive_count = if (closure != null and closure.?.function == ctx.vm.current_fiber.current_compiled_function) + ctx.vm.current_fiber.recursive_count + 1 + else + 0; + + if (ctx.vm.current_fiber.recursive_count > BuildOptions.recursive_call_limit) { + ctx.vm.throw( + VM.Error.ReachedMaximumRecursiveCall, + (ctx.vm.gc.copyString("Maximum recursive call reached") catch @panic("Maximum recursive call reached")).toValue(), + null, + null, + ) catch @panic("Maximum recursive call reached"); + } + // If bound method, replace closure on the stack by the receiver if (bound != null) { (ctx.vm.current_fiber.stack_top - arg_count - 1)[0] = bound.?.receiver; @@ -1192,6 +1207,10 @@ export fn bz_context(ctx: *NativeCtx, closure_value: Value, new_ctx: *NativeCtx, ctx.vm.jit.?.compileFunction(ctx.vm.current_ast, closure.?) catch @panic("Failed compiling function"); } + if (closure) |cls| { + ctx.vm.current_fiber.current_compiled_function = cls.function; + } + return if (closure) |cls| cls.function.native_raw.? else native.?.native; } diff --git a/src/vm.zig b/src/vm.zig index dece25a0..faf6b4d1 100644 --- a/src/vm.zig +++ b/src/vm.zig @@ -116,6 +116,8 @@ pub const Fiber = struct { frames: std.ArrayList(CallFrame), frame_count: u64 = 0, + recursive_count: u32 = 0, + current_compiled_function: ?*ObjFunction = null, stack: []Value, stack_top: [*]Value, @@ -333,6 +335,7 @@ pub const VM = struct { BadNumber, ReachedMaximumMemoryUsage, ReachedMaximumCPUUsage, + ReachedMaximumRecursiveCall, Custom, // TODO: remove when user can use this set directly in buzz code } || Allocator.Error || std.fmt.BufPrintError; @@ -3802,6 +3805,23 @@ pub const VM = struct { fn call(self: *Self, closure: *ObjClosure, arg_count: u8, catch_value: ?Value, in_fiber: bool) JIT.Error!void { closure.function.call_count += 1; + // If recursive call, update counter + self.current_fiber.recursive_count = if (self.currentFrame() != null and self.currentFrame().?.closure.function == closure.function) + self.current_fiber.recursive_count + 1 + else + 0; + + if (self.current_fiber.recursive_count > BuildOptions.recursive_call_limit) { + try self.throw( + VM.Error.ReachedMaximumRecursiveCall, + (try self.gc.copyString("Maximum recursive call reached")).toValue(), + null, + null, + ); + + return; + } + var native = closure.function.native; if (self.jit) |*jit| { jit.call_count += 1; diff --git a/tests/compile_errors/027-recursive-call.buzz b/tests/compile_errors/027-recursive-call.buzz new file mode 100644 index 00000000..668a5410 --- /dev/null +++ b/tests/compile_errors/027-recursive-call.buzz @@ -0,0 +1,8 @@ +| Maximum recursive call reached +fun eternal() > void { + eternal(); +} + +test "recursive call limit" { + eternal(); +} \ No newline at end of file