├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon ├── script ├── killzls.sh ├── run.sh └── setup.sh └── src ├── Fuzzer.zig ├── main.zig ├── mode.zig ├── modes ├── BestBehavior.zig ├── MarkovMode.zig └── markov.zig └── utils.zig /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.zig text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | paths: 6 | - "**.zig" 7 | pull_request: 8 | paths: 9 | - "**.zig" 10 | schedule: 11 | - cron: "0 0 * * *" 12 | workflow_dispatch: 13 | 14 | jobs: 15 | build: 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest] 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | submodules: recursive 25 | - uses: goto-bus-stop/setup-zig@v2 26 | with: 27 | version: master 28 | 29 | - run: zig version 30 | - run: zig env 31 | 32 | - name: Build 33 | run: zig build 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /zig-out 2 | /zig-cache 3 | /saved_logs 4 | /repos 5 | node_modules 6 | /.env 7 | d_*.log 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 sus contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sus 2 | 3 | ZLS fuzzing tooling. 4 | 5 | ```bash 6 | git clone https://github.com/ziglang/zig repos/zig 7 | git clone https://github.com/zigtools/zls repos/zls 8 | zig build -- repos/zls/zig-out/bin/zls[.exe] 9 | # example with 'markov training dir' 10 | zig build run -- --zls-path repos/zls/zig-out/bin/zls[.exe] --mode markov -- --training-dir repos/zig/src 11 | ``` 12 | 13 | # usage 14 | 15 | ```console 16 | Usage: sus [options] --mode [mode] -- 17 | 18 | Example: sus --mode markov -- --training-dir /path/to/folder/containing/zig/files/ 19 | sus --mode best_behavior -- --source_dir ~/path/to/folder/containing/zig/files/ 20 | 21 | General Options: 22 | --help Print this help and exit 23 | --output-as-dir Output fuzzing results as directories (default: false) 24 | --zls-path [path] Specify path to ZLS executable 25 | --mode [mode] Specify fuzzing mode - one of { best_behavior, markov } 26 | --cycles-per-gen How many times to fuzz a random feature before regenerating a new file. (default: 25) 27 | 28 | For a listing of mode specific options, use 'sus --mode [mode] -- --help'. 29 | For a listing of build options, use 'zig build --help'. 30 | ``` 31 | 32 | # .env 33 | if a .env file is present at project root or next to the exe, the following keys will be used as default values. 34 | ```console 35 | zls_path=~/repos/zls/zig-out/bin/zls 36 | mode=markov 37 | markov_training_dir=~/repos/zig/src 38 | ``` 39 | 40 | this allows the project to be run with no args: 41 | ```console 42 | zig build run 43 | ``` 44 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) void { 4 | const zig_lsp = b.dependency("zig-lsp", .{}).module("zig-lsp"); 5 | 6 | const target = b.standardTargetOptions(.{}); 7 | const optimize = b.standardOptimizeOption(.{}); 8 | 9 | const exe = b.addExecutable(.{ 10 | .name = "sus", 11 | .root_source_file = .{ .path = "src/main.zig" }, 12 | .target = target, 13 | .optimize = optimize, 14 | }); 15 | exe.root_module.addImport("zig-lsp", zig_lsp); 16 | b.installArtifact(exe); 17 | 18 | const run_cmd = b.addRunArtifact(exe); 19 | run_cmd.step.dependOn(b.getInstallStep()); 20 | if (b.args) |args| { 21 | run_cmd.addArgs(args); 22 | } 23 | 24 | const run_step = b.step("run", "Run the app"); 25 | run_step.dependOn(&run_cmd.step); 26 | 27 | const build_exe_tests = b.addTest(.{ 28 | .root_source_file = .{ .path = "src/main.zig" }, 29 | .target = target, 30 | .optimize = .Debug, 31 | }); 32 | const run_exe_tests = b.addRunArtifact(build_exe_tests); 33 | 34 | const test_step = b.step("test", "Run unit tests"); 35 | test_step.dependOn(&run_exe_tests.step); 36 | 37 | const block_len = b.option( 38 | u8, 39 | "block-len", 40 | "how many bytes to consider when predicting the next character. " ++ 41 | "defaults to 8. " ++ 42 | "note: this may affect performance.", 43 | ) orelse 8; 44 | const options = b.addOptions(); 45 | options.addOption(u8, "block_len", block_len); 46 | exe.root_module.addOptions("build_options", options); 47 | } 48 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = "sus", 3 | .version = "0.1.0", 4 | .dependencies = .{ 5 | .@"zig-lsp" = .{ 6 | .url = "https://github.com/ziglibs/zig-lsp/archive/1c18c0c64b076e79385e525214a7acc4fdf7d398.tar.gz", 7 | .hash = "12208c1385f4c1adca29fd17cc93397a60e54d5987bb27b9f961cb1945a96fc4a7e1", 8 | }, 9 | }, 10 | .paths = .{ 11 | "build.zig", 12 | "build.zig.zon", 13 | "src", 14 | "LICENSE", 15 | "README.md", 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /script/killzls.sh: -------------------------------------------------------------------------------- 1 | pids=$(pidof zls) 2 | pidsarr=($pids) 3 | pid=${pidsarr[0]} 4 | echo "zls pids $pids killing first pid $pid" 5 | kill $pid 6 | -------------------------------------------------------------------------------- /script/run.sh: -------------------------------------------------------------------------------- 1 | export PATH="$(pwd)/repos/zig:$PATH" 2 | zig build -Doptimize=ReleaseSafe 3 | ./zig-out/bin/sus 4 | -------------------------------------------------------------------------------- /script/setup.sh: -------------------------------------------------------------------------------- 1 | # Install Zig 2 | tarball_url=$(curl https://ziglang.org/download/index.json | jq '.master | .["x86_64-linux"] | .tarball' -r) 3 | wget $tarball_url -O repos/zig.tar.xz 4 | rm -rf repos/zig 5 | tar -xf repos/zig.tar.xz --directory repos 6 | mv repos/zig-linux* repos/zig 7 | 8 | export PATH="$(pwd)/repos/zig:$PATH" 9 | 10 | # Install zls 11 | rm -rf repos/zls 12 | git clone https://github.com/zigtools/zls repos/zls 13 | cd repos/zls 14 | zig build 15 | cd ../.. 16 | 17 | # Pull latest fuzzer 18 | git pull 19 | zig build -Doptimize=ReleaseSafe 20 | -------------------------------------------------------------------------------- /src/Fuzzer.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const lsp = @import("zig-lsp"); 3 | const utils = @import("utils.zig"); 4 | const lsp_types = lsp.types; 5 | const ChildProcess = std.ChildProcess; 6 | const Mode = @import("mode.zig").Mode; 7 | const ModeName = @import("mode.zig").ModeName; 8 | 9 | const Fuzzer = @This(); 10 | 11 | pub const Connection = lsp.Connection(std.fs.File.Reader, std.fs.File.Writer, Fuzzer); 12 | 13 | // note: if you add or change config options, update the usage in main.zig then 14 | // run `zig build run -- --help` and paste the contents into the README 15 | pub const Config = struct { 16 | output_as_dir: bool, 17 | zls_path: []const u8, 18 | mode_name: ModeName, 19 | cycles_per_gen: u32, 20 | 21 | zig_env: std.json.Parsed(ZigEnv), 22 | zls_version: []const u8, 23 | 24 | pub const Defaults = struct { 25 | pub const output_as_dir = false; 26 | pub const cycles_per_gen: u32 = 25; 27 | }; 28 | 29 | pub const ZigEnv = struct { 30 | zig_exe: []const u8, 31 | lib_dir: []const u8, 32 | std_dir: []const u8, 33 | // global_cache_dir: []const u8, 34 | version: []const u8, 35 | // target: []const u8, 36 | }; 37 | 38 | pub fn deinit(self: *Config, allocator: std.mem.Allocator) void { 39 | self.zig_env.deinit(); 40 | allocator.free(self.zls_path); 41 | allocator.free(self.zls_version); 42 | self.* = undefined; 43 | } 44 | }; 45 | 46 | allocator: std.mem.Allocator, 47 | connection: Connection, 48 | progress_node: *std.Progress.Node, 49 | mode: *Mode, 50 | config: Config, 51 | rand: std.rand.DefaultPrng, 52 | cycle: usize = 0, 53 | 54 | zls_process: ChildProcess, 55 | stderr_thread_keep_running: std.atomic.Value(bool) = std.atomic.Value(bool).init(true), 56 | stderr_thread: std.Thread, 57 | 58 | stdin_output: std.ArrayListUnmanaged(u8) = .{}, 59 | stdout_output: std.ArrayListUnmanaged(u8) = .{}, 60 | stderr_output: std.ArrayListUnmanaged(u8) = .{}, 61 | principal_file_source: []const u8 = "", 62 | principal_file_uri: []const u8, 63 | 64 | pub fn create( 65 | allocator: std.mem.Allocator, 66 | progress: *std.Progress, 67 | mode: *Mode, 68 | config: Config, 69 | ) !*Fuzzer { 70 | var fuzzer = try allocator.create(Fuzzer); 71 | errdefer allocator.destroy(fuzzer); 72 | 73 | var seed: u64 = 0; 74 | try std.os.getrandom(std.mem.asBytes(&seed)); 75 | 76 | const cwd_path = try std.process.getCwdAlloc(allocator); 77 | defer allocator.free(cwd_path); 78 | 79 | const principal_file_path = try std.fs.path.join(allocator, &.{ cwd_path, "tmp", "principal.zig" }); 80 | defer allocator.free(principal_file_path); 81 | 82 | const principal_file_uri = try std.fmt.allocPrint(allocator, "{+/}", .{std.Uri{ 83 | .scheme = "file", 84 | .user = null, 85 | .password = null, 86 | .host = null, 87 | .port = null, 88 | .path = principal_file_path, 89 | .query = null, 90 | .fragment = null, 91 | }}); 92 | errdefer allocator.free(principal_file_uri); 93 | 94 | var env_map = try allocator.create(std.process.EnvMap); 95 | env_map.* = std.process.getEnvMap(allocator) catch std.process.EnvMap.init(allocator); 96 | try env_map.put("NO_COLOR", ""); 97 | 98 | defer { 99 | env_map.deinit(); 100 | allocator.destroy(env_map); 101 | } 102 | 103 | var zls_process = std.ChildProcess.init(&.{ config.zls_path, "--enable-debug-log" }, allocator); 104 | zls_process.env_map = env_map; 105 | zls_process.stdin_behavior = .Pipe; 106 | zls_process.stderr_behavior = .Pipe; 107 | zls_process.stdout_behavior = .Pipe; 108 | 109 | try zls_process.spawn(); 110 | errdefer _ = zls_process.kill() catch @panic("failed to kill zls process"); 111 | 112 | fuzzer.* = .{ 113 | .allocator = allocator, 114 | .connection = undefined, // set below 115 | .progress_node = progress.start("fuzzer", 0), 116 | .mode = mode, 117 | .config = config, 118 | .rand = std.rand.DefaultPrng.init(seed), 119 | .zls_process = zls_process, 120 | .stderr_thread = undefined, // set below 121 | .principal_file_uri = principal_file_uri, 122 | }; 123 | 124 | fuzzer.connection = Connection.init( 125 | allocator, 126 | zls_process.stdout.?.reader(), 127 | zls_process.stdin.?.writer(), 128 | fuzzer, 129 | ); 130 | 131 | fuzzer.stderr_thread = try std.Thread.spawn(.{}, readStderr, .{fuzzer}); 132 | 133 | return fuzzer; 134 | } 135 | 136 | pub fn wait(fuzzer: *Fuzzer) void { 137 | fuzzer.stderr_thread_keep_running.store(false, .Release); 138 | fuzzer.stderr_thread.join(); 139 | 140 | _ = fuzzer.zls_process.wait() catch |err| { 141 | std.log.err("failed to await zls process: {}", .{err}); 142 | }; 143 | } 144 | 145 | pub fn destroy(fuzzer: *Fuzzer) void { 146 | const allocator = fuzzer.allocator; 147 | 148 | fuzzer.stdin_output.deinit(allocator); 149 | fuzzer.stdout_output.deinit(allocator); 150 | fuzzer.stderr_output.deinit(allocator); 151 | 152 | allocator.free(fuzzer.principal_file_source); 153 | allocator.free(fuzzer.principal_file_uri); 154 | 155 | fuzzer.connection.write_buffer.deinit(fuzzer.connection.allocator); 156 | fuzzer.connection.callback_map.deinit(fuzzer.connection.allocator); 157 | 158 | fuzzer.* = undefined; 159 | allocator.destroy(fuzzer); 160 | } 161 | 162 | fn readStderr(fuzzer: *Fuzzer) void { 163 | var buffer: [std.mem.page_size]u8 = undefined; 164 | while (fuzzer.stderr_thread_keep_running.load(.Acquire)) { 165 | const stderr = fuzzer.zls_process.stderr.?; 166 | const amt = stderr.reader().read(&buffer) catch break; 167 | fuzzer.stderr_output.appendSlice(fuzzer.allocator, buffer[0..amt]) catch break; 168 | } 169 | } 170 | 171 | pub fn random(fuzzer: *Fuzzer) std.rand.Random { 172 | return fuzzer.rand.random(); 173 | } 174 | 175 | pub fn initCycle(fuzzer: *Fuzzer) !void { 176 | fuzzer.progress_node.activate(); 177 | 178 | var arena = std.heap.ArenaAllocator.init(fuzzer.allocator); 179 | defer arena.deinit(); 180 | 181 | _ = try fuzzer.connection.requestSync(arena.allocator(), "initialize", lsp_types.InitializeParams{ 182 | .capabilities = .{}, 183 | }); 184 | try fuzzer.connection.notify("initialized", .{}); 185 | 186 | var settings = std.json.ObjectMap.init(fuzzer.allocator); 187 | defer settings.deinit(); 188 | try settings.putNoClobber("skip_std_references", .{ .bool = true }); // references collection into std is very slow 189 | try settings.putNoClobber("zig_exe_path", .{ .string = fuzzer.config.zig_env.value.zig_exe }); 190 | 191 | try fuzzer.connection.notify("workspace/didChangeConfiguration", lsp_types.DidChangeConfigurationParams{ 192 | .settings = .{ .object = settings }, 193 | }); 194 | 195 | try fuzzer.connection.notify("textDocument/didOpen", lsp_types.DidOpenTextDocumentParams{ .textDocument = .{ 196 | .uri = fuzzer.principal_file_uri, 197 | .languageId = "zig", 198 | .version = @intCast(fuzzer.cycle), 199 | .text = fuzzer.principal_file_source, 200 | } }); 201 | } 202 | 203 | pub fn closeCycle(fuzzer: *Fuzzer) !void { 204 | fuzzer.progress_node.end(); 205 | 206 | var arena = std.heap.ArenaAllocator.init(fuzzer.allocator); 207 | defer arena.deinit(); 208 | 209 | _ = try fuzzer.connection.notify("textDocument/didClose", .{ 210 | .textDocument = .{ .uri = fuzzer.principal_file_uri }, 211 | }); 212 | 213 | _ = try fuzzer.connection.requestSync(arena.allocator(), "shutdown", {}); 214 | try fuzzer.connection.notify("exit", {}); 215 | } 216 | 217 | pub fn fuzz(fuzzer: *Fuzzer) !void { 218 | fuzzer.progress_node.setCompletedItems(fuzzer.cycle); 219 | fuzzer.cycle += 1; 220 | 221 | if (fuzzer.cycle % fuzzer.config.cycles_per_gen == 0) { 222 | var arena_allocator = std.heap.ArenaAllocator.init(fuzzer.allocator); 223 | defer arena_allocator.deinit(); 224 | const arena = arena_allocator.allocator(); 225 | 226 | while (fuzzer.connection.callback_map.count() != 0) { 227 | _ = arena_allocator.reset(.retain_capacity); 228 | try fuzzer.connection.acceptUntilResponse(arena); 229 | } 230 | 231 | while (true) { 232 | fuzzer.allocator.free(fuzzer.principal_file_source); 233 | fuzzer.principal_file_source = try fuzzer.mode.gen(fuzzer.allocator); 234 | if (std.unicode.utf8ValidateSlice(fuzzer.principal_file_source)) break; 235 | } 236 | 237 | try fuzzer.connection.notify("textDocument/didChange", lsp_types.DidChangeTextDocumentParams{ 238 | .textDocument = .{ .uri = fuzzer.principal_file_uri, .version = @intCast(fuzzer.cycle) }, 239 | .contentChanges = &[1]lsp_types.TextDocumentContentChangeEvent{ 240 | .{ .literal_1 = .{ .text = fuzzer.principal_file_source } }, 241 | }, 242 | }); 243 | } 244 | try fuzzer.fuzzFeatureRandom(fuzzer.principal_file_uri, fuzzer.principal_file_source); 245 | } 246 | 247 | pub fn logPrincipal(fuzzer: *Fuzzer) !void { 248 | var bytes: [32]u8 = undefined; 249 | fuzzer.random().bytes(&bytes); 250 | 251 | try std.fs.cwd().makePath("saved_logs"); 252 | 253 | const log_entry_path = try std.fmt.allocPrint(fuzzer.allocator, "saved_logs/{d}", .{std.fmt.fmtSliceHexLower(&bytes)}); 254 | defer fuzzer.allocator.free(log_entry_path); 255 | 256 | if (fuzzer.config.output_as_dir) { 257 | try std.fs.cwd().makeDir(log_entry_path); 258 | 259 | var entry_dir = try std.fs.cwd().openDir(log_entry_path, .{}); 260 | defer entry_dir.close(); 261 | 262 | const principal_file = try entry_dir.createFile("principal.zig", .{}); 263 | defer principal_file.close(); 264 | 265 | try principal_file.writeAll(fuzzer.principal_file_source); 266 | 267 | for ( 268 | [_]std.ArrayListUnmanaged(u8){ fuzzer.stdin_output, fuzzer.stdout_output, fuzzer.stderr_output }, 269 | [_][]const u8{ "stdin.log", "stdout.log", "stderr.log" }, 270 | ) |output, path| { 271 | const output_file = try entry_dir.createFile(path, .{}); 272 | defer output_file.close(); 273 | 274 | try output_file.writeAll(output.items); 275 | } 276 | } else { 277 | const entry_file = try std.fs.cwd().createFile(log_entry_path, .{}); 278 | defer entry_file.close(); 279 | 280 | var iovecs: [13]std.os.iovec_const = undefined; 281 | 282 | for ([_][]const u8{ 283 | std.mem.asBytes(&std.time.milliTimestamp()), 284 | 285 | std.mem.asBytes(&@as(u8, @intCast(fuzzer.config.zig_env.value.version.len))), 286 | fuzzer.config.zig_env.value.version, 287 | 288 | std.mem.asBytes(&@as(u8, @intCast(fuzzer.config.zls_version.len))), 289 | fuzzer.config.zls_version, 290 | 291 | std.mem.asBytes(&@as(u32, @intCast(fuzzer.principal_file_source.len))), 292 | fuzzer.principal_file_source, 293 | 294 | std.mem.asBytes(&@as(u32, @intCast(fuzzer.stdin_output.items.len))), 295 | fuzzer.stdin_output.items, 296 | 297 | std.mem.asBytes(&@as(u32, @intCast(fuzzer.stdout_output.items.len))), 298 | fuzzer.stdout_output.items, 299 | 300 | std.mem.asBytes(&@as(u32, @intCast(fuzzer.stderr_output.items.len))), 301 | fuzzer.stderr_output.items, 302 | }, 0..) |val, i| { 303 | iovecs[i] = .{ 304 | .iov_base = val.ptr, 305 | .iov_len = val.len, 306 | }; 307 | } 308 | 309 | try entry_file.writevAll(&iovecs); 310 | } 311 | } 312 | 313 | pub const WhatToFuzz = enum { 314 | completion, 315 | declaration, 316 | definition, 317 | type_definition, 318 | implementation, 319 | references, 320 | signature_help, 321 | hover, 322 | semantic, 323 | document_symbol, 324 | folding_range, 325 | formatting, 326 | document_highlight, 327 | inlay_hint, 328 | // selection_range, 329 | rename, 330 | }; 331 | 332 | fn requestCallback(comptime method: []const u8) lsp.RequestCallback(Connection, method) { 333 | const Context = struct { 334 | pub fn res(_: *Connection, _: lsp.Result(method)) !void {} 335 | 336 | pub fn err(_: *Connection, resperr: lsp_types.ResponseError) !void { 337 | return switch (resperr.code) { 338 | @intFromEnum(lsp_types.ErrorCodes.ParseError) => error.ParseError, 339 | @intFromEnum(lsp_types.ErrorCodes.InvalidRequest) => error.InvalidRequest, 340 | @intFromEnum(lsp_types.ErrorCodes.MethodNotFound) => error.MethodNotFound, 341 | @intFromEnum(lsp_types.ErrorCodes.InvalidParams) => error.InvalidParams, 342 | @intFromEnum(lsp_types.ErrorCodes.InternalError) => error.InternalError, 343 | @intFromEnum(lsp_types.ErrorCodes.ServerNotInitialized) => error.ServerNotInitialized, 344 | @intFromEnum(lsp_types.ErrorCodes.UnknownErrorCode) => error.UnknownErrorCode, 345 | else => error.InternalError, 346 | }; 347 | } 348 | }; 349 | return .{ 350 | .onResponse = &Context.res, 351 | .onError = &Context.err, 352 | }; 353 | } 354 | 355 | pub fn fuzzFeatureRandom( 356 | fuzzer: *Fuzzer, 357 | file_uri: []const u8, 358 | file_data: []const u8, 359 | ) !void { 360 | const rand = fuzzer.random(); 361 | const wtf = rand.enumValue(WhatToFuzz); 362 | 363 | switch (wtf) { 364 | .completion => try fuzzer.connection.request( 365 | "textDocument/completion", 366 | .{ 367 | .textDocument = .{ .uri = file_uri }, 368 | .position = utils.randomPosition(rand, file_data), 369 | }, 370 | requestCallback("textDocument/completion"), 371 | ), 372 | .declaration => try fuzzer.connection.request( 373 | "textDocument/declaration", 374 | .{ 375 | .textDocument = .{ .uri = file_uri }, 376 | .position = utils.randomPosition(rand, file_data), 377 | }, 378 | requestCallback("textDocument/declaration"), 379 | ), 380 | .definition => try fuzzer.connection.request( 381 | "textDocument/definition", 382 | .{ 383 | .textDocument = .{ .uri = file_uri }, 384 | .position = utils.randomPosition(rand, file_data), 385 | }, 386 | requestCallback("textDocument/definition"), 387 | ), 388 | .type_definition => try fuzzer.connection.request( 389 | "textDocument/typeDefinition", 390 | .{ 391 | .textDocument = .{ .uri = file_uri }, 392 | .position = utils.randomPosition(rand, file_data), 393 | }, 394 | requestCallback("textDocument/typeDefinition"), 395 | ), 396 | .implementation => try fuzzer.connection.request( 397 | "textDocument/implementation", 398 | .{ 399 | .textDocument = .{ .uri = file_uri }, 400 | .position = utils.randomPosition(rand, file_data), 401 | }, 402 | requestCallback("textDocument/implementation"), 403 | ), 404 | .references => try fuzzer.connection.request( 405 | "textDocument/references", 406 | .{ 407 | .context = .{ .includeDeclaration = rand.boolean() }, 408 | .textDocument = .{ .uri = file_uri }, 409 | .position = utils.randomPosition(rand, file_data), 410 | }, 411 | requestCallback("textDocument/references"), 412 | ), 413 | .signature_help => try fuzzer.connection.request( 414 | "textDocument/signatureHelp", 415 | .{ 416 | .textDocument = .{ .uri = file_uri }, 417 | .position = utils.randomPosition(rand, file_data), 418 | }, 419 | requestCallback("textDocument/signatureHelp"), 420 | ), 421 | .hover => try fuzzer.connection.request( 422 | "textDocument/hover", 423 | .{ 424 | .textDocument = .{ .uri = file_uri }, 425 | .position = utils.randomPosition(rand, file_data), 426 | }, 427 | requestCallback("textDocument/hover"), 428 | ), 429 | .semantic => try fuzzer.connection.request( 430 | "textDocument/semanticTokens/full", 431 | .{ .textDocument = .{ .uri = file_uri } }, 432 | requestCallback("textDocument/semanticTokens/full"), 433 | ), 434 | .document_symbol => try fuzzer.connection.request( 435 | "textDocument/documentSymbol", 436 | .{ .textDocument = .{ .uri = file_uri } }, 437 | requestCallback("textDocument/documentSymbol"), 438 | ), 439 | .folding_range => { 440 | _ = try fuzzer.connection.request( 441 | "textDocument/foldingRange", 442 | .{ .textDocument = .{ .uri = file_uri } }, 443 | requestCallback("textDocument/foldingRange"), 444 | ); 445 | }, 446 | .formatting => try fuzzer.connection.request( 447 | "textDocument/formatting", 448 | .{ 449 | .textDocument = .{ .uri = file_uri }, 450 | .options = .{ 451 | .tabSize = 4, 452 | .insertSpaces = true, 453 | }, 454 | }, 455 | requestCallback("textDocument/formatting"), 456 | ), 457 | .document_highlight => try fuzzer.connection.request( 458 | "textDocument/documentHighlight", 459 | .{ 460 | .textDocument = .{ .uri = file_uri }, 461 | .position = utils.randomPosition(rand, file_data), 462 | }, 463 | requestCallback("textDocument/documentHighlight"), 464 | ), 465 | .inlay_hint => try fuzzer.connection.request( 466 | "textDocument/inlayHint", 467 | .{ 468 | .textDocument = .{ .uri = file_uri }, 469 | .range = utils.randomRange(rand, file_data), 470 | }, 471 | requestCallback("textDocument/inlayHint"), 472 | ), 473 | // TODO: Nest positions properly to avoid crash 474 | // .selection_range => { 475 | // var positions: [16]lsp_types.Position = undefined; 476 | // for (positions) |*pos| { 477 | // pos.* = utils.randomPosition(rand, file_data); 478 | // } 479 | // try fuzzer.connection.request( 480 | // "textDocument/selectionRange", 481 | // .{ 482 | // .textDocument = .{ .uri = file_uri }, 483 | // .positions = &positions, 484 | // }, 485 | // requestCallback("textDocument/selectionRange"), 486 | // ); 487 | // }, 488 | .rename => try fuzzer.connection.request( 489 | "textDocument/rename", 490 | .{ 491 | .textDocument = .{ .uri = file_uri }, 492 | .position = utils.randomPosition(rand, file_data), 493 | .newName = "helloWorld", 494 | }, 495 | requestCallback("textDocument/rename"), 496 | ), 497 | } 498 | } 499 | 500 | // Handlers 501 | 502 | pub fn @"window/logMessage"(_: *Connection, params: lsp.Params("window/logMessage")) !void { 503 | switch (params.type) { 504 | .Error => std.log.err("logMessage: {s}", .{params.message}), 505 | .Warning => std.log.warn("logMessage: {s}", .{params.message}), 506 | .Info => std.log.info("logMessage: {s}", .{params.message}), 507 | .Log => std.log.debug("logMessage: {s}", .{params.message}), 508 | } 509 | } 510 | 511 | pub fn @"window/showMessage"(_: *Connection, params: lsp.Params("window/showMessage")) !void { 512 | switch (params.type) { 513 | .Error => std.log.err("showMessage: {s}", .{params.message}), 514 | .Warning => std.log.warn("showMessage: {s}", .{params.message}), 515 | .Info => std.log.info("showMessage: {s}", .{params.message}), 516 | .Log => std.log.debug("showMessage: {s}", .{params.message}), 517 | } 518 | } 519 | 520 | pub fn @"textDocument/publishDiagnostics"(_: *Connection, _: lsp.Params("textDocument/publishDiagnostics")) !void {} 521 | pub fn @"workspace/semanticTokens/refresh"(_: *Connection, _: lsp.types.RequestId, _: lsp.Params("workspace/semanticTokens/refresh")) !void {} 522 | 523 | pub fn dataRecv( 524 | conn: *Connection, 525 | data: []const u8, 526 | ) !void { 527 | const fuzzer: *Fuzzer = conn.context; 528 | try fuzzer.stdout_output.ensureUnusedCapacity(fuzzer.allocator, data.len + 1); 529 | fuzzer.stdout_output.appendSliceAssumeCapacity(data); 530 | fuzzer.stdout_output.appendAssumeCapacity('\n'); 531 | } 532 | 533 | pub fn dataSend( 534 | conn: *Connection, 535 | data: []const u8, 536 | ) !void { 537 | const fuzzer: *Fuzzer = conn.context; 538 | try fuzzer.stdin_output.ensureUnusedCapacity(fuzzer.allocator, data.len + 1); 539 | fuzzer.stdin_output.appendSliceAssumeCapacity(data); 540 | fuzzer.stdin_output.appendAssumeCapacity('\n'); 541 | } 542 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const Fuzzer = @import("Fuzzer.zig"); 4 | const Mode = @import("mode.zig").Mode; 5 | const ModeName = @import("mode.zig").ModeName; 6 | 7 | fn loadEnv(allocator: std.mem.Allocator) !std.process.EnvMap { 8 | var envmap: std.process.EnvMap = std.process.getEnvMap(allocator) catch std.process.EnvMap.init(allocator); 9 | errdefer envmap.deinit(); 10 | 11 | const env_content = std.fs.cwd().readFileAlloc(allocator, ".env", std.math.maxInt(usize)) catch |e| switch (e) { 12 | error.FileNotFound => return envmap, 13 | else => return e, 14 | }; 15 | defer allocator.free(env_content); 16 | 17 | var line_it = std.mem.splitAny(u8, env_content, "\n\r"); 18 | while (line_it.next()) |line| { 19 | if (std.mem.indexOfScalar(u8, line, '=')) |equal_sign_index| { 20 | const key = line[0..equal_sign_index]; 21 | const val = line[equal_sign_index + 1 ..]; 22 | try envmap.put(key, val); 23 | } else { 24 | try envmap.put(line, ""); 25 | } 26 | } 27 | return envmap; 28 | } 29 | 30 | fn initConfig(allocator: std.mem.Allocator, env_map: std.process.EnvMap, arg_it: *std.process.ArgIterator) !Fuzzer.Config { 31 | _ = arg_it.skip(); 32 | 33 | var maybe_zls_path: ?[]const u8 = blk: { 34 | if (env_map.get("zls_path")) |path| { 35 | break :blk try allocator.dupe(u8, path); 36 | } 37 | break :blk findInPath(allocator, env_map, "zls"); 38 | }; 39 | errdefer if (maybe_zls_path) |path| allocator.free(path); 40 | 41 | var maybe_zig_path: ?[]const u8 = blk: { 42 | if (env_map.get("zig_path")) |path| { 43 | break :blk try allocator.dupe(u8, path); 44 | } 45 | break :blk findInPath(allocator, env_map, "zig"); 46 | }; 47 | defer if (maybe_zig_path) |path| allocator.free(path); 48 | 49 | var output_as_dir = 50 | if (env_map.get("output_as_dir")) |str| 51 | if (std.mem.eql(u8, str, "false")) 52 | false 53 | else if (std.mem.eql(u8, str, "true")) 54 | true 55 | else blk: { 56 | std.log.warn("expected boolean (true|false) in env option 'output_as_dir' but got '{s}'", .{str}); 57 | break :blk Fuzzer.Config.Defaults.output_as_dir; 58 | } 59 | else 60 | Fuzzer.Config.Defaults.output_as_dir; 61 | 62 | var mode_name: ?ModeName = blk: { 63 | if (env_map.get("mode")) |mode_name| { 64 | if (std.meta.stringToEnum(ModeName, mode_name)) |mode| { 65 | break :blk mode; 66 | } else { 67 | std.log.warn( 68 | "expected mode name (one of {s}) in env option 'mode' but got '{s}'", 69 | .{ std.meta.fieldNames(ModeName).*, mode_name }, 70 | ); 71 | } 72 | } 73 | break :blk null; 74 | }; 75 | 76 | var cycles_per_gen: u32 = blk: { 77 | if (env_map.get("cycles_per_gen")) |str| { 78 | if (std.fmt.parseUnsigned(u32, str, 10)) |cpg| { 79 | break :blk cpg; 80 | } else |err| { 81 | std.log.warn("expected unsigned integer in env option 'cycles_per_gen' but got '{s}': {}", .{ str, err }); 82 | } 83 | } 84 | break :blk Fuzzer.Config.Defaults.cycles_per_gen; 85 | }; 86 | 87 | var num_args: usize = 0; 88 | while (arg_it.next()) |arg| : (num_args += 1) { 89 | if (std.mem.eql(u8, arg, "--")) break; // all argument after '--' are mode specific arguments 90 | 91 | if (std.mem.eql(u8, arg, "--help")) { 92 | try std.io.getStdOut().writeAll(usage); 93 | std.process.exit(0); 94 | } else if (std.mem.eql(u8, arg, "--output-as-dir")) { 95 | output_as_dir = true; 96 | } else if (std.mem.eql(u8, arg, "--zls-path")) { 97 | if (maybe_zls_path) |path| { 98 | allocator.free(path); 99 | maybe_zls_path = null; 100 | } 101 | maybe_zls_path = try allocator.dupe(u8, arg_it.next() orelse fatal("expected file path after --zls-path", .{})); 102 | } else if (std.mem.eql(u8, arg, "--zig-path")) { 103 | if (maybe_zig_path) |path| { 104 | allocator.free(path); 105 | maybe_zig_path = null; 106 | } 107 | maybe_zig_path = try allocator.dupe(u8, arg_it.next() orelse fatal("expected file path after --zig-path", .{})); 108 | } else if (std.mem.eql(u8, arg, "--mode")) { 109 | const mode_arg = arg_it.next() orelse fatal("expected mode parameter after --mode", .{}); 110 | mode_name = std.meta.stringToEnum(ModeName, mode_arg) orelse fatal("unknown mode: {s}", .{mode_arg}); 111 | } else if (std.mem.eql(u8, arg, "--cycles-per-gen")) { 112 | const next_arg = arg_it.next() orelse fatal("expected unsigned integer after --cycles-per-gen", .{}); 113 | cycles_per_gen = std.fmt.parseUnsigned(u32, next_arg, 10) catch fatal("invalid unsigned integer '{s}'", .{next_arg}); 114 | } else { 115 | fatalWithUsage("unknown parameter: {s}", .{arg}); 116 | } 117 | } 118 | 119 | if (num_args == 0 and (maybe_zls_path == null or mode_name == null)) { 120 | try std.io.getStdErr().writeAll(usage); 121 | std.process.exit(1); 122 | } 123 | 124 | const zls_path = maybe_zls_path orelse fatalWithUsage("ZLS was not found in PATH. Please specify --zls-path instead", .{}); 125 | const zig_path = maybe_zig_path orelse fatalWithUsage("Zig was not found in PATH. Please specify --zig-path instead", .{}); 126 | const mode = mode_name orelse fatalWithUsage("must specify --mode", .{}); 127 | 128 | const zls_version = blk: { 129 | const result = try std.process.Child.run(.{ 130 | .allocator = allocator, 131 | .argv = &.{ zls_path, "--version" }, 132 | }); 133 | defer allocator.free(result.stdout); 134 | defer allocator.free(result.stderr); 135 | 136 | switch (result.term) { 137 | .Exited => |code| if (code != 0) fatal("command '{s} --version' exited with non zero exit code: {d}", .{ zls_path, code }), 138 | else => fatal("command '{s} --version' exited abnormally: {s}", .{ zls_path, @tagName(result.term) }), 139 | } 140 | 141 | break :blk try allocator.dupe(u8, std.mem.trim(u8, result.stdout, &std.ascii.whitespace)); 142 | }; 143 | errdefer allocator.free(zls_version); 144 | 145 | const zig_env = blk: { 146 | const result = try std.process.Child.run(.{ 147 | .allocator = allocator, 148 | .argv = &.{ zig_path, "env" }, 149 | }); 150 | defer allocator.free(result.stdout); 151 | defer allocator.free(result.stderr); 152 | 153 | switch (result.term) { 154 | .Exited => |code| if (code != 0) fatal("command '{s} --version' exited with non zero exit code: {d}", .{ zls_path, code }), 155 | else => fatal("command '{s} --version' exited abnormally: {s}", .{ zls_path, @tagName(result.term) }), 156 | } 157 | 158 | var scanner = std.json.Scanner.initCompleteInput(allocator, result.stdout); 159 | defer scanner.deinit(); 160 | 161 | var diagnostics: std.json.Diagnostics = .{}; 162 | scanner.enableDiagnostics(&diagnostics); 163 | 164 | break :blk std.json.parseFromTokenSource(Fuzzer.Config.ZigEnv, allocator, &scanner, .{ 165 | .ignore_unknown_fields = true, 166 | .allocate = .alloc_always, 167 | }) catch |err| { 168 | std.log.err( 169 | \\command '{s} env' did not respond with valid json 170 | \\stdout: 171 | \\{s} 172 | \\stderr: 173 | \\{s} 174 | \\On Line {d}, Column {d}: {} 175 | , .{ zig_path, result.stdout, result.stderr, diagnostics.getLine(), diagnostics.getColumn(), err }); 176 | std.process.exit(1); 177 | }; 178 | }; 179 | errdefer zig_env.deinit(); 180 | 181 | return .{ 182 | .output_as_dir = output_as_dir, 183 | .zls_path = zls_path, 184 | .mode_name = mode, 185 | .cycles_per_gen = cycles_per_gen, 186 | 187 | .zig_env = zig_env, 188 | .zls_version = zls_version, 189 | }; 190 | } 191 | 192 | // note: if you change this text, run `zig build run -- --help` and paste the contents into the README 193 | const usage = 194 | std.fmt.comptimePrint( 195 | \\sus - ZLS fuzzing tooling 196 | \\ 197 | \\Usage: sus [options] --mode [mode] -- 198 | \\ 199 | \\Example: sus --mode markov -- --training-dir /path/to/folder/containing/zig/files/ 200 | \\ sus --mode best_behavior -- --source_dir ~/path/to/folder/containing/zig/files/ 201 | \\ 202 | \\General Options: 203 | \\ --help Print this help and exit 204 | \\ --mode [mode] Specify fuzzing mode - one of {s} 205 | \\ --output-as-dir Output fuzzing results as directories (default: {s}) 206 | \\ --zls-path [path] Specify path to ZLS executable (default: Search in PATH) 207 | \\ --zig-path [path] Specify path to Zig executable (default: Search in PATH) 208 | \\ --cycles-per-gen How many times to fuzz a random feature before regenerating a new file. (default: {d}) 209 | \\ 210 | \\For a listing of mode specific options, use 'sus --mode [mode] -- --help'. 211 | \\For a listing of build options, use 'zig build --help'. 212 | \\ 213 | , .{ 214 | if (Fuzzer.Config.Defaults.output_as_dir) "true" else "false", 215 | std.meta.fieldNames(ModeName).*, 216 | Fuzzer.Config.Defaults.cycles_per_gen, 217 | }); 218 | 219 | fn fatalWithUsage(comptime format: []const u8, args: anytype) noreturn { 220 | std.io.getStdErr().writeAll(usage) catch {}; 221 | std.log.err(format, args); 222 | std.process.exit(1); 223 | } 224 | 225 | fn fatal(comptime format: []const u8, args: anytype) noreturn { 226 | std.log.err(format, args); 227 | std.process.exit(1); 228 | } 229 | 230 | pub fn findInPath(allocator: std.mem.Allocator, env_map: std.process.EnvMap, sub_path: []const u8) ?[]const u8 { 231 | const env_path = env_map.get("PATH") orelse return null; 232 | var it = std.mem.tokenizeScalar(u8, env_path, std.fs.path.delimiter); 233 | while (it.next()) |path| { 234 | const full_path = std.fs.path.join(allocator, &[_][]const u8{ path, sub_path }) catch continue; 235 | 236 | if (!std.fs.path.isAbsolute(full_path)) { 237 | allocator.free(full_path); 238 | continue; 239 | } 240 | std.fs.accessAbsolute(full_path, .{}) catch { 241 | allocator.free(full_path); 242 | continue; 243 | }; 244 | 245 | return full_path; 246 | } 247 | return null; 248 | } 249 | 250 | const stack_trace_frames: usize = switch (builtin.mode) { 251 | .Debug => 16, 252 | else => 0, 253 | }; 254 | 255 | pub fn main() !void { 256 | var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{ 257 | .stack_trace_frames = stack_trace_frames, 258 | }){}; 259 | defer _ = general_purpose_allocator.deinit(); 260 | const gpa = general_purpose_allocator.allocator(); 261 | 262 | var env_map: std.process.EnvMap = loadEnv(gpa) catch std.process.EnvMap.init(gpa); 263 | defer env_map.deinit(); 264 | 265 | var arg_it = try std.process.ArgIterator.initWithAllocator(gpa); 266 | defer arg_it.deinit(); 267 | 268 | var config = try initConfig(gpa, env_map, &arg_it); 269 | defer config.deinit(gpa); 270 | 271 | var progress = std.Progress{ 272 | .terminal = null, 273 | }; 274 | 275 | progress.log( 276 | \\zig-version: {s} 277 | \\zls-version: {s} 278 | \\zig-path: {s} 279 | \\zls-path: {s} 280 | \\mode: {s} 281 | \\cycles-per-gen: {d} 282 | \\ 283 | , .{ 284 | config.zig_env.value.version, 285 | config.zls_version, 286 | config.zig_env.value.zig_exe, 287 | config.zls_path, 288 | @tagName(config.mode_name), 289 | config.cycles_per_gen, 290 | }); 291 | 292 | var mode = try Mode.init(config.mode_name, gpa, &progress, &arg_it, env_map); 293 | defer mode.deinit(gpa); 294 | 295 | while (true) { 296 | var fuzzer = try Fuzzer.create(gpa, &progress, &mode, config); 297 | errdefer { 298 | fuzzer.wait(); 299 | fuzzer.destroy(); 300 | } 301 | fuzzer.progress_node.setEstimatedTotalItems(100_000); 302 | try fuzzer.initCycle(); 303 | 304 | while (true) { 305 | progress.maybeRefresh(); 306 | 307 | if (fuzzer.cycle >= 100_000) { 308 | progress.log("Fuzzer running too long with no result... restarting\n", .{}); 309 | 310 | try fuzzer.closeCycle(); 311 | fuzzer.wait(); 312 | fuzzer.destroy(); 313 | break; 314 | } 315 | 316 | fuzzer.fuzz() catch { 317 | progress.log("Restarting fuzzer...\n", .{}); 318 | 319 | fuzzer.wait(); 320 | fuzzer.logPrincipal() catch { 321 | progress.log("failed to log principal\n", .{}); 322 | }; 323 | fuzzer.destroy(); 324 | break; 325 | }; 326 | } 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /src/mode.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub const ModeName = std.meta.Tag(Mode); 4 | pub const Mode = union(enum) { 5 | best_behavior: *@import("modes/BestBehavior.zig"), 6 | markov: *@import("modes/MarkovMode.zig"), 7 | 8 | pub fn init( 9 | mode_name: ModeName, 10 | allocator: std.mem.Allocator, 11 | progress: *std.Progress, 12 | arg_it: *std.process.ArgIterator, 13 | envmap: std.process.EnvMap, 14 | ) !Mode { 15 | switch (mode_name) { 16 | inline else => |m| { 17 | const Inner = std.meta.Child(std.meta.TagPayload(Mode, m)); 18 | return @unionInit(Mode, @tagName(m), try Inner.init(allocator, progress, arg_it, envmap)); 19 | }, 20 | } 21 | } 22 | 23 | pub fn deinit(mode: *Mode, allocator: std.mem.Allocator) void { 24 | switch (mode.*) { 25 | inline else => |m| m.deinit(allocator), 26 | } 27 | mode.* = undefined; 28 | } 29 | 30 | pub fn gen(mode: *Mode, allocator: std.mem.Allocator) ![]const u8 { 31 | switch (mode.*) { 32 | inline else => |m| return try m.gen(allocator), 33 | } 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/modes/BestBehavior.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const utils = @import("../utils.zig"); 3 | 4 | const BestBehavior = @This(); 5 | 6 | random: std.rand.DefaultPrng, 7 | tests: std.ArrayListUnmanaged([]const u8), 8 | 9 | const usage = 10 | \\Usage best behavior Mode: 11 | \\ --source-dir - directory to be used for fuzzing. searched for .zig files recursively. 12 | ; 13 | 14 | fn fatalWithUsage(comptime format: []const u8, args: anytype) noreturn { 15 | std.io.getStdErr().writeAll(usage) catch {}; 16 | std.log.err(format, args); 17 | std.process.exit(1); 18 | } 19 | 20 | fn fatal(comptime format: []const u8, args: anytype) noreturn { 21 | std.log.err(format, args); 22 | std.process.exit(1); 23 | } 24 | 25 | pub fn init( 26 | allocator: std.mem.Allocator, 27 | progress: *std.Progress, 28 | arg_it: *std.process.ArgIterator, 29 | envmap: std.process.EnvMap, 30 | ) !*BestBehavior { 31 | var bb = try allocator.create(BestBehavior); 32 | errdefer allocator.destroy(bb); 33 | 34 | var seed: u64 = 0; 35 | try std.os.getrandom(std.mem.asBytes(&seed)); 36 | 37 | bb.* = .{ 38 | .random = std.rand.DefaultPrng.init(seed), 39 | .tests = .{}, 40 | }; 41 | errdefer bb.deinit(allocator); 42 | 43 | var source_dir: ?[]const u8 = envmap.get("best_behavior_source_dir"); 44 | 45 | while (arg_it.next()) |arg| { 46 | if (std.mem.eql(u8, arg, "--help")) { 47 | try std.io.getStdOut().writeAll(usage); 48 | std.process.exit(0); 49 | } else if (std.mem.eql(u8, arg, "--source-dir")) { 50 | source_dir = arg_it.next() orelse fatalWithUsage("expected directory path after --source-dir", .{}); 51 | } else { 52 | fatalWithUsage("invalid best_behavior arg '{s}'", .{arg}); 53 | } 54 | } 55 | 56 | // make sure required args weren't skipped 57 | if (source_dir == null or source_dir.?.len == 0) { 58 | fatalWithUsage("missing mode argument '--source-dir'", .{}); 59 | } 60 | 61 | progress.log( 62 | \\ 63 | \\source-dir: {s} 64 | \\ 65 | \\ 66 | , .{source_dir.?}); 67 | 68 | var progress_node = progress.start("best behavior: loading files", 0); 69 | defer progress_node.end(); 70 | 71 | var iterable_dir = try std.fs.cwd().openDir(source_dir.?, .{ .iterate = true }); 72 | defer iterable_dir.close(); 73 | 74 | { 75 | var walker = try iterable_dir.walk(allocator); 76 | defer walker.deinit(); 77 | var file_count: usize = 0; 78 | while (try walker.next()) |entry| { 79 | if (entry.kind != .file) continue; 80 | if (!std.mem.eql(u8, std.fs.path.extension(entry.basename), ".zig")) continue; 81 | file_count += 1; 82 | progress_node.setEstimatedTotalItems(file_count); 83 | } 84 | } 85 | 86 | var walker = try iterable_dir.walk(allocator); 87 | defer walker.deinit(); 88 | 89 | const cwd = try std.process.getCwdAlloc(allocator); 90 | defer allocator.free(cwd); 91 | 92 | var file_buf = std.ArrayListUnmanaged(u8){}; 93 | defer file_buf.deinit(allocator); 94 | 95 | progress_node.activate(); 96 | while (try walker.next()) |entry| { 97 | if (entry.kind != .file) continue; 98 | if (!std.mem.eql(u8, std.fs.path.extension(entry.basename), ".zig")) continue; 99 | 100 | // std.log.info("found file {s}", .{entry.path}); 101 | 102 | var file = try entry.dir.openFile(entry.basename, .{}); 103 | defer file.close(); 104 | 105 | const size = std.math.cast(usize, try file.getEndPos()) orelse return error.FileTooBig; 106 | try file_buf.ensureTotalCapacity(allocator, size); 107 | file_buf.items.len = size; 108 | _ = try file.readAll(file_buf.items); 109 | 110 | try bb.tests.ensureUnusedCapacity(allocator, 1); 111 | bb.tests.appendAssumeCapacity(try file_buf.toOwnedSlice(allocator)); 112 | 113 | progress_node.completeOne(); 114 | } 115 | 116 | return bb; 117 | } 118 | 119 | pub fn deinit(bb: *BestBehavior, allocator: std.mem.Allocator) void { 120 | for (bb.tests.items) |file_content| { 121 | allocator.free(file_content); 122 | } 123 | bb.tests.deinit(allocator); 124 | } 125 | 126 | pub fn gen(bb: *BestBehavior, allocator: std.mem.Allocator) ![]const u8 { 127 | const random = bb.random.random(); 128 | 129 | const index = random.intRangeLessThan(usize, 0, bb.tests.items.len); 130 | const file_content = bb.tests.items[index]; 131 | 132 | return try allocator.dupe(u8, file_content); 133 | } 134 | -------------------------------------------------------------------------------- /src/modes/MarkovMode.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const markov = @import("markov.zig"); 3 | const build_options = @import("build_options"); 4 | 5 | const Markov = @This(); 6 | 7 | const MarkovModel = markov.Model(build_options.block_len, false); 8 | 9 | model: MarkovModel, 10 | random: std.rand.DefaultPrng, 11 | maxlen: u32 = Defaults.maxlen, 12 | 13 | const Defaults = struct { 14 | pub const maxlen = 512; 15 | }; 16 | 17 | const usage = 18 | std.fmt.comptimePrint( 19 | \\Usage Markov Mode: 20 | \\ --training-dir - directory to be used for training. searched for .zig files recursively. 21 | \\ --maxlen - maximum length of each file generated by the markov model. (default: {d}) 22 | , .{ 23 | Defaults.maxlen, 24 | }); 25 | 26 | fn fatalWithUsage(comptime format: []const u8, args: anytype) noreturn { 27 | std.io.getStdErr().writeAll(usage) catch {}; 28 | std.log.err(format, args); 29 | std.process.exit(1); 30 | } 31 | 32 | fn fatal(comptime format: []const u8, args: anytype) noreturn { 33 | std.log.err(format, args); 34 | std.process.exit(1); 35 | } 36 | 37 | pub fn init( 38 | allocator: std.mem.Allocator, 39 | progress: *std.Progress, 40 | arg_it: *std.process.ArgIterator, 41 | envmap: std.process.EnvMap, 42 | ) !*Markov { 43 | var seed: u64 = 0; 44 | try std.os.getrandom(std.mem.asBytes(&seed)); 45 | 46 | var mm = try allocator.create(Markov); 47 | mm.* = .{ 48 | .model = undefined, // set below 49 | .random = std.rand.DefaultPrng.init(seed), 50 | }; 51 | errdefer mm.deinit(allocator); 52 | 53 | mm.model = MarkovModel.init(allocator, mm.random.random()); 54 | 55 | var training_dir: ?[]const u8 = envmap.get("markov_training_dir"); 56 | 57 | if (envmap.get("markov_maxlen")) |str| { 58 | mm.maxlen = std.fmt.parseUnsigned(u32, str, 10) catch |err| blk: { 59 | std.log.warn("expected unsigned integer in env option 'markov_maxlen' but got '{s}': {}", .{ str, err }); 60 | break :blk Defaults.maxlen; 61 | }; 62 | } 63 | 64 | while (arg_it.next()) |arg| { 65 | if (std.mem.eql(u8, arg, "--help")) { 66 | try std.io.getStdOut().writeAll(usage); 67 | std.process.exit(0); 68 | } else if (std.mem.eql(u8, arg, "--training-dir")) { 69 | training_dir = arg_it.next() orelse fatalWithUsage("expected directory path after --training-dir", .{}); 70 | } else if (std.mem.eql(u8, arg, "--maxlen")) { 71 | const next_arg = arg_it.next() orelse fatal("expected integer after --maxlen", .{}); 72 | mm.maxlen = std.fmt.parseUnsigned(u32, next_arg, 10) catch fatalWithUsage("expected integer after --maxlen", .{}); 73 | } else { 74 | fatalWithUsage("invalid markov arg '{s}'", .{arg}); 75 | } 76 | } 77 | 78 | // make sure required args weren't skipped 79 | if (training_dir == null or training_dir.?.len == 0) { 80 | fatalWithUsage("missing mode argument '--training-dir'", .{}); 81 | } 82 | 83 | progress.log( 84 | \\ 85 | \\training-dir: {s} 86 | \\maxlen: {d} 87 | \\ 88 | \\ 89 | , .{ training_dir.?, mm.maxlen }); 90 | 91 | var progress_node = progress.start("markov: feeding model", 0); 92 | defer progress_node.end(); 93 | 94 | var iterable_dir = try std.fs.cwd().openDir(training_dir.?, .{ .iterate = true }); 95 | defer iterable_dir.close(); 96 | 97 | { 98 | var walker = try iterable_dir.walk(allocator); 99 | defer walker.deinit(); 100 | var file_count: usize = 0; 101 | while (try walker.next()) |entry| { 102 | if (entry.kind != .file) continue; 103 | if (!std.mem.eql(u8, std.fs.path.extension(entry.basename), ".zig")) continue; 104 | file_count += 1; 105 | progress_node.setEstimatedTotalItems(file_count); 106 | } 107 | } 108 | 109 | var walker = try iterable_dir.walk(allocator); 110 | defer walker.deinit(); 111 | 112 | var file_buf = std.ArrayListUnmanaged(u8){}; 113 | defer file_buf.deinit(allocator); 114 | 115 | progress_node.activate(); 116 | while (try walker.next()) |entry| { 117 | if (entry.kind != .file) continue; 118 | if (!std.mem.eql(u8, std.fs.path.extension(entry.basename), ".zig")) continue; 119 | 120 | // std.log.info("found file: {s}", .{entry.path}); 121 | 122 | var file = try entry.dir.openFile(entry.basename, .{}); 123 | defer file.close(); 124 | 125 | const size = std.math.cast(usize, try file.getEndPos()) orelse return error.FileTooBig; 126 | try file_buf.ensureTotalCapacity(allocator, size); 127 | file_buf.items.len = size; 128 | _ = try file.readAll(file_buf.items); 129 | 130 | try mm.model.feed(file_buf.items); 131 | 132 | progress_node.completeOne(); 133 | } 134 | 135 | mm.model.prep(); 136 | 137 | return mm; 138 | } 139 | 140 | pub fn deinit(mm: *Markov, allocator: std.mem.Allocator) void { 141 | mm.model.deinit(allocator); 142 | mm.* = undefined; 143 | allocator.destroy(mm); 144 | } 145 | 146 | pub fn gen(mm: *Markov, allocator: std.mem.Allocator) ![]const u8 { 147 | var buffer = try std.ArrayListUnmanaged(u8).initCapacity(allocator, mm.maxlen); 148 | errdefer buffer.deinit(allocator); 149 | try mm.model.gen(buffer.writer(allocator), .{ .maxlen = mm.maxlen }); 150 | return try buffer.toOwnedSlice(allocator); 151 | } 152 | -------------------------------------------------------------------------------- /src/modes/markov.zig: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Travis Staloch 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | // 5 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | // 7 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | const std = @import("std"); 10 | const mem = std.mem; 11 | const testing = std.testing; 12 | 13 | pub fn Iterator(comptime byte_len: comptime_int) type { 14 | return struct { 15 | text: []const u8, 16 | index: isize, 17 | 18 | /// byte length 19 | pub const len = byte_len; 20 | pub const Block = [len]u8; 21 | pub const Int = std.meta.Int(.unsigned, len * 8); 22 | pub const IntAndNext = struct { int: Int, next: u8 }; 23 | const Self = @This(); 24 | pub fn next(it: *Self) ?IntAndNext { 25 | defer it.index += 1; 26 | if (it.index + len >= it.text.len) return null; 27 | const start = std.math.cast(usize, it.index) orelse 0; 28 | const end: usize = @bitCast(it.index + len); 29 | return .{ 30 | .int = strToInt(it.text[start..end]), 31 | .next = it.text[end], 32 | }; 33 | } 34 | 35 | pub fn intToBlock(int: Int) Block { 36 | return @bitCast(int); 37 | } 38 | pub fn strToBlock(str: []const u8) Block { 39 | // if (@import("builtin").mode == .Debug and str.len < len) { 40 | // std.debug.print("error str {s} with len {} too short. expected len {}\n", .{ str, str.len, len }); 41 | // std.debug.assert(false); 42 | // } 43 | // return @as(Block, str[0..len].*); 44 | // TODO optimize this by ensuring leading len zeroes 45 | var block = mem.zeroes(Block); 46 | @memcpy(block[len - str.len ..], str); 47 | return block; 48 | } 49 | pub fn strToInt(str: []const u8) Int { 50 | return @bitCast(strToBlock(str)); 51 | } 52 | pub fn blockToInt(blk: Block) Int { 53 | return @bitCast(blk); 54 | } 55 | }; 56 | } 57 | 58 | pub const Follows = union(enum) { 59 | /// special case for low number of items 60 | sso: std.BoundedArray(Item, sso_size), 61 | large: struct { 62 | map: Map = .{}, 63 | count: usize = 0, 64 | }, 65 | 66 | pub const empty = Follows{ .sso = .{} }; 67 | pub const sso_size = 8; 68 | 69 | pub const Map = std.ArrayListUnmanaged(Item); 70 | pub const Item = packed struct { 71 | char: u8, 72 | count: u24, 73 | pub fn format(item: Item, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { 74 | try writer.print("{c}-{}", .{ item.char, item.count }); 75 | } 76 | }; 77 | pub fn lessThanByCount(_: void, a: Item, b: Item) bool { 78 | return b.count < a.count; 79 | } 80 | 81 | pub fn deinit(self: *Follows, allocator: mem.Allocator) void { 82 | switch (self.*) { 83 | .sso => {}, 84 | .large => |*payload| payload.map.deinit(allocator), 85 | } 86 | } 87 | 88 | pub fn items(self: *Follows) []Item { 89 | switch (self.*) { 90 | .sso => |*array| return array.slice(), 91 | .large => |payload| return payload.map.items, 92 | } 93 | } 94 | 95 | pub fn constItems(self: *const Follows) []const Item { 96 | switch (self.*) { 97 | .sso => |*array| return array.constSlice(), 98 | .large => |payload| return payload.map.items, 99 | } 100 | } 101 | 102 | pub fn count(self: Follows) usize { 103 | switch (self) { 104 | .sso => |array| { 105 | var result: usize = 0; 106 | for (array.constSlice()) |item| { 107 | result += item.count; 108 | } 109 | return result; 110 | }, 111 | .large => |payload| return payload.count, 112 | } 113 | } 114 | 115 | pub fn addItem(self: *Follows, char: u8, allocator: mem.Allocator) error{OutOfMemory}!void { 116 | for (self.items()) |*item| { 117 | if (item.char == char) { 118 | item.count += 1; 119 | switch (self.*) { 120 | .sso => {}, 121 | .large => |*payload| payload.count += 1, 122 | } 123 | return; 124 | } 125 | } 126 | 127 | const new_item = Item{ .char = char, .count = 1 }; 128 | switch (self.*) { 129 | .sso => |*array| { 130 | array.append(new_item) catch { 131 | // convert small size optimization state into large 132 | var map = try std.ArrayListUnmanaged(Item).initCapacity(allocator, sso_size + 1); 133 | map.appendSliceAssumeCapacity(array.slice()); 134 | map.appendAssumeCapacity(new_item); 135 | const new_state = Follows{ .large = .{ 136 | .map = map, 137 | .count = self.count() + 1, 138 | } }; 139 | self.* = new_state; 140 | }; 141 | }, 142 | .large => |*payload| { 143 | try payload.map.append(allocator, new_item); 144 | payload.count += 1; 145 | }, 146 | } 147 | } 148 | }; 149 | 150 | pub fn Model(comptime byte_len: comptime_int, comptime debug: bool) type { 151 | return struct { 152 | table: Table = .{}, 153 | allocator: mem.Allocator, 154 | rand: std.rand.Random, 155 | 156 | pub const Iter = Iterator(byte_len); 157 | pub const Table = std.AutoArrayHashMapUnmanaged(Iter.Block, Follows); 158 | const Self = @This(); 159 | 160 | pub fn init(allocator: mem.Allocator, rand: std.rand.Random) Self { 161 | return .{ .allocator = allocator, .rand = rand }; 162 | } 163 | pub fn deinit(self: *Self, allocator: mem.Allocator) void { 164 | var iter = self.table.iterator(); 165 | while (iter.next()) |*m| m.value_ptr.deinit(allocator); 166 | self.table.deinit(allocator); 167 | } 168 | pub fn iterator(text: []const u8) Iter { 169 | return .{ .text = text, .index = -byte_len + 1 }; 170 | } 171 | 172 | pub fn feed(self: *Self, input: []const u8) !void { 173 | var iter = iterator(input); 174 | while (iter.next()) |it| { 175 | // std.debug.print("{s}-{c}\n", .{ @bitCast(Iter.Block, it.int), it.next }); 176 | const block: Iter.Block = @bitCast(it.int); 177 | const gop = try self.table.getOrPutValue(self.allocator, block, Follows.empty); 178 | try gop.value_ptr.addItem(it.next, self.allocator); 179 | } 180 | } 181 | pub fn prep(self: *Self) void { 182 | // sort each follow map by frequency descending so that more frequent come first 183 | for (self.table.values()) |*follows| 184 | mem.sortUnstable(Follows.Item, follows.items(), {}, Follows.lessThanByCount); 185 | } 186 | 187 | pub const GenOptions = struct { 188 | // maximum number of bytes to generate not including the start block 189 | maxlen: ?usize, 190 | // optinal starting block. if provided, must have len <= to byte_length 191 | start_block: ?[]const u8 = null, 192 | }; 193 | 194 | /// after using model.feed() and model.prep() this method can be used to generate pseudo random text based on the 195 | /// previous input to feed(). 196 | pub fn gen( 197 | self: *Self, 198 | writer: anytype, 199 | options: GenOptions, 200 | ) !void { 201 | const start_block = if (options.start_block) |start_block| 202 | Iter.strToBlock(if (start_block.len > byte_len) 203 | start_block[start_block.len - byte_len ..] 204 | else 205 | start_block) 206 | else blk: { 207 | const id = self.rand.intRangeLessThan(usize, 0, self.table.count()); 208 | break :blk self.table.keys()[id]; 209 | }; 210 | _ = try writer.write(&start_block); 211 | var int: Iter.Int = @bitCast(start_block); 212 | var i: usize = 0; 213 | while (i < options.maxlen orelse std.math.maxInt(usize)) : (i += 1) { 214 | const block = Iter.intToBlock(int); 215 | const follows = self.table.get(block) orelse blk: { 216 | // TODO recovery idea - do a substring search of self.table.entries for this block 217 | // with leading/trailing zeroes trimmed 218 | const trimmed = std.mem.trim(u8, &block, &.{0}); 219 | for (self.table.keys(), 0..) |block2, j| { 220 | if (mem.endsWith(u8, &block2, trimmed)) 221 | break :blk self.table.values()[j]; 222 | } 223 | // std.debug.print("current block {s} {c}\n", .{ block, block }); 224 | // @panic("TODO: recover somehow"); 225 | // TODO - detect eof 226 | break; 227 | }; 228 | 229 | // pick a random item 230 | var r = self.rand.intRangeAtMost(usize, 0, follows.count()); 231 | const first_follow = follows.constItems()[0]; 232 | var c = first_follow.char; 233 | if (debug) std.debug.print("follows {any} r {}\n", .{ follows.constItems(), r }); 234 | r -|= first_follow.count; 235 | for (follows.constItems()[1..]) |mit| { 236 | if (r == 0) break; 237 | r -|= mit.count; 238 | c = mit.char; 239 | } 240 | 241 | try writer.writeByte(c); 242 | int >>= 8; 243 | int |= @as(Iter.Int, c) << (8 * (Iter.len - 1)); 244 | } 245 | } 246 | }; 247 | } 248 | 249 | // pub fn main() !void { 250 | // var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 251 | // const allr = arena.allocator(); 252 | // var argit = try std.process.ArgIterator.initWithAllocator(allr); 253 | // var prng = std.rand.DefaultPrng.init(@bitCast(u64, std.time.milliTimestamp())); 254 | // const M = Model(build_options.block_len, false); 255 | // var model = M.init(allr, prng.random()); 256 | // _ = argit.next(); 257 | 258 | // var start_block: ?[]const u8 = null; 259 | // var maxlen: ?usize = null; 260 | // while (argit.next()) |arg| { 261 | // if (mem.eql(u8, "--start-block", arg)) { 262 | // start_block = argit.next(); 263 | // continue; 264 | // } 265 | // if (mem.eql(u8, "--maxlen", arg)) { 266 | // maxlen = try std.fmt.parseInt(usize, argit.next().?, 10); 267 | // continue; 268 | // } 269 | // const f = try std.fs.cwd().openFile(arg, .{}); 270 | // defer f.close(); 271 | // var br = std.io.bufferedReader(f.reader()); 272 | // const reader = br.reader(); 273 | // const input = try reader.readAllAlloc(allr, std.math.maxInt(u32)); 274 | // defer allr.free(input); 275 | // try model.feed(input); 276 | // } 277 | 278 | // model.prep(); 279 | // try model.gen( 280 | // std.io.getStdOut().writer(), 281 | // .{ .maxlen = maxlen, .start_block = start_block }, 282 | // ); 283 | // } 284 | -------------------------------------------------------------------------------- /src/utils.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const lsp = @import("zig-lsp"); 3 | const lsp_types = lsp.types; 4 | 5 | /// Use after `isArrayList` and/or `isHashMap` 6 | pub fn isManaged(comptime T: type) bool { 7 | return @hasField(T, "allocator"); 8 | } 9 | 10 | pub fn isArrayList(comptime T: type) bool { 11 | // TODO: Improve this ArrayList check, specifically by actually checking the functions we use 12 | // TODO: Consider unmanaged ArrayLists 13 | if (!@hasField(T, "items")) return false; 14 | if (!@hasField(T, "capacity")) return false; 15 | 16 | return true; 17 | } 18 | 19 | pub fn isHashMap(comptime T: type) bool { 20 | // TODO: Consider unmanaged HashMaps 21 | 22 | if (!@hasDecl(T, "KV")) return false; 23 | 24 | if (!@hasField(T.KV, "key")) return false; 25 | if (!@hasField(T.KV, "value")) return false; 26 | 27 | const Key = std.meta.fields(T.KV)[std.meta.fieldIndex(T.KV, "key") orelse unreachable].type; 28 | const Value = std.meta.fields(T.KV)[std.meta.fieldIndex(T.KV, "value") orelse unreachable].type; 29 | 30 | if (!@hasDecl(T, "put")) return false; 31 | 32 | const put = @typeInfo(@TypeOf(T.put)); 33 | 34 | if (put != .Fn) return false; 35 | 36 | switch (put.Fn.params.len) { 37 | 3 => { 38 | if (put.Fn.params[0].type.? != *T) return false; 39 | if (put.Fn.params[1].type.? != Key) return false; 40 | if (put.Fn.params[2].type.? != Value) return false; 41 | }, 42 | 4 => { 43 | if (put.Fn.params[0].type.? != *T) return false; 44 | if (put.Fn.params[1].type.? != std.mem.Allocator) return false; 45 | if (put.Fn.params[2].type.? != Key) return false; 46 | if (put.Fn.params[3].type.? != Value) return false; 47 | }, 48 | else => return false, 49 | } 50 | 51 | if (put.Fn.return_type == null) return false; 52 | 53 | const put_return = @typeInfo(put.Fn.return_type.?); 54 | if (put_return != .ErrorUnion) return false; 55 | if (put_return.ErrorUnion.payload != void) return false; 56 | 57 | return true; 58 | } 59 | 60 | pub fn randomize( 61 | comptime T: type, 62 | allocator: std.mem.Allocator, 63 | random: std.rand.Random, 64 | ) anyerror!T { 65 | if (T == std.json.Value) { 66 | const Valids = enum { 67 | Null, 68 | Bool, 69 | Integer, 70 | Float, 71 | String, 72 | Array, 73 | Object, 74 | }; 75 | const selection = random.enumValue(Valids); 76 | inline for (@typeInfo(T).Union.fields) |field| { 77 | if (std.mem.eql(u8, field.name, @tagName(selection))) return @unionInit(T, field.name, try randomize(field.type, allocator, random)); 78 | } 79 | unreachable; 80 | } 81 | 82 | return switch (@typeInfo(T)) { 83 | .Void => void{}, 84 | .Bool => random.boolean(), 85 | .Int => switch (T) { 86 | i64 => random.int(i32), 87 | u64 => random.int(u32), 88 | else => random.int(T), 89 | }, 90 | .Float => random.float(T), 91 | .Array => @compileError("bruh"), 92 | .Pointer => b: { 93 | const pi = @typeInfo(T).Pointer; 94 | switch (pi.size) { 95 | .Slice => { 96 | const n = random.intRangeLessThan(usize, 0, 10); 97 | const slice = try allocator.alloc(pi.child, n); 98 | for (slice) |*v| { 99 | if (pi.child == u8) 100 | v.* = random.intRangeLessThan(u8, 32, 126) 101 | else 102 | v.* = try randomize(pi.child, allocator, random); 103 | } 104 | break :b slice; 105 | }, 106 | else => @compileError("non-slice pointers not supported"), 107 | } 108 | }, 109 | .Struct => b: { 110 | if (comptime isArrayList(T)) { 111 | return T.init(allocator); 112 | } 113 | if (comptime isHashMap(T)) { 114 | return T.init(allocator); 115 | } 116 | 117 | var s: T = undefined; 118 | inline for (@typeInfo(T).Struct.fields) |field| { 119 | if (!field.is_comptime) { 120 | if (comptime std.mem.eql(u8, field.name, "uri")) 121 | @field(s, "uri") = "file:///C:/Programming/Zig/buzz-test/hello.zig" 122 | else 123 | @field(s, field.name) = try randomize(comptime field.type, allocator, random); 124 | } 125 | } 126 | break :b s; 127 | }, 128 | .Optional => if (random.boolean()) null else try randomize(@typeInfo(T).Optional.child, allocator, random), 129 | .Enum => random.enumValue(T), 130 | .Union => b: { 131 | const selection = random.intRangeLessThan(usize, 0, @typeInfo(T).Union.fields.len); 132 | inline for (@typeInfo(T).Union.fields, 0..) |field, index| { 133 | if (index == selection) break :b @unionInit(T, field.name, try randomize(field.type, allocator, random)); 134 | } 135 | unreachable; 136 | }, 137 | else => @compileError("not supported: " ++ @typeName(T)), 138 | }; 139 | } 140 | 141 | pub fn randomPosition(random: std.rand.Random, data: []const u8) lsp_types.Position { 142 | // TODO: Consider offsets 143 | 144 | const line_count = std.mem.count(u8, data, "\n"); 145 | const line = if (line_count == 0) 0 else random.intRangeLessThan(usize, 0, line_count); 146 | var lines = std.mem.split(u8, data, "\n"); 147 | 148 | var character: usize = 0; 149 | 150 | var index: usize = 0; 151 | while (lines.next()) |line_content| : (index += 1) { 152 | if (index == line) { 153 | character = if (line_content.len == 0) 0 else random.intRangeLessThan(usize, 0, line_content.len); 154 | break; 155 | } 156 | } 157 | 158 | return .{ 159 | .line = @intCast(line), 160 | .character = @intCast(character), 161 | }; 162 | } 163 | 164 | pub fn randomRange(random: std.rand.Random, data: []const u8) lsp_types.Range { 165 | const a = randomPosition(random, data); 166 | const b = randomPosition(random, data); 167 | 168 | const is_a_first = a.line < b.line or (a.line == b.line and a.character < b.character); 169 | 170 | return if (is_a_first) .{ .start = a, .end = b } else .{ .start = b, .end = a }; 171 | } 172 | --------------------------------------------------------------------------------