commit 0548f8de268f127f6956a483ee25b8ea3bd9f975 Author: drvxor Date: Sun Aug 23 11:11:36 2026 +0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..45ec3bb --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +zig-out +.zig-cache +zig-pkg diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..0f0dc73 --- /dev/null +++ b/build.zig @@ -0,0 +1,39 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const luau_dep = b.dependency("luau", .{ .target = target, .optimize = optimize, .use_zig_backend = false }); + + const exe = b.addExecutable(.{ + .name = "xsh", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }), + }); + + exe.root_module.addIncludePath(b.path("src/include")); + + exe.root_module.addImport( + "luau", + luau_dep.module("root"), + ); + + b.installArtifact(exe); + + const run = b.addRunArtifact(exe); + + if (b.args) |args| { + run.addArgs(args); + } + + const run_step = b.step( + "run", + "Run xsh", + ); + + run_step.dependOn(&run.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..14f63a0 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,19 @@ +.{ + .name = .xsh, + .version = "0.0.0", + + .fingerprint = 0xe2d823a53ce2c535, + .minimum_zig_version = "0.16.0", + + .dependencies = .{ + .luau = .{ + .path = "deps/luau", + }, + }, + + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/deps/luau/LICENSE b/deps/luau/LICENSE new file mode 100644 index 0000000..fe64d9b --- /dev/null +++ b/deps/luau/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Scythe Technology + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. diff --git a/deps/luau/build.zig b/deps/luau/build.zig new file mode 100644 index 0000000..004e3ad --- /dev/null +++ b/deps/luau/build.zig @@ -0,0 +1,1042 @@ +const std = @import("std"); +const zon: ZonConfig = @import("build.zig.zon"); + +const Build = std.Build; +const Step = std.Build.Step; + +pub fn build(b: *Build) !void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + // Remove the default install and uninstall steps + b.top_level_steps = .{}; + + var tag_parts = std.mem.splitBackwardsScalar(u8, zon.dependencies.luau.url, '#'); + var version_parts = std.mem.splitScalar(u8, tag_parts.first(), '.'); + const major_parsed = try std.json.parseFromSlice(u32, b.allocator, version_parts.next().?, .{}); + const minor_parsed = try std.json.parseFromSlice(u32, b.allocator, version_parts.next().?, .{}); + + const major = major_parsed.value; + major_parsed.deinit(); + const minor = minor_parsed.value; + minor_parsed.deinit(); + + const version = std.SemanticVersion{ .major = major, .minor = minor, .patch = 0 }; + + const luau_dep = b.dependency("luau", .{}); + + const build_Ast = b.option(bool, "Ast", "Build Luau Ast") orelse true; + const build_CodeGen = b.option(bool, "CodeGen", "Build Luau CodeGen") orelse switch (target.result.cpu.arch) { + .x86_64, .aarch64 => true, + else => false, + }; + const build_Analysis = b.option(bool, "Analysis", "Build Luau Analysis") orelse switch (target.result.cpu.arch) { + .wasm32, .wasm64 => false, + .powerpc64, .powerpc64le => false, + .loongarch64 => false, + else => true, + }; + const build_Compiler = b.option(bool, "Compiler", "Build Luau Compiler") orelse true; + const build_VM = b.option(bool, "VM", "Build Luau VM") orelse true; + const build_Inliner = b.option(bool, "Inliner", "Build Luau Inliner") orelse true; + + const use_zig_backend = b.option(bool, "use_zig_backend", "Build Luau with zig written backend") orelse true; + const use_4_vector = b.option(bool, "use_4_vector", "Build Luau to use 4-vectors instead of the default 3-vector.") orelse false; + + const use_longjmp = b.option(bool, "use_longjmp", "Build Luau with SJLJ instead of exceptions (applies to CodeGen and VM)") orelse true; + + const wasm_cxa_exceptions = b.option(bool, "wasm_cxa_exceptions", "Enable exception cxa implementation") orelse true; + + const hard_stack_tests = b.option(bool, "hard_stack_tests", "Enable hard stack tests") orelse false; + const hard_mem_tests = b.option(u8, "hard_mem_tests", "Enable hard memory tests") orelse 0; + + const no_llvm = b.option(bool, "no_llvm", "Build without llvm (tests only + best with zig backend)") orelse false; + const no_bin = b.option(bool, "no_bin", "Build without binary artifacts") orelse false; + + const cxxflags = b.option([]const []const u8, "cxxflag", "Build C/C++ compile flags") orelse &.{}; + + // Expose build configuration to the zig-luau module + const config = b.addOptions(); + config.addOption(bool, "use_4_vector", use_4_vector); + config.addOption(bool, "use_zig_backend", use_zig_backend); + config.addOption(u8, "hard_mem_tests", hard_mem_tests); + config.addOption(bool, "hard_stack_tests", hard_stack_tests); + config.addOption(bool, "wasm_cxa_exceptions", wasm_cxa_exceptions); + config.addOption(std.SemanticVersion, "luau_version", version); + + config.addOption(bool, "buildAst", build_Ast); + config.addOption(bool, "buildCodeGen", build_CodeGen); + config.addOption(bool, "buildAnalysis", build_Analysis); + config.addOption(bool, "buildCompiler", build_Compiler); + config.addOption(bool, "buildVM", build_VM); + config.addOption(bool, "buildInliner", build_Inliner); + + // Luau C Headers + const headers = b.addTranslateC(.{ + .root_source_file = b.path("src/bridge.h"), + .target = target, + .optimize = optimize, + }); + headers.addIncludePath(luau_dep.path("Compiler/include")); + headers.addIncludePath(luau_dep.path("VM/include")); + switch (target.result.cpu.arch) { + .x86_64, .aarch64 => headers.addIncludePath(luau_dep.path("CodeGen/include")), + else => {}, + } + + const c_module = headers.createModule(); + + var FLAGS: std.ArrayList([]const u8) = .empty; + + try FLAGS.append(b.allocator, "-DLUA_USE_LONGJMP=" ++ if (use_longjmp) "1" else "0"); + try FLAGS.append(b.allocator, "-DLUA_API=extern\"C\""); + try FLAGS.append(b.allocator, "-DLUACODE_API=extern\"C\""); + try FLAGS.append(b.allocator, "-DLUACODEGEN_API=extern\"C\""); + try FLAGS.append(b.allocator, "-DLUAJITINLINER_API=extern\"C\""); + if (hard_mem_tests > 0) + try FLAGS.append(b.allocator, b.fmt("-DHARDMEMTESTS={d}", .{hard_mem_tests})); + if (hard_stack_tests) + try FLAGS.append(b.allocator, "-DHARDSTACKTESTS"); + if (use_4_vector) + try FLAGS.append(b.allocator, "-DLUA_VECTOR_SIZE=4"); + + for (cxxflags) |flag| { + try FLAGS.append(b.allocator, flag); + } + + const compile_flags = FLAGS.items; + + const libCommon = buildCommon(b, target, luau_dep, optimize, version, compile_flags); + const libAst = buildAst(b, target, luau_dep, optimize, version, compile_flags, libCommon); + const libBytecode = buildBytecode(b, target, luau_dep, optimize, version, compile_flags, libCommon); + const libCompiler = buildCompiler(b, target, luau_dep, optimize, version, compile_flags, libAst, libBytecode); + const libVM = buildVM(b, target, luau_dep, optimize, version, compile_flags, libCommon); + const libInliner = buildInliner(b, target, luau_dep, optimize, version, compile_flags, libVM, libBytecode); + const libConfig = buildConfig(b, target, luau_dep, optimize, version, compile_flags, libCommon, libAst, libCompiler, libVM); + const libCodeGen = buildCodeGen(b, target, luau_dep, optimize, version, compile_flags, libVM); + const libAnalysis = try buildAnalysis(b, target, luau_dep, optimize, version, compile_flags, libAst, libConfig, libCompiler, libVM); + + const mod = b.addModule("root", .{ + .root_source_file = b.path("src/lib.zig"), + .target = target, + .optimize = optimize, + .link_libcpp = true, + }); + mod.addImport("c", c_module); + mod.addOptions("config", config); + + const vector_size: usize = if (use_4_vector) 4 else 3; + mod.addCMacro("LUA_VECTOR_SIZE", b.fmt("{}", .{vector_size})); + + mod.addIncludePath(b.path("src")); + + mod.addCSourceFile(.{ .file = b.path("src/bridge.cpp"), .flags = compile_flags }); + + if (build_Ast) { + linkIncludePath(mod, libAst); + mod.linkLibrary(libAst); + mod.addCSourceFiles(.{ + .flags = compile_flags, + .root = b.path("src/Ast/"), + .files = if (optimize == .Debug or optimize == .ReleaseSafe) &.{ + "Allocator.cpp", + "Lexer.cpp", + "Parser.cpp", + "Class.cpp", + } else &.{ + "Allocator.cpp", + "Lexer.cpp", + "Parser.cpp", + }, + }); + } + if (build_Analysis) { + linkIncludePath(mod, libAnalysis); + mod.linkLibrary(libAnalysis); + mod.addCSourceFiles(.{ + .flags = compile_flags, + .root = b.path("src/Analysis/"), + .files = &.{ + "FileUtils.cpp", + "AstJsonEncoder.cpp", + "Frontend.cpp", + "FileResolver.cpp", + "GenericConfigResolver.cpp", + }, + }); + } + if (build_CodeGen) { + linkIncludePath(mod, libCodeGen); + mod.linkLibrary(libCodeGen); + } + if (build_Compiler) { + linkIncludePath(mod, libCompiler); + mod.linkLibrary(libCompiler); + mod.addCSourceFile(.{ + .file = b.path("src/Compiler/Compiler.cpp"), + .flags = compile_flags, + }); + } + if (build_VM) { + linkIncludePath(mod, libVM); + mod.linkLibrary(libVM); + if (optimize == .Debug or optimize == .ReleaseSafe) { + mod.addCSourceFile(.{ + .file = b.path("src/VM/acc.cpp"), + .flags = compile_flags, + }); + mod.addIncludePath(luau_dep.path("VM/src")); + } + } + if (build_Inliner) { + linkIncludePath(mod, libInliner); + mod.linkLibrary(libInliner); + } + + const lib = b.addLibrary(.{ + .name = "luau", + .root_module = mod, + .linkage = .static, + .version = version, + }); + + // It may not be as likely that other software links against Luau, but might as well expose these anyway + lib.installHeader(luau_dep.path("VM/include/lua.h"), "lua.h"); + lib.installHeader(luau_dep.path("VM/include/lualib.h"), "lualib.h"); + lib.installHeader(luau_dep.path("VM/include/luaconf.h"), "luaconf.h"); + if (build_CodeGen) + lib.installHeader(luau_dep.path("CodeGen/include/luacodegen.h"), "luacodegen.h"); + + if (!no_bin) + b.installArtifact(lib); + + const lib_tests = b.addTest(.{ + .name = if (use_zig_backend) "zig-lib-tests" else "lib-tests", + .root_module = mod, + .use_llvm = !no_llvm, + }); + + // Tests + const tests = b.addTest(.{ + .name = if (use_zig_backend) "zig-tests" else "tests", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/tests.zig"), + .target = target, + .optimize = optimize, + }), + .use_llvm = !no_llvm, + }); + tests.root_module.addImport("luau", mod); + + const run_lib_tests = b.addRunArtifact(lib_tests); + const run_tests = b.addRunArtifact(tests); + const test_step = b.step("test", "Run zig-luau tests"); + test_step.dependOn(&run_lib_tests.step); + test_step.dependOn(&run_tests.step); + + if (!no_bin) { + b.installArtifact(lib_tests); + b.installArtifact(tests); + } + + // Examples + const examples = [_]struct { []const u8, []const u8 }{ + .{ "luau-bytecode", "examples/luau-bytecode.zig" }, + .{ "repl", "examples/repl.zig" }, + .{ "zig-fn", "examples/zig-fn.zig" }, + }; + + for (examples) |example| { + const exe = b.addExecutable(.{ + .name = example[0], + .root_module = b.createModule(.{ + .root_source_file = b.path(example[1]), + .target = target, + .optimize = optimize, + }), + }); + exe.root_module.addImport("luau", mod); + + const artifact = b.addInstallArtifact(exe, .{}); + const exe_step = b.step(b.fmt("install-example-{s}", .{example[0]}), b.fmt("Install {s} example", .{example[0]})); + exe_step.dependOn(&artifact.step); + + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| + run_cmd.addArgs(args); + + const run_step = b.step(b.fmt("run-example-{s}", .{example[0]}), b.fmt("Run {s} example", .{example[0]})); + run_step.dependOn(&run_cmd.step); + } + + const docs = b.addLibrary(.{ + .name = "luau", + .root_module = mod, + }); + + const install_docs = b.addInstallDirectory(.{ + .source_dir = docs.getEmittedDocs(), + .install_dir = .prefix, + .install_subdir = "docs", + }); + + const docs_step = b.step("docs", "Build and install the documentation"); + docs_step.dependOn(&install_docs.step); +} + +fn buildAndLinkModule( + b: *Build, + target: Build.ResolvedTarget, + dependency: *Build.Dependency, + module: *Build.Module, + config: *Step.Options, + c_module: *Build.Module, + lib: *Step.Compile, + use_4_vector: bool, +) !void { + module.addImport("c", c_module); + + module.addOptions("config", config); + + const vector_size: usize = if (use_4_vector) 4 else 3; + module.addCMacro("LUA_VECTOR_SIZE", b.fmt("{}", .{vector_size})); + + module.addIncludePath(dependency.path("Compiler/include")); + module.addIncludePath(dependency.path("VM/include")); + switch (target.result.cpu.arch) { + .x86_64, .aarch64 => module.addIncludePath(dependency.path("CodeGen/include")), + else => {}, + } + + module.linkLibrary(lib); +} + +pub fn addModuleExportSymbols(b: *Build, module: *Build.Module) void { + if (module.resolved_target.?.result.cpu.arch.isWasm()) { + var old_export_symbols = std.ArrayList([]const u8).init(b.allocator); + old_export_symbols.appendSlice(module.export_symbol_names) catch @panic("OOM"); + old_export_symbols.appendSlice(&.{ + "zig_luau_try_impl", + "zig_luau_catch_impl", + }) catch @panic("OOM"); + module.export_symbol_names = old_export_symbols.toOwnedSlice() catch @panic("OOM"); + } +} + +fn linkIncludePath( + module: *Build.Module, + source: *Step.Compile, +) void { + for (source.root_module.include_dirs.items) |dir| + switch (dir) { + .path => |path| blk: { + for (module.include_dirs.items) |target_dir| + if (target_dir == .path and + path == .dependency and target_dir.path == .dependency and + path.dependency.dependency == target_dir.path.dependency.dependency and + std.mem.eql(u8, path.dependency.sub_path, target_dir.path.dependency.sub_path)) + break :blk; + + module.addIncludePath(path); + }, + else => {}, + }; +} + +fn buildCommon( + b: *Build, + target: Build.ResolvedTarget, + dependency: *Build.Dependency, + optimize: std.builtin.OptimizeMode, + version: std.SemanticVersion, + flags: []const []const u8, +) *Step.Compile { + const mod = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libcpp = true, + }); + + const lib = b.addLibrary(.{ + .name = "Common", + .linkage = .static, + .root_module = mod, + .version = version, + }); + + for (LUAU_Common_HEADERS_DIRS) |dir| + mod.addIncludePath(dependency.path(dir)); + + mod.addCSourceFiles(.{ + .root = dependency.path(""), + .files = &LUAU_Common_SOURCE_FILES, + .flags = flags, + }); + + return lib; +} + +fn buildAst( + b: *Build, + target: Build.ResolvedTarget, + dependency: *Build.Dependency, + optimize: std.builtin.OptimizeMode, + version: std.SemanticVersion, + flags: []const []const u8, + libCommon: *Step.Compile, +) *Step.Compile { + const mod = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libcpp = true, + }); + + const lib = b.addLibrary(.{ + .name = "Ast", + .linkage = .static, + .root_module = mod, + .version = version, + }); + + linkIncludePath(mod, libCommon); + mod.linkLibrary(libCommon); + + for (LUAU_Ast_HEADERS_DIRS) |dir| + mod.addIncludePath(dependency.path(dir)); + + mod.addCSourceFiles(.{ + .root = dependency.path(""), + .files = &LUAU_Ast_SOURCE_FILES, + .flags = flags, + }); + + return lib; +} + +fn buildBytecode( + b: *Build, + target: Build.ResolvedTarget, + dependency: *Build.Dependency, + optimize: std.builtin.OptimizeMode, + version: std.SemanticVersion, + flags: []const []const u8, + libCommon: *Step.Compile, +) *Step.Compile { + const mod = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libcpp = true, + }); + + const lib = b.addLibrary(.{ + .name = "Bytecode", + .linkage = .static, + .root_module = mod, + .version = version, + }); + + linkIncludePath(mod, libCommon); + mod.linkLibrary(libCommon); + + for (LUAU_Bytecode_HEADERS_DIRS) |dir| + mod.addIncludePath(dependency.path(dir)); + + mod.addCSourceFiles(.{ + .root = dependency.path(""), + .files = &LUAU_Bytecode_SOURCE_FILES, + .flags = flags, + }); + + return lib; +} + +fn buildCompiler( + b: *Build, + target: Build.ResolvedTarget, + dependency: *Build.Dependency, + optimize: std.builtin.OptimizeMode, + version: std.SemanticVersion, + flags: []const []const u8, + libAst: *Step.Compile, + libBytecode: *Step.Compile, +) *Step.Compile { + const mod = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libcpp = true, + }); + + const lib = b.addLibrary(.{ + .name = "Compiler", + .linkage = .static, + .root_module = mod, + .version = version, + }); + + linkIncludePath(mod, libAst); + linkIncludePath(mod, libBytecode); + mod.linkLibrary(libAst); + mod.linkLibrary(libBytecode); + + for (LUAU_Compiler_HEADERS_DIRS) |dir| + mod.addIncludePath(dependency.path(dir)); + + mod.addCSourceFiles(.{ + .root = dependency.path(""), + .files = &LUAU_Compiler_SOURCE_FILES, + .flags = flags, + }); + + return lib; +} + +fn buildConfig( + b: *Build, + target: Build.ResolvedTarget, + dependency: *Build.Dependency, + optimize: std.builtin.OptimizeMode, + version: std.SemanticVersion, + flags: []const []const u8, + libCommon: *Step.Compile, + libAst: *Step.Compile, + libCompiler: *Step.Compile, + libVM: *Step.Compile, +) *Step.Compile { + const mod = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libcpp = true, + }); + + const lib = b.addLibrary(.{ + .name = "Config", + .linkage = .static, + .root_module = mod, + .version = version, + }); + + linkIncludePath(mod, libCommon); + linkIncludePath(mod, libAst); + linkIncludePath(mod, libCompiler); + linkIncludePath(mod, libVM); + + mod.linkLibrary(libCommon); + mod.linkLibrary(libAst); + mod.linkLibrary(libCompiler); + mod.linkLibrary(libVM); + + for (LUAU_Config_HEADERS_DIRS) |dir| + mod.addIncludePath(dependency.path(dir)); + + mod.addCSourceFiles(.{ + .root = dependency.path(""), + .files = &LUAU_Config_SOURCE_FILES, + .flags = flags, + }); + + return lib; +} + +fn buildAnalysis( + b: *Build, + target: Build.ResolvedTarget, + dependency: *Build.Dependency, + optimize: std.builtin.OptimizeMode, + version: std.SemanticVersion, + flags: []const []const u8, + libAst: *Step.Compile, + libConfig: *Step.Compile, + libCompiler: *Step.Compile, + libVM: *Step.Compile, +) !*Step.Compile { + const mod = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libcpp = true, + }); + + const lib = b.addLibrary(.{ + .name = "Analysis", + .linkage = .static, + .root_module = mod, + .version = version, + }); + + linkIncludePath(mod, libAst); + linkIncludePath(mod, libConfig); + linkIncludePath(mod, libCompiler); + linkIncludePath(mod, libVM); + + mod.linkLibrary(libAst); + mod.linkLibrary(libConfig); + mod.linkLibrary(libCompiler); + mod.linkLibrary(libVM); + + for (LUAU_Analysis_HEADERS_DIRS) |dir| + mod.addIncludePath(dependency.path(dir)); + + mod.addCSourceFiles(.{ + .root = dependency.path(""), + .files = &LUAU_Analysis_SOURCE_FILES, + .flags = flags, + }); + + return lib; +} + +fn buildCodeGen( + b: *Build, + target: Build.ResolvedTarget, + dependency: *Build.Dependency, + optimize: std.builtin.OptimizeMode, + version: std.SemanticVersion, + flags: []const []const u8, + libVM: *Step.Compile, +) *Step.Compile { + const mod = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libcpp = true, + }); + + const lib = b.addLibrary(.{ + .name = "CodeGen", + .linkage = .static, + .root_module = mod, + .version = version, + }); + + linkIncludePath(mod, libVM); + mod.linkLibrary(libVM); + + for (LUAU_CodeGen_HEADERS_DIRS) |dir| + mod.addIncludePath(dependency.path(dir)); + + mod.addCSourceFiles(.{ + .root = dependency.path(""), + .files = &LUAU_CodeGen_SOURCE_FILES, + .flags = flags, + }); + + return lib; +} + +fn buildVM( + b: *Build, + target: Build.ResolvedTarget, + dependency: *Build.Dependency, + optimize: std.builtin.OptimizeMode, + version: std.SemanticVersion, + flags: []const []const u8, + libCommon: *Step.Compile, +) *Step.Compile { + const mod = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libcpp = true, + }); + + const lib = b.addLibrary(.{ + .name = "VM", + .linkage = .static, + .root_module = mod, + .version = version, + }); + + linkIncludePath(mod, libCommon); + + for (LUAU_VM_HEADERS_DIRS) |dir| + mod.addIncludePath(dependency.path(dir)); + + mod.addCSourceFiles(.{ + .root = dependency.path(""), + .files = &LUAU_VM_SOURCE_FILES, + .flags = flags, + }); + + return lib; +} + +fn buildInliner( + b: *Build, + target: Build.ResolvedTarget, + dependency: *Build.Dependency, + optimize: std.builtin.OptimizeMode, + version: std.SemanticVersion, + flags: []const []const u8, + libVM: *Step.Compile, + libBytecode: *Step.Compile, +) *Step.Compile { + const mod = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libcpp = true, + }); + + const lib = b.addLibrary(.{ + .name = "Inliner", + .linkage = .static, + .root_module = mod, + .version = version, + }); + + linkIncludePath(mod, libVM); + linkIncludePath(mod, libBytecode); + mod.linkLibrary(libVM); + mod.linkLibrary(libBytecode); + + for (LUAU_Inliner_HEADERS_DIRS) |dir| + mod.addIncludePath(dependency.path(dir)); + + mod.addCSourceFiles(.{ + .root = dependency.path(""), + .files = &LUAU_Inliner_SOURCE_FILES, + .flags = flags, + }); + + return lib; +} + +fn buildRequire( + b: *Build, + target: Build.ResolvedTarget, + dependency: *Build.Dependency, + optimize: std.builtin.OptimizeMode, + version: std.SemanticVersion, + flags: []const []const u8, + libVM: *Step.Compile, + libRequireNavigator: *Step.Compile, +) *Step.Compile { + const mod = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libcpp = true, + }); + + const lib = b.addLibrary(.{ + .name = "Require", + .target = target, + .optimize = optimize, + .version = version, + }); + + linkIncludePath(mod, libVM); + linkIncludePath(mod, libRequireNavigator); + + for (LUAU_Require_HEADERS_DIRS) |dir| + mod.addIncludePath(dependency.path(dir)); + + mod.addCSourceFiles(.{ + .root = dependency.path(""), + .files = &LUAU_Require_SOURCE_FILES, + .flags = flags, + }); + + return lib; +} + +fn buildRequireNavigator( + b: *Build, + target: Build.ResolvedTarget, + dependency: *Build.Dependency, + optimize: std.builtin.OptimizeMode, + version: std.SemanticVersion, + flags: []const []const u8, + libConfig: *Step.Compile, +) *Step.Compile { + const mod = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libcpp = true, + }); + + const lib = b.addLibrary(.{ + .name = "RequireNavigator", + .target = target, + .optimize = optimize, + .version = version, + }); + + linkIncludePath(mod, libConfig); + + mod.linkLibCpp(); + + for (LUAU_RequireNavigator_HEADERS_DIRS) |dir| + mod.addIncludePath(dependency.path(dir)); + + mod.addCSourceFiles(.{ + .root = dependency.path(""), + .files = &LUAU_RequireNavigator_SOURCE_FILES, + .flags = flags, + }); + + return lib; +} + +const LUAU_Analysis_HEADERS_DIRS = [_][]const u8{ + "Analysis/include/", + "Analysis/src/", +}; +const LUAU_Analysis_SOURCE_FILES = [_][]const u8{ + "Analysis/src/Anyification.cpp", + "Analysis/src/ApplyTypeFunction.cpp", + "Analysis/src/AstJsonEncoder.cpp", + "Analysis/src/AstQuery.cpp", + "Analysis/src/AstUtils.cpp", + "Analysis/src/Autocomplete.cpp", + "Analysis/src/AutocompleteCore.cpp", + "Analysis/src/BuiltinDefinitions.cpp", + "Analysis/src/BuiltinTypeFunctions.cpp", + "Analysis/src/Clone.cpp", + "Analysis/src/Constraint.cpp", + "Analysis/src/ConstraintGenerator.cpp", + "Analysis/src/ConstraintGraph.cpp", + "Analysis/src/ConstraintSolver.cpp", + "Analysis/src/ControlFlowGraph.cpp", + "Analysis/src/DataFlowGraph.cpp", + "Analysis/src/DcrLogger.cpp", + "Analysis/src/DumpCFG.cpp", + "Analysis/src/Def.cpp", + "Analysis/src/EmbeddedBuiltinDefinitions.cpp", + "Analysis/src/Error.cpp", + "Analysis/src/ExpectedTypeVisitor.cpp", + "Analysis/src/FileResolver.cpp", + "Analysis/src/FragmentAutocomplete.cpp", + "Analysis/src/Frontend.cpp", + "Analysis/src/Generalization.cpp", + "Analysis/src/GlobalTypes.cpp", + "Analysis/src/Instantiation.cpp", + "Analysis/src/Instantiation2.cpp", + "Analysis/src/IostreamHelpers.cpp", + "Analysis/src/IterativeTypeVisitor.cpp", + "Analysis/src/IterativeTypeFunctionTypeVisitor.cpp", + "Analysis/src/JsonEmitter.cpp", + "Analysis/src/Linter.cpp", + "Analysis/src/LValue.cpp", + "Analysis/src/Module.cpp", + "Analysis/src/NativeStackGuard.cpp", + "Analysis/src/NonStrictTypeChecker.cpp", + "Analysis/src/Normalize.cpp", + "Analysis/src/OverloadResolver.cpp", + "Analysis/src/Quantify.cpp", + "Analysis/src/RecursionCounter.cpp", + "Analysis/src/Refinement.cpp", + "Analysis/src/RequireTracer.cpp", + "Analysis/src/Scope.cpp", + "Analysis/src/Simplify.cpp", + "Analysis/src/StructuralTypeEquality.cpp", + "Analysis/src/Substitution.cpp", + "Analysis/src/Subtyping.cpp", + "Analysis/src/SubtypingUnifier.cpp", + "Analysis/src/Symbol.cpp", + "Analysis/src/TableLiteralInference.cpp", + "Analysis/src/ToDot.cpp", + "Analysis/src/TopoSortStatements.cpp", + "Analysis/src/ToString.cpp", + "Analysis/src/TxnLog.cpp", + "Analysis/src/Type.cpp", + "Analysis/src/TypeArena.cpp", + "Analysis/src/TypeAttach.cpp", + "Analysis/src/TypeChecker2.cpp", + "Analysis/src/TypedAllocator.cpp", + "Analysis/src/TypeFunction.cpp", + "Analysis/src/TypeFunctionError.cpp", + "Analysis/src/TypeFunctionReductionGuesser.cpp", + "Analysis/src/TypeFunctionRuntime.cpp", + "Analysis/src/TypeFunctionRuntimeBuilder.cpp", + "Analysis/src/TypeIds.cpp", + "Analysis/src/TypeInfer.cpp", + "Analysis/src/TypeOrPack.cpp", + "Analysis/src/TypePack.cpp", + "Analysis/src/TypePath.cpp", + "Analysis/src/TypeStateMap.cpp", + "Analysis/src/TypeUtils.cpp", + "Analysis/src/Unifiable.cpp", + "Analysis/src/Unifier.cpp", + "Analysis/src/Unifier2.cpp", + "Analysis/src/UserDefinedTypeFunction.cpp", +}; + +const LUAU_Ast_HEADERS_DIRS = [_][]const u8{ + "Ast/include/", +}; +const LUAU_Ast_SOURCE_FILES = [_][]const u8{ + "Ast/src/Ast.cpp", + "Ast/src/Cst.cpp", + "Ast/src/Allocator.cpp", + "Ast/src/Confusables.cpp", + "Ast/src/Lexer.cpp", + "Ast/src/Location.cpp", + "Ast/src/Parser.cpp", + "Ast/src/PrettyPrinter.cpp", +}; + +const LUAU_Bytecode_HEADERS_DIRS = [_][]const u8{ + "Bytecode/include/", + "Bytecode/src/", +}; +const LUAU_Bytecode_SOURCE_FILES = [_][]const u8{ + "Bytecode/src/BytecodeBuilder.cpp", + "Bytecode/src/BytecodeGraph.cpp", + "Bytecode/src/Sccp.cpp", +}; + +const LUAU_Inliner_HEADERS_DIRS = [_][]const u8{ + "Inliner/include/", + "Inliner/src/", +}; +const LUAU_Inliner_SOURCE_FILES = [_][]const u8{ + "Inliner/src/JitInliner.cpp", + "Inliner/src/luajitinliner.cpp", +}; + +const LUAU_CodeGen_HEADERS_DIRS = [_][]const u8{ + "CodeGen/include/", + "CodeGen/src/", +}; +const LUAU_CodeGen_SOURCE_FILES = [_][]const u8{ + "CodeGen/src/AssemblyBuilderA64.cpp", + "CodeGen/src/AssemblyBuilderX64.cpp", + "CodeGen/src/CodeAllocator.cpp", + "CodeGen/src/CodeBlockUnwind.cpp", + "CodeGen/src/CodeGen.cpp", + "CodeGen/src/CodeGenAssembly.cpp", + "CodeGen/src/CodeGenContext.cpp", + "CodeGen/src/CodeGenUtils.cpp", + "CodeGen/src/CodeGenA64.cpp", + "CodeGen/src/CodeGenX64.cpp", + "CodeGen/src/EmitBuiltinsX64.cpp", + "CodeGen/src/EmitCommonX64.cpp", + "CodeGen/src/EmitInstructionX64.cpp", + "CodeGen/src/IrAnalysis.cpp", + "CodeGen/src/IrBuilder.cpp", + "CodeGen/src/IrCallWrapperX64.cpp", + "CodeGen/src/IrDump.cpp", + "CodeGen/src/IrLoweringA64.cpp", + "CodeGen/src/IrLoweringX64.cpp", + "CodeGen/src/IrRegAllocA64.cpp", + "CodeGen/src/IrRegAllocX64.cpp", + "CodeGen/src/IrTranslateBuiltins.cpp", + "CodeGen/src/IrTranslation.cpp", + "CodeGen/src/IrUtils.cpp", + "CodeGen/src/IrValueLocationTracking.cpp", + "CodeGen/src/lcodegen.cpp", + "CodeGen/src/NativeProtoExecData.cpp", + "CodeGen/src/NativeState.cpp", + "CodeGen/src/OptimizeConstProp.cpp", + "CodeGen/src/OptimizeDeadStore.cpp", + "CodeGen/src/OptimizeFinalX64.cpp", + "CodeGen/src/UnwindBuilderDwarf2.cpp", + "CodeGen/src/UnwindBuilderWin.cpp", + "CodeGen/src/BytecodeAnalysis.cpp", + "CodeGen/src/BytecodeSummary.cpp", + "CodeGen/src/SharedCodeAllocator.cpp", +}; + +const LUAU_Common_HEADERS_DIRS = [_][]const u8{ + "Common/include/", +}; +const LUAU_Common_SOURCE_FILES = [_][]const u8{ + "Common/src/StringUtils.cpp", + "Common/src/TimeTrace.cpp", + "Common/src/BytecodeWire.cpp", +}; + +const LUAU_Compiler_HEADERS_DIRS = [_][]const u8{ + "Compiler/include/", + "Compiler/src/", +}; +const LUAU_Compiler_SOURCE_FILES = [_][]const u8{ + "Compiler/src/BuiltinFolding.cpp", + "Compiler/src/Builtins.cpp", + "Compiler/src/Compiler.cpp", + "Compiler/src/ConstantFolding.cpp", + "Compiler/src/CostModel.cpp", + "Compiler/src/TableShape.cpp", + "Compiler/src/Types.cpp", + "Compiler/src/ValueTracking.cpp", + "Compiler/src/lcode.cpp", +}; + +const LUAU_Config_HEADERS_DIRS = [_][]const u8{ + "Config/include/", +}; +const LUAU_Config_SOURCE_FILES = [_][]const u8{ + "Config/src/Config.cpp", + "Config/src/LuauConfig.cpp", + "Config/src/LinterConfig.cpp", +}; + +const LUAU_Require_HEADERS_DIRS = [_][]const u8{ + "Require/Runtime/include/", + "Require/Runtime/src/", +}; +const LUAU_Require_SOURCE_FILES = [_][]const u8{ + "Require/Runtime/src/Navigation.cpp", + "Require/Runtime/src/Require.cpp", + "Require/Runtime/src/RequireImpl.cpp", +}; + +const LUAU_RequireNavigator_HEADERS_DIRS = [_][]const u8{ + "Require/Navigator/include/", +}; +const LUAU_RequireNavigator_SOURCE_FILES = [_][]const u8{ + "Require/Navigator/src/PathUtilities.cpp", + "Require/Navigator/src/RequireNavigator.cpp", +}; + +const LUAU_VM_HEADERS_DIRS = [_][]const u8{ + "VM/include/", + "VM/src/", +}; + +const LUAU_VM_SOURCE_FILES = [_][]const u8{ + "VM/src/lapi.cpp", + "VM/src/laux.cpp", + "VM/src/lbaselib.cpp", + "VM/src/lbitlib.cpp", + "VM/src/lbuffer.cpp", + "VM/src/lbuflib.cpp", + "VM/src/lbuiltins.cpp", + "VM/src/lclass.cpp", + "VM/src/lclasslib.cpp", + "VM/src/lcorolib.cpp", + "VM/src/ldblib.cpp", + "VM/src/ldebug.cpp", + "VM/src/ldo.cpp", + "VM/src/lfunc.cpp", + "VM/src/lgc.cpp", + "VM/src/lgcdebug.cpp", + "VM/src/linit.cpp", + "VM/src/lintlib.cpp", + "VM/src/lmathlib.cpp", + "VM/src/lmem.cpp", + "VM/src/lnumprint.cpp", + "VM/src/lobject.cpp", + "VM/src/loslib.cpp", + "VM/src/lperf.cpp", + "VM/src/lstate.cpp", + "VM/src/lstring.cpp", + "VM/src/lstrlib.cpp", + "VM/src/ltable.cpp", + "VM/src/ltablib.cpp", + "VM/src/ltm.cpp", + "VM/src/ludata.cpp", + "VM/src/lutf8lib.cpp", + "VM/src/lvmexecute.cpp", + "VM/src/lveclib.cpp", + "VM/src/lvmload.cpp", + "VM/src/lvmutils.cpp", +}; + +const ZonConfig = struct { + name: enum { luau }, + fingerprint: u64, + version: []const u8, + minimum_zig_version: []const u8, + dependencies: struct { + luau: struct { url: []const u8, hash: []const u8 }, + }, + paths: []const []const u8, +}; diff --git a/deps/luau/build.zig.zon b/deps/luau/build.zig.zon new file mode 100644 index 0000000..83c5ed4 --- /dev/null +++ b/deps/luau/build.zig.zon @@ -0,0 +1,18 @@ +.{ + .name = .luau, + .fingerprint = 0x5e07e249a70630b8, + .version = "0.0.0+730", + .minimum_zig_version = "0.16.0", + .dependencies = .{ + .luau = .{ + .url = "git+https://github.com/luau-lang/luau#0.730", + .hash = "N-V-__8AANL5zQDipKKyPgqjHf_oS7MBYpGH4D7DmYQ3GKRv", + }, + }, + .paths = .{ + "src", + "LICENSE", + "build.zig", + "build.zig.zon", + }, +} diff --git a/deps/luau/src/Analysis/AstJsonEncoder.cpp b/deps/luau/src/Analysis/AstJsonEncoder.cpp new file mode 100644 index 0000000..c1871ca --- /dev/null +++ b/deps/luau/src/Analysis/AstJsonEncoder.cpp @@ -0,0 +1,23 @@ +#include + +#include "Luau/AstJsonEncoder.h" + +#define ZIG_LUAU_ANALYSIS(name) ZIG_FN(Luau_Analysis_##name) + +ZIG_EXPORT const char* ZIG_LUAU_ANALYSIS(AstJsonEncoder_toJson)(Luau::AstNode* node, size_t* len) +{ + std::string res = Luau::toJson(node); + + char* copy = static_cast(malloc(res.size())); + if (!copy) + return nullptr; + + memcpy(copy, res.data(), res.size()); + *len = res.size(); + return copy; +} + +ZIG_EXPORT void ZIG_LUAU_ANALYSIS(AstJsonEncoder_free)(const char* json) +{ + free((void*)json); +} diff --git a/deps/luau/src/Analysis/AstJsonEncoder.zig b/deps/luau/src/Analysis/AstJsonEncoder.zig new file mode 100644 index 0000000..60717a9 --- /dev/null +++ b/deps/luau/src/Analysis/AstJsonEncoder.zig @@ -0,0 +1,46 @@ +const std = @import("std"); + +const Ast = @import("../Ast/Ast.zig"); + +extern "c" fn zig_Luau_Analysis_AstJsonEncoder_toJson(*Ast.Node, *usize) [*c]const u8; +extern "c" fn zig_Luau_Analysis_AstJsonEncoder_free([*c]const u8) void; + +pub fn toJson(allocator: std.mem.Allocator, node: *Ast.Node) ![]const u8 { + var size: usize = 0; + const json = zig_Luau_Analysis_AstJsonEncoder_toJson(node, &size); + defer zig_Luau_Analysis_AstJsonEncoder_free(json); + if (json == null) + return error.OutOfMemory; + const result = try allocator.dupe(u8, json[0..size]); + return result; +} + +test toJson { + const Lexer = @import("../Ast/Lexer.zig"); + const Parser = @import("../Ast/Parser.zig"); + const Allocator = @import("../Ast/Allocator.zig"); + + { + const allocator = Allocator.init(); + defer allocator.deinit(); + + const table = Lexer.AstNameTable.init(allocator); + defer table.deinit(); + const source = + \\local x = 1 + \\ + ; + + var parse_result = Parser.parse(source, table, allocator, .{}); + defer parse_result.deinit(); + + const root = parse_result.root; + + const data = try toJson(std.testing.allocator, @ptrCast(@alignCast(root))); + defer std.testing.allocator.free(data); + + try std.testing.expectEqualStrings( + \\{"type":"AstStatBlock","location":"0,0 - 1,0","hasEnd":true,"body":[{"type":"AstStatLocal","location":"0,0 - 0,11","vars":[{"luauType":null,"name":"x","isConst":false,"type":"AstLocal","location":"0,6 - 0,7"}],"values":[{"type":"AstExprConstantNumber","location":"0,10 - 0,11","value":1}]}]} + , data); + } +} diff --git a/deps/luau/src/Analysis/FileResolver.cpp b/deps/luau/src/Analysis/FileResolver.cpp new file mode 100644 index 0000000..30ed26a --- /dev/null +++ b/deps/luau/src/Analysis/FileResolver.cpp @@ -0,0 +1,113 @@ +#include + +#include "Luau/Ast.h" +#include "Luau/Frontend.h" +#include "Luau/ModuleResolver.h" + +#define ZIG_LUAU_ANALYSIS(name) ZIG_FN(Luau_Analysis_##name) + +ZIG_EXPORT using FileResolver_readSource = const char* (*)(void* ctx, const char* name, size_t len, size_t* outLen, unsigned char* outType); +ZIG_EXPORT using FileResolver_resolveModule = const char* (*)(void* ctx, const char* name, size_t len, const char* node, size_t nodeLen, size_t* outLen); +ZIG_EXPORT using FileResolver_getHumanReadableModuleName = const char* (*)(void* ctx, const char* name, size_t len, size_t* outLen); +ZIG_EXPORT using FileResolver_freeString = void (*)(void* ctx, const char* str, size_t len); +struct zig_FileResolver : Luau::FileResolver +{ + void* ctx = nullptr; + FileResolver_readSource c_readSource; + FileResolver_resolveModule c_resolveModule; + FileResolver_getHumanReadableModuleName c_getHumanReadableModuleName; + FileResolver_freeString c_freeString; + + zig_FileResolver( + void* ctx, + FileResolver_readSource fn_readSource, + FileResolver_resolveModule fn_resolveModule, + FileResolver_getHumanReadableModuleName fn_getHumanReadableModuleName, + FileResolver_freeString fn_freeString + ) + : ctx(ctx), + c_readSource(fn_readSource), + c_resolveModule(fn_resolveModule), + c_getHumanReadableModuleName(fn_getHumanReadableModuleName), + c_freeString(fn_freeString) + { + } + + std::optional readSource(const Luau::ModuleName& name) override + { + size_t len = 0; + unsigned char type = 0; + const char* source = c_readSource(ctx, name.data(), name.size(), &len, &type); + if (!source) + return std::nullopt; + + Luau::SourceCode::Type sourceType; + if (type == 0) + { + sourceType = Luau::SourceCode::Script; + } + else if (type == 1) + { + sourceType = Luau::SourceCode::Module; + } + else + { + sourceType = Luau::SourceCode::None; + } + + std::string sourceStr(source, len); + c_freeString(ctx, source, len); + + return Luau::SourceCode{sourceStr, sourceType}; + } + + std::optional resolveModule(const Luau::ModuleInfo* context, Luau::AstExpr* node, const Luau::TypeCheckLimits& limits) override + { + if (Luau::AstExprConstantString* expr = node->as()) + { + std::string path{expr->value.data, expr->value.size}; + size_t len = 0; + const char* result = c_resolveModule(ctx, context->name.c_str(), context->name.size(), path.c_str(), path.size(), &len); + if (result) + { + std::string resolvedPath(result, len); + c_freeString(ctx, result, len); + return {{resolvedPath}}; + } + } + return std::nullopt; + } + + std::string getHumanReadableModuleName(const Luau::ModuleName& name) const override + { + size_t len = 0; + const char* nameStr = c_getHumanReadableModuleName(ctx, name.data(), name.size(), &len); + std::string result(nameStr, len); + c_freeString(ctx, nameStr, len); + return result; + } +}; + +ZIG_EXPORT zig_FileResolver* ZIG_LUAU_ANALYSIS(FileResolver_init)( + void* ctx, + FileResolver_readSource fn_readSource, + FileResolver_resolveModule fn_resolveModule, + FileResolver_getHumanReadableModuleName fn_getHumanReadableModuleName, + FileResolver_freeString fn_freeString +) +{ + return new zig_FileResolver( + ctx, + fn_readSource, + fn_resolveModule, + fn_getHumanReadableModuleName, + fn_freeString + ); +} + +ZIG_EXPORT void* ZIG_LUAU_ANALYSIS(FileResolver_dtor)(zig_FileResolver* resolver) +{ + void* ctx = resolver->ctx; + delete resolver; + return ctx; +} diff --git a/deps/luau/src/Analysis/FileResolver.zig b/deps/luau/src/Analysis/FileResolver.zig new file mode 100644 index 0000000..f528b53 --- /dev/null +++ b/deps/luau/src/Analysis/FileResolver.zig @@ -0,0 +1,92 @@ +const std = @import("std"); + +const FileResolver_readSource = fn (ud: *anyopaque, name: [*c]const u8, len: usize, outLen: *usize, outType: *u8) callconv(.c) ?[*]const u8; +const FileResolver_resolveModule = fn (ud: *anyopaque, name: [*c]const u8, len: usize, node: [*c]const u8, nodeLen: usize, outLen: *usize) callconv(.c) ?[*]const u8; +const FileResolver_getHumanReadableModuleName = fn (ud: *anyopaque, name: [*c]const u8, len: usize, outLen: *usize) callconv(.c) [*]const u8; +const FileResolver_freeString = fn (ud: *anyopaque, str: [*c]const u8, len: usize) callconv(.c) void; + +extern "c" fn zig_Luau_Analysis_FileResolver_init( + *anyopaque, + *const FileResolver_readSource, + *const FileResolver_resolveModule, + *const FileResolver_getHumanReadableModuleName, + *const FileResolver_freeString, +) *anyopaque; +extern "c" fn zig_Luau_Analysis_FileResolver_dtor(*anyopaque) *anyopaque; + +pub const SourceCodeType = enum { + Script, + Module, + None, +}; + +pub fn FileResolver(comptime T: type) type { + return opaque { + const Self = @This(); + pub fn init(ctx: *T) *Self { + return @ptrCast(zig_Luau_Analysis_FileResolver_init( + @ptrCast(@alignCast(ctx)), + struct { + fn inner(ud: *anyopaque, name: [*c]const u8, len: usize, outLen: *usize, outType: *u8) callconv(.c) ?[*]const u8 { + const res: struct { []const u8, SourceCodeType } = @call(.always_inline, T.readSource, .{ @as(*T, @ptrCast(@alignCast(ud))), name[0..len] }) orelse return null; + const buf, const t = res; + outLen.* = buf.len; + outType.* = @intFromEnum(t); + return buf.ptr; + } + }.inner, + struct { + fn inner(ud: *anyopaque, name: [*c]const u8, len: usize, node: [*c]const u8, nodeLen: usize, outLen: *usize) callconv(.c) ?[*]const u8 { + const res: []const u8 = @call(.always_inline, T.resolveModule, .{ @as(*T, @ptrCast(@alignCast(ud))), name[0..len], node[0..nodeLen] }) orelse return null; + outLen.* = res.len; + return res.ptr; + } + }.inner, + struct { + fn inner(ud: *anyopaque, name: [*c]const u8, len: usize, outLen: *usize) callconv(.c) [*]const u8 { + const res: []const u8 = @call(.always_inline, T.getHumanReadableModuleName, .{ @as(*T, @ptrCast(@alignCast(ud))), name[0..len] }); + outLen.* = res.len; + return res.ptr; + } + }.inner, + struct { + fn inner(ud: *anyopaque, str: [*c]const u8, len: usize) callconv(.c) void { + @call(.always_inline, T.freeString, .{ @as(*T, @ptrCast(@alignCast(ud))), str[0..len] }); + } + }.inner, + )); + } + + pub fn deinit(self: *Self) void { + const ctx = zig_Luau_Analysis_FileResolver_dtor(self); + if (@hasDecl(T, "deinit")) { + @call(.always_inline, T.deinit, .{@as(*T, @ptrCast(@alignCast(ctx)))}); + } + } + }; +} + +test "FileResolver" { + const Sample = struct { + const Self = @This(); + pub fn readSource(_: *Self, _: []const u8) ?struct { []const u8, SourceCodeType } { + return null; + } + + pub fn resolveModule(_: *Self, _: []const u8, _: []const u8) ?[]const u8 { + return null; + } + + pub fn getHumanReadableModuleName(_: *Self, name: []const u8) []const u8 { + return name; + } + + pub fn freeString(_: *Self, _: []const u8) void {} + }; + + const SampleResolver = FileResolver(Sample); + + var sample: Sample = .{}; + const resolver = SampleResolver.init(&sample); + defer resolver.deinit(); +} diff --git a/deps/luau/src/Analysis/FileUtils.cpp b/deps/luau/src/Analysis/FileUtils.cpp new file mode 100644 index 0000000..66717ca --- /dev/null +++ b/deps/luau/src/Analysis/FileUtils.cpp @@ -0,0 +1,466 @@ +// This code is based on https://github.com/luau-lang/luau/blob/68cdcc4a3a5f3ed23186c4f7f6b8a5aacf835bee/CLI/src/FileUtils.cpp +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "./FileUtils.h" + +#include "Luau/Common.h" + +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#else +#include +#include +#include +#include +#endif + +#include +#include + +#ifdef _WIN32 +static std::wstring fromUtf8(const std::string& path) +{ + size_t result = MultiByteToWideChar(CP_UTF8, 0, path.data(), int(path.size()), nullptr, 0); + LUAU_ASSERT(result); + + std::wstring buf(result, L'\0'); + MultiByteToWideChar(CP_UTF8, 0, path.data(), int(path.size()), &buf[0], int(buf.size())); + + return buf; +} + +static std::string toUtf8(const std::wstring& path) +{ + size_t result = WideCharToMultiByte(CP_UTF8, 0, path.data(), int(path.size()), nullptr, 0, nullptr, nullptr); + LUAU_ASSERT(result); + + std::string buf(result, '\0'); + WideCharToMultiByte(CP_UTF8, 0, path.data(), int(path.size()), &buf[0], int(buf.size()), nullptr, nullptr); + + return buf; +} +#endif + +bool isAbsolutePath(std::string_view path) +{ +#ifdef _WIN32 + // Must either begin with "X:/", "X:\", "/", or "\", where X is a drive letter + return (path.size() >= 3 && isalpha(path[0]) && path[1] == ':' && (path[2] == '/' || path[2] == '\\')) || + (path.size() >= 1 && (path[0] == '/' || path[0] == '\\')); +#else + // Must begin with '/' + return path.size() >= 1 && path[0] == '/'; +#endif +} + +std::optional getCurrentWorkingDirectory() +{ + // 2^17 - derived from the Windows path length limit + constexpr size_t maxPathLength = 131072; + constexpr size_t initialPathLength = 260; + + std::string directory(initialPathLength, '\0'); + char* cstr = nullptr; + + while (!cstr && directory.size() <= maxPathLength) + { +#ifdef _WIN32 + cstr = _getcwd(directory.data(), static_cast(directory.size())); +#else + cstr = getcwd(directory.data(), directory.size()); +#endif + if (cstr) + { + directory.resize(strlen(cstr)); + return directory; + } + else if (errno != ERANGE || directory.size() * 2 > maxPathLength) + { + return std::nullopt; + } + else + { + directory.resize(directory.size() * 2); + } + } + return std::nullopt; +} + +std::string normalizePath(std::string_view path) +{ + const std::vector components = splitPath(path); + std::vector normalizedComponents; + + const bool isAbsolute = isAbsolutePath(path); + + // 1. Normalize path components + const size_t startIndex = isAbsolute ? 1 : 0; + for (size_t i = startIndex; i < components.size(); i++) + { + std::string_view component = components[i]; + if (component == "..") + { + if (normalizedComponents.empty()) + { + if (!isAbsolute) + { + normalizedComponents.emplace_back(".."); + } + } + else if (normalizedComponents.back() == "..") + { + normalizedComponents.emplace_back(".."); + } + else + { + normalizedComponents.pop_back(); + } + } + else if (!component.empty() && component != ".") + { + normalizedComponents.emplace_back(component); + } + } + + std::string normalizedPath; + + // 2. Add correct prefix to formatted path + if (isAbsolute) + { + normalizedPath += components[0]; + normalizedPath += "/"; + } + else if (normalizedComponents.empty() || normalizedComponents[0] != "..") + { + normalizedPath += "./"; + } + + // 3. Join path components to form the normalized path + for (auto iter = normalizedComponents.begin(); iter != normalizedComponents.end(); ++iter) + { + if (iter != normalizedComponents.begin()) + normalizedPath += "/"; + + normalizedPath += *iter; + } + if (normalizedPath.size() >= 2 && normalizedPath[normalizedPath.size() - 1] == '.' && normalizedPath[normalizedPath.size() - 2] == '.') + normalizedPath += "/"; + + return normalizedPath; +} + +std::optional resolvePath(std::string_view path, std::string_view baseFilePath) +{ + std::optional baseFilePathParent = getParentPath(baseFilePath); + if (!baseFilePathParent) + return std::nullopt; + + return normalizePath(joinPaths(*baseFilePathParent, path)); +} + +bool hasFileExtension(std::string_view name, const std::vector& extensions) +{ + for (const std::string& extension : extensions) + { + if (name.size() >= extension.size() && name.substr(name.size() - extension.size()) == extension) + return true; + } + return false; +} + +std::optional readFile(const std::string& name) +{ +#ifdef _WIN32 + FILE* file = _wfopen(fromUtf8(name).c_str(), L"rb"); +#else + FILE* file = fopen(name.c_str(), "rb"); +#endif + + if (!file) + return std::nullopt; + + fseek(file, 0, SEEK_END); + long length = ftell(file); + if (length < 0) + { + fclose(file); + return std::nullopt; + } + fseek(file, 0, SEEK_SET); + + std::string result(length, 0); + + size_t read = fread(result.data(), 1, length, file); + fclose(file); + + if (read != size_t(length)) + return std::nullopt; + + // Skip first line if it's a shebang + if (length > 2 && result[0] == '#' && result[1] == '!') + result.erase(0, result.find('\n')); + + return result; +} + +std::optional readStdin() +{ + std::string result; + char buffer[4096] = {}; + + while (fgets(buffer, sizeof(buffer), stdin) != nullptr) + result.append(buffer); + + // If eof was not reached for stdin, then a read error occurred + if (!feof(stdin)) + return std::nullopt; + + return result; +} + +template +static void joinPaths(std::basic_string& str, const Ch* lhs, const Ch* rhs) +{ + str = lhs; + if (!str.empty() && str.back() != '/' && str.back() != '\\' && *rhs != '/' && *rhs != '\\') + str += '/'; + str += rhs; +} + +#ifdef _WIN32 +static bool traverseDirectoryRec(const std::wstring& path, const std::function& callback) +{ + std::wstring query = path + std::wstring(L"/*"); + + WIN32_FIND_DATAW data; + HANDLE h = FindFirstFileW(query.c_str(), &data); + + if (h == INVALID_HANDLE_VALUE) + return false; + + std::wstring buf; + + do + { + if (wcscmp(data.cFileName, L".") != 0 && wcscmp(data.cFileName, L"..") != 0) + { + joinPaths(buf, path.c_str(), data.cFileName); + + if (data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) + { + // Skip reparse points to avoid handling cycles + } + else if (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + { + traverseDirectoryRec(buf, callback); + } + else + { + callback(toUtf8(buf)); + } + } + } while (FindNextFileW(h, &data)); + + FindClose(h); + + return true; +} + +bool traverseDirectory(const std::string& path, const std::function& callback) +{ + return traverseDirectoryRec(fromUtf8(path), callback); +} +#else +static bool traverseDirectoryRec(const std::string& path, const std::function& callback) +{ + int fd = open(path.c_str(), O_DIRECTORY); + DIR* dir = fdopendir(fd); + + if (!dir) + return false; + + std::string buf; + + while (dirent* entry = readdir(dir)) + { + const dirent& data = *entry; + + if (strcmp(data.d_name, ".") != 0 && strcmp(data.d_name, "..") != 0) + { + joinPaths(buf, path.c_str(), data.d_name); + +#if defined(DTTOIF) + mode_t mode = DTTOIF(data.d_type); +#else + mode_t mode = 0; +#endif + + // we need to stat an UNKNOWN to be able to tell the type + if ((mode & S_IFMT) == 0) + { + struct stat st = {}; +#ifdef _ATFILE_SOURCE + fstatat(fd, data.d_name, &st, 0); +#else + lstat(buf.c_str(), &st); +#endif + + mode = st.st_mode; + } + + if (mode == S_IFDIR) + { + traverseDirectoryRec(buf, callback); + } + else if (mode == S_IFREG) + { + callback(buf); + } + else if (mode == S_IFLNK) + { + // Skip symbolic links to avoid handling cycles + } + } + } + + closedir(dir); + + return true; +} + +bool traverseDirectory(const std::string& path, const std::function& callback) +{ + return traverseDirectoryRec(path, callback); +} +#endif + +bool isFile(const std::string& path) +{ +#ifdef _WIN32 + DWORD fileAttributes = GetFileAttributesW(fromUtf8(path).c_str()); + if (fileAttributes == INVALID_FILE_ATTRIBUTES) + return false; + return (fileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0; +#else + struct stat st = {}; + lstat(path.c_str(), &st); + return (st.st_mode & S_IFMT) == S_IFREG; +#endif +} + +bool isDirectory(const std::string& path) +{ +#ifdef _WIN32 + DWORD fileAttributes = GetFileAttributesW(fromUtf8(path).c_str()); + if (fileAttributes == INVALID_FILE_ATTRIBUTES) + return false; + return (fileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0; +#else + struct stat st = {}; + lstat(path.c_str(), &st); + return (st.st_mode & S_IFMT) == S_IFDIR; +#endif +} + +std::vector splitPath(std::string_view path) +{ + std::vector components; + + size_t pos = 0; + size_t nextPos = path.find_first_of("\\/", pos); + + while (nextPos != std::string::npos) + { + components.push_back(path.substr(pos, nextPos - pos)); + pos = nextPos + 1; + nextPos = path.find_first_of("\\/", pos); + } + components.push_back(path.substr(pos)); + + return components; +} + +std::string joinPaths(std::string_view lhs, std::string_view rhs) +{ + std::string result = std::string(lhs); + if (!result.empty() && result.back() != '/' && result.back() != '\\') + result += '/'; + result += rhs; + return result; +} + +std::optional getParentPath(std::string_view path) +{ + if (path == "" || path == "." || path == "/") + return std::nullopt; + +#ifdef _WIN32 + if (path.size() == 2 && path.back() == ':') + return std::nullopt; +#endif + + size_t slash = path.find_last_of("\\/", path.size() - 1); + + if (slash == 0) + return "/"; + + if (slash != std::string::npos) + return std::string(path.substr(0, slash)); + + return ""; +} + +static std::string getExtension(const std::string& path) +{ + size_t dot = path.find_last_of(".\\/"); + + if (dot == std::string::npos || path[dot] != '.') + return ""; + + return path.substr(dot); +} + +std::vector getSourceFiles(int argc, char** argv) +{ + std::vector files; + + for (int i = 1; i < argc; ++i) + { + // Early out once we reach --program-args,-a since the remaining args are passed to lua + if (strcmp(argv[i], "--program-args") == 0 || strcmp(argv[i], "-a") == 0) + return files; + + // Treat '-' as a special file whose source is read from stdin + // All other arguments that start with '-' are skipped + if (argv[i][0] == '-' && argv[i][1] != '\0') + continue; + + std::string normalized = normalizePath(argv[i]); + + if (isDirectory(normalized)) + { + traverseDirectory( + normalized, + [&](const std::string& name) + { + std::string ext = getExtension(name); + + if (ext == ".lua" || ext == ".luau") + files.push_back(name); + } + ); + } + else + { + files.push_back(normalized); + } + } + + return files; +} \ No newline at end of file diff --git a/deps/luau/src/Analysis/FileUtils.h b/deps/luau/src/Analysis/FileUtils.h new file mode 100644 index 0000000..72f6986 --- /dev/null +++ b/deps/luau/src/Analysis/FileUtils.h @@ -0,0 +1,30 @@ +// This code is based on https://github.com/luau-lang/luau/blob/68cdcc4a3a5f3ed23186c4f7f6b8a5aacf835bee/CLI/include/Luau/FileUtils.h +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include +#include +#include +#include +#include + +std::optional getCurrentWorkingDirectory(); + +std::string normalizePath(std::string_view path); +std::optional resolvePath(std::string_view relativePath, std::string_view baseFilePath); + +std::optional readFile(const std::string& name); +std::optional readStdin(); + +bool hasFileExtension(std::string_view name, const std::vector& extensions); + +bool isAbsolutePath(std::string_view path); +bool isFile(const std::string& path); +bool isDirectory(const std::string& path); +bool traverseDirectory(const std::string& path, const std::function& callback); + +std::vector splitPath(std::string_view path); +std::string joinPaths(std::string_view lhs, std::string_view rhs); +std::optional getParentPath(std::string_view path); + +std::vector getSourceFiles(int argc, char** argv); \ No newline at end of file diff --git a/deps/luau/src/Analysis/Frontend.cpp b/deps/luau/src/Analysis/Frontend.cpp new file mode 100644 index 0000000..810c005 --- /dev/null +++ b/deps/luau/src/Analysis/Frontend.cpp @@ -0,0 +1,182 @@ +#include + +#include "Luau/Frontend.h" +#include "Luau/BuiltinDefinitions.h" + +#define ZIG_LUAU_ANALYSIS(name) ZIG_FN(Luau_Analysis_##name) + +ZIG_EXPORT struct luau_FrontendOptions +{ + // When true, we retain full type information about every term in the AST. + // Setting this to false cuts back on RAM and is a good idea for batch + // jobs where the type graph is not deeply inspected after typechecking + // is complete. + bool retainFullTypeGraphs = false; + + // Run typechecking only in mode required for autocomplete (strict mode in + // order to get more precise type information) + bool forAutocomplete = false; + + bool runLintChecks = false; + + // When true, some internal complexity limits will be scaled down for modules that miss the limit set by moduleTimeLimitSec + bool applyInternalLimitScaling = false; +}; + +ZIG_EXPORT Luau::Frontend* ZIG_LUAU_ANALYSIS(Frontend_init)(Luau::FileResolver* fileResolver, Luau::ConfigResolver* configResolver, luau_FrontendOptions options) +{ + Luau::FrontendOptions frontendOptions; + frontendOptions.retainFullTypeGraphs = options.retainFullTypeGraphs; + frontendOptions.runLintChecks = options.runLintChecks; + frontendOptions.forAutocomplete = options.forAutocomplete; + frontendOptions.applyInternalLimitScaling = options.applyInternalLimitScaling; + + return new Luau::Frontend(fileResolver, configResolver, frontendOptions); +} + +ZIG_EXPORT using Frontend_loadDefinitionError = bool (*)(void* ctx, const char* str, size_t len, Luau::Location location); +ZIG_EXPORT bool ZIG_LUAU_ANALYSIS(Frontend_loadDefinitionFile)( + Luau::Frontend* frontend, + const char* src, + size_t srcLen, + const char* packagename, + bool captureComments, + bool typeCheckForAutocomplete, + void* ctx, + Frontend_loadDefinitionError fn_loadDefinitionError +) +{ + std::string source(src, srcLen); + std::string packageName(packagename); + Luau::LoadDefinitionFileResult result = frontend->loadDefinitionFile(frontend->globals, frontend->globals.globalScope, source, packageName, captureComments, typeCheckForAutocomplete); + if (!result.success) + { + if (fn_loadDefinitionError) + { + if (result.parseResult.errors.size() > 0){ + Luau::ParseError error = result.parseResult.errors.front(); + std::string msg = error.getMessage(); + Luau::Location location = error.getLocation(); + fn_loadDefinitionError(ctx, msg.c_str(), msg.size(), location); + } else if (result.module->errors.size() > 0) { + Luau::TypeError error = result.module->errors.front(); + std::string msg = ""; + Luau::Location location = error.location; + fn_loadDefinitionError(ctx, msg.c_str(), msg.size(), location); + } + } + } + return result.success; +} + +ZIG_EXPORT void ZIG_LUAU_ANALYSIS(Frontend_registerBuiltinGlobals)(Luau::Frontend& frontend) +{ + Luau::registerBuiltinGlobals(frontend, frontend.globals); +} + +ZIG_EXPORT void ZIG_LUAU_ANALYSIS(Frontend_freeze)(Luau::Frontend* frontend) +{ + Luau::freeze(frontend->globals.globalTypes); +} + +ZIG_EXPORT void ZIG_LUAU_ANALYSIS(Frontend_queueModuleCheck)(Luau::Frontend* frontend, const char* path, size_t pathLen) +{ + std::string modulePath(path, pathLen); + frontend->queueModuleCheck(modulePath); +} + +ZIG_EXPORT using Frontend_checkedModule = bool (*)(void* ctx, const char* str, size_t len); +ZIG_EXPORT using Frontend_checkedModuleError = void (*)( + void* ctx, + const char* moduleName, + size_t moduleNameLen, + const char* errorMessage, + size_t errorMessageLen, + Luau::Location location +); +ZIG_EXPORT bool ZIG_LUAU_ANALYSIS(Frontend_checkQueuedModules)( + Luau::Frontend* frontend, + void* ctx, + Frontend_checkedModule fn_checkedModule, + Frontend_checkedModuleError fn_checkedModuleError +) +{ + std::vector checkedModules; + try + { + checkedModules = frontend->checkQueuedModules(std::nullopt); + } + catch (const Luau::InternalCompilerError& ice) + { + Luau::Location location = ice.location ? *ice.location : Luau::Location(); + + std::string moduleName = ice.moduleName ? *ice.moduleName : ""; + std::string readableName = frontend->fileResolver->getHumanReadableModuleName(moduleName); + + if (fn_checkedModuleError) + fn_checkedModuleError(ctx, readableName.c_str(), readableName.size(), ice.message.c_str(), ice.message.size(), location); + + return false; + } + + for (const auto& module : checkedModules) + { + if (!fn_checkedModule(ctx, module.c_str(), module.size())) + return false; + } + + return true; +} + +ZIG_EXPORT using Frontend_checkedResult = void (*)( + void* ctx, + unsigned char kind, + const char* moduleName, + size_t moduleNameLen, + const char* errorMessage, + size_t errorMessageLen, + const char* typeName, + Luau::Location location +); +ZIG_EXPORT unsigned char ZIG_LUAU_ANALYSIS(Frontend_getCheckResult)( + Luau::Frontend* frontend, + const char* moduleName, + size_t moduleNameLen, + bool accumulateNested, + bool forAutocomplete, + void* ctx, + Frontend_checkedResult fn_checkedResult +) +{ + std::string name(moduleName, moduleNameLen); + std::optional cr = frontend->getCheckResult(name, false); + if (!cr) + { + return 0; + } + + for (auto& error : cr->errors) + { + std::string readableName = frontend->fileResolver->getHumanReadableModuleName(error.moduleName); + + if (const Luau::SyntaxError* syntaxError = Luau::get_if(&error.data)) + fn_checkedResult(ctx, 0, readableName.c_str(), readableName.size(), syntaxError->message.c_str(), syntaxError->message.size(), "SyntaxError", error.location); + else + { + std::string msg = Luau::toString(error, Luau::TypeErrorToStringOptions{frontend->fileResolver}); + fn_checkedResult(ctx, 0, readableName.c_str(), readableName.size(), msg.c_str(), msg.size(), "TypeError", error.location); + } + } + + std::string readableName = frontend->fileResolver->getHumanReadableModuleName(name); + for (auto& error : cr->lintResult.errors) + fn_checkedResult(ctx, 1, readableName.c_str(), readableName.size(), error.text.c_str(), error.text.size(), Luau::LintWarning::getName(error.code), error.location); + for (auto& warning : cr->lintResult.warnings) + fn_checkedResult(ctx, 2, readableName.c_str(), readableName.size(), warning.text.c_str(), warning.text.size(), Luau::LintWarning::getName(warning.code), warning.location); + return cr->errors.empty() && cr->lintResult.errors.empty() ? 1 : 2; +} + +ZIG_EXPORT void ZIG_LUAU_ANALYSIS(Frontend_dtor)(Luau::Frontend* frontend) +{ + delete frontend; +} diff --git a/deps/luau/src/Analysis/Frontend.zig b/deps/luau/src/Analysis/Frontend.zig new file mode 100644 index 0000000..3d468f5 --- /dev/null +++ b/deps/luau/src/Analysis/Frontend.zig @@ -0,0 +1,383 @@ +const std = @import("std"); + +const Location = @import("../Ast/Location.zig").Location; + +const GenericConfigResolver = @import("GenericConfigResolver.zig"); + +pub const LoadDefinitionResult = extern struct { + success: bool, +}; + +pub const Options = extern struct { + /// When true, we retain full type information about every term in the AST. + /// Setting this to false cuts back on RAM and is a good idea for batch + /// jobs where the type graph is not deeply inspected after typechecking + /// is complete. + retainFullTypeGraphs: bool = false, + + /// Run typechecking only in mode required for autocomplete (strict mode in + /// order to get more precise type information) + forAutocomplete: bool = false, + + runLintChecks: bool = false, + + /// When true, some internal complexity limits will be scaled down for modules that miss the limit set by moduleTimeLimitSec + applyInternalLimitScaling: bool = false, +}; + +pub const CheckResultStatus = enum(u8) { + None, + Success, + Error, +}; + +pub const CheckResultErrorKind = enum(u8) { + Error, + LintError, + LintWarning, +}; + +const loadDefinitionFileErrorFn = fn (?*anyopaque, [*c]const u8, usize, Location) callconv(.c) void; +const CheckedModuleFn = fn (?*anyopaque, [*c]const u8, usize) callconv(.c) bool; +const CheckedModuleErrorFn = fn (?*anyopaque, [*c]const u8, usize, [*c]const u8, usize, Location) callconv(.c) void; +const CheckedResultFn = fn (?*anyopaque, u8, [*c]const u8, usize, [*c]const u8, usize, [*c]const u8, Location) callconv(.c) void; + +extern "c" fn zig_luau_free(ptr: *anyopaque) void; + +extern "c" fn zig_Luau_Analysis_Frontend_init(*anyopaque, *GenericConfigResolver.GenericConfigResolver, Options) *Frontend; +extern "c" fn zig_Luau_Analysis_Frontend_registerBuiltinGlobals(*Frontend) void; +extern "c" fn zig_Luau_Analysis_Frontend_freeze(*Frontend) void; +extern "c" fn zig_Luau_Analysis_Frontend_loadDefinitionFile(*Frontend, [*c]const u8, usize, [*c]const u8, bool, bool, ?*anyopaque, ?*const loadDefinitionFileErrorFn) bool; +extern "c" fn zig_Luau_Analysis_Frontend_queueModuleCheck(*Frontend, [*c]const u8, usize) void; +extern "c" fn zig_Luau_Analysis_Frontend_checkQueuedModules(*Frontend, ?*anyopaque, *const CheckedModuleFn, ?*const CheckedModuleErrorFn) bool; +extern "c" fn zig_Luau_Analysis_Frontend_getCheckResult(*Frontend, [*c]const u8, usize, bool, bool, ?*anyopaque, *const CheckedResultFn) u8; +extern "c" fn zig_Luau_Analysis_Frontend_dtor(*Frontend) void; + +pub const Frontend = opaque { + pub fn registerBuiltinGlobals(self: *Frontend) void { + zig_Luau_Analysis_Frontend_registerBuiltinGlobals(self); + } + + pub fn freeze(self: *Frontend) void { + zig_Luau_Analysis_Frontend_freeze(self); + } + + pub fn queueModuleCheck(self: *Frontend, path: []const u8) void { + return zig_Luau_Analysis_Frontend_queueModuleCheck(self, path.ptr, path.len); + } + + pub fn checkQueuedModules( + self: *Frontend, + context: anytype, + comptime checkedModule: *const fn (@TypeOf(context), [:0]const u8) bool, + comptime checkedModuleError: *const fn (@TypeOf(context), moduleName: [:0]const u8, errMsg: [:0]const u8, Location) void, + ) bool { + const T = @TypeOf(context); + if (@typeInfo(T) != .pointer and T != void) + @compileError("context must be a pointer type or void"); + if (T != void and @typeInfo(T).pointer.is_const) + @compileError("context must be a mutable pointer type or void"); + return zig_Luau_Analysis_Frontend_checkQueuedModules( + self, + if (T == void) null else @ptrCast(@alignCast(context)), + struct { + fn inner( + ud: ?*anyopaque, + name: [*c]const u8, + len: usize, + ) callconv(.c) bool { + return @call(.always_inline, checkedModule, .{ + if (T == void) undefined else @as(T, @ptrCast(@alignCast(ud.?))), + name[0..len :0], + }); + } + }.inner, + struct { + fn inner( + ud: ?*anyopaque, + name: [*c]const u8, + len: usize, + msg: [*c]const u8, + msgLen: usize, + loc: Location, + ) callconv(.c) void { + @call(.always_inline, checkedModuleError, .{ + if (T == void) undefined else @as(T, @ptrCast(@alignCast(ud.?))), + name[0..len :0], + msg[0..msgLen :0], + loc, + }); + } + }.inner, + ); + } + + pub fn getCheckResult( + self: *Frontend, + moduleName: []const u8, + captureComments: bool, + typeCheckForAutocomplete: bool, + context: anytype, + comptime checkFn: *const fn (@TypeOf(context), CheckResultErrorKind, [:0]const u8, [:0]const u8, [:0]const u8, Location) void, + ) CheckResultStatus { + const T = @TypeOf(context); + if (@typeInfo(T) != .pointer and T != void) + @compileError("context must be a pointer type or void"); + if (T != void and @typeInfo(T).pointer.is_const) + @compileError("context must be a mutable pointer type or void"); + const result = zig_Luau_Analysis_Frontend_getCheckResult( + self, + moduleName.ptr, + moduleName.len, + captureComments, + typeCheckForAutocomplete, + if (T == void) null else @as(*anyopaque, @ptrCast(@alignCast(context))), + struct { + fn inner( + ud: ?*anyopaque, + kind: u8, + readableModuleName: [*c]const u8, + readableModuleNameLen: usize, + errorMessage: [*c]const u8, + errorMessageLen: usize, + contextName: [*c]const u8, + loc: Location, + ) callconv(.c) void { + @call(.always_inline, checkFn, .{ + if (T == void) undefined else @as(T, @ptrCast(@alignCast(ud.?))), + @as(CheckResultErrorKind, @enumFromInt(kind)), + readableModuleName[0..readableModuleNameLen :0], + errorMessage[0..errorMessageLen :0], + std.mem.span(contextName), + loc, + }); + } + }.inner, + ); + return @enumFromInt(result); + } + + pub fn loadDefinitionFile( + self: *Frontend, + src: []const u8, + packageName: [:0]const u8, + captureComments: bool, + typeCheckForAutocomplete: ?bool, + ) bool { + return zig_Luau_Analysis_Frontend_loadDefinitionFile(self, src.ptr, src.len, packageName, captureComments, typeCheckForAutocomplete orelse false, null, null); + } + + const LoadDefintionResult = struct { + message: []const u8, + location: Location, + allocator: std.mem.Allocator, + + pub fn deinit(self: *LoadDefintionResult) void { + self.allocator.free(self.message); + } + }; + + pub fn loadDefinitionFileWithAlloc( + self: *Frontend, + allocator: std.mem.Allocator, + src: []const u8, + packageName: [:0]const u8, + captureComments: bool, + typeCheckForAutocomplete: ?bool, + ) !?LoadDefintionResult { + var result: struct { anyerror, LoadDefintionResult } = .{ error.None, .{ + .allocator = allocator, + .message = undefined, + .location = undefined, + } }; + const success = zig_Luau_Analysis_Frontend_loadDefinitionFile(self, src.ptr, src.len, packageName, captureComments, typeCheckForAutocomplete orelse false, &result, struct { + fn inner( + ud: ?*anyopaque, + msg: [*c]const u8, + len: usize, + loc: Location, + ) callconv(.c) void { + const res: *struct { anyerror, LoadDefintionResult } = @ptrCast(@alignCast(ud.?)); + res.@"1".location = loc; + res.@"1".message = res.@"1".allocator.dupe(u8, msg[0..len]) catch |err| { + res.@"0" = err; + return; + }; + } + }.inner); + if (success) { + return null; + } + if (result.@"0" != error.None) { + return result.@"0"; + } + return result.@"1"; + } + + pub fn deinit(self: *Frontend) void { + zig_Luau_Analysis_Frontend_dtor(self); + } +}; + +pub fn init(fileResolver: anytype, configResolver: *GenericConfigResolver.GenericConfigResolver, opts: Options) *Frontend { + return zig_Luau_Analysis_Frontend_init(@ptrCast(@alignCast(fileResolver)), configResolver, opts); +} + +test Frontend { + const FileResolver = @import("FileResolver.zig"); + + { + const FileImpl = struct { + const Self = @This(); + pub fn readSource(_: *Self, _: []const u8) ?struct { []const u8, FileResolver.SourceCodeType } { + return null; + } + + pub fn resolveModule(_: *Self, _: []const u8, _: []const u8) ?[]const u8 { + return null; + } + + pub fn getHumanReadableModuleName(_: *Self, name: []const u8) []const u8 { + return name; + } + + pub fn freeString(_: *Self, _: []const u8) void {} + }; + + const FileImplResolver = FileResolver.FileResolver(FileImpl); + + var file_impl: FileImpl = .{}; + const file_resolver = FileImplResolver.init(&file_impl); + defer file_resolver.deinit(); + const config_resolver = GenericConfigResolver.init(.Strict); + defer config_resolver.deinit(); + + const frontend = init(file_resolver, config_resolver, .{}); + defer frontend.deinit(); + + frontend.registerBuiltinGlobals(); + + var load_result = try frontend.loadDefinitionFileWithAlloc( + std.testing.allocator, + \\ - This is a test + , + "@test", + false, + null, + ) orelse @panic("no fail"); + defer load_result.deinit(); + try std.testing.expectEqualStrings("Expected identifier when parsing expression, got '-'", load_result.message); + try std.testing.expectEqual(0, load_result.location.begin.line); + try std.testing.expectEqual(1, load_result.location.begin.column); + try std.testing.expectEqual(0, load_result.location.end.line); + try std.testing.expectEqual(2, load_result.location.end.column); + } + { + const StaticFileTree = std.StaticStringMap([]const u8).initComptime(.{ + .{ + "./main.luau", + \\local module = require("./module.luau") + , + }, + .{ + "./module.luau", + \\print("module"); + \\return {}; + , + }, + .{ + "./sub/test.luau", + \\local test = global.foo; + \\local test2 = g.foo; + \\ + , + }, + }); + + const FileImpl = struct { + const Self = @This(); + pub fn readSource(_: *Self, path: []const u8) ?struct { []const u8, FileResolver.SourceCodeType } { + const source = StaticFileTree.get(path) orelse @panic("failed to find source"); + return .{ source, .Module }; + } + + pub fn resolveModule(_: *Self, _: []const u8, to: []const u8) ?[]const u8 { + return to; + } + + pub fn getHumanReadableModuleName(_: *Self, name: []const u8) []const u8 { + return name; + } + + pub fn freeString(_: *Self, _: []const u8) void {} + }; + + const FileImplResolver = FileResolver.FileResolver(FileImpl); + + var file_impl: FileImpl = .{}; + const file_resolver = FileImplResolver.init(&file_impl); + defer file_resolver.deinit(); + const config_resolver = GenericConfigResolver.init(.Strict); + defer config_resolver.deinit(); + + const frontend = init(file_resolver, config_resolver, .{}); + defer frontend.deinit(); + + frontend.registerBuiltinGlobals(); + + try std.testing.expectEqual(null, try frontend.loadDefinitionFileWithAlloc( + std.testing.allocator, + \\declare global: { + \\ foo: string, + \\} + , + "@main", + false, + null, + )); + + frontend.queueModuleCheck("./main.luau"); + frontend.queueModuleCheck("./sub/test.luau"); + + const success = frontend.checkQueuedModules( + frontend, + struct { + fn checkedModule(f: *Frontend, name: [:0]const u8) bool { + switch (f.getCheckResult(name, false, false, @as(void, undefined), struct { + fn inner(_: void, kind: CheckResultErrorKind, readableModuleName: [:0]const u8, errorMessage: [:0]const u8, typeName: [:0]const u8, loc: Location) void { + if (!std.mem.eql(u8, readableModuleName, "./sub/test.luau")) + @panic("Expected no errors in main module"); + std.testing.expectEqual(.Error, kind) catch @panic("failed"); + std.testing.expectEqualStrings("Unknown global 'g'; consider assigning to it first", errorMessage) catch @panic("failed"); + std.testing.expectEqualStrings("TypeError", typeName) catch @panic("failed"); + std.testing.expectEqual(1, loc.begin.line) catch @panic("failed"); + std.testing.expectEqual(14, loc.begin.column) catch @panic("failed"); + std.testing.expectEqual(1, loc.end.line) catch @panic("failed"); + std.testing.expectEqual(15, loc.end.column) catch @panic("failed"); + } + }.inner)) { + .None => unreachable, + .Success => {}, + .Error => if (!std.mem.eql(u8, name, "./sub/test.luau")) @panic("Expected no errors in main module"), + } + return true; + } + }.checkedModule, + struct { + fn checkedModuleError(_: *Frontend, name: [:0]const u8, errMsg: [:0]const u8, loc: Location) void { + std.debug.print("Error in module {s}: {s} at {d}:{d}-{d}:{d}\n", .{ + name, + errMsg, + loc.begin.line, + loc.begin.column, + loc.end.line, + loc.end.column, + }); + @panic("Module check failed"); + } + }.checkedModuleError, + ); + + try std.testing.expect(success); + } +} diff --git a/deps/luau/src/Analysis/GenericConfigResolver.cpp b/deps/luau/src/Analysis/GenericConfigResolver.cpp new file mode 100644 index 0000000..0b75590 --- /dev/null +++ b/deps/luau/src/Analysis/GenericConfigResolver.cpp @@ -0,0 +1,84 @@ +#include + +#include "Luau/ConfigResolver.h" + +#include "./FileUtils.h" + +#define ZIG_LUAU_ANALYSIS(name) ZIG_FN(Luau_Analysis_##name) + +// This code is based on https://github.com/luau-lang/luau/blob/68cdcc4a3a5f3ed23186c4f7f6b8a5aacf835bee/CLI/src/Analyze.cpp#L202 +struct GenericConfigResolver : Luau::ConfigResolver +{ + mutable std::vector> configErrors; + mutable std::unordered_map configCache; + + Luau::Config defaultConfig; + + GenericConfigResolver(Luau::Mode mode) + { + defaultConfig.mode = mode; + } + + const Luau::Config& getConfig(const Luau::ModuleName& name, const Luau::TypeCheckLimits& limits) const override + { + std::optional path = getParentPath(name); + if (!path) + return defaultConfig; + + return readConfigRec(*path, limits); + } + + const Luau::Config& readConfigRec(const std::string& path, const Luau::TypeCheckLimits& limits) const + { + auto it = configCache.find(path); + if (it != configCache.end()) + return it->second; + + std::optional parent = getParentPath(path); + Luau::Config result = parent ? readConfigRec(*parent, limits) : defaultConfig; + + std::string configPath = joinPaths(path, Luau::kConfigName); + + if (std::optional contents = readFile(configPath)) + { + Luau::ConfigOptions::AliasOptions aliasOpts; + aliasOpts.configLocation = configPath; + aliasOpts.overwriteAliases = true; + + Luau::ConfigOptions opts; + opts.aliasOptions = std::move(aliasOpts); + + std::optional error = Luau::parseConfig(*contents, result, opts); + if (error) + configErrors.push_back({configPath, *error}); + } + + return configCache[path] = result; + } +}; + +ZIG_EXPORT GenericConfigResolver* ZIG_LUAU_ANALYSIS(GenericConfigResolver_init)(unsigned char mode) +{ + Luau::Mode luauMode = Luau::Mode::NoCheck; + if (mode == 0) + luauMode = Luau::Mode::NoCheck; + else if (mode == 1) + luauMode = Luau::Mode::Nonstrict; + else if (mode == 2) + luauMode = Luau::Mode::Strict; + else if (mode == 3) + luauMode = Luau::Mode::Definition; + return new GenericConfigResolver(luauMode); +} + +ZIG_EXPORT struct ErrorGroup +{ + const char **paths; + const char **messages; + size_t size; +}; + +ZIG_EXPORT void ZIG_LUAU_ANALYSIS(GenericConfigResolver_dtor)(GenericConfigResolver* resolver) +{ + delete resolver; +} diff --git a/deps/luau/src/Analysis/GenericConfigResolver.zig b/deps/luau/src/Analysis/GenericConfigResolver.zig new file mode 100644 index 0000000..f632f39 --- /dev/null +++ b/deps/luau/src/Analysis/GenericConfigResolver.zig @@ -0,0 +1,59 @@ +const std = @import("std"); + +const cpp_std = @import("../cpp_std.zig"); + +const ErrorGroup = extern struct { + paths: [*][*c]const u8, + messages: [*][*c]const u8, + size: usize, +}; + +const Mode = enum(u8) { + NoCheck = 0, + Nonstrict = 1, + Strict = 2, + Definition = 3, +}; + +const ConfigErrors = cpp_std.Vector(cpp_std.Pair(cpp_std.String, cpp_std.String)); +const ResolverInterface = extern struct { + vtable: *const anyopaque, + errors: ConfigErrors, +}; + +extern "c" fn zig_Luau_Analysis_GenericConfigResolver_init(u8) *GenericConfigResolver; +extern "c" fn zig_Luau_Analysis_GenericConfigResolver_dtor(*GenericConfigResolver) void; + +pub const GenericConfigResolver = opaque { + pub const AnyErrorGroup = struct { + group: ErrorGroup, + + pub fn paths(self: AnyErrorGroup) []const [*c]const u8 { + return self.group.paths[0..self.group.size]; + } + pub fn messages(self: AnyErrorGroup) []const [*c]const u8 { + return self.group.messages[0..self.group.size]; + } + }; + + pub fn getErrors(self: *GenericConfigResolver) ConfigErrors { + return @as(*ResolverInterface, @ptrCast(@alignCast(self))).errors; + } + + pub fn deinit(self: *GenericConfigResolver) void { + zig_Luau_Analysis_GenericConfigResolver_dtor(self); + } +}; + +pub fn init(mode: Mode) *GenericConfigResolver { + return zig_Luau_Analysis_GenericConfigResolver_init(@intFromEnum(mode)); +} + +test GenericConfigResolver { + const resolver = init(.Strict); + defer resolver.deinit(); + + const errors = resolver.getErrors(); + + try std.testing.expect(errors.size() == 0); +} diff --git a/deps/luau/src/Ast/Allocator.cpp b/deps/luau/src/Ast/Allocator.cpp new file mode 100644 index 0000000..0bb38dd --- /dev/null +++ b/deps/luau/src/Ast/Allocator.cpp @@ -0,0 +1,15 @@ +#include + +#include "Luau/Allocator.h" + +#define ZIG_LUAU_AST(name) ZIG_FN(Luau_Ast_##name) + +ZIG_EXPORT Luau::Allocator* ZIG_LUAU_AST(Allocator_init)() +{ + return new Luau::Allocator(); +} + +ZIG_EXPORT void ZIG_LUAU_AST(Allocator_dtor)(Luau::Allocator* allocator) +{ + delete allocator; +} diff --git a/deps/luau/src/Ast/Allocator.zig b/deps/luau/src/Ast/Allocator.zig new file mode 100644 index 0000000..91ea046 --- /dev/null +++ b/deps/luau/src/Ast/Allocator.zig @@ -0,0 +1,46 @@ +const std = @import("std"); + +// extern fn zig_delete_any(*Page) callconv(.c) void; + +extern "c" fn zig_Luau_Ast_Allocator_init() *This; +extern "c" fn zig_Luau_Ast_Allocator_dtor(*This) void; + +const This = @This(); + +root: [*c]Page, +offset: usize = 0, + +pub const Page = extern struct { + next: [*c]Page = null, + data: [8192]u8 align(8), +}; + +// /// cleans up the luau allocator +// /// frees all pages created by C++ +// pub fn destroy(self: This) void { +// var page = self.root; +// while (page != null) { +// const next = page.*.next; +// // pages are C++ allocated, so we need to use the C++ deallocator +// zig_delete_any(page); +// std.debug.print("clean page\n", .{}); +// page = next; +// } +// } + +pub fn init() *This { + return zig_Luau_Ast_Allocator_init(); +} + +pub fn deinit(self: *This) void { + zig_Luau_Ast_Allocator_dtor(self); +} + +test This { + const allocator = This.init(); + defer allocator.deinit(); +} + +// sources: +// https://github.com/luau-lang/luau/blob/a2303a6ae68c53035eccf230c4450b9f068536af/Ast/include/Luau/Allocator.h +// https://github.com/luau-lang/luau/blob/a2303a6ae68c53035eccf230c4450b9f068536af/Ast/src/Allocator.cpp diff --git a/deps/luau/src/Ast/Ast.zig b/deps/luau/src/Ast/Ast.zig new file mode 100644 index 0000000..e0925af --- /dev/null +++ b/deps/luau/src/Ast/Ast.zig @@ -0,0 +1,2454 @@ +const std = @import("std"); + +const Location = @import("Location.zig").Location; + +const Variant = @import("../Common/Variant.zig").Variant; + +const cpp_std = @import("../cpp_std.zig"); + +const Ast = @This(); + +pub const Name = extern struct { + value: [*:0]const u8, +}; + +pub const Local = extern struct { + name: Name, + location: Location, + shadow: ?*Local, + functionDepth: usize, + loopDepth: usize, + isConst: bool, + /// exported is only a property set after construction + isExported: bool = false, + + annotation: ?*Type, +}; + +pub fn Array(comptime T: type) type { + return extern struct { + data: ?[*]T = null, + size: usize = 0, + + const This = @This(); + + pub fn slice(self: This) []T { + return if (self.data) |d| d[0..self.size] else &.{}; + } + }; +} + +pub const TypeList = extern struct { + types: Array(*Type), + /// Null indicates no tail, not an untyped tail. + tailType: ?*TypePack = null, +}; + +pub const Node = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind, + location: Location, + + pub const Kind = enum(u32) { + unknown, + attr, + generic_type, + generic_type_pack, + expr_group, + expr_constant_nil, + expr_constant_bool, + expr_constant_number, + expr_constant_integer, + expr_constant_string, + expr_local, + expr_global, + expr_varargs, + expr_call, + expr_index_name, + expr_index_expr, + expr_function, + expr_table, + expr_unary, + expr_binary, + expr_type_assertion, + expr_if_else, + expr_interp_string, + expr_instantiate, + stat_block, + stat_if, + stat_while, + stat_repeat, + stat_break, + stat_continue, + stat_return, + stat_expr, + stat_local, + stat_for, + stat_for_in, + stat_assign, + stat_compound_assign, + stat_function, + stat_local_function, + stat_type_alias, + stat_type_function, + stat_declare_global, + stat_declare_function, + stat_class, + stat_declare_extern_type, + type_reference, + type_table, + type_function, + type_typeof, + type_optional, + type_union, + type_intersection, + expr_error, + stat_error, + type_error, + type_singleton_bool, + type_singleton_string, + type_group, + type_pack_explicit, + type_pack_variadic, + type_pack_generic, + + pub fn Type(self: Kind) type { + return switch (self) { + .unknown => Node, + .attr => Attr, + .generic_type => GenericType, + .generic_type_pack => GenericTypePack, + .expr_group => ExprGroup, + .expr_constant_nil => ExprConstantNil, + .expr_constant_bool => ExprConstantBool, + .expr_constant_number => ExprConstantNumber, + .expr_constant_integer => ExprConstantInteger, + .expr_constant_string => ExprConstantString, + .expr_local => ExprLocal, + .expr_global => ExprGlobal, + .expr_varargs => ExprVarargs, + .expr_call => ExprCall, + .expr_index_name => ExprIndexName, + .expr_index_expr => ExprIndexExpr, + .expr_function => ExprFunction, + .expr_table => ExprTable, + .expr_unary => ExprUnary, + .expr_binary => ExprBinary, + .expr_type_assertion => ExprTypeAssertion, + .expr_if_else => ExprIfElse, + .expr_interp_string => ExprInterpString, + .expr_instantiate => ExprInstantiate, + .stat_block => StatBlock, + .stat_if => StatIf, + .stat_while => StatWhile, + .stat_repeat => StatRepeat, + .stat_break => StatBreak, + .stat_continue => StatContinue, + .stat_return => StatReturn, + .stat_expr => StatExpr, + .stat_local => StatLocal, + .stat_for => StatFor, + .stat_for_in => StatForIn, + .stat_assign => StatAssign, + .stat_compound_assign => StatCompoundAssign, + .stat_function => StatFunction, + .stat_local_function => StatLocalFunction, + .stat_type_alias => StatTypeAlias, + .stat_type_function => StatTypeFunction, + .stat_declare_global => StatDeclareGlobal, + .stat_declare_function => StatDeclareFunction, + .stat_class => StatClass, + .stat_declare_extern_type => StatDeclareExternType, + .type_reference => TypeReference, + .type_table => TypeTable, + .type_function => TypeFunction, + .type_typeof => TypeTypeof, + .type_optional => TypeOptional, + .type_union => TypeUnion, + .type_intersection => TypeIntersection, + .expr_error => ExprError, + .stat_error => StatError, + .type_error => TypeError, + .type_singleton_bool => TypeSingletonBool, + .type_singleton_string => TypeSingletonString, + .type_group => TypeGroup, + .type_pack_explicit => TypePackExplicit, + .type_pack_variadic => TypePackVariadic, + .type_pack_generic => TypePackGeneric, + }; + } + + pub fn Parent(self: Kind) type { + return switch (self) { + .unknown => Node, + .attr, + .generic_type, + .generic_type_pack, + => Node, + .expr_group, + .expr_constant_nil, + .expr_constant_bool, + .expr_constant_number, + .expr_constant_string, + .expr_local, + .expr_global, + .expr_varargs, + .expr_call, + .expr_index_name, + .expr_index_expr, + .expr_function, + .expr_table, + .expr_unary, + .expr_binary, + .expr_type_assertion, + .expr_if_else, + .expr_interp_string, + .expr_instantiate, + .expr_error, + => Expr, + .stat_block, + .stat_if, + .stat_while, + .stat_repeat, + .stat_break, + .stat_continue, + .stat_return, + .stat_expr, + .stat_local, + .stat_for, + .stat_for_in, + .stat_assign, + .stat_compound_assign, + .stat_function, + .stat_local_function, + .stat_type_alias, + .stat_type_function, + .stat_declare_global, + .stat_declare_function, + .stat_class, + .stat_declare_extern_type, + .stat_error, + => Stat, + .type_reference, + .type_table, + .type_function, + .type_typeof, + .type_optional, + .type_union, + .type_intersection, + .type_error, + .type_singleton_bool, + .type_singleton_string, + .type_group, + => Ast.Type, + .type_pack_explicit, + .type_pack_variadic, + .type_pack_generic, + => Ast.TypePack, + }; + } + }; + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; +}; + +pub fn IsFn(base: anytype, comptime to: Node.Kind) bool { + return base.classIndex == to; +} + +pub fn AsCastFn(base: anytype, comptime to: Node.Kind) ?*to.Type() { + return if (base.classIndex == to) @ptrCast(@alignCast(base)) else null; +} + +pub fn AsStatCastFn(base: anytype) *Stat { + return @ptrCast(@alignCast(base)); +} + +pub fn AsExprCastFn(base: anytype) *Expr { + return @ptrCast(@alignCast(base)); +} + +pub fn AsTypeCastFn(base: anytype) *Type { + return @ptrCast(@alignCast(base)); +} + +pub const Attr = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .attr, + location: Location, + + type: Attr.Type, + args: Array(*Expr), + name: Name, + + pub const Type = enum(c_int) { + Checked = 0, + Native = 1, + Deprecated = 2, + DebugNoinline = 3, + Unknown = 4, + }; + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + _ = try Visitor.visit(visitor, self); + } +}; + +pub const Expr = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind, + location: Location, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + return Visitor.selfVisit(visitor, self); + } + + pub fn isLValue(expr: *const Expr) bool { + return switch (expr.classIndex) { + .expr_local, .expr_global, .expr_index_name, .expr_index_expr => true, + else => false, + }; + } + + pub fn getIdentifier(node: *Expr) ?Name { + if (node.as(.expr_global)) |expr| + return expr.name; + + if (node.as(.expr_local)) |expr| + return expr.local.name; + + return null; + } +}; + +pub const Stat = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind, + location: Location, + + hasSemicolon: bool, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + return Visitor.selfVisit(visitor, self); + } +}; + +pub const GenericType = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .generic_type, + location: Location, + + name: Name, + defaultValue: ?*Type = null, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + if (self.defaultValue) |node| + try node.visit(visitor); + } + } +}; + +pub const GenericTypePack = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .generic_type_pack, + location: Location, + + name: Name, + defaultValue: ?*TypePack = null, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + if (self.defaultValue) |node| + try node.visit(visitor); + } + } +}; + +pub const ExprGroup = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .expr_group, + location: Location, + + expr: *Expr, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + try self.expr.visit(visitor); + } + } +}; + +pub const ExprConstantNil = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .expr_constant_nil, + location: Location, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + _ = try Visitor.visit(visitor, self); + } +}; + +pub const ExprConstantBool = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .expr_constant_bool, + location: Location, + + value: bool, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + _ = try Visitor.visit(visitor, self); + } +}; + +pub const ConstantNumberParseResult = enum(c_int) { + Ok = 0, + Imprecise = 1, + Malformed = 2, + BinOverflow = 3, + HexOverflow = 4, + IntOverflow = 5, +}; + +pub const ExprConstantNumber = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .expr_constant_number, + location: Location, + + value: f64, + parseResult: ConstantNumberParseResult, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + _ = try Visitor.visit(visitor, self); + } +}; + +pub const ExprConstantInteger = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .expr_constant_integer, + location: Location, + + value: i64, + parseResult: ConstantNumberParseResult, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + _ = try Visitor.visit(visitor, self); + } +}; + +pub const ExprConstantString = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .expr_constant_string, + location: Location, + + value: Array(u8), + quoteStyle: QuoteStyle, + + pub const QuoteStyle = enum(c_int) { + /// A string created using double quotes or an interpolated string, + /// as in: + /// + /// "foo", `My name is {protagonist}! / And I'm {antagonist}!` + /// + QuotedSimple = 0, + /// A string created using single quotes, as in: + /// + /// 'bar' + /// + QuotedSingle = 1, + /// A string created using `[[ ... ]]` as in: + /// + /// [[ Gee, this sure is a long string. + /// it even has a new line in it! ]] + /// + QuotedRaw = 2, + /// A "string" in the context of a table literal, as in: + /// + /// { foo = 42 } -- `foo` here is a "constant string" + /// + Unquoted = 3, + }; + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + _ = try Visitor.visit(visitor, self); + } +}; + +pub const ExprLocal = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .expr_local, + location: Location, + + local: ?*Local, + upvalue: bool, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + _ = try Visitor.visit(visitor, self); + } +}; + +pub const ExprGlobal = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .expr_global, + location: Location, + + name: Name, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + _ = try Visitor.visit(visitor, self); + } +}; + +pub const ExprVarargs = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .expr_varargs, + location: Location, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + _ = try Visitor.visit(visitor, self); + } +}; + +pub const ExprCall = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .expr_call, + location: Location, + + func: *Expr, + /// These will only be filled in specifically `t:f<>()`. + /// In `f<>()`, this is parsed as `f<>` as an expression, + /// which is then called. + typeArguments: Array(TypeOrPack), + args: Array(*Expr), + self: bool, + argLocation: Location, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + try self.func.visit(visitor); + + for (self.args.slice()) |arg| + try arg.visit(visitor); + } + } +}; + +pub const ExprIndexName = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .expr_index_name, + location: Location, + + expr: *Expr, + index: Name, + indexLocation: Location, + opPosition: Location.Position, + op: u8 = '.', + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + try self.expr.visit(visitor); + } + } +}; + +pub const ExprIndexExpr = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .expr_index_expr, + location: Location, + + expr: *Expr, + index: *Expr, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + try self.expr.visit(visitor); + try self.index.visit(visitor); + } + } +}; + +pub const ExprFunction = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .expr_function, + location: Location, + + attributes: Array(*Attr), + generics: Array(*GenericType), + genericPacks: Array(*GenericTypePack), + self: ?*Local, + args: Array(*Local), + returnAnnotation: ?*TypePack, + vararg: bool = false, + varargLocation: Location, + varargAnnotation: ?*TypePack, + body: *StatBlock, + functionDepth: usize, + debugname: Name, + argLocation: cpp_std.Optional(Location), + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + for (self.args.slice()) |arg| + if (arg.annotation) |node| + try node.visit(visitor); + + if (self.varargAnnotation) |node| + try node.visit(visitor); + + if (self.returnAnnotation) |annotation| + try annotation.visit(visitor); + + try self.body.visit(visitor); + } + } + + pub fn hasNativeAttribute(self: *ExprFunction) bool { + for (self.attributes.slice()) |attr| { + if (attr.type == .Native) + return true; + } + return false; + } + + pub fn hasAttribute(self: *ExprFunction, attrType: Attr.Type) bool { + for (self.attributes.slice()) |attr| { + if (attr.type == attrType) + return true; + } + return false; + } +}; + +pub const ExprTable = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .expr_table, + location: Location, + + items: Array(Item), + + pub const Item = extern struct { + pub const Kind = enum(c_int) { + List, // foo, in which case key is a nullptr + Record, // foo=bar, in which case key is a AstExprConstantString + General, // [foo]=bar + }; + + kind: Kind, + /// can be nullptr! + key: ?*Expr, + value: *Expr, + + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + }; + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + for (self.items.slice()) |item| { + if (item.key) |key| + try key.visit(visitor); + + try item.value.visit(visitor); + } + } + } +}; + +pub const ExprUnary = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .expr_unary, + location: Location, + + op: Op, + expr: *Expr, + + pub const Op = enum(c_int) { + Not = 0, + Minus = 1, + Len = 2, + + pub fn toString(self: Op) []const u8 { + return switch (self) { + .Not => "not", + .Minus => "-", + .Len => "#", + }; + } + }; + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + try self.expr.visit(visitor); + } + } +}; + +pub const ExprBinary = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .expr_binary, + location: Location, + + op: Op, + left: *Expr, + right: *Expr, + + pub const Op = enum(c_int) { + Add = 0, + Sub = 1, + Mul = 2, + Div = 3, + FloorDiv = 4, + Mod = 5, + Pow = 6, + Concat = 7, + CompareNe = 8, + CompareEq = 9, + CompareLt = 10, + CompareLe = 11, + CompareGt = 12, + CompareGe = 13, + And = 14, + Or = 15, + __Count = 16, + + pub fn toString(self: Op) []const u8 { + return switch (self) { + .Add => "+", + .Sub => "-", + .Mul => "*", + .Div => "/", + .FloorDiv => "//", + .Mod => "%", + .Pow => "^", + .Concat => "..", + .CompareNe => "~=", + .CompareEq => "==", + .CompareLt => "<", + .CompareLe => "<=", + .CompareGt => ">", + .CompareGe => ">=", + .And => "and", + .Or => "or", + .__Count => unreachable, + }; + } + }; + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + try self.left.visit(visitor); + try self.right.visit(visitor); + } + } +}; + +pub const ExprTypeAssertion = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .expr_type_assertion, + location: Location, + + expr: *Expr, + annotation: *Type, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + try self.expr.visit(visitor); + try self.annotation.visit(visitor); + } + } +}; + +pub const ExprIfElse = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .expr_if_else, + location: Location, + + condition: *Expr, + hasThen: bool, + trueExpr: *Expr, + hasElse: bool, + falseExpr: *Expr, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + try self.condition.visit(visitor); + try self.trueExpr.visit(visitor); + try self.falseExpr.visit(visitor); + } + } +}; + +pub const ExprInterpString = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .expr_interp_string, + location: Location, + + /// An interpolated string such as `foo{bar}baz` is represented as + /// an array of strings for "foo" and "bar", and an array of expressions for "baz". + /// `strings` will always have one more element than `expressions`. + strings: Array(Array(u8)), + expressions: Array(*Expr), + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + for (self.expressions.slice()) |expr| + try expr.visit(visitor); + } + } +}; + +/// f<> +pub const ExprInstantiate = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .expr_instantiate, + location: Location, + + expr: *Expr, + typeArguments: Array(TypeOrPack), + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + try self.expr.visit(visitor); + try TypeOrPack.visitArray(self.typeArguments, visitor); + } + } +}; + +pub const StatBlock = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .stat_block, + location: Location, + hasSemicolon: bool = false, + + body: Array(*Stat), + /// Indicates whether or not this block has been terminated in a + /// syntactically valid way. + /// + /// This is usually but not always done with the 'end' keyword. StatIf + /// and StatRepeat are the two main exceptions to this. + /// + /// The 'then' clause of an if statement can properly be closed by the + /// keywords 'else' or 'elseif'. A 'repeat' loop's body is closed with the + /// 'until' keyword. + hasEnd: bool = false, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) + for (self.body.slice()) |stat| + try stat.visit(visitor); + } +}; + +pub const StatIf = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .stat_if, + location: Location, + hasSemicolon: bool = false, + + condition: *Expr, + thenbody: *StatBlock, + elsebody: ?*Stat, + thenLocation: cpp_std.Optional(Location), + /// Active for 'elseif' as well + elseLocation: cpp_std.Optional(Location), + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + try self.condition.visit(visitor); + try self.thenbody.visit(visitor); + + if (self.elsebody) |elsebody| + try elsebody.visit(visitor); + } + } +}; + +pub const StatWhile = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .stat_while, + location: Location, + hasSemicolon: bool = false, + + condition: *Expr, + body: *StatBlock, + hasDo: bool = false, + doLocation: Location, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + try self.condition.visit(visitor); + try self.body.visit(visitor); + } + } +}; + +pub const StatRepeat = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .stat_repeat, + location: Location, + hasSemicolon: bool = false, + + condition: *Expr, + body: *StatBlock, + DEPRECATED_hasUntil: bool = false, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + try self.body.visit(visitor); + try self.condition.visit(visitor); + } + } +}; + +pub const StatBreak = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .stat_break, + location: Location, + hasSemicolon: bool = false, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + _ = try Visitor.visit(visitor, self); + } +}; + +pub const StatContinue = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .stat_continue, + location: Location, + hasSemicolon: bool = false, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + _ = try Visitor.visit(visitor, self); + } +}; + +pub const StatReturn = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .stat_return, + location: Location, + hasSemicolon: bool = false, + + list: Array(*Expr), + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + for (self.list.slice()) |expr| + try expr.visit(visitor); + } + } +}; + +pub const StatExpr = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .stat_expr, + location: Location, + hasSemicolon: bool = false, + + expr: *Expr, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) + try self.expr.visit(visitor); + } +}; + +pub const StatLocal = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .stat_local, + location: Location, + hasSemicolon: bool = false, + + vars: Array(*Local), + values: Array(*Expr), + + isConst: bool = false, + isExported: bool = false, + + keywordLocation: cpp_std.Optional(Location), + equalsSignLocation: cpp_std.Optional(Location), + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + for (self.vars.slice()) |@"var"| + if (@"var".annotation) |node| + try node.visit(visitor); + + for (self.values.slice()) |expr| + try expr.visit(visitor); + } + } +}; + +pub const StatFor = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .stat_for, + location: Location, + hasSemicolon: bool = false, + + variable: *Local, + from: *Expr, + to: *Expr, + step: ?*Expr, + body: *StatBlock, + hasDo: bool = false, + doLocation: Location, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + if (self.variable.annotation) |node| + try node.visit(visitor); + + try self.from.visit(visitor); + try self.to.visit(visitor); + + if (self.step) |step| + try step.visit(visitor); + + try self.body.visit(visitor); + } + } +}; + +pub const StatForIn = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .stat_for_in, + location: Location, + hasSemicolon: bool = false, + + vars: Array(*Local), + values: Array(*Expr), + body: *StatBlock, + hasIn: bool = false, + inLocation: Location, + hasDo: bool = false, + doLocation: Location, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + for (self.vars.slice()) |@"var"| + if (@"var".annotation) |node| + try node.visit(visitor); + + for (self.values.slice()) |expr| + try expr.visit(visitor); + + try self.body.visit(visitor); + } + } +}; + +pub const StatAssign = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .stat_assign, + location: Location, + hasSemicolon: bool = false, + + vars: Array(*Expr), + values: Array(*Expr), + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + for (self.vars.slice()) |lvalue| + try lvalue.visit(visitor); + + for (self.values.slice()) |expr| + try expr.visit(visitor); + } + } +}; + +pub const StatCompoundAssign = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .stat_compound_assign, + location: Location, + hasSemicolon: bool = false, + + op: ExprBinary.Op, + variable: *Expr, + value: *Expr, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + try self.variable.visit(visitor); + try self.value.visit(visitor); + } + } +}; + +pub const StatFunction = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .stat_function, + location: Location, + hasSemicolon: bool = false, + + name: *Expr, + func: *ExprFunction, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + try self.name.visit(visitor); + try self.func.visit(visitor); + } + } +}; + +pub const StatLocalFunction = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .stat_local_function, + location: Location, + hasSemicolon: bool = false, + + name: *Local, + func: *ExprFunction, + isConst: bool = false, + /// Position of the `const` keyword; Position::missing() when isConst is false. + constKeywordBegin: Location.Position, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + try self.func.visit(visitor); + } + } +}; + +pub const StatTypeAlias = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .stat_type_alias, + location: Location, + hasSemicolon: bool = false, + + name: Name, + nameLocation: Location, + generics: Array(*GenericType), + genericPacks: Array(*GenericTypePack), + type: *Type, + exported: bool, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + for (self.generics.slice()) |el| + try el.visit(visitor); + + for (self.genericPacks.slice()) |el| + try el.visit(visitor); + + try self.type.visit(visitor); + } + } +}; + +pub const StatTypeFunction = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .stat_type_function, + location: Location, + hasSemicolon: bool = false, + + name: Name, + nameLocation: Location, + body: *ExprFunction = undefined, + exported: bool = false, + hasErrors: bool = false, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + try self.body.visit(visitor); + } + } +}; + +pub const StatDeclareGlobal = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .stat_declare_global, + location: Location, + hasSemicolon: bool = false, + + name: Name, + nameLocation: Location, + type: *Type, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + try self.type.visit(visitor); + } + } +}; + +pub const ArgumentName = extern struct { + name: Name, + location: Location, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; +}; + +pub const StatDeclareFunction = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .stat_declare_function, + location: Location, + hasSemicolon: bool = false, + + attributes: Array(*Attr), + name: Name, + nameLocation: Location, + generics: Array(*GenericType), + genericPacks: Array(*GenericTypePack), + params: TypeList, + paramNames: Array(ArgumentName), + vararg: bool = false, + varargLocation: Location, + retTypes: *TypePack, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + try Visitor.visitTypeList(visitor, self.params); + try self.retTypes.visit(visitor); + } + } + + pub fn isCheckedFunction(self: *StatDeclareFunction) bool { + for (self.attributes.slice()) |attr| { + if (attr.type == .Checked) + return true; + } + return false; + } + + pub fn hasAttribute(self: *StatDeclareFunction, attrType: Attr.Type) bool { + for (self.attributes.slice()) |attr| { + if (attr.type == attrType) + return true; + } + return false; + } +}; + +pub const TableAccess = enum(c_int) { + Read = 1, + Write = 2, + ReadWrite = 3, +}; + +pub const DeclaredExternTypeProperty = extern struct { + name: Name, + nameLocation: Location, + ty: *Type = undefined, + isMethod: bool = false, + location: Location, + access: TableAccess = .ReadWrite, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; +}; + +pub const ClassProperty = extern struct { + qualifierLocation: Location, + name: Name, + nameLocation: Location, + typeColonLocation: cpp_std.Optional(Location) = .nullopt, + ty: ?*Type = null, +}; + +pub const ClassMethod = extern struct { + qualifierLocation: cpp_std.Optional(Location), + keywordLocation: Location, + functionName: Name, + nameLocation: Location, + function: *ExprFunction, +}; + +const ClassMember = Variant(&.{ ClassProperty, ClassMethod }); + +pub const StatClass = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .stat_class, + location: Location, + hasSemicolon: bool = false, + + name: Name, + members: Array(ClassMember), + exported: bool, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + for (self.members.slice()) |member| { + switch (member.typeId) { + 0 => if (member.@"union"().@"0".ty) |ty| + try ty.visit(visitor), + 1 => try member.@"union"().@"1".function.visit(visitor), + else => unreachable, + } + } + } + } +}; + +pub const TableIndexer = extern struct { + indexType: *Type, + resultType: *Type, + location: Location, + access: TableAccess = .ReadWrite, + accessLocation: cpp_std.Optional(Location), +}; + +pub const StatDeclareExternType = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .stat_declare_extern_type, + location: Location, + hasSemicolon: bool = false, + + name: Name, + superName: cpp_std.Optional(Name), + props: Array(DeclaredExternTypeProperty), + indexer: *TableIndexer, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + for (self.props.slice()) |prop| + try prop.ty.visit(visitor); + } + } +}; + +pub const Type = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind, + location: Location, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + return Visitor.selfVisit(visitor, self); + } +}; + +/// Don't have Luau::Variant available, it's a bit of an overhead, but a plain struct is nice to use +pub const TypeOrPack = extern struct { + type: ?*Type = null, + typePack: ?*TypePack = null, + + pub fn visitArray(self: Array(TypeOrPack), visitor: anytype) !void { + for (self.slice()) |param| { + if (param.type) |node| + try node.visit(visitor) + else + try param.typePack.?.visit(visitor); + } + } +}; + +pub const TypeReference = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .type_reference, + location: Location, + + hasParameterList: bool, + prefix: cpp_std.Optional(Name), + prefixLocation: cpp_std.Optional(Location), + prefixLocal: ?*Local = null, + name: Name, + nameLocation: Location, + parameters: Array(TypeOrPack), + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + try TypeOrPack.visitArray(self.parameters, visitor); + } + } +}; + +pub const TableProp = extern struct { + name: Name, + location: Location, + type: *Type, + access: TableAccess = .ReadWrite, + accessLocation: cpp_std.Optional(Location), +}; + +pub const TypeTable = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .type_table, + location: Location, + + props: Array(TableProp), + indexer: ?*TableIndexer, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + for (self.props.slice()) |node| + try node.type.visit(visitor); + + if (self.indexer) |indexer| { + try indexer.indexType.visit(visitor); + try indexer.resultType.visit(visitor); + } + } + } +}; + +pub const TypeFunction = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .type_function, + location: Location, + + attributes: Array(*Attr), + generics: Array(*GenericType), + genericPacks: Array(*GenericTypePack), + argTypes: TypeList, + argNames: Array(cpp_std.Optional(ArgumentName)), + returnTypes: *TypePack, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + try Visitor.visitTypeList(visitor, self.argTypes); + try self.returnTypes.visit(visitor); + } + } + + pub fn isCheckedFunction(self: *TypeFunction) bool { + for (self.attributes.slice()) |attr| { + if (attr.type == .Checked) + return true; + } + return false; + } + + pub fn hasAttribute(self: *TypeFunction, attrType: Attr.Type) bool { + for (self.attributes.slice()) |attr| { + if (attr.type == attrType) + return true; + } + return false; + } +}; + +pub const TypeTypeof = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .type_typeof, + location: Location, + + expr: *Expr, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + try self.expr.visit(visitor); + } + } +}; + +pub const TypeOptional = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .type_optional, + location: Location, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + _ = try Visitor.visit(visitor, self); + } +}; + +pub const TypeUnion = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .type_union, + location: Location, + + types: Array(*Type), + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + for (self.types.slice()) |node| + try node.visit(visitor); + } + } +}; + +pub const TypeIntersection = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .type_intersection, + location: Location, + + types: Array(*Type), + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + for (self.types.slice()) |node| + try node.visit(visitor); + } + } +}; + +pub const ExprError = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .expr_error, + location: Location, + + expressions: Array(*Expr), + messageIndex: c_uint, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + for (self.expressions.slice()) |expression| + try expression.visit(visitor); + } + } +}; + +pub const StatError = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .stat_error, + location: Location, + MAYBE_hasSemicolon: bool = false, + + expressions: Array(*Expr), + statements: Array(*Stat), + messageIndex: c_uint, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + for (self.expressions.slice()) |expression| + try expression.visit(visitor); + + for (self.statements.slice()) |statement| + try statement.visit(visitor); + } + } +}; + +pub const TypeError = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .type_error, + location: Location, + + types: Array(*Type), + isMissing: bool, + messageIndex: c_uint, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + for (self.types.slice()) |node| + try node.visit(visitor); + } + } +}; + +pub const TypeSingletonBool = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .type_singleton_bool, + location: Location, + + value: bool, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + _ = try Visitor.visit(visitor, self); + } +}; + +pub const TypeSingletonString = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .type_singleton_string, + location: Location, + + value: Array(u8), + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + _ = try Visitor.visit(visitor, self); + } +}; + +pub const TypeGroup = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .type_group, + location: Location, + + type: *Type, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + try self.type.visit(visitor); + } + } +}; + +pub const TypePack = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind, + location: Location, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + return Visitor.selfVisit(visitor, self); + } +}; + +pub const TypePackExplicit = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .type_pack_explicit, + location: Location, + + typeList: TypeList, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) { + for (self.typeList.types.slice()) |node| + try node.visit(visitor); + + if (self.typeList.tailType) |node| + try node.visit(visitor); + } + } +}; + +pub const TypePackVariadic = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .type_pack_variadic, + location: Location, + + variadicType: *Type, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + if (try Visitor.visit(visitor, self)) + try self.variadicType.visit(visitor); + } +}; + +pub const TypePackGeneric = extern struct { + vtable: *const anyopaque, + + classIndex: Node.Kind = .type_pack_generic, + location: Location, + + genericName: Name, + + pub const is = IsFn; + pub const as = AsCastFn; + pub const asExpr = AsExprCastFn; + pub const asStat = AsStatCastFn; + pub const asType = AsTypeCastFn; + + pub fn visit(self: *@This(), visitor: anytype) !void { + _ = try Visitor.visit(visitor, self); + } +}; + +pub const Visitor = struct { + pub fn visitTypeList(self: anytype, this: TypeList) !void { + for (this.types.slice()) |node| + try node.visit(self); + + if (this.tailType) |node| + try node.visit(self); + } + + fn getVisitorStruct(comptime self: type) type { + return switch (@typeInfo(self)) { + .pointer => |ptr| ptr.child, + .@"struct" => |s| s, + else => |t| @compileError("Visitor type unsupported: " ++ @typeName(t)), + }; + } + + fn hasVistDecl(comptime self: type, comptime name: [:0]const u8) bool { + return @hasDecl(getVisitorStruct(self), name); + } + + fn callVisitorDecl(self: anytype, comptime name: [:0]const u8, this: anytype) !bool { + const visitor_type = @TypeOf(self); + const visitor_struct = getVisitorStruct(visitor_type); + const visitor_fn = @field(visitor_struct, name); + return switch (@typeInfo(visitor_type)) { + .type => visitor_fn(@ptrCast(@alignCast(this))), + .pointer, .@"struct" => visitor_fn(self, @ptrCast(@alignCast(this))), + else => unreachable, + }; + } + + fn visitByName(self: anytype, comptime name: [:0]const u8, this: anytype) ?bool { + const namespace = @typeName(Ast); + const ast_name = name[namespace.len + 1 ..]; + const fn_name = "visit" ++ ast_name; + + if (comptime hasVistDecl(@TypeOf(self), fn_name)) + return callVisitorDecl(self, fn_name, this); + return null; + } + + fn getParent(comptime ast: type) type { + const namespace = @typeName(Ast); + const ast_name = @typeName(ast)[namespace.len + 1 ..]; + if (std.mem.eql(u8, ast_name, "Attr") or + std.mem.eql(u8, ast_name, "GenericType") or + std.mem.eql(u8, ast_name, "GenericTypePack") or + std.mem.eql(u8, ast_name, "Expr") or + std.mem.eql(u8, ast_name, "Stat")) + return Ast.Node + else if (std.mem.startsWith(u8, ast_name, "Expr")) + return Ast.Expr + else if (std.mem.startsWith(u8, ast_name, "Stat")) + return Ast.Stat + else if (std.mem.startsWith(u8, ast_name, "TypePack")) + return Ast.TypePack + else if (std.mem.startsWith(u8, ast_name, "Type")) + return Ast.Type; + @compileError("Invalid Ast type"); + } + + pub fn selfVisit(self: anytype, this: anytype) anyerror!void { + switch (this.classIndex) { + inline else => |kind| { + const kind_type = kind.Type(); + if (@hasDecl(kind_type, "visit")) + try kind_type.visit(@ptrCast(@alignCast(this)), self); + }, + } + } + + pub fn visit(self: anytype, this: anytype) anyerror!bool { + const node_type = @typeInfo(@TypeOf(this)); + const ast_type = node_type.pointer.child; + comptime if (node_type != .pointer) + @compileError("Invalid Ast type"); + comptime if (!std.mem.startsWith(u8, @typeName(ast_type), @typeName(Ast))) + @compileError("Invalid Ast type"); + + const namespace = @typeName(Ast); + if (ast_type == Ast.Node) { + if (comptime hasVistDecl(@TypeOf(self), "visit")) + return callVisitorDecl(self, "visit", this); + return true; + } else if (ast_type == Ast.Type or ast_type == Ast.TypePack) { + const ast_name = @typeName(ast_type)[namespace.len + 1 ..]; + if (comptime hasVistDecl(@TypeOf(self), "visit" ++ ast_name)) + return callVisitorDecl(self, "visit" ++ ast_name, this); + return false; + } else { + const ast_name = @typeName(ast_type)[namespace.len + 1 ..]; + const fn_name = "visit" ++ ast_name; + + if (comptime hasVistDecl(@TypeOf(self), fn_name)) + return callVisitorDecl(self, fn_name, this); + + const parent = comptime getParent(ast_type); + return Visitor.visit(self, @as(*parent, @ptrCast(@alignCast(this)))); + } + } +}; + +test Node { + const Lexer = @import("Lexer.zig"); + const Parser = @import("Parser.zig"); + const Allocator = @import("Allocator.zig"); + + { + const allocator = Allocator.init(); + defer allocator.deinit(); + + const table = Lexer.AstNameTable.init(allocator); + defer table.deinit(); + const source = + \\local x = 1; + \\local x = 2 + \\local x = 3 + \\ + ; + + var parse_result = Parser.parse(source, table, allocator, .{}); + defer parse_result.deinit(); + + const root = parse_result.root; + + try std.testing.expectEqual(Node.Kind.stat_block, root.classIndex); + + const stats = root.body.slice(); + try std.testing.expectEqual(3, stats.len); + + for (stats, 1..) |node, order| { + switch (node.classIndex) { + .stat_local => { + const local: *StatLocal = node.as(.stat_local).?; + try std.testing.expectEqualStrings("x", std.mem.span(local.vars.slice()[0].name.value)); + try std.testing.expect(@as(f64, @floatFromInt(order)) == local.values.slice()[0].as(.expr_constant_number).?.value); + }, + else => {}, + } + } + } + + { + const allocator = Allocator.init(); + defer allocator.deinit(); + + const astNameTable = Lexer.AstNameTable.init(allocator); + defer astNameTable.deinit(); + const source = + \\@native + \\function test() + \\end + \\ + ; + + const parseResult = Parser.parse(source, astNameTable, allocator, .{}); + defer parseResult.deinit(); + + { + const FunctionVisitor = struct { + hasNativeFunction: bool = false, + + pub fn visitExprFunction(self: *@This(), node: *Ast.ExprFunction) !bool { + errdefer unreachable; + try node.body.visit(self); + + if (!self.hasNativeFunction and node.hasNativeAttribute()) + self.hasNativeFunction = true; + + return false; + } + }; + var visitor: FunctionVisitor = .{}; + + parseResult.root.visit(&visitor) catch unreachable; + // no errors expected, since none of the visitor method does error + + try std.testing.expect(visitor.hasNativeFunction); + } + { + const FunctionVisitor = struct { + hasNativeFunction: bool = false, + + pub fn visitExprFunction(self: *@This(), node: *Ast.ExprFunction) !bool { + try node.body.visit(self); + + if (!self.hasNativeFunction and node.hasNativeAttribute()) { + self.hasNativeFunction = true; + return error.Done; + } + + return false; + } + }; + var visitor: FunctionVisitor = .{}; + + parseResult.root.visit(&visitor) catch |err| switch (err) { + error.Done => {}, // this error is defined in the visitor + else => unreachable, + }; + + try std.testing.expect(visitor.hasNativeFunction); + } + } +} + +test "AstValuesCheck" { + if (@import("builtin").cpu.arch.isWasm() or @import("builtin").os.tag == .windows) + return error.SkipZigTest; + const AstValues = struct { + pub extern "c" const AstAttrIndex: u8; + pub extern "c" const AstGenericTypeIndex: u8; + pub extern "c" const AstGenericTypePackIndex: u8; + pub extern "c" const AstExprGroupIndex: u8; + pub extern "c" const AstExprConstantNilIndex: u8; + pub extern "c" const AstExprConstantBoolIndex: u8; + pub extern "c" const AstExprConstantNumberIndex: u8; + pub extern "c" const AstExprConstantIntegerIndex: u8; + pub extern "c" const AstExprConstantStringIndex: u8; + pub extern "c" const AstExprLocalIndex: u8; + pub extern "c" const AstExprGlobalIndex: u8; + pub extern "c" const AstExprVarargsIndex: u8; + pub extern "c" const AstExprCallIndex: u8; + pub extern "c" const AstExprIndexNameIndex: u8; + pub extern "c" const AstExprIndexExprIndex: u8; + pub extern "c" const AstExprFunctionIndex: u8; + pub extern "c" const AstExprTableIndex: u8; + pub extern "c" const AstExprUnaryIndex: u8; + pub extern "c" const AstExprBinaryIndex: u8; + pub extern "c" const AstExprTypeAssertionIndex: u8; + pub extern "c" const AstExprIfElseIndex: u8; + pub extern "c" const AstExprInterpStringIndex: u8; + pub extern "c" const AstExprInstantiateIndex: u8; + pub extern "c" const AstStatBlockIndex: u8; + pub extern "c" const AstStatIfIndex: u8; + pub extern "c" const AstStatWhileIndex: u8; + pub extern "c" const AstStatRepeatIndex: u8; + pub extern "c" const AstStatBreakIndex: u8; + pub extern "c" const AstStatContinueIndex: u8; + pub extern "c" const AstStatReturnIndex: u8; + pub extern "c" const AstStatExprIndex: u8; + pub extern "c" const AstStatLocalIndex: u8; + pub extern "c" const AstStatForIndex: u8; + pub extern "c" const AstStatForInIndex: u8; + pub extern "c" const AstStatAssignIndex: u8; + pub extern "c" const AstStatCompoundAssignIndex: u8; + pub extern "c" const AstStatFunctionIndex: u8; + pub extern "c" const AstStatLocalFunctionIndex: u8; + pub extern "c" const AstStatTypeAliasIndex: u8; + pub extern "c" const AstStatTypeFunctionIndex: u8; + pub extern "c" const AstStatDeclareFunctionIndex: u8; + pub extern "c" const AstStatDeclareGlobalIndex: u8; + pub extern "c" const AstStatClassIndex: u8; + pub extern "c" const AstStatDeclareExternTypeIndex: u8; + pub extern "c" const AstTypeReferenceIndex: u8; + pub extern "c" const AstTypeTableIndex: u8; + pub extern "c" const AstTypeFunctionIndex: u8; + pub extern "c" const AstTypeTypeofIndex: u8; + pub extern "c" const AstTypeOptionalIndex: u8; + pub extern "c" const AstTypeUnionIndex: u8; + pub extern "c" const AstTypeIntersectionIndex: u8; + pub extern "c" const AstExprErrorIndex: u8; + pub extern "c" const AstStatErrorIndex: u8; + pub extern "c" const AstTypeErrorIndex: u8; + pub extern "c" const AstTypeSingletonBoolIndex: u8; + pub extern "c" const AstTypeSingletonStringIndex: u8; + pub extern "c" const AstTypeGroupIndex: u8; + pub extern "c" const AstTypePackExplicitIndex: u8; + pub extern "c" const AstTypePackVariadicIndex: u8; + pub extern "c" const AstTypePackGenericIndex: u8; + + pub extern "c" const AstAttrSize: usize; + pub extern "c" const AstGenericTypeSize: usize; + pub extern "c" const AstGenericTypePackSize: usize; + pub extern "c" const AstExprGroupSize: usize; + pub extern "c" const AstExprConstantNilSize: usize; + pub extern "c" const AstExprConstantBoolSize: usize; + pub extern "c" const AstExprConstantNumberSize: usize; + pub extern "c" const AstExprConstantIntegerSize: usize; + pub extern "c" const AstExprConstantStringSize: usize; + pub extern "c" const AstExprLocalSize: usize; + pub extern "c" const AstExprGlobalSize: usize; + pub extern "c" const AstExprVarargsSize: usize; + pub extern "c" const AstExprCallSize: usize; + pub extern "c" const AstExprIndexNameSize: usize; + pub extern "c" const AstExprIndexExprSize: usize; + pub extern "c" const AstExprFunctionSize: usize; + pub extern "c" const AstExprTableSize: usize; + pub extern "c" const AstExprUnarySize: usize; + pub extern "c" const AstExprBinarySize: usize; + pub extern "c" const AstExprTypeAssertionSize: usize; + pub extern "c" const AstExprIfElseSize: usize; + pub extern "c" const AstExprInterpStringSize: usize; + pub extern "c" const AstExprInstantiateSize: usize; + pub extern "c" const AstStatBlockSize: usize; + pub extern "c" const AstStatIfSize: usize; + pub extern "c" const AstStatWhileSize: usize; + pub extern "c" const AstStatRepeatSize: usize; + pub extern "c" const AstStatBreakSize: usize; + pub extern "c" const AstStatContinueSize: usize; + pub extern "c" const AstStatReturnSize: usize; + pub extern "c" const AstStatExprSize: usize; + pub extern "c" const AstStatLocalSize: usize; + pub extern "c" const AstStatForSize: usize; + pub extern "c" const AstStatForInSize: usize; + pub extern "c" const AstStatAssignSize: usize; + pub extern "c" const AstStatCompoundAssignSize: usize; + pub extern "c" const AstStatFunctionSize: usize; + pub extern "c" const AstStatLocalFunctionSize: usize; + pub extern "c" const AstStatTypeAliasSize: usize; + pub extern "c" const AstStatTypeFunctionSize: usize; + pub extern "c" const AstStatDeclareFunctionSize: usize; + pub extern "c" const AstStatDeclareGlobalSize: usize; + pub extern "c" const AstStatClassSize: usize; + pub extern "c" const AstStatDeclareExternTypeSize: usize; + pub extern "c" const AstTypeReferenceSize: usize; + pub extern "c" const AstTypeTableSize: usize; + pub extern "c" const AstTypeFunctionSize: usize; + pub extern "c" const AstTypeTypeofSize: usize; + pub extern "c" const AstTypeOptionalSize: usize; + pub extern "c" const AstTypeUnionSize: usize; + pub extern "c" const AstTypeIntersectionSize: usize; + pub extern "c" const AstExprErrorSize: usize; + pub extern "c" const AstStatErrorSize: usize; + pub extern "c" const AstTypeErrorSize: usize; + pub extern "c" const AstTypeSingletonBoolSize: usize; + pub extern "c" const AstTypeSingletonStringSize: usize; + pub extern "c" const AstTypeGroupSize: usize; + pub extern "c" const AstTypePackExplicitSize: usize; + pub extern "c" const AstTypePackVariadicSize: usize; + pub extern "c" const AstTypePackGenericSize: usize; + }; + + @setEvalBranchQuota(2000); + inline for (@typeInfo(AstValues).@"struct".decls) |decl| { + if (comptime std.mem.endsWith(u8, decl.name, "Index")) { + const name = decl.name[3 .. decl.name.len - 5]; + + const ast_node_type = @field(Ast, name); + const info = @typeInfo(ast_node_type).@"struct"; + + comptime var field: ?std.builtin.Type.StructField = null; + inline for (info.fields) |f| { + if (comptime std.mem.eql(u8, f.name, "classIndex")) { + field = f; + break; + } + } + if (field == null) + @compileError("classIndex field not found"); + const default_value_ptr = field.?.default_value_ptr orelse @compileError("classIndex field does not have a default value"); + const enum_value = @as(*const Ast.Node.Kind, @ptrCast(@alignCast(default_value_ptr))).*; + + std.testing.expectEqual(@field(AstValues, decl.name), @intFromEnum(enum_value)) catch |err| { + std.debug.print("index error for {s}\n", .{name}); + return err; + }; + } else if (comptime std.mem.endsWith(u8, decl.name, "Size")) { + const name = decl.name[3 .. decl.name.len - 4]; + + const ast_node_type = @field(Ast, name); + + std.testing.expectEqual(@field(AstValues, decl.name), @sizeOf(ast_node_type)) catch |err| { + std.debug.print("size error for {s}\n", .{name}); + return err; + }; + } else @compileError("unknown exported constant"); + } +} + +// sources: +// https://github.com/luau-lang/luau/blob/40d4815888f63362a6cb79b3e74c4aafa0b2cbf4/Ast/include/Luau/Ast.h +// https://github.com/luau-lang/luau/blob/40d4815888f63362a6cb79b3e74c4aafa0b2cbf4/Ast/src/Ast.cpp diff --git a/deps/luau/src/Ast/Class.cpp b/deps/luau/src/Ast/Class.cpp new file mode 100644 index 0000000..30ae585 --- /dev/null +++ b/deps/luau/src/Ast/Class.cpp @@ -0,0 +1,211 @@ +#include + +#include "Luau/Ast.h" +#include "Luau/Cst.h" + +using namespace Luau; + +// AST +ZIG_EXPORT const unsigned char AstAttrIndex = AstAttr::ClassIndex(); +ZIG_EXPORT const unsigned char AstGenericTypeIndex = AstGenericType::ClassIndex(); +ZIG_EXPORT const unsigned char AstGenericTypePackIndex = AstGenericTypePack::ClassIndex(); +ZIG_EXPORT const unsigned char AstExprGroupIndex = AstExprGroup::ClassIndex(); +ZIG_EXPORT const unsigned char AstExprConstantNilIndex = AstExprConstantNil::ClassIndex(); +ZIG_EXPORT const unsigned char AstExprConstantBoolIndex = AstExprConstantBool::ClassIndex(); +ZIG_EXPORT const unsigned char AstExprConstantNumberIndex = AstExprConstantNumber::ClassIndex(); +ZIG_EXPORT const unsigned char AstExprConstantIntegerIndex = AstExprConstantInteger::ClassIndex(); +ZIG_EXPORT const unsigned char AstExprConstantStringIndex = AstExprConstantString::ClassIndex(); +ZIG_EXPORT const unsigned char AstExprLocalIndex = AstExprLocal::ClassIndex(); +ZIG_EXPORT const unsigned char AstExprGlobalIndex = AstExprGlobal::ClassIndex(); +ZIG_EXPORT const unsigned char AstExprVarargsIndex = AstExprVarargs::ClassIndex(); +ZIG_EXPORT const unsigned char AstExprCallIndex = AstExprCall::ClassIndex(); +ZIG_EXPORT const unsigned char AstExprIndexNameIndex = AstExprIndexName::ClassIndex(); +ZIG_EXPORT const unsigned char AstExprIndexExprIndex = AstExprIndexExpr::ClassIndex(); +ZIG_EXPORT const unsigned char AstExprFunctionIndex = AstExprFunction::ClassIndex(); +ZIG_EXPORT const unsigned char AstExprTableIndex = AstExprTable::ClassIndex(); +ZIG_EXPORT const unsigned char AstExprUnaryIndex = AstExprUnary::ClassIndex(); +ZIG_EXPORT const unsigned char AstExprBinaryIndex = AstExprBinary::ClassIndex(); +ZIG_EXPORT const unsigned char AstExprTypeAssertionIndex = AstExprTypeAssertion::ClassIndex(); +ZIG_EXPORT const unsigned char AstExprIfElseIndex = AstExprIfElse::ClassIndex(); +ZIG_EXPORT const unsigned char AstExprInterpStringIndex = AstExprInterpString::ClassIndex(); +ZIG_EXPORT const unsigned char AstExprInstantiateIndex = AstExprInstantiate::ClassIndex(); +ZIG_EXPORT const unsigned char AstStatBlockIndex = AstStatBlock::ClassIndex(); +ZIG_EXPORT const unsigned char AstStatIfIndex = AstStatIf::ClassIndex(); +ZIG_EXPORT const unsigned char AstStatWhileIndex = AstStatWhile::ClassIndex(); +ZIG_EXPORT const unsigned char AstStatRepeatIndex = AstStatRepeat::ClassIndex(); +ZIG_EXPORT const unsigned char AstStatBreakIndex = AstStatBreak::ClassIndex(); +ZIG_EXPORT const unsigned char AstStatContinueIndex = AstStatContinue::ClassIndex(); +ZIG_EXPORT const unsigned char AstStatReturnIndex = AstStatReturn::ClassIndex(); +ZIG_EXPORT const unsigned char AstStatExprIndex = AstStatExpr::ClassIndex(); +ZIG_EXPORT const unsigned char AstStatLocalIndex = AstStatLocal::ClassIndex(); +ZIG_EXPORT const unsigned char AstStatForIndex = AstStatFor::ClassIndex(); +ZIG_EXPORT const unsigned char AstStatForInIndex = AstStatForIn::ClassIndex(); +ZIG_EXPORT const unsigned char AstStatAssignIndex = AstStatAssign::ClassIndex(); +ZIG_EXPORT const unsigned char AstStatCompoundAssignIndex = AstStatCompoundAssign::ClassIndex(); +ZIG_EXPORT const unsigned char AstStatFunctionIndex = AstStatFunction::ClassIndex(); +ZIG_EXPORT const unsigned char AstStatLocalFunctionIndex = AstStatLocalFunction::ClassIndex(); +ZIG_EXPORT const unsigned char AstStatTypeAliasIndex = AstStatTypeAlias::ClassIndex(); +ZIG_EXPORT const unsigned char AstStatTypeFunctionIndex = AstStatTypeFunction::ClassIndex(); +ZIG_EXPORT const unsigned char AstStatDeclareFunctionIndex = AstStatDeclareFunction::ClassIndex(); +ZIG_EXPORT const unsigned char AstStatDeclareGlobalIndex = AstStatDeclareGlobal::ClassIndex(); +ZIG_EXPORT const unsigned char AstStatClassIndex = AstStatClass::ClassIndex(); +ZIG_EXPORT const unsigned char AstStatDeclareExternTypeIndex = AstStatDeclareExternType::ClassIndex(); +ZIG_EXPORT const unsigned char AstTypeReferenceIndex = AstTypeReference::ClassIndex(); +ZIG_EXPORT const unsigned char AstTypeTableIndex = AstTypeTable::ClassIndex(); +ZIG_EXPORT const unsigned char AstTypeFunctionIndex = AstTypeFunction::ClassIndex(); +ZIG_EXPORT const unsigned char AstTypeTypeofIndex = AstTypeTypeof::ClassIndex(); +ZIG_EXPORT const unsigned char AstTypeOptionalIndex = AstTypeOptional::ClassIndex(); +ZIG_EXPORT const unsigned char AstTypeUnionIndex = AstTypeUnion::ClassIndex(); +ZIG_EXPORT const unsigned char AstTypeIntersectionIndex = AstTypeIntersection::ClassIndex(); +ZIG_EXPORT const unsigned char AstExprErrorIndex = AstExprError::ClassIndex(); +ZIG_EXPORT const unsigned char AstStatErrorIndex = AstStatError::ClassIndex(); +ZIG_EXPORT const unsigned char AstTypeErrorIndex = AstTypeError::ClassIndex(); +ZIG_EXPORT const unsigned char AstTypeSingletonBoolIndex = AstTypeSingletonBool::ClassIndex(); +ZIG_EXPORT const unsigned char AstTypeSingletonStringIndex = AstTypeSingletonString::ClassIndex(); +ZIG_EXPORT const unsigned char AstTypeGroupIndex = AstTypeGroup::ClassIndex(); +ZIG_EXPORT const unsigned char AstTypePackExplicitIndex = AstTypePackExplicit::ClassIndex(); +ZIG_EXPORT const unsigned char AstTypePackVariadicIndex = AstTypePackVariadic::ClassIndex(); +ZIG_EXPORT const unsigned char AstTypePackGenericIndex = AstTypePackGeneric::ClassIndex(); + +ZIG_EXPORT const unsigned long AstAttrSize = sizeof(AstAttr); +ZIG_EXPORT const unsigned long AstGenericTypeSize = sizeof(AstGenericType); +ZIG_EXPORT const unsigned long AstGenericTypePackSize = sizeof(AstGenericTypePack); +ZIG_EXPORT const unsigned long AstExprGroupSize = sizeof(AstExprGroup); +ZIG_EXPORT const unsigned long AstExprConstantNilSize = sizeof(AstExprConstantNil); +ZIG_EXPORT const unsigned long AstExprConstantBoolSize = sizeof(AstExprConstantBool); +ZIG_EXPORT const unsigned long AstExprConstantNumberSize = sizeof(AstExprConstantNumber); +ZIG_EXPORT const unsigned long AstExprConstantIntegerSize = sizeof(AstExprConstantInteger); +ZIG_EXPORT const unsigned long AstExprConstantStringSize = sizeof(AstExprConstantString); +ZIG_EXPORT const unsigned long AstExprLocalSize = sizeof(AstExprLocal); +ZIG_EXPORT const unsigned long AstExprGlobalSize = sizeof(AstExprGlobal); +ZIG_EXPORT const unsigned long AstExprVarargsSize = sizeof(AstExprVarargs); +ZIG_EXPORT const unsigned long AstExprCallSize = sizeof(AstExprCall); +ZIG_EXPORT const unsigned long AstExprIndexNameSize = sizeof(AstExprIndexName); +ZIG_EXPORT const unsigned long AstExprIndexExprSize = sizeof(AstExprIndexExpr); +ZIG_EXPORT const unsigned long AstExprFunctionSize = sizeof(AstExprFunction); +ZIG_EXPORT const unsigned long AstExprTableSize = sizeof(AstExprTable); +ZIG_EXPORT const unsigned long AstExprUnarySize = sizeof(AstExprUnary); +ZIG_EXPORT const unsigned long AstExprBinarySize = sizeof(AstExprBinary); +ZIG_EXPORT const unsigned long AstExprTypeAssertionSize = sizeof(AstExprTypeAssertion); +ZIG_EXPORT const unsigned long AstExprIfElseSize = sizeof(AstExprIfElse); +ZIG_EXPORT const unsigned long AstExprInterpStringSize = sizeof(AstExprInterpString); +ZIG_EXPORT const unsigned long AstExprInstantiateSize = sizeof(AstExprInstantiate); +ZIG_EXPORT const unsigned long AstStatBlockSize = sizeof(AstStatBlock); +ZIG_EXPORT const unsigned long AstStatIfSize = sizeof(AstStatIf); +ZIG_EXPORT const unsigned long AstStatWhileSize = sizeof(AstStatWhile); +ZIG_EXPORT const unsigned long AstStatRepeatSize = sizeof(AstStatRepeat); +ZIG_EXPORT const unsigned long AstStatBreakSize = sizeof(AstStatBreak); +ZIG_EXPORT const unsigned long AstStatContinueSize = sizeof(AstStatContinue); +ZIG_EXPORT const unsigned long AstStatReturnSize = sizeof(AstStatReturn); +ZIG_EXPORT const unsigned long AstStatExprSize = sizeof(AstStatExpr); +ZIG_EXPORT const unsigned long AstStatLocalSize = sizeof(AstStatLocal); +ZIG_EXPORT const unsigned long AstStatForSize = sizeof(AstStatFor); +ZIG_EXPORT const unsigned long AstStatForInSize = sizeof(AstStatForIn); +ZIG_EXPORT const unsigned long AstStatAssignSize = sizeof(AstStatAssign); +ZIG_EXPORT const unsigned long AstStatCompoundAssignSize = sizeof(AstStatCompoundAssign); +ZIG_EXPORT const unsigned long AstStatFunctionSize = sizeof(AstStatFunction); +ZIG_EXPORT const unsigned long AstStatLocalFunctionSize = sizeof(AstStatLocalFunction); +ZIG_EXPORT const unsigned long AstStatTypeAliasSize = sizeof(AstStatTypeAlias); +ZIG_EXPORT const unsigned long AstStatTypeFunctionSize = sizeof(AstStatTypeFunction); +ZIG_EXPORT const unsigned long AstStatDeclareFunctionSize = sizeof(AstStatDeclareFunction); +ZIG_EXPORT const unsigned long AstStatDeclareGlobalSize = sizeof(AstStatDeclareGlobal); +ZIG_EXPORT const unsigned long AstStatClassSize = sizeof(AstStatClass); +ZIG_EXPORT const unsigned long AstStatDeclareExternTypeSize = sizeof(AstStatDeclareExternType); +ZIG_EXPORT const unsigned long AstTypeReferenceSize = sizeof(AstTypeReference); +ZIG_EXPORT const unsigned long AstTypeTableSize = sizeof(AstTypeTable); +ZIG_EXPORT const unsigned long AstTypeFunctionSize = sizeof(AstTypeFunction); +ZIG_EXPORT const unsigned long AstTypeTypeofSize = sizeof(AstTypeTypeof); +ZIG_EXPORT const unsigned long AstTypeOptionalSize = sizeof(AstTypeOptional); +ZIG_EXPORT const unsigned long AstTypeUnionSize = sizeof(AstTypeUnion); +ZIG_EXPORT const unsigned long AstTypeIntersectionSize = sizeof(AstTypeIntersection); +ZIG_EXPORT const unsigned long AstExprErrorSize = sizeof(AstExprError); +ZIG_EXPORT const unsigned long AstStatErrorSize = sizeof(AstStatError); +ZIG_EXPORT const unsigned long AstTypeErrorSize = sizeof(AstTypeError); +ZIG_EXPORT const unsigned long AstTypeSingletonBoolSize = sizeof(AstTypeSingletonBool); +ZIG_EXPORT const unsigned long AstTypeSingletonStringSize = sizeof(AstTypeSingletonString); +ZIG_EXPORT const unsigned long AstTypeGroupSize = sizeof(AstTypeGroup); +ZIG_EXPORT const unsigned long AstTypePackExplicitSize = sizeof(AstTypePackExplicit); +ZIG_EXPORT const unsigned long AstTypePackVariadicSize = sizeof(AstTypePackVariadic); +ZIG_EXPORT const unsigned long AstTypePackGenericSize = sizeof(AstTypePackGeneric); + +// CST +ZIG_EXPORT const unsigned char CstAttrIndex = CstAttr::CstClassIndex(); +ZIG_EXPORT const unsigned char CstParametrizedAttrIndex = CstParametrizedAttr::CstClassIndex(); +ZIG_EXPORT const unsigned char CstExprGroupIndex = CstExprGroup::CstClassIndex(); +ZIG_EXPORT const unsigned char CstExprConstantNumberIndex = CstExprConstantNumber::CstClassIndex(); +ZIG_EXPORT const unsigned char CstExprConstantIntegerIndex = CstExprConstantInteger::CstClassIndex(); +ZIG_EXPORT const unsigned char CstExprConstantStringIndex = CstExprConstantString::CstClassIndex(); +ZIG_EXPORT const unsigned char CstExprCallIndex = CstExprCall::CstClassIndex(); +ZIG_EXPORT const unsigned char CstExprIndexExprIndex = CstExprIndexExpr::CstClassIndex(); +ZIG_EXPORT const unsigned char CstExprFunctionIndex = CstExprFunction::CstClassIndex(); +ZIG_EXPORT const unsigned char CstExprTableIndex = CstExprTable::CstClassIndex(); +ZIG_EXPORT const unsigned char CstExprOpIndex = CstExprOp::CstClassIndex(); +ZIG_EXPORT const unsigned char CstExprTypeAssertionIndex = CstExprTypeAssertion::CstClassIndex(); +ZIG_EXPORT const unsigned char CstExprIfElseIndex = CstExprIfElse::CstClassIndex(); +ZIG_EXPORT const unsigned char CstExprInterpStringIndex = CstExprInterpString::CstClassIndex(); +ZIG_EXPORT const unsigned char CstExprExplicitTypeInstantiationIndex = CstExprExplicitTypeInstantiation::CstClassIndex(); +ZIG_EXPORT const unsigned char CstStatDoIndex = CstStatDo::CstClassIndex(); +ZIG_EXPORT const unsigned char CstStatRepeatIndex = CstStatRepeat::CstClassIndex(); +ZIG_EXPORT const unsigned char CstStatReturnIndex = CstStatReturn::CstClassIndex(); +ZIG_EXPORT const unsigned char CstStatLocalIndex = CstStatLocal::CstClassIndex(); +ZIG_EXPORT const unsigned char CstStatForIndex = CstStatFor::CstClassIndex(); +ZIG_EXPORT const unsigned char CstStatForInIndex = CstStatForIn::CstClassIndex(); +ZIG_EXPORT const unsigned char CstStatAssignIndex = CstStatAssign::CstClassIndex(); +ZIG_EXPORT const unsigned char CstStatCompoundAssignIndex = CstStatCompoundAssign::CstClassIndex(); +ZIG_EXPORT const unsigned char CstStatFunctionIndex = CstStatFunction::CstClassIndex(); +ZIG_EXPORT const unsigned char CstStatLocalFunctionIndex = CstStatLocalFunction::CstClassIndex(); +ZIG_EXPORT const unsigned char CstGenericTypeIndex = CstGenericType::CstClassIndex(); +ZIG_EXPORT const unsigned char CstGenericTypePackIndex = CstGenericTypePack::CstClassIndex(); +ZIG_EXPORT const unsigned char CstStatTypeAliasIndex = CstStatTypeAlias::CstClassIndex(); +ZIG_EXPORT const unsigned char CstStatTypeFunctionIndex = CstStatTypeFunction::CstClassIndex(); +ZIG_EXPORT const unsigned char CstTypeReferenceIndex = CstTypeReference::CstClassIndex(); +ZIG_EXPORT const unsigned char CstTypeTableIndex = CstTypeTable::CstClassIndex(); +ZIG_EXPORT const int CstTypeTableItemKindIndexer = static_cast(CstTypeTable::Item::Kind::Indexer); +ZIG_EXPORT const int CstTypeTableItemKindProperty = static_cast(CstTypeTable::Item::Kind::Property); +ZIG_EXPORT const int CstTypeTableItemKindStringProperty = static_cast(CstTypeTable::Item::Kind::StringProperty); +ZIG_EXPORT const unsigned char CstTypeFunctionIndex = CstTypeFunction::CstClassIndex(); +ZIG_EXPORT const unsigned char CstTypeTypeofIndex = CstTypeTypeof::CstClassIndex(); +ZIG_EXPORT const unsigned char CstTypeUnionIndex = CstTypeUnion::CstClassIndex(); +ZIG_EXPORT const unsigned char CstTypeIntersectionIndex = CstTypeIntersection::CstClassIndex(); +ZIG_EXPORT const unsigned char CstTypeSingletonStringIndex = CstTypeSingletonString::CstClassIndex(); +ZIG_EXPORT const unsigned char CstTypeGroupIndex = CstTypeGroup::CstClassIndex(); +ZIG_EXPORT const unsigned char CstTypePackExplicitIndex = CstTypePackExplicit::CstClassIndex(); +ZIG_EXPORT const unsigned char CstTypePackGenericIndex = CstTypePackGeneric::CstClassIndex(); + +ZIG_EXPORT const unsigned long CstExprGroupSize = sizeof(CstExprGroup); +ZIG_EXPORT const unsigned long CstExprConstantNumberSize = sizeof(CstExprConstantNumber); +ZIG_EXPORT const unsigned long CstExprConstantIntegerSize = sizeof(CstExprConstantInteger); +ZIG_EXPORT const unsigned long CstExprConstantStringSize = sizeof(CstExprConstantString); +ZIG_EXPORT const unsigned long CstExprCallSize = sizeof(CstExprCall); +ZIG_EXPORT const unsigned long CstExprIndexExprSize = sizeof(CstExprIndexExpr); +ZIG_EXPORT const unsigned long CstExprFunctionSize = sizeof(CstExprFunction); +ZIG_EXPORT const unsigned long CstExprTableSize = sizeof(CstExprTable); +ZIG_EXPORT const unsigned long CstExprOpSize = sizeof(CstExprOp); +ZIG_EXPORT const unsigned long CstExprTypeAssertionSize = sizeof(CstExprTypeAssertion); +ZIG_EXPORT const unsigned long CstExprIfElseSize = sizeof(CstExprIfElse); +ZIG_EXPORT const unsigned long CstExprInterpStringSize = sizeof(CstExprInterpString); +ZIG_EXPORT const unsigned long CstExprExplicitTypeInstantiationSize = sizeof(CstExprExplicitTypeInstantiation); +ZIG_EXPORT const unsigned long CstStatDoSize = sizeof(CstStatDo); +ZIG_EXPORT const unsigned long CstStatRepeatSize = sizeof(CstStatRepeat); +ZIG_EXPORT const unsigned long CstStatReturnSize = sizeof(CstStatReturn); +ZIG_EXPORT const unsigned long CstStatLocalSize = sizeof(CstStatLocal); +ZIG_EXPORT const unsigned long CstStatForSize = sizeof(CstStatFor); +ZIG_EXPORT const unsigned long CstStatForInSize = sizeof(CstStatForIn); +ZIG_EXPORT const unsigned long CstStatAssignSize = sizeof(CstStatAssign); +ZIG_EXPORT const unsigned long CstStatCompoundAssignSize = sizeof(CstStatCompoundAssign); +ZIG_EXPORT const unsigned long CstStatFunctionSize = sizeof(CstStatFunction); +ZIG_EXPORT const unsigned long CstStatLocalFunctionSize = sizeof(CstStatLocalFunction); +ZIG_EXPORT const unsigned long CstGenericTypeSize = sizeof(CstGenericType); +ZIG_EXPORT const unsigned long CstGenericTypePackSize = sizeof(CstGenericTypePack); +ZIG_EXPORT const unsigned long CstStatTypeAliasSize = sizeof(CstStatTypeAlias); +ZIG_EXPORT const unsigned long CstStatTypeFunctionSize = sizeof(CstStatTypeFunction); +ZIG_EXPORT const unsigned long CstTypeReferenceSize = sizeof(CstTypeReference); +ZIG_EXPORT const unsigned long CstTypeTableSize = sizeof(CstTypeTable); +ZIG_EXPORT const unsigned long CstTypeFunctionSize = sizeof(CstTypeFunction); +ZIG_EXPORT const unsigned long CstTypeTypeofSize = sizeof(CstTypeTypeof); +ZIG_EXPORT const unsigned long CstTypeUnionSize = sizeof(CstTypeUnion); +ZIG_EXPORT const unsigned long CstTypeIntersectionSize = sizeof(CstTypeIntersection); +ZIG_EXPORT const unsigned long CstTypeSingletonStringSize = sizeof(CstTypeSingletonString); +ZIG_EXPORT const unsigned long CstTypeGroupSize = sizeof(CstTypeGroup); +ZIG_EXPORT const unsigned long CstTypePackExplicitSize = sizeof(CstTypePackExplicit); +ZIG_EXPORT const unsigned long CstTypePackGenericSize = sizeof(CstTypePackGeneric); diff --git a/deps/luau/src/Ast/Cst.zig b/deps/luau/src/Ast/Cst.zig new file mode 100644 index 0000000..4787b89 --- /dev/null +++ b/deps/luau/src/Ast/Cst.zig @@ -0,0 +1,648 @@ +const std = @import("std"); + +const Ast = @import("Ast.zig"); +const Location = @import("Location.zig").Location; + +const cpp_std = @import("../cpp_std.zig"); + +const Cst = @This(); + +pub const Node = extern struct { + classIndex: Kind, + + pub const Kind = enum(i32) { + unknown, + attr, + parametrized_attr, + expr_group, + expr_constant_number, + expr_constant_integer, + expr_constant_string, + expr_call, + expr_index_expr, + expr_function, + expr_table, + expr_op, + expr_type_assertion, + expr_if_else, + expr_interp_string, + expr_explicit_type_instantiation, + stat_do, + stat_repeat, + stat_return, + stat_local, + stat_for, + stat_for_in, + stat_assign, + stat_compound_assign, + stat_function, + stat_local_function, + generic_type, + generic_type_pack, + stat_type_alias, + stat_type_function, + type_reference, + type_table, + type_function, + type_typeof, + type_union, + type_intersection, + type_singleton_string, + type_group, + type_pack_explicit, + type_pack_generic, + + pub fn Type(comptime self: Kind) type { + return switch (self) { + .unknown => Node, + .attr => Attr, + .parametrized_attr => ParametrizedAttr, + .expr_group => ExprGroup, + .expr_constant_number => ExprConstantNumber, + .expr_constant_integer => ExprConstantInteger, + .expr_constant_string => ExprConstantString, + .expr_call => ExprCall, + .expr_index_expr => ExprIndexExpr, + .expr_function => ExprFunction, + .expr_table => ExprTable, + .expr_op => ExprOp, + .expr_type_assertion => ExprTypeAssertion, + .expr_if_else => ExprIfElse, + .expr_interp_string => ExprInterpString, + .expr_explicit_type_instantiation => ExprExplicitTypeInstantiation, + .stat_do => StatDo, + .stat_repeat => StatRepeat, + .stat_return => StatReturn, + .stat_local => StatLocal, + .stat_for => StatFor, + .stat_for_in => StatForIn, + .stat_assign => StatAssign, + .stat_compound_assign => StatCompoundAssign, + .stat_function => StatFunction, + .stat_local_function => StatLocalFunction, + .generic_type => GenericType, + .generic_type_pack => GenericTypePack, + .stat_type_alias => StatTypeAlias, + .stat_type_function => StatTypeFunction, + .type_reference => TypeReference, + .type_table => TypeTable, + .type_function => TypeFunction, + .type_typeof => TypeTypeof, + .type_union => TypeUnion, + .type_intersection => TypeIntersection, + .type_singleton_string => TypeSingletonString, + .type_group => TypeGroup, + .type_pack_explicit => TypePackExplicit, + .type_pack_generic => TypePackGeneric, + }; + } + }; + + pub const is = IsFn; + pub const as = AsCastFn; +}; + +pub fn IsFn(base: anytype, comptime to: Node.Kind) bool { + return base.classIndex == to; +} + +pub fn AsCastFn(base: anytype, comptime to: Node.Kind) ?*to.Type() { + return if (base.classIndex == to) @ptrCast(@alignCast(base)) else null; +} + +pub const Attr = extern struct { + classIndex: Node.Kind = .attr, + + /// false when inside an attribute list, ie @[native checked] + hasAt: bool, +}; + +pub const ParametrizedAttr = extern struct { + classIndex: Node.Kind = .parametrized_attr, + + /// for `@x(args)` form + openParenPosition: Location.Position, + closeParenPosition: Location.Position, + + /// Commas inside the `(a, b, c)` arg list + argsCommaPositions: Ast.Array(Location.Position), +}; + +pub const AttrList = extern struct { + atBracketPosition: Location.Position, + closeBracketPosition: Location.Position, + commaPositions: Ast.Array(Location.Position), +}; + +pub const ExprGroup = extern struct { + classIndex: Node.Kind = .expr_group, + + closePosition: Location.Position, +}; + +pub const ExprConstantNumber = extern struct { + classIndex: Node.Kind = .expr_constant_number, + + value: Ast.Array(u8), +}; + +pub const ExprConstantInteger = extern struct { + classIndex: Node.Kind = .expr_constant_integer, + + value: Ast.Array(u8), +}; + +pub const ExprConstantString = extern struct { + classIndex: Node.Kind = .expr_constant_string, + + sourceString: Ast.Array(u8), + quoteStyle: QuoteStyle, + blockDepth: u32, + + pub const QuoteStyle = enum(u32) { + quoted_single, + quoted_double, + quoted_raw, + quoted_interp, + }; +}; + +pub const TypeInstantiation = extern struct { + leftArrow1Position: Location.Position = .missing, + leftArrow2Position: Location.Position = .missing, + + commaPositions: Ast.Array(Location.Position), + + rightArrow1Position: Location.Position = .missing, + rightArrow2Position: Location.Position = .missing, +}; + +pub const ExprCall = extern struct { + classIndex: Node.Kind = .expr_call, + + openParens: Location.Position, + closeParens: Location.Position, + commaPositions: Ast.Array(Location.Position), + explicitTypes: ?*TypeInstantiation = null, +}; + +pub const ExprIndexExpr = extern struct { + classIndex: Node.Kind = .expr_index_expr, + + openBracketPosition: Location.Position, + closeBracketPosition: Location.Position, +}; + +pub const ExprFunction = extern struct { + classIndex: Node.Kind = .expr_function, + + attrLists: Ast.Array(*AttrList) = .{}, + functionKeywordPosition: Location.Position = .missing, + openGenericsPosition: Location.Position = .missing, + genericsCommaPositions: Ast.Array(Location.Position), + closeGenericsPosition: Location.Position = .missing, + argsAnnotationColonPositions: Ast.Array(Location.Position), + argsCommaPositions: Ast.Array(Location.Position), + varargAnnotationColonPosition: Location.Position = .missing, + returnSpecifierPosition: Location.Position = .missing, +}; + +pub const ExprTable = extern struct { + classIndex: Node.Kind = .expr_table, + + items: Ast.Array(Item), + + pub const Separator = enum(u32) { + comma, + semicolon, + missing, + }; + + pub const Item = extern struct { + /// '[', only if Kind == General + indexerOpenPosition: Location.Position, + /// ']', only if Kind == General + indexerClosePosition: Location.Position, + /// only if Kind != List + equalsPosition: Location.Position, + /// may be missing for last Item + separator: Separator, + /// may be missing for last Item + separatorPosition: Location.Position, + }; +}; + +pub const ExprOp = extern struct { + classIndex: Node.Kind = .expr_op, + + opPosition: Location.Position, +}; + +pub const ExprTypeAssertion = extern struct { + classIndex: Node.Kind = .expr_type_assertion, + + opPosition: Location.Position, +}; + +pub const ExprIfElse = extern struct { + classIndex: Node.Kind = .expr_if_else, + + thenPosition: Location.Position, + elsePosition: Location.Position, + isElseIf: bool, +}; + +pub const ExprInterpString = extern struct { + classIndex: Node.Kind = .expr_interp_string, + + sourceStrings: Ast.Array(Ast.Array(u8)), + stringPositions: Ast.Array(Location.Position), +}; + +pub const ExprExplicitTypeInstantiation = extern struct { + classIndex: Node.Kind = .expr_explicit_type_instantiation, + + instantiation: TypeInstantiation, +}; + +pub const StatDo = extern struct { + classIndex: Node.Kind = .stat_do, + + statsStartPosition: Location.Position, + endPosition: Location.Position, +}; + +pub const StatRepeat = extern struct { + classIndex: Node.Kind = .stat_repeat, + + untilPosition: Location.Position, +}; + +pub const StatReturn = extern struct { + classIndex: Node.Kind = .stat_return, + + commaPositions: Ast.Array(Location.Position), +}; + +pub const StatLocal = extern struct { + classIndex: Node.Kind = .stat_local, + + varsAnnotationColonPositions: Ast.Array(Location.Position), + varsCommaPositions: Ast.Array(Location.Position), + valuesCommaPositions: Ast.Array(Location.Position), +}; + +pub const StatFor = extern struct { + classIndex: Node.Kind = .stat_for, + + annotationColonPosition: Location.Position, + equalsPosition: Location.Position, + endCommaPosition: Location.Position, + stepCommaPosition: Location.Position, +}; + +pub const StatForIn = extern struct { + classIndex: Node.Kind = .stat_for_in, + + varsAnnotationColonPositions: Ast.Array(Location.Position), + varsCommaPositions: Ast.Array(Location.Position), + valuesCommaPositions: Ast.Array(Location.Position), +}; + +pub const StatAssign = extern struct { + classIndex: Node.Kind = .stat_assign, + + varsCommaPositions: Ast.Array(Location.Position), + equalsPosition: Location.Position, + valuesCommaPositions: Ast.Array(Location.Position), +}; + +pub const StatCompoundAssign = extern struct { + classIndex: Node.Kind = .stat_compound_assign, + + opPosition: Location.Position, +}; + +pub const StatFunction = extern struct { + classIndex: Node.Kind = .stat_function, + + attrLists: Ast.Array(*AttrList), + functionKeywordPosition: Location.Position, +}; + +pub const StatLocalFunction = extern struct { + classIndex: Node.Kind = .stat_local_function, + + attrLists: Ast.Array(*AttrList), + localKeywordPosition: Location.Position, + functionKeywordPosition: Location.Position, +}; + +pub const GenericType = extern struct { + classIndex: Node.Kind = .generic_type, + + defaultEqualsPosition: Location.Position, +}; + +pub const GenericTypePack = extern struct { + classIndex: Node.Kind = .generic_type_pack, + + ellipsisPosition: Location.Position, + defaultEqualsPosition: Location.Position, +}; + +pub const StatTypeAlias = extern struct { + classIndex: Node.Kind = .stat_type_alias, + + typeKeywordPosition: Location.Position, + genericsOpenPosition: Location.Position, + genericsCommaPositions: Ast.Array(Location.Position), + genericsClosePosition: Location.Position, + equalsPosition: Location.Position, +}; + +pub const StatTypeFunction = extern struct { + classIndex: Node.Kind = .stat_type_function, + + typeKeywordPosition: Location.Position, + functionKeywordPosition: Location.Position, +}; + +pub const TypeReference = extern struct { + classIndex: Node.Kind = .type_reference, + + prefixPointPosition: Location.Position, + openParametersPosition: Location.Position, + parametersCommaPositions: Ast.Array(Location.Position), + closeParametersPosition: Location.Position, +}; + +pub const TypeTable = extern struct { + classIndex: Node.Kind = .type_table, + + items: Ast.Array(Item), + isArray: bool, + + pub const Item = extern struct { + kind: Kind, + indexerOpenPosition: Location.Position, // '[', only if Kind != Property + indexerClosePosition: Location.Position, // ']' only if Kind != Property + colonPosition: Location.Position, + separator: ExprTable.Separator, // may be missing for last Item + separatorPosition: Location.Position, + + stringInfo: ?*ExprConstantString, // only if Kind == StringProperty + stringPosition: Location.Position, // only if Kind == StringProperty + + pub const Kind = enum(u32) { + indexer, + property, + string_property, + }; + }; +}; + +pub const TypeFunction = extern struct { + classIndex: Node.Kind = .type_function, + + openGenericsPosition: Location.Position, + genericsCommaPositions: Ast.Array(Location.Position), + closeGenericsPosition: Location.Position, + openArgsPosition: Location.Position, + argumentNameColonPositions: Ast.Array(Location.Position), + argumentsCommaPositions: Ast.Array(Location.Position), + closeArgsPosition: Location.Position, + returnArrowPosition: Location.Position, +}; + +pub const TypeTypeof = extern struct { + classIndex: Node.Kind = .type_typeof, + + openPosition: Location.Position, + closePosition: Location.Position, +}; + +pub const TypeUnion = extern struct { + classIndex: Node.Kind = .type_union, + + leadingPosition: Location.Position, + separatorPositions: Ast.Array(Location.Position), +}; + +pub const TypeIntersection = extern struct { + classIndex: Node.Kind = .type_intersection, + + leadingPosition: Location.Position, + separatorPositions: Ast.Array(Location.Position), +}; + +pub const TypeSingletonString = extern struct { + classIndex: Node.Kind = .type_singleton_string, + + sourceString: Ast.Array(u8), + quoteStyle: ExprConstantString.QuoteStyle, + blockDepth: u32, +}; + +pub const TypeGroup = extern struct { + classIndex: Node.Kind = .type_group, + + closePosition: Location.Position, +}; + +pub const TypePackExplicit = extern struct { + classIndex: Node.Kind = .type_pack_explicit, + + openParenthesesPosition: Location.Position, + closeParenthesesPosition: Location.Position, + commaPositions: Ast.Array(Location.Position), +}; + +pub const TypePackGeneric = extern struct { + classIndex: Node.Kind = .type_pack_generic, + + ellipsisPosition: Location.Position, +}; + +test Node { + const Lexer = @import("Lexer.zig"); + const Parser = @import("Parser.zig"); + const Allocator = @import("Allocator.zig"); + + { + const allocator = Allocator.init(); + defer allocator.deinit(); + + const table = Lexer.AstNameTable.init(allocator); + defer table.deinit(); + const source = + \\local x: number = 1; + \\local x = 2 + \\local x = 3 + \\ + ; + + var parse_result = Parser.parse(source, table, allocator, .{ + .storeCstData = true, + }); + defer parse_result.deinit(); + + const root = parse_result.root; + + try std.testing.expectEqual(Ast.Node.Kind.stat_block, root.classIndex); + + const stats = root.body.slice(); + try std.testing.expectEqual(3, stats.len); + + try std.testing.expectEqual(7, parse_result.cstNodeMap.count); + + try std.testing.expect(parse_result.cstNodeMap.find(@ptrCast(@alignCast(root))) == null); + + for (stats, 1..) |node, order| { + switch (node.classIndex) { + .stat_local => { + const local: *Ast.StatLocal = node.as(.stat_local).?; + const cst_node = (parse_result.cstNodeMap.find(@ptrCast(@alignCast(node))) orelse @panic("Not found")).second; + const cst_local: *StatLocal = cst_node.as(.stat_local).?; + try std.testing.expect(cst_local.varsCommaPositions.size == 0); + try std.testing.expect(cst_local.valuesCommaPositions.size == 0); + try std.testing.expect(cst_local.varsAnnotationColonPositions.size == 1); + try std.testing.expectEqualStrings("x", std.mem.span(local.vars.slice()[0].name.value)); + try std.testing.expect(@as(f64, @floatFromInt(order)) == local.values.slice()[0].as(.expr_constant_number).?.value); + }, + else => {}, + } + } + } +} + +test "CstValuesCheck" { + if (@import("builtin").cpu.arch.isWasm() or @import("builtin").os.tag == .windows) + return error.SkipZigTest; + const CstValues = struct { + pub extern "c" const CstAttrIndex: u8; + pub extern "c" const CstParametrizedAttrIndex: u8; + pub extern "c" const CstExprGroupIndex: u8; + pub extern "c" const CstExprConstantNumberIndex: u8; + pub extern "c" const CstExprConstantIntegerIndex: u8; + pub extern "c" const CstExprConstantStringIndex: u8; + pub extern "c" const CstExprCallIndex: u8; + pub extern "c" const CstExprIndexExprIndex: u8; + pub extern "c" const CstExprFunctionIndex: u8; + pub extern "c" const CstExprTableIndex: u8; + pub extern "c" const CstExprOpIndex: u8; + pub extern "c" const CstExprTypeAssertionIndex: u8; + pub extern "c" const CstExprIfElseIndex: u8; + pub extern "c" const CstExprInterpStringIndex: u8; + pub extern "c" const CstExprExplicitTypeInstantiationIndex: u8; + pub extern "c" const CstStatDoIndex: u8; + pub extern "c" const CstStatRepeatIndex: u8; + pub extern "c" const CstStatReturnIndex: u8; + pub extern "c" const CstStatLocalIndex: u8; + pub extern "c" const CstStatForIndex: u8; + pub extern "c" const CstStatForInIndex: u8; + pub extern "c" const CstStatAssignIndex: u8; + pub extern "c" const CstStatCompoundAssignIndex: u8; + pub extern "c" const CstStatFunctionIndex: u8; + pub extern "c" const CstStatLocalFunctionIndex: u8; + pub extern "c" const CstGenericTypeIndex: u8; + pub extern "c" const CstGenericTypePackIndex: u8; + pub extern "c" const CstStatTypeAliasIndex: u8; + pub extern "c" const CstStatTypeFunctionIndex: u8; + pub extern "c" const CstTypeReferenceIndex: u8; + pub extern "c" const CstTypeTableIndex: u8; + pub extern "c" const CstTypeTableItemKindIndexer: u8; + pub extern "c" const CstTypeTableItemKindProperty: u8; + pub extern "c" const CstTypeTableItemKindStringProperty: u8; + pub extern "c" const CstTypeFunctionIndex: u8; + pub extern "c" const CstTypeTypeofIndex: u8; + pub extern "c" const CstTypeUnionIndex: u8; + pub extern "c" const CstTypeIntersectionIndex: u8; + pub extern "c" const CstTypeSingletonStringIndex: u8; + pub extern "c" const CstTypeGroupIndex: u8; + pub extern "c" const CstTypePackExplicitIndex: u8; + pub extern "c" const CstTypePackGenericIndex: u8; + + pub extern "c" const CstExprGroupSize: usize; + pub extern "c" const CstExprConstantNumberSize: usize; + pub extern "c" const CstExprConstantIntegerSize: usize; + pub extern "c" const CstExprConstantStringSize: usize; + pub extern "c" const CstExprCallSize: usize; + pub extern "c" const CstExprIndexExprSize: usize; + pub extern "c" const CstExprFunctionSize: usize; + pub extern "c" const CstExprTableSize: usize; + pub extern "c" const CstExprOpSize: usize; + pub extern "c" const CstExprTypeAssertionSize: usize; + pub extern "c" const CstExprIfElseSize: usize; + pub extern "c" const CstExprInterpStringSize: usize; + pub extern "c" const CstExprExplicitTypeInstantiationSize: usize; + pub extern "c" const CstStatDoSize: usize; + pub extern "c" const CstStatRepeatSize: usize; + pub extern "c" const CstStatReturnSize: usize; + pub extern "c" const CstStatLocalSize: usize; + pub extern "c" const CstStatForSize: usize; + pub extern "c" const CstStatForInSize: usize; + pub extern "c" const CstStatAssignSize: usize; + pub extern "c" const CstStatCompoundAssignSize: usize; + pub extern "c" const CstStatFunctionSize: usize; + pub extern "c" const CstStatLocalFunctionSize: usize; + pub extern "c" const CstGenericTypeSize: usize; + pub extern "c" const CstGenericTypePackSize: usize; + pub extern "c" const CstStatTypeAliasSize: usize; + pub extern "c" const CstStatTypeFunctionSize: usize; + pub extern "c" const CstTypeReferenceSize: usize; + pub extern "c" const CstTypeTableSize: usize; + pub extern "c" const CstTypeFunctionSize: usize; + pub extern "c" const CstTypeTypeofSize: usize; + pub extern "c" const CstTypeUnionSize: usize; + pub extern "c" const CstTypeIntersectionSize: usize; + pub extern "c" const CstTypeSingletonStringSize: usize; + pub extern "c" const CstTypeGroupSize: usize; + pub extern "c" const CstTypePackExplicitSize: usize; + pub extern "c" const CstTypePackGenericSize: usize; + }; + + try std.testing.expect(CstValues.CstTypeTableItemKindIndexer == @intFromEnum(TypeTable.Item.Kind.indexer)); + try std.testing.expect(CstValues.CstTypeTableItemKindProperty == @intFromEnum(TypeTable.Item.Kind.property)); + try std.testing.expect(CstValues.CstTypeTableItemKindStringProperty == @intFromEnum(TypeTable.Item.Kind.string_property)); + + @setEvalBranchQuota(2000); + inline for (@typeInfo(CstValues).@"struct".decls) |decl| { + if (comptime std.mem.endsWith(u8, decl.name, "Index")) { + const name = decl.name[3 .. decl.name.len - 5]; + + const cst_node_type = @field(Cst, name); + const info = @typeInfo(cst_node_type).@"struct"; + + comptime var field: ?std.builtin.Type.StructField = null; + inline for (info.fields) |f| { + if (comptime std.mem.eql(u8, f.name, "classIndex")) { + field = f; + break; + } + } + if (field == null) + @compileError("classIndex field not found"); + const default_value_ptr = field.?.default_value_ptr orelse @compileError("classIndex field does not have a default value"); + const enum_value = @as(*const Cst.Node.Kind, @ptrCast(@alignCast(default_value_ptr))).*; + + std.testing.expectEqual(@field(CstValues, decl.name), @intFromEnum(enum_value)) catch |err| { + std.debug.print("index error for {s}\n", .{name}); + return err; + }; + } else if (comptime std.mem.endsWith(u8, decl.name, "Size")) { + const name = decl.name[3 .. decl.name.len - 4]; + + const cst_node_type = @field(Cst, name); + + std.testing.expectEqual(@field(CstValues, decl.name), @sizeOf(cst_node_type)) catch |err| { + std.debug.print("size error for {s}\n", .{name}); + return err; + }; + } + } +} + +// sources: +// https://github.com/luau-lang/luau/blob/40d4815888f63362a6cb79b3e74c4aafa0b2cbf4/Ast/include/Luau/Cst.h +// https://github.com/luau-lang/luau/blob/40d4815888f63362a6cb79b3e74c4aafa0b2cbf4/Ast/src/Cst.cpp diff --git a/deps/luau/src/Ast/Lexer.cpp b/deps/luau/src/Ast/Lexer.cpp new file mode 100644 index 0000000..49a039a --- /dev/null +++ b/deps/luau/src/Ast/Lexer.cpp @@ -0,0 +1,15 @@ +#include + +#include "Luau/Lexer.h" + +#define ZIG_LUAU_AST(name) ZIG_FN(Luau_Ast_##name) + +ZIG_EXPORT Luau::AstNameTable* ZIG_LUAU_AST(Lexer_AstNameTable_init)(Luau::Allocator* allocator) +{ + return new Luau::AstNameTable(*allocator); +} + +ZIG_EXPORT void ZIG_LUAU_AST(Lexer_AstNameTable_dtor)(Luau::AstNameTable* names) +{ + delete names; +} diff --git a/deps/luau/src/Ast/Lexer.zig b/deps/luau/src/Ast/Lexer.zig new file mode 100644 index 0000000..4ed6201 --- /dev/null +++ b/deps/luau/src/Ast/Lexer.zig @@ -0,0 +1,130 @@ +const std = @import("std"); + +const Ast = @import("Ast.zig"); +const Allocator = @import("Allocator.zig"); +const DenseHash = @import("../Common/DenseHash.zig"); + +extern "c" fn zig_Luau_Ast_Lexer_AstNameTable_init(*Allocator) *AstNameTable; +extern "c" fn zig_Luau_Ast_Lexer_AstNameTable_dtor(*AstNameTable) void; + +pub const Lexeme = struct { + pub const Type = enum(c_int) { + Eof = 0, + + // 1..255 means actual character values + Char_END = 256, + + Equal, + LessEqual, + GreaterEqual, + NotEqual, + Dot2, + Dot3, + SkinnyArrow, + DoubleColon, + FloorDiv, + + InterpStringBegin, + InterpStringMid, + InterpStringEnd, + // An interpolated string with no expressions (like `x`) + InterpStringSimple, + + AddAssign, + SubAssign, + MulAssign, + DivAssign, + FloorDivAssign, + ModAssign, + PowAssign, + ConcatAssign, + + RawString, + QuotedString, + Number, + Name, + + Comment, + BlockComment, + + Attribute, + AttributeOpen, + + BrokenString, + BrokenComment, + BrokenUnicode, + BrokenInterpDoubleBrace, + Error, + + // Reserved_BEGIN, + ReservedAnd, + ReservedBreak, + ReservedDo, + ReservedElse, + ReservedElseif, + ReservedEnd, + ReservedFalse, + ReservedFor, + ReservedFunction, + ReservedIf, + ReservedIn, + ReservedLocal, + ReservedNil, + ReservedNot, + ReservedOr, + ReservedRepeat, + ReservedReturn, + ReservedThen, + ReservedTrue, + ReservedUntil, + ReservedWhile, + Reserved_END, + + pub const Reserved_BEGIN = Type.ReservedAnd; + }; +}; + +pub const AstNameTable = extern struct { + data: DenseHash.DenseHashSet(Entry, EntryHash), + allocator: *Allocator, + + const Entry = extern struct { + value: Ast.Name, + length: u32, + type: Lexeme.Type, + }; + + const EntryHash = extern struct { + pub fn hash(e: *const Entry) usize { + var h: u32 = 2166136261; + for (0..e.length) |i| { + h ^= @as(u8, e.value[i]); + h *= 16777619; + } + return h; + } + pub fn eq(_: *const Entry, _: *const Entry) bool { + @compileError("not implemented"); + } + }; + + pub fn init(allocator: *Allocator) *AstNameTable { + return zig_Luau_Ast_Lexer_AstNameTable_init(allocator); + } + + pub fn deinit(self: *AstNameTable) void { + zig_Luau_Ast_Lexer_AstNameTable_dtor(self); + } +}; + +test AstNameTable { + const allocator = Allocator.init(); + defer allocator.deinit(); + + const astNameTable = AstNameTable.init(allocator); + defer astNameTable.deinit(); +} + +// sources: +// https://github.com/luau-lang/luau/blob/a2303a6ae68c53035eccf230c4450b9f068536af/Ast/include/Luau/Lexer.h +// https://github.com/luau-lang/luau/blob/a2303a6ae68c53035eccf230c4450b9f068536af/Ast/src/Lexer.cpp diff --git a/deps/luau/src/Ast/Location.zig b/deps/luau/src/Ast/Location.zig new file mode 100644 index 0000000..5693ead --- /dev/null +++ b/deps/luau/src/Ast/Location.zig @@ -0,0 +1,60 @@ +const std = @import("std"); + +pub const Location = extern struct { + begin: Position = .zeros, + end: Position = .zeros, + + pub fn eq(self: Location, other: Location) bool { + return self.begin.eq(other.begin) and self.end.eq(other.end); + } + + pub fn encloses(self: Location, other: Location) bool { + return self.begin.lessThanOrEq(other.begin) and self.end.greaterThanOrEq(other.end); + } + + pub fn overlaps(self: Location, other: Location) bool { + return (self.begin.lessThanOrEq(other.begin) and self.end.greaterThanOrEq(other.begin)) or (self.begin.lessThanOrEq(other.end) and self.end.greaterThanOrEq(other.end)) or (other.begin.greaterThanOrEq(self.begin) and other.end.lessThanOrEq(self.end)); + } + + pub fn contains(self: Location, position: Position) bool { + return self.begin.lessThanOrEq(position) and position.lessThan(self.end); + } + + pub fn containsClosed(self: Location, position: Position) bool { + return self.begin.lessThanOrEq(position) and position.lessThanOrEq(self.end); + } + + pub const Position = extern struct { + line: c_uint, + column: c_uint, + + pub const missing: Position = .{ .line = std.math.maxInt(u32), .column = std.math.maxInt(u32) }; + pub const zeros: Position = .{ .line = 0, .column = 0 }; + + pub fn eq(self: Position, other: Position) bool { + return self.line == other.line and self.column == other.column; + } + pub fn lessThan(self: Position, other: Position) bool { + if (self.line == other.line) + return self.column < other.column; + return self.line < other.line; + } + pub inline fn lessThanOrEq(self: Position, other: Position) bool { + return self.eq(other) or self.lessThan(other); + } + pub inline fn greaterThan(self: Position, other: Position) bool { + return !self.lessThanOrEq(other); + } + pub inline fn greaterThanOrEq(self: Position, other: Position) bool { + return !self.lessThan(other); + } + + pub fn hasValue(self: Position) bool { + return self.line != std.math.maxInt(u32) and self.column != std.math.maxInt(u32); + } + }; +}; + +// sources: +// https://github.com/luau-lang/luau/blob/a2303a6ae68c53035eccf230c4450b9f068536af/Ast/include/Luau/Location.h +// https://github.com/luau-lang/luau/blob/a2303a6ae68c53035eccf230c4450b9f068536af/Ast/src/Location.cpp diff --git a/deps/luau/src/Ast/Parser.cpp b/deps/luau/src/Ast/Parser.cpp new file mode 100644 index 0000000..57786bb --- /dev/null +++ b/deps/luau/src/Ast/Parser.cpp @@ -0,0 +1,78 @@ +#include + +#include "Luau/Ast.h" +#include "Luau/Parser.h" + +#define ZIG_LUAU_AST(name) ZIG_FN(Luau_Ast_##name) + +ZIG_EXPORT struct luau_ParseOptions +{ + unsigned char data[sizeof(Luau::ParseOptions)]; +}; + +ZIG_EXPORT Luau::ParseResult* ZIG_LUAU_AST(Parser_parse)( + const char* source, size_t sourceLen, + Luau::AstNameTable* names, + Luau::Allocator* allocator, + const luau_ParseOptions* options +) +{ + Luau::ParseOptions parseOptions; + if (options) + { + static_assert(sizeof(luau_ParseOptions) == sizeof(Luau::ParseOptions), "C and C++ interface must match"); + memcpy(static_cast(&parseOptions), options, sizeof(parseOptions)); + } + Luau::ParseResult result = Luau::Parser::parse(source, sourceLen, *names, *allocator, parseOptions); + return new Luau::ParseResult(std::move(result)); +} + +ZIG_EXPORT void ZIG_LUAU_AST(ParseResult_dtor)(Luau::ParseResult* result) +{ + delete result; +} + +ZIG_EXPORT Luau::ParseNodeResult* ZIG_LUAU_AST(Parser_parseExpr)( + const char* source, size_t sourceLen, + Luau::AstNameTable* names, + Luau::Allocator* allocator, + const luau_ParseOptions* options +) +{ + Luau::ParseOptions parseOptions; + if (options) + { + static_assert(sizeof(luau_ParseOptions) == sizeof(Luau::ParseOptions), "C and C++ interface must match"); + memcpy(static_cast(&parseOptions), options, sizeof(parseOptions)); + } + Luau::ParseNodeResult result = Luau::Parser::parseExpr(source, sourceLen, *names, *allocator, parseOptions); + return new Luau::ParseNodeResult(std::move(result)); +} + +ZIG_EXPORT void ZIG_LUAU_AST(ParseNodeResult_AstExpr_dtor)(Luau::ParseNodeResult* result) +{ + delete result; +} + + +ZIG_EXPORT Luau::ParseNodeResult* ZIG_LUAU_AST(Parser_parseType)( + const char* source, size_t sourceLen, + Luau::AstNameTable* names, + Luau::Allocator* allocator, + const luau_ParseOptions* options +) +{ + Luau::ParseOptions parseOptions; + if (options) + { + static_assert(sizeof(luau_ParseOptions) == sizeof(Luau::ParseOptions), "C and C++ interface must match"); + memcpy(static_cast(&parseOptions), options, sizeof(parseOptions)); + } + Luau::ParseNodeResult result = Luau::Parser::parseType(source, sourceLen, *names, *allocator, parseOptions); + return new Luau::ParseNodeResult(std::move(result)); +} + +ZIG_EXPORT void ZIG_LUAU_AST(ParseNodeResult_AstType_dtor)(Luau::ParseNodeResult* result) +{ + delete result; +} diff --git a/deps/luau/src/Ast/Parser.zig b/deps/luau/src/Ast/Parser.zig new file mode 100644 index 0000000..f941763 --- /dev/null +++ b/deps/luau/src/Ast/Parser.zig @@ -0,0 +1,156 @@ +const std = @import("std"); + +const cpp_std = @import("../cpp_std.zig"); + +const Ast = @import("Ast.zig"); +const Cst = @import("Cst.zig"); +const Lexer = @import("Lexer.zig"); +const Location = @import("Location.zig").Location; +const Allocator = @import("Allocator.zig"); +const DenseHash = @import("../Common/DenseHash.zig"); + +pub const ParseError = cpp_std.Exception(extern struct { + location: Location, + message: cpp_std.String, +}); + +pub const ParseErrors = cpp_std.Exception(extern struct { + errors: cpp_std.Vector(ParseError), + message: cpp_std.String, +}); + +pub const HotComment = extern struct { + header: bool, + location: Location, + content: cpp_std.String, +}; + +pub const Comment = extern struct { + type: Lexer.Lexeme.Type, // Comment, BlockComment, or BrokenComment + location: Location, +}; + +pub const ParseOptions = extern struct { + allowDeclarationSyntax: bool = false, + captureComments: bool = false, + parseFragment: cpp_std.Optional(FragmentParseResumeSettings) = .nullopt, + storeCstData: bool = false, + noErrorLimit: bool = false, + + pub const FragmentParseResumeSettings = extern struct { + localMap: DenseHash.DenseHashMap(Ast.Name, *Ast.Local, struct {}) = .init(.{ .value = "" }, 0), + localStack: cpp_std.Vector(*Ast.Local) = undefined, + resumePosition: Location.Position, + }; +}; + +extern "c" fn zig_Luau_Ast_Parser_parse([*]const u8, usize, *Lexer.AstNameTable, *Allocator, *const ParseOptions) *ParseResult; +extern "c" fn zig_Luau_Ast_Parser_parseExpr([*]const u8, usize, *Lexer.AstNameTable, *Allocator, *const ParseOptions) *ParseNodeResult(Ast.Expr); +extern "c" fn zig_Luau_Ast_Parser_parseType([*]const u8, usize, *Lexer.AstNameTable, *Allocator, *const ParseOptions) *ParseNodeResult(Ast.Type); +extern "c" fn zig_Luau_Ast_ParseResult_dtor(*ParseResult) void; +extern "c" fn zig_Luau_Ast_ParseNodeResult_AstExpr_dtor(*ParseNodeResult(Ast.Expr)) void; +extern "c" fn zig_Luau_Ast_ParseNodeResult_AstType_dtor(*ParseNodeResult(Ast.Type)) void; + +pub fn parse(source: []const u8, nameTable: *Lexer.AstNameTable, allocator: *Allocator, options: ParseOptions) *ParseResult { + return zig_Luau_Ast_Parser_parse(source.ptr, source.len, nameTable, allocator, &options); +} + +pub fn parseExpr(source: []const u8, nameTable: *Lexer.AstNameTable, allocator: *Allocator, options: ParseOptions) *ParseNodeResult(Ast.Expr) { + return zig_Luau_Ast_Parser_parseExpr(source.ptr, source.len, nameTable, allocator, &options); +} + +pub fn parseType(source: []const u8, nameTable: *Lexer.AstNameTable, allocator: *Allocator, options: ParseOptions) *ParseNodeResult(Ast.Type) { + return zig_Luau_Ast_Parser_parseType(source.ptr, source.len, nameTable, allocator, &options); +} + +pub const CstNodeMap = DenseHash.DenseHashMap(*Ast.Node, *Cst.Node, struct {}); + +pub const ParseResult = extern struct { + root: *Ast.StatBlock, + lines: usize = 0, + + hotcomments: cpp_std.Vector(HotComment), + errors: cpp_std.Vector(ParseError), + + commentLocations: cpp_std.Vector(Comment), + + cstNodeMap: CstNodeMap, + + pub inline fn deinit(self: *ParseResult) void { + zig_Luau_Ast_ParseResult_dtor(self); + } +}; + +pub fn ParseNodeResult(comptime T: type) type { + return extern struct { + expr: *T, + lines: usize = 0, + + hotcomments: cpp_std.Vector(HotComment), + errors: cpp_std.Vector(ParseError), + + commentLocations: cpp_std.Vector(Comment), + + cstNodeMap: CstNodeMap, + + pub const Self = @This(); + + pub inline fn deinit(self: *Self) void { + comptime std.debug.assert(T == Ast.Type or T == Ast.Expr); + if (T == Ast.Type) { + zig_Luau_Ast_ParseNodeResult_AstType_dtor(self); + } else { + zig_Luau_Ast_ParseNodeResult_AstExpr_dtor(self); + } + } + }; +} + +test ParseResult { + { + const allocator = Allocator.init(); + defer allocator.deinit(); + + const astNameTable = Lexer.AstNameTable.init(allocator); + defer astNameTable.deinit(); + const source = + \\--!test + \\-- This is a test comment + \\local x = + \\ + ; + + var parseResult = parse(source, astNameTable, allocator, .{}); + defer parseResult.deinit(); + + { + var iter = parseResult.hotcomments.iterator(); + var count: usize = 0; + while (iter.next()) |comment| : (count += 1) { + const string = comment.content.slice(); + try std.testing.expectEqualStrings("test", string); + try std.testing.expectEqual(true, comment.header); + try std.testing.expectEqual(0, comment.location.begin.line); + try std.testing.expectEqual(0, comment.location.begin.column); + try std.testing.expectEqual(0, comment.location.end.line); + try std.testing.expectEqual(7, comment.location.end.column); + } + + try std.testing.expectEqual(1, count); + } + + { + try std.testing.expectEqual(1, parseResult.errors.size()); + const first = parseResult.errors.at(0).value; + try std.testing.expectEqualStrings("Expected identifier when parsing expression, got ", first.message.slice()); + try std.testing.expectEqual(3, first.location.begin.line); + try std.testing.expectEqual(0, first.location.begin.column); + try std.testing.expectEqual(3, first.location.end.line); + try std.testing.expectEqual(0, first.location.end.column); + } + } +} + +// sources: +// https://github.com/luau-lang/luau/blob/a2303a6ae68c53035eccf230c4450b9f068536af/Ast/include/Luau/Parser.h +// https://github.com/luau-lang/luau/blob/a2303a6ae68c53035eccf230c4450b9f068536af/Ast/src/Parser.cpp diff --git a/deps/luau/src/CodeGen/lcodegen.zig b/deps/luau/src/CodeGen/lcodegen.zig new file mode 100644 index 0000000..95f6d0e --- /dev/null +++ b/deps/luau/src/CodeGen/lcodegen.zig @@ -0,0 +1,15 @@ +const c = @import("c"); + +const lua = @import("../VM/lua.zig"); + +pub inline fn supported() bool { + return c.luau_codegen_supported() != 0; +} + +pub inline fn create(L: *lua.State) void { + c.luau_codegen_create(@ptrCast(L)); +} + +pub inline fn compile(L: *lua.State, idx: i32) void { + c.luau_codegen_compile(@ptrCast(L), idx); +} diff --git a/deps/luau/src/Common/Bytecode.zig b/deps/luau/src/Common/Bytecode.zig new file mode 100644 index 0000000..ea3c583 --- /dev/null +++ b/deps/luau/src/Common/Bytecode.zig @@ -0,0 +1,731 @@ +// This file contains the bytecode definition for Luau interpreter +// Creating the bytecode is outside the scope of this file and is handled by bytecode builder (BytecodeBuilder.h) and bytecode compiler (Compiler.h) +// Note that ALL enums declared in this file are order-sensitive since the values are baked into bytecode that needs to be processed by legacy clients. + +// # Bytecode definitions +// Bytecode instructions are using "word code" - each instruction is one or many 32-bit words. +// The first word in the instruction is always the instruction header, and *must* contain the opcode (enum below) in the least significant byte. +// +// Instruction word can be encoded using one of the following encodings: +// ABC - least-significant byte for the opcode, followed by three bytes, A, B and C; each byte declares a register index, small index into some other table or an unsigned integral value +// AD - least-significant byte for the opcode, followed by A byte, followed by D half-word (16-bit integer). D is a signed integer that commonly specifies constant table index or jump offset +// E - least-significant byte for the opcode, followed by E (24-bit integer). E is a signed integer that commonly specifies a jump offset +// +// Instruction word is sometimes followed by one extra word, indicated as AUX - this is just a 32-bit word and is decoded according to the specification for each opcode. +// For each opcode the encoding is *static* - that is, based on the opcode you know a-priory how large the instruction is, with the exception of NEWCLOSURE + +// # Bytecode indices +// Bytecode instructions commonly refer to integer values that define offsets or indices for various entities. For each type, there's a maximum encodable value. +// Note that in some cases, the compiler will set a lower limit than the maximum encodable value is to prevent fragile code into bumping against the limits whenever we change the compilation details. +// Additionally, in some specific instructions such as ANDK, the limit on the encoded value is smaller; this means that if a value is larger, a different instruction must be selected. +// +// Registers: 0-254. Registers refer to the values on the function's stack frame, including arguments. +// Upvalues: 0-199. Upvalues refer to the values stored in the closure object. +// Constants: 0-2^23-1. Constants are stored in a table allocated with each proto; to allow for future bytecode tweaks the encodable value is limited to 23 bits. +// Closures: 0-2^15-1. Closures are created from child protos via a child index; the limit is for the number of closures immediately referenced in each function. +// Jumps: -2^23..2^23. Jump offsets are specified in word increments, so jumping over an instruction may sometimes require an offset of 2 or more. Note that for jump instructions with AUX, the AUX word is included as part of the jump offset. + +// # Bytecode versions +// Bytecode serialized format embeds a version number, that dictates both the serialized form as well as the allowed instructions. As long as the bytecode version falls into supported +// range (indicated by BYTECODE_MIN / BYTECODE_MAX) and was produced by Luau compiler, it should load and execute correctly. +// +// Note that Luau runtime doesn't provide indefinite bytecode compatibility: support for older versions gets removed over time. As such, bytecode isn't a durable storage format and it's expected +// that Luau users can recompile bytecode from source on Luau version upgrades if necessary. + +// # Bytecode version history +// +// Note: due to limitations of the versioning scheme, some bytecode blobs that carry version 2 are using features from version 3. Starting from version 3, version should be sufficient to indicate bytecode compatibility. +// +// Version 1: Baseline version for the open-source release. Supported until 0.521. +// Version 2: Adds Proto::linedefined. Supported until 0.544. +// Version 3: Adds FORGPREP/JUMPXEQK* and enhances AUX encoding for FORGLOOP. Removes FORGLOOP_NEXT/INEXT and JUMPIFEQK/JUMPIFNOTEQK. Currently supported. +// Version 4: Adds Proto::flags, typeinfo, and floor division opcodes IDIV/IDIVK. Currently supported. +// Version 5: Adds SUBRK/DIVRK and vector constants. Currently supported. +// Version 6: Adds FASTCALL3. Currently supported. + +// # Bytecode type information history +// Version 1: (from bytecode version 4) Type information for function signature. Currently supported. +// Version 2: (from bytecode version 4) Type information for arguments, upvalues, locals and some temporaries. Currently supported. + +// Bytecode opcode, part of the instruction header +pub const Opcode = enum(u32) { + // NOP: noop + NOP, + + // BREAK: debugger break + BREAK, + + // LOADNIL: sets register to nil + // A: target register + LOADNIL, + + // LOADB: sets register to boolean and jumps to a given short offset (used to compile comparison results into a boolean) + // A: target register + // B: value (0/1) + // C: jump offset + LOADB, + + // LOADN: sets register to a number literal + // A: target register + // D: value (-32768..32767) + LOADN, + + // LOADK: sets register to an entry from the constant table from the proto (number/vector/string) + // A: target register + // D: constant table index (0..32767) + LOADK, + + // MOVE: move (copy) value from one register to another + // A: target register + // B: source register + MOVE, + + // GETGLOBAL: load value from global table using constant string as a key + // A: target register + // C: predicted slot index (based on hash) + // AUX: constant table index + GETGLOBAL, + + // SETGLOBAL: set value in global table using constant string as a key + // A: source register + // C: predicted slot index (based on hash) + // AUX: constant table index + SETGLOBAL, + + // GETUPVAL: load upvalue from the upvalue table for the current function + // A: target register + // B: upvalue index + GETUPVAL, + + // SETUPVAL: store value into the upvalue table for the current function + // A: target register + // B: upvalue index + SETUPVAL, + + // CLOSEUPVALS: close (migrate to heap) all upvalues that were captured for registers >= target + // A: target register + CLOSEUPVALS, + + // GETIMPORT: load imported global table global from the constant table + // A: target register + // D: constant table index (0..32767); we assume that imports are loaded into the constant table + // AUX: 3 10-bit indices of constant strings that, combined, constitute an import path; length of the path is set by the top 2 bits (1,2,3) + GETIMPORT, + + // GETTABLE: load value from table into target register using key from register + // A: target register + // B: table register + // C: index register + GETTABLE, + + // SETTABLE: store source register into table using key from register + // A: source register + // B: table register + // C: index register + SETTABLE, + + // GETTABLEKS: load value from table into target register using constant string as a key + // A: target register + // B: table register + // C: predicted slot index (based on hash) + // AUX: constant table index + GETTABLEKS, + + // SETTABLEKS: store source register into table using constant string as a key + // A: source register + // B: table register + // C: predicted slot index (based on hash) + // AUX: constant table index + SETTABLEKS, + + // GETTABLEN: load value from table into target register using small integer index as a key + // A: target register + // B: table register + // C: index-1 (index is 1..256) + GETTABLEN, + + // SETTABLEN: store source register into table using small integer index as a key + // A: source register + // B: table register + // C: index-1 (index is 1..256) + SETTABLEN, + + // NEWCLOSURE: create closure from a child proto; followed by a CAPTURE instruction for each upvalue + // A: target register + // D: child proto index (0..32767) + NEWCLOSURE, + + // NAMECALL: prepare to call specified method by name by loading function from source register using constant index into target register and copying source register into target register + 1 + // A: target register + // B: source register + // C: predicted slot index (based on hash) + // AUX: constant table index + // Note that this instruction must be followed directly by CALL; it prepares the arguments + // This instruction is roughly equivalent to GETTABLEKS + MOVE pair, but we need a special instruction to support custom __namecall metamethod + NAMECALL, + + // CALL: call specified function + // A: register where the function object lives, followed by arguments; results are placed starting from the same register + // B: argument count + 1, or 0 to preserve all arguments up to top (MULTRET) + // C: result count + 1, or 0 to preserve all values and adjust top (MULTRET) + CALL, + + // RETURN: returns specified values from the function + // A: register where the returned values start + // B: number of returned values + 1, or 0 to return all values up to top (MULTRET) + RETURN, + + // JUMP: jumps to target offset + // D: jump offset (-32768..32767; 0 means "next instruction" aka "don't jump") + JUMP, + + // JUMPBACK: jumps to target offset; this is equivalent to JUMP but is used as a safepoint to be able to interrupt while/repeat loops + // D: jump offset (-32768..32767; 0 means "next instruction" aka "don't jump") + JUMPBACK, + + // JUMPIF: jumps to target offset if register is not nil/false + // A: source register + // D: jump offset (-32768..32767; 0 means "next instruction" aka "don't jump") + JUMPIF, + + // JUMPIFNOT: jumps to target offset if register is nil/false + // A: source register + // D: jump offset (-32768..32767; 0 means "next instruction" aka "don't jump") + JUMPIFNOT, + + // JUMPIFEQ, JUMPIFLE, JUMPIFLT, JUMPIFNOTEQ, JUMPIFNOTLE, JUMPIFNOTLT: jumps to target offset if the comparison is true (or false, for NOT variants) + // A: source register 1 + // D: jump offset (-32768..32767; 1 means "next instruction" aka "don't jump") + // AUX: source register 2 + JUMPIFEQ, + JUMPIFLE, + JUMPIFLT, + JUMPIFNOTEQ, + JUMPIFNOTLE, + JUMPIFNOTLT, + + // ADD, SUB, MUL, DIV, MOD, POW: compute arithmetic operation between two source registers and put the result into target register + // A: target register + // B: source register 1 + // C: source register 2 + ADD, + SUB, + MUL, + DIV, + MOD, + POW, + + // ADDK, SUBK, MULK, DIVK, MODK, POWK: compute arithmetic operation between the source register and a constant and put the result into target register + // A: target register + // B: source register + // C: constant table index (0..255); must refer to a number + ADDK, + SUBK, + MULK, + DIVK, + MODK, + POWK, + + // AND, OR: perform `and` or `or` operation (selecting first or second register based on whether the first one is truthy) and put the result into target register + // A: target register + // B: source register 1 + // C: source register 2 + AND, + OR, + + // ANDK, ORK: perform `and` or `or` operation (selecting source register or constant based on whether the source register is truthy) and put the result into target register + // A: target register + // B: source register + // C: constant table index (0..255) + ANDK, + ORK, + + // CONCAT: concatenate all strings between B and C (inclusive) and put the result into A + // A: target register + // B: source register start + // C: source register end + CONCAT, + + // NOT, MINUS, LENGTH: compute unary operation for source register and put the result into target register + // A: target register + // B: source register + NOT, + MINUS, + LENGTH, + + // NEWTABLE: create table in target register + // A: target register + // B: table size, stored as 0 for v=0 and ceil(log2(v))+1 for v!=0 + // AUX: array size + NEWTABLE, + + // DUPTABLE: duplicate table using the constant table template to target register + // A: target register + // D: constant table index (0..32767) + DUPTABLE, + + // SETLIST: set a list of values to table in target register + // A: target register + // B: source register start + // C: value count + 1, or 0 to use all values up to top (MULTRET) + // AUX: table index to start from + SETLIST, + + // FORNPREP: prepare a numeric for loop, jump over the loop if first iteration doesn't need to run + // A: target register; numeric for loops assume a register layout [limit, step, index, variable] + // D: jump offset (-32768..32767) + // limit/step are immutable, index isn't visible to user code since it's copied into variable + FORNPREP, + + // FORNLOOP: adjust loop variables for one iteration, jump back to the loop header if loop needs to continue + // A: target register; see FORNPREP for register layout + // D: jump offset (-32768..32767) + FORNLOOP, + + // FORGLOOP: adjust loop variables for one iteration of a generic for loop, jump back to the loop header if loop needs to continue + // A: target register; generic for loops assume a register layout [generator, state, index, variables...] + // D: jump offset (-32768..32767) + // AUX: variable count (1..255) in the low 8 bits, high bit indicates whether to use ipairs-style traversal in the fast path + // loop variables are adjusted by calling generator(state, index) and expecting it to return a tuple that's copied to the user variables + // the first variable is then copied into index; generator/state are immutable, index isn't visible to user code + FORGLOOP, + + // FORGPREP_INEXT: prepare FORGLOOP with 2 output variables (no AUX encoding), assuming generator is luaB_inext, and jump to FORGLOOP + // A: target register (see FORGLOOP for register layout) + FORGPREP_INEXT, + + // FASTCALL3: perform a fast call of a built-in function using 3 register arguments + // A: builtin function id (see LuauBuiltinFunction) + // B: source argument register + // C: jump offset to get to following CALL + // AUX: source register 2 in least-significant byte + // AUX: source register 3 in second least-significant byte + FASTCALL3, + + // FORGPREP_NEXT: prepare FORGLOOP with 2 output variables (no AUX encoding), assuming generator is luaB_next, and jump to FORGLOOP + // A: target register (see FORGLOOP for register layout) + FORGPREP_NEXT, + + // NATIVECALL: start executing new function in native code + // this is a pseudo-instruction that is never emitted by bytecode compiler, but can be constructed at runtime to accelerate native code dispatch + NATIVECALL, + + // GETVARARGS: copy variables into the target register from vararg storage for current function + // A: target register + // B: variable count + 1, or 0 to copy all variables and adjust top (MULTRET) + GETVARARGS, + + // DUPCLOSURE: create closure from a pre-created function object (reusing it unless environments diverge) + // A: target register + // D: constant table index (0..32767) + DUPCLOSURE, + + // PREPVARARGS: prepare stack for variadic functions so that GETVARARGS works correctly + // A: number of fixed arguments + PREPVARARGS, + + // LOADKX: sets register to an entry from the constant table from the proto (number/string) + // A: target register + // AUX: constant table index + LOADKX, + + // JUMPX: jumps to the target offset; like JUMPBACK, supports interruption + // E: jump offset (-2^23..2^23; 0 means "next instruction" aka "don't jump") + JUMPX, + + // FASTCALL: perform a fast call of a built-in function + // A: builtin function id (see LuauBuiltinFunction) + // C: jump offset to get to following CALL + // FASTCALL is followed by one of (GETIMPORT, MOVE, GETUPVAL) instructions and by CALL instruction + // This is necessary so that if FASTCALL can't perform the call inline, it can continue normal execution + // If FASTCALL *can* perform the call, it jumps over the instructions *and* over the next CALL + // Note that FASTCALL will read the actual call arguments, such as argument/result registers and counts, from the CALL instruction + FASTCALL, + + // COVERAGE: update coverage information stored in the instruction + // E: hit count for the instruction (0..2^23-1) + // The hit count is incremented by VM every time the instruction is executed, and saturates at 2^23-1 + COVERAGE, + + // CAPTURE: capture a local or an upvalue as an upvalue into a newly created closure; only valid after NEWCLOSURE + // A: capture type, see LuauCaptureType + // B: source register (for VAL/REF) or upvalue index (for UPVAL/UPREF) + CAPTURE, + + // SUBRK, DIVRK: compute arithmetic operation between the constant and a source register and put the result into target register + // A: target register + // B: constant table index (0..255); must refer to a number + // C: source register + SUBRK, + DIVRK, + + // FASTCALL1: perform a fast call of a built-in function using 1 register argument + // A: builtin function id (see LuauBuiltinFunction) + // B: source argument register + // C: jump offset to get to following CALL + FASTCALL1, + + // FASTCALL2: perform a fast call of a built-in function using 2 register arguments + // A: builtin function id (see LuauBuiltinFunction) + // B: source argument register + // C: jump offset to get to following CALL + // AUX: source register 2 in least-significant byte + FASTCALL2, + + // FASTCALL2K: perform a fast call of a built-in function using 1 register argument and 1 constant argument + // A: builtin function id (see LuauBuiltinFunction) + // B: source argument register + // C: jump offset to get to following CALL + // AUX: constant index + FASTCALL2K, + + // FORGPREP: prepare loop variables for a generic for loop, jump to the loop backedge unconditionally + // A: target register; generic for loops assume a register layout [generator, state, index, variables...] + // D: jump offset (-32768..32767) + FORGPREP, + + // JUMPXEQKNIL, JUMPXEQKB: jumps to target offset if the comparison with constant is true (or false, see AUX) + // A: source register 1 + // D: jump offset (-32768..32767; 1 means "next instruction" aka "don't jump") + // AUX: constant value (for boolean) in low bit, NOT flag (that flips comparison result) in high bit + JUMPXEQKNIL, + JUMPXEQKB, + + // JUMPXEQKN, JUMPXEQKS: jumps to target offset if the comparison with constant is true (or false, see AUX) + // A: source register 1 + // D: jump offset (-32768..32767; 1 means "next instruction" aka "don't jump") + // AUX: constant table index in low 24 bits, NOT flag (that flips comparison result) in high bit + JUMPXEQKN, + JUMPXEQKS, + + // IDIV: compute floor division between two source registers and put the result into target register + // A: target register + // B: source register 1 + // C: source register 2 + IDIV, + + // IDIVK compute floor division between the source register and a constant and put the result into target register + // A: target register + // B: source register + // C: constant table index (0..255) + IDIVK, + + // Atom-based userdata field access acceleration + // These are equivalent to their GETTABLEKS/SETTABLEKS/NAMECALL counterparts, except tailored towards userdata field accesses + // If the user has registered metamethods for a userdata tag, callbacks will be called by these instructions + GETUDATAKS, + SETUDATAKS, + NAMECALLUDATA, + + // NEWCLASSMEMBER: register this method on a class object. + // A: target register of class + // B: reserved + // C: initial value of this member. currently must be a function. + // AUX: The name of this member as a constant string + NEWCLASSMEMBER, + + // CALLFB: call specified function with collecting runtime stats in a feedback slot + // A: register where the function object lives, followed by arguments; results are placed starting from the same register + // B: argument count + 1, or 0 to preserve all arguments up to top (MULTRET) + // C: result count + 1, or 0 to preserve all values and adjust top (MULTRET) + // AUX: feedback slot id. 0xFFFFFFFF - sealed + CALLFB, + + // CMPPROTO: check if a register contains a closure with a specified Luau function proto id + // A: closure register + // D: jump offset if proto doesn't match + // AUX: proto id + CMPPROTO, + + // Enum entry for number of opcodes, not a valid opcode by itself! + _COUNT, +}; + +// Bytecode instruction header: it's always a 32-bit integer, with low byte (first byte in little endian) containing the opcode +// Some instruction types require more data and have more 32-bit integers following the header +pub inline fn INSN_OP(insn: u32) u8 { + return (insn) & 0xff; +} + +// ABC encoding: three 8-bit values, containing registers or small numbers +pub inline fn INSN_A(insn: u32) u8 { + return (((insn) >> 8) & 0xff); +} +pub inline fn INSN_B(insn: u32) u8 { + return (((insn) >> 16) & 0xff); +} +pub inline fn INSN_C(insn: u32) u8 { + return (((insn) >> 24) & 0xff); +} + +// AD encoding: one 8-bit value, one signed 16-bit value +pub inline fn INSN_D(insn: u32) i16 { + return (@as(i32, @bitCast(insn)) >> 16); +} + +// E encoding: one signed 24-bit value +pub inline fn INSN_E(insn: u32) i32 { + return (@as(i32, @bitCast(insn)) >> 8); +} + +// Bytecode tags, used internally for bytecode encoded as a string +pub const BytecodeTag = enum(u32) { + CONSTANT_NIL = 0, + CONSTANT_BOOLEAN, + CONSTANT_NUMBER, + CONSTANT_STRING, + CONSTANT_IMPORT, + CONSTANT_TABLE, + CONSTANT_CLOSURE, + CONSTANT_VECTOR, + CONSTANT_TABLE_WITH_CONSTANTS, + CONSTANT_INTEGER, + CONSTANT_CLASS_SHAPE, + + // WARNING: This must always be last. + CONSTANT__COUNT, + + // Bytecode version; runtime supports [MIN, MAX], compiler emits TARGET by default but may emit a higher version when flags are enabled + // Type encoding version + // Types of constant table entries + pub const VERSION_MIN = 3; + pub const VERSION_MAX = 6; + pub const VERSION_TARGET = 6; + pub const TYPE_VERSION_MIN = 1; + pub const TYPE_VERSION_MAX = 3; + pub const TYPE_VERSION_TARGET = 3; +}; + +// Type table tags +pub const BytecodeType = enum(u32) { + TYPE_NIL = 0, + TYPE_BOOLEAN, + TYPE_NUMBER, + TYPE_STRING, + TYPE_TABLE, + TYPE_FUNCTION, + TYPE_THREAD, + TYPE_USERDATA, + TYPE_VECTOR, + TYPE_BUFFER, + + TYPE_ANY = 15, + + TYPE_TAGGED_USERDATA_BASE = 64, + TYPE_TAGGED_USERDATA_END = 64 + 32, + + TYPE_OPTIONAL_BIT = 1 << 7, + + TYPE_INVALID = 256, +}; + +// Builtin function ids, used in FASTCALL +pub const BuiltinFunction = enum(u32) { + LBF_NONE = 0, + + // assert() + LBF_ASSERT, + + // math. + LBF_MATH_ABS, + LBF_MATH_ACOS, + LBF_MATH_ASIN, + LBF_MATH_ATAN2, + LBF_MATH_ATAN, + LBF_MATH_CEIL, + LBF_MATH_COSH, + LBF_MATH_COS, + LBF_MATH_DEG, + LBF_MATH_EXP, + LBF_MATH_FLOOR, + LBF_MATH_FMOD, + LBF_MATH_FREXP, + LBF_MATH_LDEXP, + LBF_MATH_LOG10, + LBF_MATH_LOG, + LBF_MATH_MAX, + LBF_MATH_MIN, + LBF_MATH_MODF, + LBF_MATH_POW, + LBF_MATH_RAD, + LBF_MATH_SINH, + LBF_MATH_SIN, + LBF_MATH_SQRT, + LBF_MATH_TANH, + LBF_MATH_TAN, + + // bit32. + LBF_BIT32_ARSHIFT, + LBF_BIT32_BAND, + LBF_BIT32_BNOT, + LBF_BIT32_BOR, + LBF_BIT32_BXOR, + LBF_BIT32_BTEST, + LBF_BIT32_EXTRACT, + LBF_BIT32_LROTATE, + LBF_BIT32_LSHIFT, + LBF_BIT32_REPLACE, + LBF_BIT32_RROTATE, + LBF_BIT32_RSHIFT, + + // type() + LBF_TYPE, + + // string. + LBF_STRING_BYTE, + LBF_STRING_CHAR, + LBF_STRING_LEN, + + // typeof() + LBF_TYPEOF, + + // string. + LBF_STRING_SUB, + + // math. + LBF_MATH_CLAMP, + LBF_MATH_SIGN, + LBF_MATH_ROUND, + + // raw* + LBF_RAWSET, + LBF_RAWGET, + LBF_RAWEQUAL, + + // table. + LBF_TABLE_INSERT, + LBF_TABLE_UNPACK, + + // vector ctor + LBF_VECTOR, + + // bit32.count + LBF_BIT32_COUNTLZ, + LBF_BIT32_COUNTRZ, + + // select(_, ...) + LBF_SELECT_VARARG, + + // rawlen + LBF_RAWLEN, + + // bit32.extract(_, k, k) + LBF_BIT32_EXTRACTK, + + // get/setmetatable + LBF_GETMETATABLE, + LBF_SETMETATABLE, + + // tonumber/tostring + LBF_TONUMBER, + LBF_TOSTRING, + + // bit32.byteswap(n) + LBF_BIT32_BYTESWAP, + + // buffer. + LBF_BUFFER_READI8, + LBF_BUFFER_READU8, + LBF_BUFFER_WRITEU8, + LBF_BUFFER_READI16, + LBF_BUFFER_READU16, + LBF_BUFFER_WRITEU16, + LBF_BUFFER_READI32, + LBF_BUFFER_READU32, + LBF_BUFFER_WRITEU32, + LBF_BUFFER_READF32, + LBF_BUFFER_WRITEF32, + LBF_BUFFER_READF64, + LBF_BUFFER_WRITEF64, + + // vector. + LBF_VECTOR_MAGNITUDE, + LBF_VECTOR_NORMALIZE, + LBF_VECTOR_CROSS, + LBF_VECTOR_DOT, + LBF_VECTOR_FLOOR, + LBF_VECTOR_CEIL, + LBF_VECTOR_ABS, + LBF_VECTOR_SIGN, + LBF_VECTOR_CLAMP, + LBF_VECTOR_MIN, + LBF_VECTOR_MAX, + + // math.lerp + LBF_MATH_LERP, + + // vector.lerp + LBF_VECTOR_LERP, + + // math. + LBF_MATH_ISNAN, + LBF_MATH_ISINF, + LBF_MATH_ISFINITE, + + // integer + LBF_INTEGER_CREATE, + LBF_INTEGER_TONUMBER, + LBF_INTEGER_NEG, + LBF_INTEGER_ADD, + LBF_INTEGER_SUB, + LBF_INTEGER_MUL, + LBF_INTEGER_DIV, + LBF_INTEGER_MIN, + LBF_INTEGER_MAX, + LBF_INTEGER_REM, + LBF_INTEGER_IDIV, + LBF_INTEGER_UDIV, + LBF_INTEGER_UREM, + LBF_INTEGER_MOD, + LBF_INTEGER_CLAMP, + LBF_INTEGER_BAND, + LBF_INTEGER_BOR, + LBF_INTEGER_BNOT, + LBF_INTEGER_BXOR, + LBF_INTEGER_LT, + LBF_INTEGER_LE, + LBF_INTEGER_ULT, + LBF_INTEGER_ULE, + LBF_INTEGER_GT, + LBF_INTEGER_GE, + LBF_INTEGER_UGT, + LBF_INTEGER_UGE, + LBF_INTEGER_LSHIFT, + LBF_INTEGER_RSHIFT, + LBF_INTEGER_ARSHIFT, + LBF_INTEGER_LROTATE, + LBF_INTEGER_RROTATE, + LBF_INTEGER_EXTRACT, + LBF_INTEGER_BTEST, + LBF_INTEGER_COUNTRZ, + LBF_INTEGER_COUNTLZ, + LBF_INTEGER_BSWAP, + + // buffer.readinteger / buffer.writeinteger (int64_t) + LBF_BUFFER_READINTEGER, + LBF_BUFFER_WRITEINTEGER, +}; + +// Capture type, used in CAPTURE +pub const CaptureType = enum(u32) { + LCT_VAL = 0, + LCT_REF, + LCT_UPVAL, +}; + +// Proto flag bitmask, stored in Proto::flags +pub const ProtoFlag = enum(u32) { + /// used to tag main proto for modules with --!native + LPF_NATIVE_MODULE = 1 << 0, + /// used to tag individual protos as not profitable to compile natively + LPF_NATIVE_COLD = 1 << 1, + /// used to tag main proto for modules that have at least one function with native attribute + LPF_NATIVE_FUNCTION = 1 << 2, + /// function can be inlined + LPF_INLINABLE = 1 << 3, +}; + +pub const LuauFeedbackType = enum(u32) { LFT_CALLTARGET = 0 }; + +// sources: +// https://github.com/luau-lang/luau/blob/a2303a6ae68c53035eccf230c4450b9f068536af/Common/include/Luau/Bytecode.h diff --git a/deps/luau/src/Common/BytecodeUtils.zig b/deps/luau/src/Common/BytecodeUtils.zig new file mode 100644 index 0000000..cc92bb9 --- /dev/null +++ b/deps/luau/src/Common/BytecodeUtils.zig @@ -0,0 +1,40 @@ +const Bytecode = @import("Bytecode.zig"); + +pub inline fn getOpLength(op: Bytecode.Opcode) usize { + return switch (op) { + .GETGLOBAL, + .SETGLOBAL, + .GETIMPORT, + .GETTABLEKS, + .SETTABLEKS, + .NAMECALL, + .JUMPIFEQ, + .JUMPIFLE, + .JUMPIFLT, + .JUMPIFNOTEQ, + .JUMPIFNOTLE, + .JUMPIFNOTLT, + .NEWTABLE, + .SETLIST, + .FORGLOOP, + .LOADKX, + .FASTCALL2, + .FASTCALL2K, + .FASTCALL3, + .JUMPXEQKNIL, + .JUMPXEQKB, + .JUMPXEQKN, + .JUMPXEQKS, + .GETUDATAKS, + .SETUDATAKS, + .NAMECALLUDATA, + .NEWCLASSMEMBER, + .CALLFB, + .CMPPROTO, + => 2, + else => 1, + }; +} + +// sources: +// https://github.com/luau-lang/luau/blob/32d52d1b2ceef46fc25d87094a2d7f201c3ea5b8/Common/include/Luau/BytecodeUtils.h diff --git a/deps/luau/src/Common/DenseHash.zig b/deps/luau/src/Common/DenseHash.zig new file mode 100644 index 0000000..388c20e --- /dev/null +++ b/deps/luau/src/Common/DenseHash.zig @@ -0,0 +1,191 @@ +const std = @import("std"); + +const cpp_std = @import("../cpp_std.zig"); + +extern fn zig_new_any(size: usize) callconv(.c) *anyopaque; +extern fn zig_delete_any(*anyopaque) callconv(.c) void; + +pub fn DenseHashPointer(key: *const anyopaque) usize { + // return (@intFromPtr(key) >> 4) ^ (@intFromPtr(key) >> 9); + + // The idea to use this hash function was suggested here originally: https://maskray.me/blog/2026-06-07-recent-llvm-hash-table-improvements + // Hash function implementation is detailed here: https://github.com/MaskRay/llvm-project/blob/main/llvm/include/llvm/ADT/DenseMapInfo.h + // This hash produces better scattering for arena allocated types, because the pointers usually share the higher order bits. + // When inserting lots of keys, quadratic probing is not enough to save DenseHash, although it usually takes many more elements, + // before it becomes a problem + var u: u64 = @intFromPtr(key); + u *%= 0xbf58476d1ce4e5b9; + u ^= u >> 31; + // On 32-bit platforms uint64_t to size_t is a narrowing, so we need + // to static cast here. + return @truncate(u); +} + +pub const detail = struct { + pub fn DenseHashTable( + comptime Key: type, + comptime Item: type, + comptime MutableItem: type, + comptime ItemInterface: type, + comptime Hasher: type, + ) type { + const hash = if (@hasDecl(Hasher, "hash")) Hasher.hash else struct { + pub fn hash(e: Key) usize { + if (comptime @typeInfo(Key) == .pointer) { + return DenseHashPointer(@ptrCast(@alignCast(e))); + } else { + @compileError("Hasher must implement 'hash' function"); + } + } + }.hash; + + const eq = if (@hasDecl(ItemInterface, "eq")) ItemInterface.eq else struct { + pub fn eq(a: Key, b: Key) bool { + return a == b; + } + }.eq; + + _ = MutableItem; + return extern struct { + data: ?[*]Item = null, + capacity: usize = 0, + count: usize = 0, + empty_key: Key, + hasher: u8 = 0, + eq: u8 = 0, + + const This = @This(); + + pub fn init(empty_key: Key, buckets: usize) This { + var data: ?[*]Item = null; + var capacity: usize = 0; + if (buckets > 0) { + data = @ptrCast(@alignCast(zig_new_any(@sizeOf(Item) * buckets))); + capacity = buckets; + + ItemInterface.fill(data.?, buckets, empty_key); + } + return .{ + .data = data, + .capacity = capacity, + .count = 0, + .empty_key = empty_key, + }; + } + + pub fn find(self: *This, key: Key) ?*const Item { + if (self.count == 0) + return null; + if (eq(key, self.empty_key)) + return null; + + const hashmod = self.capacity - 1; + var bucket = hash(key) & hashmod; + for (0..hashmod) |probe| { + const probe_item = &self.data.?[bucket]; + + // Element exists + if (eq(ItemInterface.getKey(probe_item), key)) + return probe_item; + + // Element does not exist + if (eq(ItemInterface.getKey(probe_item), self.empty_key)) + return null; + + // Hash collision, quadratic probing + bucket = (bucket + probe + 1) & hashmod; + } + + // Hash table is full - this should not happen + std.debug.assert(false); + return null; + } + + pub fn size(self: This) usize { + return self.count; + } + + pub fn deinit(self: *This) void { + if (self.data) |data| { + ItemInterface.destroy(data, self.capacity); + + zig_delete_any(@ptrCast(@alignCast(data))); + self.data = null; + + self.capacity = 0; + } + } + }; + } +}; + +pub fn ItemInterfaceSet(comptime Key: type) type { + return struct { + pub fn getKey(item: *const Key) Key { + return item.*; + } + + pub fn setKey(item: *Key, key: Key) void { + item.* = key; + } + + pub fn fill(data: [*]Key, count: usize, key: Key) void { + for (0..count) |i| + data[i] = key; + } + + pub fn destroy(data: [*]Key, count: usize) void { + if (@hasDecl(Key, "deinit")) + for (0..count) |i| { + Key.deinit(data[i]); + }; + } + }; +} + +pub fn ItemInterfaceMap(comptime Key: type, comptime Value: type) type { + return struct { + pub fn getKey(item: *const cpp_std.Pair(Key, Value)) Key { + return item.first; + } + + pub fn setKey(item: *cpp_std.Pair(Key, Value), key: Key) void { + item.first = key; + } + + pub fn fill(data: [*]cpp_std.Pair(Key, Value), count: usize, key: Key) void { + for (0..count) |i| { + data[i].first = key; + data[i].second = .{}; + } + } + + pub fn destroy(data: [*]cpp_std.Pair(Key, Value), count: usize) void { + for (0..count) |i| { + const ptr = data[i]; + if (@hasDecl(Key, "deinit")) + Key.deinit(&ptr.first); + if (@hasDecl(Value, "deinit")) + Value.deinit(&ptr.second); + } + } + }; +} + +pub fn DenseHashSet( + comptime Key: type, + comptime Hasher: type, +) type { + return detail.DenseHashTable(Key, Key, Key, ItemInterfaceSet(Key), Hasher); +} + +pub fn DenseHashMap( + comptime Key: type, + comptime Value: type, + comptime Hasher: type, +) type { + return detail.DenseHashTable(Key, cpp_std.Pair(Key, Value), cpp_std.Pair(Key, Value), ItemInterfaceMap(Key, Value), Hasher); +} + +// sources: +// https://github.com/luau-lang/luau/blob/a2303a6ae68c53035eccf230c4450b9f068536af/Common/include/Luau/DenseHash.h diff --git a/deps/luau/src/Common/ExperimentalFlags.zig b/deps/luau/src/Common/ExperimentalFlags.zig new file mode 100644 index 0000000..962fc82 --- /dev/null +++ b/deps/luau/src/Common/ExperimentalFlags.zig @@ -0,0 +1,28 @@ +const std = @import("std"); + +pub inline fn isAnalysisFlagExperimental(flag: []const u8) bool { + // Flags in this list are disabled by default in various command-line tools. They may have behavior that is not fully final, + // or critical bugs that are found after the code has been submitted. This list is intended _only_ for flags that affect + // Luau's type checking. Flags that may change runtime behavior (e.g.: parser or VM flags) are not appropriate for this list. + const kList = [_][]const u8{ + "LuauInstantiateInSubtyping", // requires some fixes to lua-apps code + "LuauFixIndexerSubtypingOrdering", // requires some small fixes to lua-apps code since this fixes a false negative + "StudioReportLuauAny2", // takes telemetry data for usage of any types + "LuauTableCloneClonesType3", // requires fixes in lua-apps code, terrifyingly + "LuauSolverV2", + "UseNewLuauTypeSolverDefaultEnabled", // This can change the default solver used in cli applications, so it also needs to be disabled. Will require fixes in lua-apps code + }; + + for (comptime kList) |item| + if (std.mem.eql(u8, flag, item)) + return true; + + return false; +} + +test { + std.testing.refAllDecls(@This()); +} + +// sources: +// https://github.com/luau-lang/luau/blob/a2303a6ae68c53035eccf230c4450b9f068536af/Common/include/Luau/ExperimentalFlags.h diff --git a/deps/luau/src/Common/Variant.zig b/deps/luau/src/Common/Variant.zig new file mode 100644 index 0000000..00e08ab --- /dev/null +++ b/deps/luau/src/Common/Variant.zig @@ -0,0 +1,66 @@ +const std = @import("std"); + +pub fn Variant(comptime Ts: []const type) type { + comptime { + if (Ts.len == 0) @compileError("variant must have at least 1 type"); + } + + const storage_size = comptime blk: { + var max: usize = 0; + for (Ts) |T| + max = @max(max, @sizeOf(T)); + break :blk max; + }; + + const storage_align = comptime blk: { + var max: usize = 1; + for (Ts) |T| + max = @max(max, @alignOf(T)); + break :blk max; + }; + + const TaggedUnion = blk: { + var names: [Ts.len][]const u8 = undefined; + var field_types: [Ts.len]type = undefined; + var field_attributes: [Ts.len]std.builtin.Type.UnionField.Attributes = undefined; + inline for (Ts, 0..) |T, i| { + names[i] = std.fmt.comptimePrint("{d}", .{i}); // becomes @"0", @"1", etc. + field_types[i] = T; + field_attributes[i] = .{ + .@"align" = @alignOf(T), + }; + } + break :blk @Union( + .auto, + null, + &names, + &field_types, + &field_attributes, + ); + }; + + return extern struct { + typeId: c_int, + storage: [storage_size]u8 align(storage_align), + + pub const Union = TaggedUnion; + + pub fn @"union"(self: *const @This()) Union { + inline for (Ts, 0..) |T, i| { + if (self.typeId == @as(c_int, @intCast(i))) { + const active_ptr: *const T = @ptrCast(@alignCast(&self.storage)); + return @unionInit( + Union, + std.fmt.comptimePrint("{d}", .{i}), + active_ptr.*, + ); + } + } + unreachable; + } + + pub fn index(self: *const @This()) c_int { + return self.typeId; + } + }; +} diff --git a/deps/luau/src/Compiler/Compiler.cpp b/deps/luau/src/Compiler/Compiler.cpp new file mode 100644 index 0000000..9f4ecdd --- /dev/null +++ b/deps/luau/src/Compiler/Compiler.cpp @@ -0,0 +1,133 @@ +#include + +#include "luacode.h" + +#include "Luau/Common.h" + +#include "Luau/Parser.h" +#include "Luau/BytecodeBuilder.h" +#include "Luau/Compiler.h" +#include "Luau/TimeTrace.h" + +#define ZIG_LUAU_COMPILER(name) ZIG_FN(Luau_Compiler_##name) + +const char* outputBytes(const std::string& result, size_t* len) +{ + char* copy = static_cast(malloc(result.size())); + if (!copy) + return nullptr; + + memcpy(copy, result.data(), result.size()); + *len = result.size(); + return copy; +} + +ZIG_EXPORT const char* ZIG_LUAU_COMPILER(compile_ParseResult)( + const Luau::ParseResult* result, + Luau::AstNameTable* names, + size_t* len, + lua_CompileOptions* options, + Luau::BytecodeEncoder* encoder = nullptr +) { + Luau::CompileOptions opts; + + if (options) + { + static_assert(sizeof(lua_CompileOptions) == sizeof(Luau::CompileOptions), "C and C++ interface must match"); + memcpy(static_cast(&opts), options, sizeof(opts)); + } + + LUAU_TIMETRACE_SCOPE("compile", "Compiler"); + + if (!result->errors.empty()) + { + // Users of this function expect only a single error message + const Luau::ParseError& parseError = result->errors.front(); + std::string error = Luau::format(":%d: %s", parseError.getLocation().begin.line + 1, parseError.what()); + + return outputBytes(Luau::BytecodeBuilder::getError(error), len); + } + + try + { + Luau::BytecodeBuilder bcb(encoder); + Luau::compileOrThrow(bcb, *result, *names, opts); + + return outputBytes(bcb.getBytecode(), len); + } + catch (Luau::CompileError& e) + { + std::string error = Luau::format(":%d: %s", e.getLocation().begin.line + 1, e.what()); + return outputBytes(Luau::BytecodeBuilder::getError(error), len); + } +} + +ZIG_EXPORT int ZIG_LUAU_COMPILER(compileLoad_ParseResult)( + const Luau::ParseResult* result, + Luau::AstNameTable* names, + lua_State* L, + const char* moduleName, + const lua_CompileOptions* options, + int env, + Luau::BytecodeEncoder* encoder = nullptr +) { + Luau::CompileOptions opts; + + if (options) + { + static_assert(sizeof(lua_CompileOptions) == sizeof(Luau::CompileOptions), "C and C++ interface must match"); + memcpy(static_cast(&opts), options, sizeof(opts)); + } + + LUAU_TIMETRACE_SCOPE("compile", "Compiler"); + + if (!result->errors.empty()) + { + // Users of this function expect only a single error message + const Luau::ParseError& parseError = result->errors.front(); + std::string error = Luau::format(":%d: %s", parseError.getLocation().begin.line + 1, parseError.what()); + + std::string bytecode = Luau::BytecodeBuilder::getError(error); + return luau_load(L, moduleName, bytecode.data(), bytecode.size(), env); + } + + try + { + Luau::BytecodeBuilder bcb(encoder); + Luau::compileOrThrow(bcb, *result, *names, opts); + + std::string bytecode = bcb.getBytecode(); + return luau_load(L, moduleName, bytecode.data(), bytecode.size(), env); + } + catch (Luau::CompileError& e) + { + std::string error = Luau::format(":%d: %s", e.getLocation().begin.line + 1, e.what()); + std::string bytecode = Luau::BytecodeBuilder::getError(error); + return luau_load(L, moduleName, bytecode.data(), bytecode.size(), env); + } +} + +ZIG_EXPORT int ZIG_LUAU_COMPILER(compileLoad)( + lua_State* L, + const char* moduleName, + const char* contents, + size_t len, + const lua_CompileOptions* options, + int env +) { + Luau::CompileOptions opts; + + if (options) + { + static_assert(sizeof(lua_CompileOptions) == sizeof(Luau::CompileOptions), "C and C++ interface must match"); + memcpy(static_cast(&opts), options, sizeof(opts)); + } + + std::string bytecode = Luau::compile(std::string(contents, len), opts); + return luau_load(L, moduleName, bytecode.data(), bytecode.size(), env); +} + +ZIG_EXPORT void ZIG_LUAU_COMPILER(compile_free)(void *ptr) +{ + free(ptr); +} diff --git a/deps/luau/src/Compiler/Compiler.zig b/deps/luau/src/Compiler/Compiler.zig new file mode 100644 index 0000000..7416df7 --- /dev/null +++ b/deps/luau/src/Compiler/Compiler.zig @@ -0,0 +1,157 @@ +const std = @import("std"); + +const lua = @import("../VM/lua.zig"); +const Parser = @import("../Ast/Parser.zig"); +const Lexer = @import("../Ast/Lexer.zig"); +const Location = @import("../Ast/Location.zig").Location; + +const cpp_std = @import("../cpp_std.zig"); + +pub const CompileConstant = *anyopaque; + +/// return a type identifier for a global library member +/// values are defined by 'enum LuauBytecodeType' in Bytecode.h +pub const LibraryMemberTypeCallback = *const fn (library: [*c]const u8, member: [*c]const u8) callconv(.c) c_int; + +/// setup a value of a constant for a global library member +/// use setCompileConstant*** set of functions for values +pub const LibraryMemberConstantCallback = *const fn (library: [*c]const u8, member: [*c]const u8, constant: *CompileConstant) callconv(.c) c_int; + +pub const CompileOptions = extern struct { + /// 0 - no optimization + /// 1 - baseline optimization level that doesn't prevent debuggability + /// 2 - includes optimizations that harm debuggability such as inlining + optimizationLevel: c_int = 1, + /// 0 - no debugging support + /// 1 - line info & function names only; sufficient for backtraces + /// 2 - full debug info with local & upvalue names; necessary for debugger + debugLevel: c_int = 1, + /// type information is used to guide native code generation decisions + /// information includes testable typeArguments for function arguments, locals, upvalues and some temporaries + /// 0 - generate for native modules + /// 1 - generate for all modules + typeInfoLevel: c_int = 0, + /// 0 - no code coverage support + /// 1 - statement coverage + /// 2 - statement and expression coverage (verbose) + coverageLevel: c_int = 0, + + /// alternative global builtin to construct vectors, in addition to default builtin 'vector.create' + vectorLib: [*c]const u8 = null, + vectorCtor: [*c]const u8 = null, + + /// alternative vector type name for type tables, in addition to default type 'vector' + vectorType: [*c]const u8 = null, + + /// null-terminated array of globals that are mutable; disables the import optimization for fields accessed through these + mutableGlobals: [*c]const [*c]const u8 = null, + + /// null-terminated array of userdata typeArguments that will be included in the type information + userdataTypes: [*c]const [*c]const u8 = null, + + /// null-terminated array of globals which act as libraries and have members with known type and/or constant value + /// when an import of one of these libraries is accessed, callbacks below will be called to receive that information + librariesWithKnownMembers: [*c]const [*c]const u8 = null, + libraryMemberTypeCb: ?LibraryMemberTypeCallback = null, + libraryMemberConstantCb: ?LibraryMemberConstantCallback = null, + + // null-terminated array of library functions that should not be compiled into a built-in fastcall ("name" "lib.name") + disabledBuiltins: [*c]const [*c]const u8 = null, +}; + +pub const CompilerError = cpp_std.Exception(extern struct { + location: Location, + message: cpp_std.String, +}); + +extern "c" fn zig_Luau_Compiler_compile_ParseResult( + *const Parser.ParseResult, + *const Lexer.AstNameTable, + *usize, + ?*const CompileOptions, + ?*anyopaque, +) ?[*]const u8; +extern "c" fn zig_Luau_Compiler_compileLoad_ParseResult( + *const Parser.ParseResult, + *const Lexer.AstNameTable, + *lua.State, + [*c]const u8, + ?*const CompileOptions, + c_int, + ?*anyopaque, +) c_int; +extern "c" fn zig_Luau_Compiler_compileLoad( + *lua.State, + [*c]const u8, + [*c]const u8, + usize, + ?*const CompileOptions, + c_int, +) c_int; +extern "c" fn zig_Luau_Compiler_compile_free(*anyopaque) void; + +pub fn compileParseResult( + allocator: std.mem.Allocator, + parseResult: *Parser.ParseResult, + namesTable: *Lexer.AstNameTable, + options: ?CompileOptions, +) error{OutOfMemory}![]const u8 { + var size: usize = 0; + const bytes = zig_Luau_Compiler_compile_ParseResult(parseResult, namesTable, &size, if (options) |*o| o else null, null) orelse return error.OutOfMemory; + defer zig_Luau_Compiler_compile_free(@ptrCast(@constCast(bytes))); + return try allocator.dupe(u8, bytes[0..size]); +} + +pub fn compileLoadParseResult( + L: *lua.State, + moduleName: [:0]const u8, + parseResult: *Parser.ParseResult, + namesTable: *Lexer.AstNameTable, + options: ?CompileOptions, + env: i32, +) error{Fail}!void { + if (zig_Luau_Compiler_compileLoad_ParseResult(parseResult, namesTable, L, moduleName, if (options) |*o| o else null, env, null) != 0) + return error.Fail; +} + +pub fn compileLoad( + L: *lua.State, + moduleName: [:0]const u8, + source: []const u8, + options: ?CompileOptions, + env: i32, +) error{Fail}!void { + if (zig_Luau_Compiler_compileLoad(L, moduleName, source.ptr, source.len, if (options) |*o| o else null, env) != 0) + return error.Fail; +} + +test compileParseResult { + const Allocator = @import("../Ast/Allocator.zig"); + + const allocator = Allocator.init(); + defer allocator.deinit(); + + const astNameTable = Lexer.AstNameTable.init(allocator); + defer astNameTable.deinit(); + + const source = + \\--!test + \\-- This is a test comment + \\local x = + \\ + ; + + const parseResult = Parser.parse(source, astNameTable, allocator, .{}); + defer parseResult.deinit(); + + const zig_allocator = std.testing.allocator; + const bytes = try compileParseResult(zig_allocator, parseResult, astNameTable, null); + defer zig_allocator.free(bytes); + + try std.testing.expect(bytes[0] == 0); + try std.testing.expectEqualStrings(":4: Expected identifier when parsing expression, got ", bytes[1..]); +} + +// sources: +// https://github.com/luau-lang/luau/blob/8fe64db609ccbffb0abb7507c7ecef8c88327ef3/Compiler/include/Luau/Compiler.h +// https://github.com/luau-lang/luau/blob/8fe64db609ccbffb0abb7507c7ecef8c88327ef3/Compiler/src/Compiler.cpp diff --git a/deps/luau/src/Compiler/luacode.zig b/deps/luau/src/Compiler/luacode.zig new file mode 100644 index 0000000..ae2e91f --- /dev/null +++ b/deps/luau/src/Compiler/luacode.zig @@ -0,0 +1,49 @@ +const c = @import("c"); +const std = @import("std"); + +const Compiler = @import("Compiler.zig"); + +extern "c" fn zig_luau_free(ptr: *anyopaque) void; + +extern "c" fn luau_compile(source: [*c]const u8, size: usize, options: ?*const Compiler.CompileOptions, outsize: [*c]usize) [*c]u8; +extern "c" fn luau_set_compile_constant_nil(constant: *Compiler.CompileConstant) void; +extern "c" fn luau_set_compile_constant_boolean(constant: *Compiler.CompileConstant, b: c_int) void; +extern "c" fn luau_set_compile_constant_number(constant: *Compiler.CompileConstant, n: f64) void; +extern "c" fn luau_set_compile_constant_vector(constant: *Compiler.CompileConstant, x: f32, y: f32, z: f32, w: f32) void; +extern "c" fn luau_set_compile_constant_string(constant: *Compiler.CompileConstant, s: [*c]const u8, l: usize) void; + +/// Compile luau source into bytecode, return callee owned buffer allocated through the given allocator. +pub fn compile(allocator: std.mem.Allocator, source: []const u8, options: ?Compiler.CompileOptions) ![]const u8 { + var size: usize = 0; + + const bytecode = luau_compile(source.ptr, source.len, if (options) |*o| o else null, &size); + if (bytecode == null) + return error.OutOfMemory; + defer zig_luau_free(bytecode); + + return try allocator.dupe(u8, bytecode[0..size]); +} + +pub fn set_compile_constant_nil(constant: *Compiler.CompileConstant) void { + luau_set_compile_constant_nil(constant); +} + +pub fn set_compile_constant_boolean(constant: *Compiler.CompileConstant, b: bool) void { + luau_set_compile_constant_boolean(constant, if (b) 1 else 0); +} + +pub fn set_compile_constant_number(constant: *Compiler.CompileConstant, n: f64) void { + luau_set_compile_constant_number(constant, n); +} + +pub fn set_compile_constant_vector(constant: *Compiler.CompileConstant, x: f32, y: f32, z: f32, w: f32) void { + luau_set_compile_constant_vector(constant, x, y, z, w); +} + +pub fn set_compile_constant_string(constant: *Compiler.CompileConstant, s: []const u8) void { + luau_set_compile_constant_string(constant, s.ptr, s.len); +} + +// sources: +// https://github.com/luau-lang/luau/blob/8fe64db609ccbffb0abb7507c7ecef8c88327ef3/Compiler/include/luacode.h +// https://github.com/luau-lang/luau/blob/8fe64db609ccbffb0abb7507c7ecef8c88327ef3/Compiler/src/lcode.cpp diff --git a/deps/luau/src/Inliner/luajitinliner.zig b/deps/luau/src/Inliner/luajitinliner.zig new file mode 100644 index 0000000..92a82d6 --- /dev/null +++ b/deps/luau/src/Inliner/luajitinliner.zig @@ -0,0 +1,12 @@ +const lua = @import("../VM/lua.zig"); + +extern "c" fn luau_enable_jit_inliner(lua_State: *lua.State) void; +extern "c" fn luau_disable_jit_inliner(lua_State: *lua.State) void; + +pub fn enable(L: *lua.State) void { + luau_enable_jit_inliner(L); +} + +pub fn disable(L: *lua.State) void { + luau_disable_jit_inliner(L); +} diff --git a/deps/luau/src/VM/acc.cpp b/deps/luau/src/VM/acc.cpp new file mode 100644 index 0000000..7249278 --- /dev/null +++ b/deps/luau/src/VM/acc.cpp @@ -0,0 +1,25 @@ +#include + +#include "lobject.h" +#include "lstate.h" + +ZIG_EXPORT const unsigned char GCObject_size = sizeof(GCObject); +ZIG_EXPORT const unsigned char GCheader_size = sizeof(GCheader); +ZIG_EXPORT const unsigned char Value_size = sizeof(Value); +ZIG_EXPORT const unsigned char TValue_size = sizeof(TValue); +ZIG_EXPORT const unsigned char TString_size = sizeof(TString); +ZIG_EXPORT const unsigned char Udata_size = sizeof(Udata); +ZIG_EXPORT const unsigned char LuauBuffer_size = sizeof(LuauBuffer); +ZIG_EXPORT const unsigned char Proto_size = sizeof(Proto); +ZIG_EXPORT const unsigned char LocVar_size = sizeof(LocVar); +ZIG_EXPORT const unsigned char UpVal_size = sizeof(UpVal); +ZIG_EXPORT const unsigned char Closure_size = sizeof(Closure); +ZIG_EXPORT const unsigned char TKey_size = sizeof(TKey); +ZIG_EXPORT const unsigned char LuaNode_size = sizeof(LuaNode); +ZIG_EXPORT const unsigned char LuaTable_size = sizeof(LuaTable); +ZIG_EXPORT const unsigned char LuauClass_size = sizeof(LuauClass); +ZIG_EXPORT const unsigned char LuauObject_size = sizeof(LuauObject); + +ZIG_EXPORT const unsigned char TString_data_offset = offsetof(TString, data); +ZIG_EXPORT const unsigned char Udata_data_offset = offsetof(Udata, data); +ZIG_EXPORT const unsigned char LuauBuffer_data_offset = offsetof(LuauBuffer, data); \ No newline at end of file diff --git a/deps/luau/src/VM/errorset.zig b/deps/luau/src/VM/errorset.zig new file mode 100644 index 0000000..7298f04 --- /dev/null +++ b/deps/luau/src/VM/errorset.zig @@ -0,0 +1,11 @@ +pub const Memory = error{ BlockTooBig, OutOfMemory }; + +pub const Table = Memory || error{ + @"table overflow", + @"attempt to modify a readonly table", + @"table index is nil", + @"table index is nan", + @"table index contains nan", +}; + +pub const TableReadonly = Table.@"attempt to modify a readonly table"; diff --git a/deps/luau/src/VM/lapi.zig b/deps/luau/src/VM/lapi.zig new file mode 100644 index 0000000..992d75c --- /dev/null +++ b/deps/luau/src/VM/lapi.zig @@ -0,0 +1,1650 @@ +const c = @import("c"); +const std = @import("std"); + +const build_config = @import("config"); + +const lua = @import("lua.zig"); + +const lstring = @import("lstring.zig"); +const lgc = @import("lgc.zig"); +const ltm = @import("ltm.zig"); +const ldo = @import("ldo.zig"); +const lvm = @import("lvm.zig"); +const lfunc = @import("lfunc.zig"); +const ltable = @import("ltable.zig"); +const ludata = @import("ludata.zig"); +const lstate = @import("lstate.zig"); +const lbuffer = @import("lbuffer.zig"); +const lobject = @import("lobject.zig"); +const lvmutils = @import("lvmutils.zig"); + +const Errorset = @import("errorset.zig"); + +const State = lua.State; + +pub fn api_check(L: *State, cond: bool) void { + _ = L; + std.debug.assert(cond); +} + +pub inline fn api_checknelems(L: *State, n: u32) void { + api_check(L, n <= L.top - L.base); +} + +pub inline fn api_checkvalidindex(L: *State, obj: *const lobject.TValue) void { + api_check(L, obj != lobject.Onilobject); +} + +pub inline fn api_incr_top(L: *State) void { + api_check(L, @intFromPtr(L.top) < @intFromPtr(L.ci.?[0].top)); + L.top += 1; +} + +pub inline fn api_update_top(L: *State, p: *lobject.TValue) void { + api_check(L, @intFromPtr(p) >= @intFromPtr(L.base) and @intFromPtr(p) <= @intFromPtr(L.ci.?[0].top)); + L.top = @ptrCast(p); +} + +pub fn getcurrenv(L: *lua.State) *lobject.LuaTable { + if (L.ci == L.base_ci) // no enclosing function? + return L.gt.? // use global table as environment + else + return L.curr_func().env; +} + +pub noinline fn pseudo2addr(L: *State, idx: i32) *lobject.TValue { + api_check(L, lua.ispseudo(idx)); + switch (idx) { + lua.REGISTRYINDEX => return L.registry(), + lua.ENVIRONINDEX => { + const pt = &L.global.pseudotemp; + pt.sethvalue(L, getcurrenv(L)); + return &L.global.pseudotemp; + }, + lua.GLOBALSINDEX => { + const pt = &L.global.pseudotemp; + pt.sethvalue(L, L.gt.?); + return &L.global.pseudotemp; + }, + else => { + const func = L.curr_func(); + const i = lua.GLOBALSINDEX - idx; + return if (i <= @as(i32, @intCast(func.nupvalues))) + &func.d.c.upvalues()[@intCast(i - 1)] + else + @constCast(lobject.Onilobject); + }, + } +} + +pub inline fn index2addr(L: *State, idx: i32) *lobject.TValue { + if (idx > 0) { + const o: usize = @intFromPtr(&L.base[@intCast(idx - 1)]); + api_check(L, idx <= L.ci.?[0].top - L.base); + if (o >= @intFromPtr(L.top)) + return @constCast(lobject.Onilobject) + else + return @ptrFromInt(o); + } else if (idx > lua.REGISTRYINDEX) { + api_check(L, idx != 0 and -idx <= L.top - L.base); + return @ptrCast(L.top - @as(usize, @intCast(-idx))); + } else { + return pseudo2addr(L, idx); + } +} + +pub fn Atoobject(L: *lua.State, idx: i32) ?*const lobject.TValue { + const p = index2addr(L, idx); + return if (p == lobject.Onilobject) null else p; +} + +pub fn Apushvalue(L: *lua.State, o: *const lobject.TValue) void { + L.top[0].setobj(L, o); + api_incr_top(L); +} + +pub fn Apushclass(L: *lua.State, lco: *lobject.LuauClass) void { + api_check(L, @as(?*anyopaque, @ptrCast(@alignCast(lco))) != null); + L.top[0].setclassvalue(L, lco); + api_incr_top(L); +} + +pub fn checkstack(L: *lua.State, size: usize) Errorset.Memory!bool { + if (comptime !build_config.use_zig_backend) { + return c.lua_checkstack(@ptrCast(L), @as(i32, @intCast(size))) != 0; + } + if (size > lua.config.I_MAXCSTACK or (L.top - L.base + size) > lua.config.I_MAXCSTACK) + return false + else { + if (ldo.stacklimitreached(L, size)) { + try ldo.Dgrowstack(L, size); + } else { + if (comptime build_config.hard_stack_tests) + try ldo.Dreallocstack(L, L.stacksize - lua.config.EXTRA_SIZE, false); + } + ldo.expandstacklimit(L, &L.top[size]); + return true; + } +} +pub fn rawcheckstack(L: *lua.State, size: usize) Errorset.Memory!void { + if (comptime !build_config.use_zig_backend) { + return c.lua_rawcheckstack(@ptrCast(L), @as(i32, @intCast(size))); + } + try ldo.Dcheckstack(L, size); + ldo.expandstacklimit(L, &L.top[size]); +} + +pub fn xmove(from: *lua.State, to: *lua.State, n: u32) void { + if (comptime !build_config.use_zig_backend) { + return c.lua_xmove(@ptrCast(from), @ptrCast(to), @as(i32, @intCast(n))); + } + if (from == to) + return; + + api_checknelems(from, n); + api_check(from, from.global == to.global); + api_check(from, to.ci.?[0].top - to.top >= n); + lgc.Cthreadbarrier(to); + + const ttop = to.top; + const ftop = from.top - n; + for (0..@intCast(n)) |i| + ttop[i].setobj(to, &ftop[i]); + + from.top = @ptrCast(ftop); + to.top = ttop[n..]; +} + +pub fn xpush(from: *lua.State, to: *lua.State, idx: i32) void { + if (comptime !build_config.use_zig_backend) { + return c.lua_xpush(@ptrCast(from), @ptrCast(to), idx); + } + api_check(from, from.global == to.global); + lgc.Cthreadbarrier(to); + to.top[0].setobj(to, index2addr(from, idx)); + api_incr_top(to); +} + +pub fn newthread(L: *lua.State) Errorset.Table!*lua.State { + if (comptime !build_config.use_zig_backend) { + return @ptrCast(@alignCast(c.lua_newthread(@ptrCast(L)))); + } + try lgc.CcheckGC(L); + lgc.Cthreadbarrier(L); + const L1 = try lstate.Enewthread(L); + L.top[0].setthvalue(L, L1); + api_incr_top(L); + const g = L.global; + if (g.cb.userthread) |userthread| + userthread(L, L1); + return L1; +} + +pub fn mainthread(L: *lua.State) *lua.State { + if (comptime !build_config.use_zig_backend) { + return @ptrCast(@alignCast(c.lua_mainthread(@ptrCast(L)))); + } + return L.global.mainthread; +} + +// +// basic stack manipulation +// + +pub fn absindex(L: *lua.State, idx: i32) i32 { + if (comptime !build_config.use_zig_backend) { + return c.lua_absindex(@ptrCast(L), idx); + } + api_check(L, (idx > 0 and idx <= L.top - L.base) or (idx < 0 and -idx <= L.top - L.base) or lua.ispseudo(idx)); + return if (idx > 0 or lua.ispseudo(idx)) + idx + else + @as(i32, @intCast(L.top - L.base)) + idx + 1; +} + +pub fn gettop(L: *lua.State) usize { + if (comptime !build_config.use_zig_backend) { + return @intCast(c.lua_gettop(@ptrCast(L))); + } + return L.top - L.base; +} + +pub fn settop(L: *lua.State, idx: i32) void { + if (comptime !build_config.use_zig_backend) { + return c.lua_settop(@ptrCast(L), idx); + } + if (idx >= 0) { + api_check(L, idx <= L.stack_last - L.base); + const t = L.base[@intCast(idx)..]; + while (@intFromPtr(L.top) < @intFromPtr(t)) : (L.top = L.top[1..]) + L.top[0].setnilvalue(); + L.top = t; + } else { + api_check(L, -(idx + 1) <= L.top - L.base); + L.top -= @as(usize, @intCast(-(idx + 1))); // `subtract' index (index is negative) + } +} +pub inline fn pop(L: *lua.State, n: i32) void { + settop(L, -n - 1); +} + +pub fn remove(L: *lua.State, idx: i32) void { + if (comptime !build_config.use_zig_backend) { + return c.lua_remove(@ptrCast(L), idx); + } + var p: [*]lobject.TValue = @ptrCast(index2addr(L, idx)); + api_checkvalidindex(L, @ptrCast(p)); + p += 1; + while (@intFromPtr(p) < @intFromPtr(L.top)) : (p += 1) + (p - 1)[0].setobj(L, @ptrCast(p)); + L.top -= 1; +} + +pub fn insert(L: *lua.State, idx: i32) void { + if (comptime !build_config.use_zig_backend) { + return c.lua_insert(@ptrCast(L), idx); + } + lgc.Cthreadbarrier(L); + const p = index2addr(L, idx); + api_checkvalidindex(L, p); + var q = L.top; + while (@intFromPtr(q) > @intFromPtr(p)) { + const n = q - 1; + q[0].setobj(L, @ptrCast(n)); + q = n; + } + p.setobj(L, &L.top[0]); +} + +pub fn replace(L: *lua.State, idx: i32) void { + if (comptime !build_config.use_zig_backend) { + return c.lua_replace(@ptrCast(L), idx); + } + api_checknelems(L, 1); + lgc.Cthreadbarrier(L); + const o = index2addr(L, idx); + api_checkvalidindex(L, o); + switch (idx) { + lua.ENVIRONINDEX => { + api_check(L, L.ci != L.base_ci); + const func = L.curr_func(); + api_check(L, (L.top - 1)[0].ttistable()); + func.env = (L.top - 1)[0].hvalue(); + lgc.Cbarrier(L, @ptrCast(@alignCast(func)), @ptrCast(L.top - 1)); + }, + lua.GLOBALSINDEX => { + api_check(L, (L.top - 1)[0].ttistable()); + L.gt = (L.top - 1)[0].hvalue(); + }, + else => { + o.setobj(L, @ptrCast(L.top - 1)); + if (idx < lua.GLOBALSINDEX) // function upvalue? + lgc.Cbarrier(L, @ptrCast(@alignCast(L.curr_func())), @ptrCast(L.top - 1)); + }, + } + L.top -= 1; +} + +pub fn pushvalue(L: *lua.State, idx: i32) void { + if (comptime !build_config.use_zig_backend) { + return c.lua_pushvalue(@ptrCast(L), idx); + } + lgc.Cthreadbarrier(L); + const o = index2addr(L, idx); + L.top[0].setobj(L, o); + api_incr_top(L); +} + +// +// access functions (stack -> C) +// + +pub fn @"type"(L: *lua.State, idx: i32) i32 { + if (comptime !build_config.use_zig_backend) { + return c.lua_type(@ptrCast(L), idx); + } + const o: *const lobject.TValue = index2addr(L, idx); + return if (o == lobject.Onilobject) @intFromEnum(lua.Type.None) else o.ttype(); +} +pub inline fn isfunction(L: *lua.State, idx: i32) bool { + return @"type"(L, idx) == @intFromEnum(lua.Type.Function); +} +pub inline fn istable(L: *lua.State, idx: i32) bool { + return @"type"(L, idx) == @intFromEnum(lua.Type.Table); +} +pub inline fn islightuserdata(L: *lua.State, idx: i32) bool { + return @"type"(L, idx) == @intFromEnum(lua.Type.LightUserdata); +} +pub inline fn isnil(L: *lua.State, idx: i32) bool { + return @"type"(L, idx) == @intFromEnum(lua.Type.Nil); +} +pub inline fn isboolean(L: *lua.State, idx: i32) bool { + return @"type"(L, idx) == @intFromEnum(lua.Type.Boolean); +} +pub inline fn isinteger64(L: *lua.State, idx: i32) bool { + return @"type"(L, idx) == @intFromEnum(lua.Type.Integer); +} +pub inline fn isvector(L: *lua.State, idx: i32) bool { + return @"type"(L, idx) == @intFromEnum(lua.Type.Vector); +} +pub inline fn isthread(L: *lua.State, idx: i32) bool { + return @"type"(L, idx) == @intFromEnum(lua.Type.Thread); +} +pub inline fn isbuffer(L: *lua.State, idx: i32) bool { + return @"type"(L, idx) == @intFromEnum(lua.Type.Buffer); +} +pub inline fn isnone(L: *lua.State, idx: i32) bool { + return @"type"(L, idx) == @intFromEnum(lua.Type.None); +} +pub inline fn isnoneornil(L: *lua.State, idx: i32) bool { + return @"type"(L, idx) <= @intFromEnum(lua.Type.Nil); +} + +pub fn typeOf(L: *lua.State, idx: i32) lua.Type { + if (comptime !build_config.use_zig_backend) { + return @enumFromInt(c.lua_type(@ptrCast(L), idx)); + } + return index2addr(L, idx).typeOf(); +} + +pub fn typename(t: lua.Type) [:0]const u8 { + return if (t == .None) "no value" else ltm.typenames[@intCast(@intFromEnum(t))]; +} + +pub fn iscfunction(L: *lua.State, idx: i32) bool { + if (comptime !build_config.use_zig_backend) { + return c.lua_iscfunction(@ptrCast(L), idx) != 0; + } + const o: *const lobject.TValue = index2addr(L, idx); + return o.iscfunction(); +} + +pub fn isLfunction(L: *lua.State, idx: i32) bool { + if (comptime !build_config.use_zig_backend) { + return c.lua_isLfunction(@ptrCast(L), idx) != 0; + } + const o: *const lobject.TValue = index2addr(L, idx); + return o.isLfunction(); +} + +pub fn isnumber(L: *lua.State, idx: i32) bool { + if (comptime !build_config.use_zig_backend) { + return c.lua_isnumber(@ptrCast(L), idx) != 0; + } + var n: lobject.TValue = undefined; + const o: *const lobject.TValue = index2addr(L, idx); + return o.ttisnumber() and lvmutils.Vtonumber(o, &n) != null; +} + +pub fn isstring(L: *lua.State, idx: i32) bool { + if (comptime !build_config.use_zig_backend) { + return c.lua_isstring(@ptrCast(L), idx) != 0; + } + const t = @"type"(L, idx); + return t == @intFromEnum(lua.Type.String) or t == @intFromEnum(lua.Type.Number); +} + +pub fn isuserdata(L: *lua.State, idx: i32) bool { + if (comptime !build_config.use_zig_backend) { + return c.lua_isuserdata(@ptrCast(L), idx) != 0; + } + const o: *const lobject.TValue = index2addr(L, idx); + return o.ttisuserdata() or o.ttislightuserdata(); +} + +pub fn rawequal(L: *lua.State, index1: i32, index2: i32) bool { + if (comptime !build_config.use_zig_backend) { + return c.lua_rawequal(@ptrCast(L), index1, index2) != 0; + } + const o1 = index2addr(L, index1); + const o2 = index2addr(L, index2); + return if (o1 == lobject.Onilobject or o2 == lobject.Onilobject) false else lobject.OrawequalObj(o1, o2); +} + +pub inline fn equal(L: *lua.State, index1: i32, index2: i32) !bool { + return c.lua_equal(@ptrCast(L), index1, index2) != 0; + // const o1 = index2addr(L, index1); + // const o2 = index2addr(L, index2); + // return if (o1 == lobject.Onilobject or o2 == lobject.Onilobject) false else lvm.equalobj(L, o1, o2); +} + +pub inline fn lessthan(L: *lua.State, index1: i32, index2: i32) !bool { + return c.lua_lessthan(@ptrCast(L), index1, index2) != 0; +} + +pub fn tonumberx(L: *lua.State, idx: i32) ?f64 { + if (comptime !build_config.use_zig_backend) { + var isnum: i32 = 0; + const v = c.lua_tonumberx(@ptrCast(L), idx, &isnum); + if (isnum != 0) + return v; + return null; + } + var n: lobject.TValue = undefined; + const o: *const lobject.TValue = index2addr(L, idx); + if (lvmutils.Vtonumber(o, &n)) |obj| + return obj.nvalue() + else + return null; +} +pub inline fn tonumber(L: *lua.State, idx: i32) ?f64 { + return tonumberx(L, idx); +} + +pub fn tointegerx(L: *lua.State, idx: i32) ?i32 { + if (comptime !build_config.use_zig_backend) { + var isnum: i32 = 0; + const v = c.lua_tointegerx(@ptrCast(L), idx, &isnum); + if (isnum != 0) + return v; + return null; + } + var n: lobject.TValue = undefined; + const o: *const lobject.TValue = index2addr(L, idx); + if (lvmutils.Vtonumber(o, &n)) |obj| + return @truncate(@as(isize, @intFromFloat(obj.nvalue()))) + else + return null; +} +pub fn tointeger(L: *lua.State, idx: i32) ?i32 { + return tointegerx(L, idx); +} + +pub fn tounsignedx(L: *lua.State, idx: i32) ?u32 { + if (comptime !build_config.use_zig_backend) { + var isnum: i32 = 0; + const v = c.lua_tounsignedx(@ptrCast(L), idx, &isnum); + if (isnum != 0) + return v; + return null; + } + var n: lobject.TValue = undefined; + const o: *const lobject.TValue = index2addr(L, idx); + if (lvmutils.Vtonumber(o, &n)) |obj| + return @bitCast(@as(i32, @truncate(@as(isize, @intFromFloat(obj.nvalue()))))) + else + return null; +} +pub fn tounsigned(L: *lua.State, idx: i32) ?u32 { + return tounsignedx(L, idx); +} + +pub fn toboolean(L: *lua.State, idx: i32) bool { + if (comptime !build_config.use_zig_backend) { + return c.lua_toboolean(@ptrCast(L), idx) != 0; + } + const o = index2addr(L, idx); + return !o.l_isfalse(); +} + +pub fn tointeger64(L: *lua.State, idx: i32) ?i64 { + if (comptime !build_config.use_zig_backend) { + var isnum: i32 = 0; + const v = c.lua_tointeger64x(@ptrCast(L), idx, &isnum); + if (isnum != 0) + return v; + return null; + } + var n: lobject.TValue = undefined; + const o: *const lobject.TValue = index2addr(L, idx); + if (lvmutils.Vtonumber(o, &n)) |obj| + return @truncate(@as(i64, @intFromFloat(obj.nvalue()))) + else + return null; +} + +pub fn tolstring(L: *lua.State, idx: i32) ?[:0]const u8 { + if (comptime !build_config.use_zig_backend) { + var len: usize = 0; + return if (c.lua_tolstring(@ptrCast(L), idx, &len)) |str| str[0..len :0] else null; + } + var o = index2addr(L, idx); + if (!o.ttisstring()) { + lgc.Cthreadbarrier(L); + if (!lvmutils.Vtostring(L, o)) + return null; // conversion failed? + lgc.CcheckGC(L) catch return null; + o = index2addr(L, idx); + } + return o.tsvalue().toSlice(); +} +pub fn tostring(L: *lua.State, idx: i32) ?[:0]const u8 { + return std.mem.span(@as([*c]const u8, @ptrCast((tolstring(L, idx) orelse return null).ptr))); +} + +pub fn tolstringatom(L: *lua.State, idx: i32, atom: ?*i16) ?[:0]const u8 { + if (comptime !build_config.use_zig_backend) { + var len: usize = 0; + var atomptr: c_int = 0; + return if (c.lua_tolstringatom(@ptrCast(L), idx, &len, &atomptr)) |str| { + if (atom) |a| + a.* = @intCast(atomptr); + return str[0..len :0]; + } else null; + } + const o = index2addr(L, idx); + if (!o.ttisstring()) + return null; + const s = o.tsvalue(); + if (atom) |a| { + lstring.Supdateatom(L, s); + a.* = s.atom; + } + return s.toSlice(); +} + +pub inline fn tostringatom(L: *lua.State, idx: i32, atom: ?*i16) ?[:0]const u8 { + return tolstringatom(L, idx, atom); +} + +pub fn namecallatom(L: *lua.State, atom: ?*i16) ?[:0]const u8 { + if (comptime !build_config.use_zig_backend) { + var atomptr: c_int = 0; + return if (c.lua_namecallatom(@ptrCast(L), &atomptr)) |str| { + if (atom) |a| + a.* = @intCast(atomptr); + return std.mem.span(str); + } else null; + } + const s = L.namecall orelse return null; + if (atom) |a| { + lstring.Supdateatom(L, s); + a.* = s.atom; + } + return s.toSlice(); +} + +pub fn namecallstr(L: *lua.State) ?[]const u8 { + if (comptime !build_config.use_zig_backend) { + return if (c.lua_namecallatom(@ptrCast(L), null)) |str| std.mem.span(str) else null; + } + const s = L.namecall; + if (s) |str| + return str.toSlice(); + return null; +} + +pub fn tovector(L: *lua.State, idx: i32) ?[]const f32 { + if (comptime !build_config.use_zig_backend) { + return if (c.lua_tovector(@ptrCast(L), idx)) |vec| vec[0..lua.config.VECTOR_SIZE] else null; + } + const o = index2addr(L, idx); + return if (!o.ttisvector()) + null + else + o.vvalue(); +} + +pub fn objlen(L: *lua.State, idx: i32) usize { + if (comptime !build_config.use_zig_backend) { + return @intCast(c.lua_objlen(@ptrCast(L), idx)); + } + const o = index2addr(L, idx); + switch (o.ttype()) { + @intFromEnum(lua.Type.String) => return @intCast(o.tsvalue().len), + @intFromEnum(lua.Type.Userdata) => return @intCast(o.uvalue().len), + @intFromEnum(lua.Type.Buffer) => return @intCast(o.bufvalue().len), + @intFromEnum(lua.Type.Table) => return ltable.Hgetn(o.hvalue()), + else => return 0, + } +} +pub inline fn strlen(L: *lua.State, idx: i32) usize { + return objlen(L, idx); +} + +pub fn tocfunction(L: *lua.State, idx: i32) ?lua.CFunction { + if (comptime !build_config.use_zig_backend) { + return @ptrCast(@alignCast(c.lua_tocfunction(@ptrCast(L), idx))); + } + const o: *const lobject.TValue = index2addr(L, idx); + return if (!o.iscfunction()) + null + else + o.clvalue().d.c.f; +} + +pub fn tolightuserdata(L: *lua.State, comptime T: type, idx: i32) ?*T { + if (comptime !build_config.use_zig_backend) { + return @ptrCast(@alignCast(c.lua_tolightuserdata(@ptrCast(L), idx))); + } + const o: *const lobject.TValue = index2addr(L, idx); + return if (!o.ttislightuserdata()) + null + else + @ptrCast(@alignCast(o.pvalue())); +} + +pub fn tolightuserdatatagged(L: *lua.State, comptime T: type, idx: i32, tag: i32) ?*T { + if (comptime !build_config.use_zig_backend) { + return @ptrCast(@alignCast(c.lua_tolightuserdatatagged(@ptrCast(L), idx, @intCast(tag)))); + } + const o: *const lobject.TValue = index2addr(L, idx); + return if (!o.ttislightuserdata() or o.lightuserdatatag() != tag) + null + else + @ptrCast(@alignCast(o.pvalue())); +} + +pub fn touserdata(L: *lua.State, comptime T: type, idx: i32) ?*T { + if (comptime !build_config.use_zig_backend) { + return @ptrCast(@alignCast(c.lua_touserdata(@ptrCast(L), idx))); + } + const o: *const lobject.TValue = index2addr(L, idx); + if (o.ttisuserdata()) + return @ptrCast(@alignCast(&o.uvalue().data)) + else if (o.ttislightuserdata()) + return @ptrCast(@alignCast(o.pvalue())) + else + return null; +} + +pub fn touserdatatagged(L: *lua.State, comptime T: type, idx: i32, tag: u8) ?*T { + if (comptime !build_config.use_zig_backend) { + return @ptrCast(@alignCast(c.lua_touserdatatagged(@ptrCast(L), idx, @intCast(tag)))); + } + const o: *const lobject.TValue = index2addr(L, idx); + return if (!o.ttisuserdata() or o.uvalue().tag != tag) + null + else + @ptrCast(@alignCast(&o.uvalue().data)); +} + +pub fn userdatatag(L: *lua.State, idx: i32) ?u8 { + if (comptime !build_config.use_zig_backend) { + const tag = c.lua_userdatatag(@ptrCast(L), idx); + return if (tag >= 0) @intCast(tag) else null; + } + const o: *const lobject.TValue = index2addr(L, idx); + return if (o.ttisuserdata()) + @intCast(o.uvalue().tag) + else + null; +} + +pub fn lightuserdatatag(L: *lua.State, idx: i32) ?u8 { + if (comptime !build_config.use_zig_backend) { + const tag = c.lua_lightuserdatatag(@ptrCast(L), idx); + return if (tag >= 0) @intCast(tag) else null; + } + const o: *const lobject.TValue = index2addr(L, idx); + return if (o.ttislightuserdata()) + o.lightuserdatatag() + else + null; +} + +pub fn tothread(L: *lua.State, idx: i32) ?*lua.State { + if (comptime !build_config.use_zig_backend) { + return @ptrCast(@alignCast(c.lua_tothread(@ptrCast(L), idx))); + } + const o: *const lobject.TValue = index2addr(L, idx); + return if (!o.ttisthread()) + null + else + o.thvalue(); +} + +pub fn tobuffer(L: *lua.State, idx: i32) ?[]u8 { + if (comptime !build_config.use_zig_backend) { + var len: usize = 0; + return if (c.lua_tobuffer(@ptrCast(L), idx, &len)) |buf| @as([*]u8, @ptrCast(@alignCast(buf)))[0..len] else null; + } + const o: *const lobject.TValue = index2addr(L, idx); + if (!o.ttisbuffer()) + return null; + const b = o.bufvalue(); + return @as([*]u8, @ptrCast(&b.data))[0..@intCast(b.len)]; +} + +pub fn topointer(L: *lua.State, idx: i32) ?*const anyopaque { + if (comptime !build_config.use_zig_backend) { + return if (c.lua_topointer(@ptrCast(L), idx)) |ptr| ptr else null; + } + const o: *const lobject.TValue = index2addr(L, idx); + switch (o.tt) { + @intFromEnum(lua.Type.Userdata) => return @ptrCast(&o.uvalue().data), + @intFromEnum(lua.Type.LightUserdata) => return @ptrCast(o.pvalue()), + else => return if (o.iscollectable()) + @ptrCast(o.gcvalue()) + else + null, + } +} + +// +// push functions (C -> stack) +// +pub fn pushnil(L: *lua.State) void { + if (comptime !build_config.use_zig_backend) { + return c.lua_pushnil(@ptrCast(L)); + } + L.top[0].setnilvalue(); + api_incr_top(L); +} + +pub fn pushnumber(L: *lua.State, n: f64) void { + if (comptime !build_config.use_zig_backend) { + return c.lua_pushnumber(@ptrCast(L), n); + } + L.top[0].setnvalue(n); + api_incr_top(L); +} + +pub fn pushinteger(L: *lua.State, n: i32) void { + if (comptime !build_config.use_zig_backend) { + return c.lua_pushinteger(@ptrCast(L), n); + } + L.top[0].setnvalue(@floatFromInt(n)); + api_incr_top(L); +} + +pub fn pushinteger64(L: *lua.State, n: i64) void { + if (comptime !build_config.use_zig_backend) { + return c.lua_pushinteger64(@ptrCast(L), n); + } + L.top[0].setlvalue(n); + api_incr_top(L); +} + +pub fn pushunsigned(L: *lua.State, n: u32) void { + if (comptime !build_config.use_zig_backend) { + return c.lua_pushunsigned(@ptrCast(L), n); + } + L.top[0].setnvalue(@floatFromInt(n)); + api_incr_top(L); +} + +pub fn pushvector(L: *lua.State, x: f32, y: f32, z: f32, w: ?f32) void { + if (comptime !build_config.use_zig_backend) { + if (comptime lua.config.VECTOR_SIZE == 4) + @compileError("use zig backend for 4D vectors"); + return c.lua_pushvector(@ptrCast(L), x, y, z); + } + L.top[0].setvvalue(x, y, z, w); + api_incr_top(L); +} + +pub fn pushlstring(L: *lua.State, s: []const u8) Errorset.Table!void { + if (comptime !build_config.use_zig_backend) { + return c.lua_pushlstring(@ptrCast(L), s.ptr, s.len); + } + try lgc.CcheckGC(L); + lgc.Cthreadbarrier(L); + L.top[0].setsvalue(L, try lstring.Snewlstr(L, s)); + api_incr_top(L); +} + +pub inline fn pushstring(L: *lua.State, str: ?[:0]const u8) Errorset.Table!void { + if (str) |s| { + try pushlstring(L, std.mem.span(@as([*c]const u8, @ptrCast(s.ptr)))); + } else pushnil(L); +} + +pub fn pushvfstring(L: *lua.State, comptime fmt: []const u8, args: anytype) Errorset.Table!void { + try lobject.Opushvfstring(L, fmt, args); +} +pub inline fn pushfstring(L: *lua.State, comptime fmt: []const u8, args: anytype) Errorset.Table!void { + try pushvfstring(L, fmt, args); +} + +pub fn pushcclosurek( + L: *lua.State, + f: lua.CFunction, + debugname: [:0]const u8, + nup: u8, + cont: ?lua.Continuation, +) Errorset.Table!void { + if (comptime !build_config.use_zig_backend) { + return c.lua_pushcclosurek(@ptrCast(L), @ptrCast(@alignCast(f)), debugname, nup, @ptrCast(@alignCast(cont))); + } + try lgc.CcheckGC(L); + lgc.Cthreadbarrier(L); + api_checknelems(L, nup); + const cl = try lfunc.FnewCclosure(L, nup, getcurrenv(L)); + cl.d.c.f = f; + cl.d.c.cont = cont; + cl.d.c.debugname = debugname; + L.top -= nup; + var n: u8 = nup; + while (n > 0) : (n -= 1) { + const nu = n - 1; + cl.d.c.upvalues()[nu].setobj(L, @ptrCast(L.top + nu)); + } + L.top[0].setclvalue(L, cl); + std.debug.assert(lgc.iswhite(cl.obj2gco())); + api_incr_top(L); +} +pub inline fn pushcfunction(L: *lua.State, f: lua.CFunction, debugname: [:0]const u8) Errorset.Table!void { + try pushcclosurek(L, f, debugname, 0, null); +} +pub inline fn pushcclosure(L: *lua.State, f: lua.CFunction, debugname: [:0]const u8, nup: u8) Errorset.Table!void { + try pushcclosurek(L, f, debugname, nup, null); +} + +pub fn pushboolean(L: *lua.State, b: bool) void { + if (comptime !build_config.use_zig_backend) { + return c.lua_pushboolean(@ptrCast(L), if (b) 1 else 0); + } + L.top[0].setbvalue(b); + api_incr_top(L); +} + +pub fn pushlightuserdatatagged(L: *lua.State, p: ?*anyopaque, tag: u8) void { + if (comptime !build_config.use_zig_backend) { + return c.lua_pushlightuserdatatagged(@ptrCast(L), p, @intCast(tag)); + } + api_check(L, tag < lua.config.LUTAG_LIMIT); + L.top[0].setpvalue(p, tag); + api_incr_top(L); +} +pub inline fn pushlightuserdata(L: *lua.State, p: ?*anyopaque) void { + pushlightuserdatatagged(L, p, 0); +} + +pub fn pushthread(L: *lua.State) bool { + if (comptime !build_config.use_zig_backend) { + return c.lua_pushthread(@ptrCast(L)) != 0; + } + lgc.Cthreadbarrier(L); + L.top[0].setthvalue(L, L); + api_incr_top(L); + return L.global.mainthread == L; +} + +// +// get functions (Lua -> stack) +// + +pub inline fn gettable(L: *lua.State, idx: i32) !lua.Type { + return @enumFromInt(c.lua_gettable(@ptrCast(L), idx)); +} + +pub inline fn getfield(L: *lua.State, idx: i32, k: [:0]const u8) !lua.Type { + return @enumFromInt(c.lua_getfield(@ptrCast(L), idx, k.ptr)); +} +pub inline fn getglobal(L: *lua.State, k: [:0]const u8) !lua.Type { + return getfield(L, lua.GLOBALSINDEX, k); +} + +pub fn rawgetfield(L: *lua.State, idx: i32, k: []const u8) lua.Type { + if (comptime !build_config.use_zig_backend) { + // small static buffer for field because the 'k' type + // does not require a zero sentinel, but the C api does. + var static: [256:0]u8 = undefined; + if (k.len > static.len) + @panic("key too long, use zig backend"); + @memcpy(static[0..k.len], k); + static[k.len] = 0; + return @enumFromInt(c.lua_rawgetfield(@ptrCast(L), idx, &static)); + } + lgc.Cthreadbarrier(L); + const t = index2addr(L, idx); + api_check(L, t.ttistable()); + var ttype: lua.Type = .Nil; + if (lstring.Sassumelstr(L, k)) |ts| { + const o = ltable.Hgetstr(t.hvalue(), ts); + L.top[0].setobj(L, ltable.Hgetstr(t.hvalue(), ts)); + ttype = o.typeOf(); + } else L.top[0].setobj(L, lobject.Onilobject); + api_incr_top(L); + return ttype; +} +pub inline fn rawgetglobal(L: *lua.State, k: []const u8) lua.Type { + return rawgetfield(L, lua.GLOBALSINDEX, k); +} + +/// get value from table value at `idx` +/// * gets value from index at `top` +/// * stores value at `top` +/// * expects value at `idx` to be *table* +pub fn rawget(L: *lua.State, idx: i32) lua.Type { + if (comptime !build_config.use_zig_backend) { + return @enumFromInt(c.lua_rawget(@ptrCast(L), idx)); + } + lgc.Cthreadbarrier(L); + const t = index2addr(L, idx); + api_check(L, t.ttistable()); + (L.top - 1)[0].setobj(L, ltable.Hget(t.hvalue(), @ptrCast(L.top - 1))); + return (L.top - 1)[0].typeOf(); +} + +/// get value from table value at `idx` with number key `n` +/// * pushes value to stack +/// * expects value at `idx` to be *table* +pub fn rawgeti(L: *lua.State, idx: i32, n: i32) lua.Type { + if (comptime !build_config.use_zig_backend) { + return @enumFromInt(c.lua_rawgeti(@ptrCast(L), idx, n)); + } + lgc.Cthreadbarrier(L); + const t = index2addr(L, idx); + api_check(L, t.ttistable()); + L.top[0].setobj(L, ltable.Hgetnum(t.hvalue(), n)); + api_incr_top(L); + return (L.top - 1)[0].typeOf(); +} +pub inline fn getref(L: *State, idx: i32) lua.Type { + return rawgeti(L, lua.REGISTRYINDEX, idx); +} + +/// create a new table and push it to the stack +pub fn createtable(L: *lua.State, narray: u32, nrec: u32) !void { + if (comptime !build_config.use_zig_backend) { + return c.lua_createtable(@ptrCast(L), @intCast(narray), @intCast(nrec)); + } + try lgc.CcheckGC(L); + lgc.Cthreadbarrier(L); + L.top[0].sethvalue(L, try ltable.Hnew(L, narray, nrec)); + api_incr_top(L); +} +/// create a new table and push it to the stack +pub inline fn newtable(L: *lua.State) !void { + try createtable(L, 0, 0); +} + +/// set readonly flag for table at `idx` +pub fn setreadonly(L: *lua.State, idx: i32, enabled: bool) void { + if (comptime !build_config.use_zig_backend) { + return c.lua_setreadonly(@ptrCast(L), idx, if (enabled) 1 else 0); + } + const o = index2addr(L, idx); + api_check(L, o.ttistable()); + const t = o.hvalue(); + api_check(L, t != L.registry().hvalue()); + t.readonly = if (enabled) 1 else 0; +} + +/// get readonly flag for table at `idx` +pub fn getreadonly(L: *lua.State, idx: i32) bool { + if (comptime !build_config.use_zig_backend) { + return c.lua_getreadonly(@ptrCast(L), idx) != 0; + } + const o: *const lobject.TValue = index2addr(L, idx); + api_check(L, o.ttistable()); + return o.hvalue().readonly != 0; +} + +/// set safeenv flag for table at `idx` +pub fn setsafeenv(L: *lua.State, idx: i32, enabled: bool) void { + if (comptime !build_config.use_zig_backend) { + return c.lua_setsafeenv(@ptrCast(L), idx, if (enabled) 1 else 0); + } + const o = index2addr(L, idx); + api_check(L, o.ttistable()); + o.hvalue().safeenv = if (enabled) 1 else 0; +} + +/// get metatable from `idx` +/// * returns **true** if metatable is found +/// * pushes metatable to stack +pub fn getmetatable(L: *lua.State, idx: i32) bool { + if (comptime !build_config.use_zig_backend) { + return c.lua_getmetatable(@ptrCast(L), idx) != 0; + } + lgc.Cthreadbarrier(L); + var mt: ?*lobject.LuaTable = null; + const o: *const lobject.TValue = index2addr(L, idx); + switch (o.tt) { + @intFromEnum(lua.Type.Table) => mt = o.hvalue().metatable, + @intFromEnum(lua.Type.Userdata) => mt = o.uvalue().metatable, + @intFromEnum(lua.Type.Object) => mt = o.objectvalue().lclass.instancemetatable, + else => mt = L.global.mt[@intCast(o.tt)], + } + if (mt) |ptr| { + L.top[0].sethvalue(L, ptr); + api_incr_top(L); + } + return mt != null; +} + +pub fn getfenv(L: *lua.State, idx: i32) void { + if (comptime !build_config.use_zig_backend) { + return c.lua_getfenv(@ptrCast(L), idx); + } + lgc.Cthreadbarrier(L); + const o: *const lobject.TValue = index2addr(L, idx); + api_checkvalidindex(L, o); + switch (o.tt) { + @intFromEnum(lua.Type.Function) => L.top[0].sethvalue(L, o.clvalue().env), + @intFromEnum(lua.Type.Thread) => L.top[0].sethvalue(L, o.thvalue().gt.?), + else => L.top[0].setnilvalue(), + } + api_incr_top(L); +} + +// +// set functions (stack -> Lua) +// + +/// set table value at `idx` with value:**top** and key:**top-1** +/// * pops **top** x2 +/// * throws lua error if *table* is readonly +/// * throws lua error if key is *nil*/*NaN*/*NaN vector* +/// use `rawset` instead +pub inline fn settable(L: *lua.State, idx: i32) !void { + c.lua_settable(@ptrCast(L), idx); +} + +/// set table value at `idx` with value:**top** and `k` +/// * pops **top** +/// * throws lua error if *table* is readonly +/// use `rawsetfield` instead +pub inline fn setfield(L: *lua.State, idx: i32, k: [:0]const u8) !void { + c.lua_setfield(@ptrCast(L), idx, k.ptr); +} +pub inline fn setglobal(L: *lua.State, k: [:0]const u8) !void { + try setfield(L, lua.GLOBALSINDEX, k); +} + +/// set table value at `idx` with value:**top** and `k` +/// ignoring metamethods. +/// * pops **top** +pub fn rawsetfield(L: *lua.State, idx: i32, k: []const u8) Errorset.Table!void { + if (comptime !build_config.use_zig_backend) { + // small static buffer for field because the 'k' type + // does not require a zero sentinel, but the C api does. + var static: [256:0]u8 = undefined; + if (k.len > static.len) + @panic("key too long, use zig backend"); + @memcpy(static[0..k.len], k); + static[k.len] = 0; + return c.lua_rawsetfield(@ptrCast(L), idx, &static); + } + api_checknelems(L, 1); + const t = index2addr(L, idx); + api_check(L, t.ttistable()); + if (t.hvalue().readonly != 0) + return Errorset.TableReadonly; + (try ltable.Hsetstr(L, t.hvalue(), try lstring.Snew(L, k))).setobj(L, @ptrCast(L.top - 1)); + lgc.Cbarriert(L, t.hvalue(), @ptrCast(L.top - 1)); + L.top -= 1; +} +pub inline fn rawsetglobal(L: *lua.State, k: []const u8) !void { + return rawsetfield(L, lua.GLOBALSINDEX, k); +} + +/// set table value at `idx` with value:**top** and key:**top-1** +/// ignoring metamethods. +/// * pops **top** x2 +/// * returns error "readonly" if *table* is readonly +/// * returns error index if key is *nil*/*NaN*/*NaN vector* +pub fn rawset(L: *lua.State, idx: i32) Errorset.Table!void { + if (comptime !build_config.use_zig_backend) { + return c.lua_rawset(@ptrCast(L), idx); + } + api_checknelems(L, 2); + const t = index2addr(L, idx); + api_check(L, t.ttistable()); + if (t.hvalue().readonly != 0) + return Errorset.TableReadonly; + (try ltable.Hset(L, t.hvalue(), @ptrCast(L.top - 2))).setobj(L, @ptrCast(L.top - 1)); + lgc.Cbarriert(L, t.hvalue(), @ptrCast(L.top - 1)); + L.top -= 2; +} + +/// set table value at `idx` with **top** +/// ignoring metamethods. +/// * pops **top** +/// * throws lua error if *table* is readonly +pub fn rawseti(L: *lua.State, idx: i32, n: i32) Errorset.Table!void { + if (comptime !build_config.use_zig_backend) { + return c.lua_rawseti(@ptrCast(L), idx, n); + } + api_checknelems(L, 2); + const t = index2addr(L, idx); + api_check(L, t.ttistable()); + if (t.hvalue().readonly != 0) + return Errorset.TableReadonly; + (try ltable.Hsetnum(L, t.hvalue(), n)).setobj(L, @ptrCast(L.top - 1)); + lgc.Cbarriert(L, t.hvalue(), @ptrCast(L.top - 1)); + L.top -= 1; +} + +/// set metatable for `idx` with **top** +/// * pops **top** +/// * expects **top** to be *table* or *nil* +/// * always returns `1` +pub fn setmetatable(L: *lua.State, idx: i32) Errorset.Table!u1 { + if (comptime !build_config.use_zig_backend) { + return @intCast(c.lua_setmetatable(@ptrCast(L), idx)); + } + api_checknelems(L, 1); + const obj = index2addr(L, idx); + api_checkvalidindex(L, obj); + var mt: ?*lobject.LuaTable = null; + if (!(L.top - 1)[0].ttisnil()) { + api_check(L, (L.top - 1)[0].ttistable()); + mt = (L.top - 1)[0].hvalue(); + } + switch (obj.ttype()) { + @intFromEnum(lua.Type.Table) => { + if (obj.hvalue().readonly != 0) + return Errorset.TableReadonly; + obj.hvalue().metatable = mt; + if (mt) |m| + lgc.Cobjbarrier(L, @ptrCast(@alignCast(obj.hvalue())), @ptrCast(@alignCast(m))); + }, + @intFromEnum(lua.Type.Userdata) => { + obj.uvalue().metatable = mt; + if (mt) |m| + lgc.Cobjbarrier(L, @ptrCast(@alignCast(obj.uvalue())), @ptrCast(@alignCast(m))); + }, + else => L.global.mt[@intCast(obj.ttype())] = mt, + } + L.top -= 1; + return 1; +} + +pub fn setfenv(L: *lua.State, idx: i32) bool { + if (comptime !build_config.use_zig_backend) { + return c.lua_setfenv(@ptrCast(L), idx) != 0; + } + api_checknelems(L, 1); + const o = index2addr(L, idx); + api_checkvalidindex(L, o); + api_check(L, (L.top - 1)[0].ttistable()); + defer L.top -= 1; + switch (o.tt) { + @intFromEnum(lua.Type.Function) => o.clvalue().env = (L.top - 1)[0].hvalue(), + @intFromEnum(lua.Type.Thread) => o.thvalue().gt = (L.top - 1)[0].hvalue(), + else => return false, + } + lgc.Cobjbarrier(L, @ptrCast(@alignCast(&o.gcvalue().gch)), @ptrCast(@alignCast(L.top - 1))); + return true; +} + +// +// `load' and `call' functions (run Lua code) +// + +pub inline fn call(L: *lua.State, nargs: i32, nresults: i32) void { + c.lua_call(@ptrCast(L), nargs, nresults); +} + +pub fn pcall(L: *lua.State, nargs: i32, nresults: i32, msgh: i32) lua.Status { + return @enumFromInt(c.lua_pcall(@ptrCast(L), nargs, nresults, msgh)); +} + +pub fn cpcall(L: *lua.State, func: lua.CFunction, ud: *anyopaque) lua.Status { + return @enumFromInt(c.lua_cpcall(@ptrCast(L), @ptrCast(func), ud)); +} + +pub fn status(L: *lua.State) lua.Status { + return @enumFromInt(L.curr_status); +} + +pub fn costatus(L: *lua.State, co: *lua.State) lua.CoStatus { + api_check(L, L.global == co.global); + + if (co == L) + return .Running; + switch (co.status()) { + .Yield => return .Suspended, + .Break => return .Normal, + .Ok => {}, + else => return .FinishedErr, // some error occurred + } + if (co.ci != co.base_ci) // does it have frames? + return .Normal; + if (co.top == co.base) // is it empty? + return .Finished; + return .Suspended; // initial state +} + +pub fn getthreaddata(L: *lua.State, comptime T: type) T { + switch (@typeInfo(T)) { + .pointer => {}, + .optional => |opt| { + if (@typeInfo(opt.child) != .pointer) + @compileError("T optional type must be a pointer type"); + }, + else => @compileError("T must be optional or a pointer type"), + } + if (comptime !build_config.use_zig_backend) { + return @ptrCast(@alignCast(c.lua_getthreaddata(@ptrCast(L)))); + } + return @ptrCast(@alignCast(L.userdata)); +} + +pub fn setthreaddata(L: *lua.State, comptime T: type, data: T) void { + switch (@typeInfo(T)) { + .pointer => {}, + .optional => |opt| { + if (@typeInfo(opt.child) != .pointer) + @compileError("T optional type must be a pointer type"); + }, + else => @compileError("T must be optional or a pointer type"), + } + if (comptime !build_config.use_zig_backend) { + return c.lua_setthreaddata(@ptrCast(L), @ptrCast(@alignCast(data))); + } + L.userdata = @ptrCast(data); +} + +// +// Garbage-collection function +// + +pub inline fn gc(L: *lua.State, what: lua.GCOp, data: i32) i32 { + return c.lua_gc(@ptrCast(L), @intFromEnum(what), data); +} + +pub fn @"error"(L: *lua.State) noreturn { + if (comptime !build_config.use_zig_backend) { + return c.lua_error(@ptrCast(L)); + } + api_checknelems(L, 1); + ldo.throw(L, .ErrRun); + unreachable; +} + +pub fn next(L: *lua.State, idx: i32) !bool { + return c.lua_next(@ptrCast(L), idx) != 0; +} + +pub fn rawiter(L: *lua.State, idx: i32, _iter: i32) i32 { + if (comptime !build_config.use_zig_backend) { + return c.lua_rawiter(@ptrCast(L), idx, _iter); + } + std.debug.assert(_iter >= 0); + var iter: usize = @intCast(_iter); + lgc.Cthreadbarrier(L); + const t = index2addr(L, idx); + api_check(L, t.ttistable()); + api_check(L, iter >= 0); + + const h = t.hvalue(); + const sizearray: usize = @intCast(h.sizearray); + + // first we advance iter through the array portion + while (iter < sizearray) : (iter += 1) { + const e = &h.array.?[iter]; + + if (!e.ttisnil()) { + const top = L.top; + top[0].setnvalue(@floatFromInt(iter + 1)); + top[1].setobj(L, e); + api_update_top(L, &top[2]); + return @intCast(iter + 1); + } + } + + const sizenode = lobject.sizenode(h); + + // then we advance iter through the hash portion + while (iter - sizearray < sizenode) : (iter += 1) { + const n = &h.node[iter - sizearray]; + + if (!n.gval().ttisnil()) { + const top = L.top; + lobject.getnodekey(L, @ptrCast(top), n); + top[1].setobj(L, n.gval()); + api_update_top(L, &top[2]); + return @intCast(iter + 1); + } + } + + // traversal finished + return -1; +} + +pub inline fn concat(L: *lua.State, idx: i32) !void { + c.lua_concat(@ptrCast(L), idx); +} + +pub fn newuserdatatagged(L: *lua.State, comptime T: type, tag: u8) Errorset.Table!*T { + if (comptime !build_config.use_zig_backend) { + return @ptrCast(@alignCast(c.lua_newuserdatatagged(@ptrCast(L), @sizeOf(T), @intCast(tag)).?)); + } + api_check(L, tag < lua.config.UTAG_LIMIT or tag == ludata.UTAG_PROXY); + try lgc.CcheckGC(L); + lgc.Cthreadbarrier(L); + const u = try ludata.Unewudata(L, @sizeOf(T), tag); + L.top[0].setuvalue(L, u); + api_incr_top(L); + return @ptrCast(@alignCast(&u.data)); +} +pub inline fn newuserdata(L: *lua.State, comptime T: type) Errorset.Table!*T { + return newuserdatatagged(L, T, 0); +} + +pub fn newuserdatataggedwithmetatable(L: *lua.State, comptime T: type, tag: u8) Errorset.Table!*T { + if (comptime !build_config.use_zig_backend) { + return @ptrCast(@alignCast(c.lua_newuserdatataggedwithmetatable(@ptrCast(L), @sizeOf(T), @intCast(tag)).?)); + } + api_check(L, tag < lua.config.UTAG_LIMIT); + try lgc.CcheckGC(L); + lgc.Cthreadbarrier(L); + const u = try ludata.Unewudata(L, @sizeOf(T), tag); + + // currently, we always allocate unmarked objects, so forward barrier can be skipped + std.debug.assert(!lgc.isblack(u.obj2gco())); + + const h = L.global.udatamt[tag]; + api_check(L, h != null); + + u.metatable = h; + + L.top[0].setuvalue(L, u); + api_incr_top(L); + return @ptrCast(@alignCast(&u.data)); +} + +pub fn newuserdatadtor(L: *lua.State, comptime T: type, comptime dtorFn: *const fn (dtor: *T) void) !*T { + const dtor: *const fn (?*anyopaque) callconv(.c) void = struct { + fn inner(dtor: ?*anyopaque) callconv(.c) void { + @call(.always_inline, dtorFn, .{@as(*T, @ptrCast(@alignCast(dtor.?)))}); + } + }.inner; + const sz = @sizeOf(T); + if (comptime !build_config.use_zig_backend) { + return @ptrCast(@alignCast(c.lua_newuserdatadtor(@ptrCast(L), sz, dtor).?)); + } + + try lgc.CcheckGC(L); + lgc.Cthreadbarrier(L); + // make sure sz + sizeof(dtor) doesn't overflow; luaU_newdata will reject SIZE_MAX correctly + const as = if (sz < std.math.maxInt(usize) - @sizeOf(@TypeOf(dtor))) sz + @sizeOf(@TypeOf(dtor)) else std.math.maxInt(usize); + const u = try ludata.Unewudata(L, as, ludata.UTAG_IDTOR); + @memcpy(@as([*]u8, &u.data)[sz..], &@as([@sizeOf(usize)]u8, @bitCast(@intFromPtr(dtor)))); + L.top[0].setuvalue(L, u); + api_incr_top(L); + return @ptrCast(@alignCast(&u.data)); +} + +pub fn newbuffer(L: *lua.State, sz: usize) Errorset.Table![]u8 { + if (comptime !build_config.use_zig_backend) { + return @as([*]u8, @ptrCast(@alignCast(c.lua_newbuffer(@ptrCast(L), sz).?)))[0..sz]; + } + try lgc.CcheckGC(L); + lgc.Cthreadbarrier(L); + const b = try lbuffer.Bnewbuffer(L, sz); + L.top[0].setbufvalue(L, b); + api_incr_top(L); + return @as([*]u8, @ptrCast(@alignCast(&b.data)))[0..sz]; +} + +fn aux_upvalue(fi: *lobject.TValue, n: u32, val: **lobject.TValue) ?[:0]const u8 { + if (!fi.ttisfunction()) + return null; + const f = fi.clvalue(); + if (f.isC != 0) { + if (!(1 <= n and n <= f.nupvalues)) + return null; + val.* = &f.d.c.upvalues()[n - 1]; + return ""; + } else { + const p = f.d.l.p; + if (!(1 <= n and n <= p.nups)) // not a valid upvalue + return null; + const r = &f.d.l.upreferences()[n - 1]; + val.* = if (r.ttisupval()) r.upvalue().v else r; + if (!(1 <= n and n <= p.sizeupvalues)) // don't have a name for this upvalue + return ""; + return p.upvalues.?[n - 1].?.toSlice(); + } +} + +pub fn getupvalue(L: *lua.State, funcindex: i32, n: u32) ?[:0]const u8 { + if (comptime !build_config.use_zig_backend) { + const name = c.lua_getupvalue(@ptrCast(L), funcindex, @intCast(n)); + if (name != null) + return std.mem.span(name); + return null; + } + lgc.Cthreadbarrier(L); + var val: *lobject.TValue = undefined; + if (aux_upvalue(index2addr(L, funcindex), n, &val)) |name| { + L.top[0].setobj(L, val); + api_incr_top(L); + return name; + } else return null; +} + +pub fn setupvalue(L: *lua.State, funcidx: i32, n: u32) ?[:0]const u8 { + if (comptime !build_config.use_zig_backend) { + const name = c.lua_setupvalue(@ptrCast(L), funcidx, @intCast(n)); + if (name != null) + return std.mem.span(name); + return null; + } + api_checknelems(L, 1); + const fi = index2addr(L, funcidx); + var val: *lobject.TValue = undefined; + if (aux_upvalue(fi, n, &val)) |name| { + L.top -= 1; + val.setobj(L, @ptrCast(L.top)); + lgc.Cbarrier(L, @ptrCast(@alignCast(fi.clvalue())), @ptrCast(L.top)); + return name; + } else return null; +} + +pub fn encodepointer(L: *lua.State, p: usize) usize { + if (comptime !build_config.use_zig_backend) { + return c.lua_encodepointer(@ptrCast(L), p); + } + const g = L.global; + return @intCast(g.ptrenckey[0] * p + g.ptrenckey[2] ^ (g.ptrenckey[1] * p + g.ptrenckey[3])); +} + +pub fn ref(L: *lua.State, idx: i32) Errorset.Table!?i32 { + if (comptime !build_config.use_zig_backend) { + const r = c.lua_ref(@ptrCast(L), idx); + if (r == lua.REFNIL) + return null; + return @intCast(r); + } + api_check(L, idx != lua.REGISTRYINDEX); // idx is a stack index for value + + const g = L.global; + const p = index2addr(L, idx); + if (!p.ttisnil()) { + const reg = L.registry().hvalue(); + var r: i32 = undefined; + if (g.registryfree != 0) { // reuse existing slot + r = g.registryfree; + } else { // no free elements + r = @intCast(ltable.Hgetn(reg)); + r += 1; // create new reference + } + + const slot = try ltable.Hsetnum(L, reg, r); + if (g.registryfree != 0) + g.registryfree = @intFromFloat(slot.nvalue()); + slot.setobj(L, p); + lgc.Cbarriert(L, reg, p); + return r; + } else return null; // no value to reference +} + +pub fn unref(L: *lua.State, r: i32) void { + if (comptime !build_config.use_zig_backend) { + return c.lua_unref(@ptrCast(L), r); + } + if (r <= lua.REFNIL) + return; + + const g = L.global; + const reg = L.registry().hvalue(); + + const slot = ltable.Hgetnum(reg, r); + api_check(L, slot != lobject.Onilobject); + + // similar to how 'luaH_setnum' makes non-nil slot value mutable + const mutableSlot: *lobject.TValue = @constCast(slot); + + // NB: no barrier needed because value isn't collectable + mutableSlot.setnvalue(@floatFromInt(g.registryfree)); + + g.registryfree = r; +} + +pub fn setuserdatatag(L: *lua.State, idx: i32, tag: u8) void { + if (comptime !build_config.use_zig_backend) { + return c.lua_setuserdatatag(@ptrCast(L), idx, @intCast(tag)); + } + api_check(L, tag < lua.config.UTAG_LIMIT); + const o = index2addr(L, idx); + api_check(L, o.ttisuserdata()); + o.uvalue().tag = tag; +} + +pub fn setuserdatadtor(L: *lua.State, comptime T: type, tag: u8, comptime dtorfn: ?*const fn (L: *lua.State, ptr: *T) void) void { + const dtor: ?*const fn (L: *lua.State, ptr: ?*anyopaque) callconv(.c) void = if (dtorfn) |dtor| struct { + fn inner(state: *lua.State, ptr: ?*anyopaque) callconv(.c) void { + @call(.always_inline, dtor, .{ + state, + @as(*T, @ptrCast(@alignCast(ptr.?))), + }); + } + }.inner else null; + if (comptime !build_config.use_zig_backend) { + return c.lua_setuserdatadtor(@ptrCast(L), @intCast(tag), @ptrCast(@alignCast(dtor))); + } + api_check(L, tag < lua.config.UTAG_LIMIT); + L.global.udatagc[tag] = dtor; +} + +pub fn getuserdatadtor(L: *lua.State, tag: u8) ?lua.Destructor { + if (comptime !build_config.use_zig_backend) { + return @ptrCast(@alignCast(c.lua_getuserdatadtor(@ptrCast(L), @intCast(tag)))); + } + api_check(L, tag < lua.config.UTAG_LIMIT); + return L.global.udatagc[tag]; +} + +pub fn setuserdatametatable(L: *lua.State, tag: u8) void { + if (comptime !build_config.use_zig_backend) { + return c.lua_setuserdatametatable(@ptrCast(L), @intCast(tag)); + } + api_checknelems(L, 1); + api_check(L, tag < lua.config.UTAG_LIMIT); + api_check(L, L.global.udatamt[tag] == null); // reassignment not supported + const n = L.top - 1; + api_check(L, n[0].ttistable()); + L.global.udatamt[tag] = n[0].hvalue(); + L.top = n; +} + +pub fn getuserdatametatable(L: *lua.State, tag: u8) void { + if (comptime !build_config.use_zig_backend) { + return c.lua_getuserdatametatable(@ptrCast(L), @intCast(tag)); + } + api_check(L, tag < lua.config.UTAG_LIMIT); + lgc.Cthreadbarrier(L); + + if (L.global.udatamt[tag]) |h| + L.top[0].sethvalue(L, h) + else + L.top[0].setnilvalue(); + + api_incr_top(L); +} + +pub fn setlightuserdataname(L: *lua.State, tag: u8, name: []const u8) Errorset.Memory!void { + if (comptime !build_config.use_zig_backend) { + // small static buffer for name because the 'name' type + // does not require a zero sentinel, but the C api does. + var static: [256:0]u8 = undefined; + if (name.len > static.len) + @panic("name too long, use zig backend"); + @memcpy(static[0..name.len], name); + static[name.len] = 0; + return c.lua_setlightuserdataname(@ptrCast(L), @intCast(tag), &static); + } + api_check(L, tag < lua.config.LUTAG_LIMIT); + api_check(L, L.global.lightuserdataname[tag] == null); // renaming not supported + L.global.lightuserdataname[tag] = try lstring.Snew(L, name); + lstring.Sfix(L.global.lightuserdataname[tag].?); // never collect these names +} + +pub fn getlightuserdataname(L: *lua.State, tag: u8) ?[:0]const u8 { + if (comptime !build_config.use_zig_backend) { + const name = c.lua_getlightuserdataname(@ptrCast(L), @intCast(tag)); + if (name != null) + return std.mem.span(name); + return null; + } + api_check(L, tag < lua.config.LUTAG_LIMIT); + const name = L.global.lightuserdataname[tag]; + return if (name) |s| + s.toSlice() + else + null; +} + +pub fn clonefunction(L: *lua.State, idx: i32) !void { + if (comptime !build_config.use_zig_backend) { + return c.lua_clonefunction(@ptrCast(L), idx); + } + try lgc.CcheckGC(L); + lgc.Cthreadbarrier(L); + const p = index2addr(L, idx); + api_check(L, p.isLfunction()); + const cl = p.clvalue(); + const newcl = try lfunc.FnewLclosure(L, cl.nupvalues, L.gt.?, cl.d.l.p); + for (0..cl.nupvalues) |i| + newcl.d.l.upreferences()[i].setobj(L, &cl.d.l.upreferences()[i]); + L.top[0].setclvalue(L, newcl); + api_incr_top(L); +} + +pub fn cleartable(L: *lua.State, idx: i32) Errorset.Table!void { + if (comptime !build_config.use_zig_backend) { + return c.lua_cleartable(@ptrCast(L), idx); + } + const t = index2addr(L, idx); + api_check(L, t.ttistable()); + const tt = t.hvalue(); + if (tt.readonly != 0) + return Errorset.TableReadonly; + ltable.Hclear(tt); +} + +pub fn clonetable(L: *lua.State, idx: i32) Errorset.Table!void { + if (comptime !build_config.use_zig_backend) { + return c.lua_clonetable(@ptrCast(L), idx); + } + const t = index2addr(L, idx); + api_check(L, t.ttistable()); + + const tt = try ltable.Hclone(L, t.hvalue()); + L.top[0].sethvalue(L, tt); + api_incr_top(L); +} + +pub fn callbacks(L: *lua.State) *lua.Callbacks { + if (comptime !build_config.use_zig_backend) { + return @ptrCast(@alignCast(c.lua_callbacks(@ptrCast(L)))); + } + return &L.global.cb; +} + +pub fn setmemcat(L: *lua.State, category: u8) void { + if (comptime !build_config.use_zig_backend) { + c.lua_setmemcat(@ptrCast(L), @intCast(category)); + return; + } + api_check(L, category < lua.config.MEMORY_CATEGORIES); + L.activememcat = category; +} + +pub fn totalbytes(L: *lua.State, category: u8) usize { + if (comptime !build_config.use_zig_backend) { + return c.lua_totalbytes(@ptrCast(L), @intCast(category)); + } + api_check(L, category < lua.config.MEMORY_CATEGORIES); + return if (category < 0) + L.global.totalbytes + else + L.global.memcatbytes[@intCast(category)]; +} + +pub fn getallocf(L: *lua.State, ud: ?*?*anyopaque) ?lua.Alloc { + if (comptime !build_config.use_zig_backend) { + return @ptrCast(@alignCast(c.lua_getallocf(@ptrCast(L), @ptrCast(ud)))); + } + const f = L.global.frealloc; + if (ud) |ptr| + ptr.* = L.global.ud; + return f; +} diff --git a/deps/luau/src/VM/laux.zig b/deps/luau/src/VM/laux.zig new file mode 100644 index 0000000..1803d2a --- /dev/null +++ b/deps/luau/src/VM/laux.zig @@ -0,0 +1,278 @@ +const c = @import("c"); +const std = @import("std"); + +const lua = @import("lua.zig"); +const ltm = @import("ltm.zig"); +const lapi = @import("lapi.zig"); +const lobject = @import("lobject.zig"); + +const Errorset = @import("errorset.zig"); + +pub const Reg = struct { + name: [:0]const u8, + func: ?lua.CFunction, +}; + +pub fn OptionalValue(comptime T: type, L: *lua.State, check: anytype, narg: i32, d: T) T { + if (L.isnoneornil(narg)) + return d + else + return check(L, narg); +} + +pub fn currfuncname(L: *lua.State) ?[:0]const u8 { + const cl: ?*lobject.Closure = if (@intFromPtr(L.ci) > @intFromPtr(L.base_ci)) + L.curr_func() + else + null; + const debugname: ?[:0]const u8 = if (cl != null and cl.?.isC != 0) + std.mem.span(cl.?.d.c.debugname) + else + null; + + if (debugname != null and std.mem.eql(u8, debugname.?, "__namecall")) { + return if (L.namecall) |namecall| + std.mem.span(namecall.getstr()) + else + null; + } else return debugname; +} + +pub inline fn LargerrorL(L: *lua.State, narg: i32, extramsg: [:0]const u8) Errorset.Table!noreturn { + const fname = currfuncname(L); + + if (fname) |name| + try LerrorL(L, "invalid argument #{d} to '{s}' ({s})", .{ narg, name, extramsg }) + else + try LerrorL(L, "invalid argument #{d} ({s})", .{ narg, extramsg }); +} +pub inline fn Largerror(L: *lua.State, narg: i32, extramsg: [:0]const u8) Errorset.Table!noreturn { + try LargerrorL(L, narg, extramsg); +} +pub inline fn Largcheck(L: *lua.State, cond: bool, narg: i32, extramsg: [:0]const u8) Errorset.Table!noreturn { + if (!cond) try LargerrorL(L, narg, extramsg); +} + +pub inline fn LtypeerrorL(L: *lua.State, narg: i32, tname: [:0]const u8) noreturn { + c.luaL_typeerror(@as(*c.lua_State, @ptrCast(L)), narg, tname); +} + +inline fn tag_error(L: *lua.State, narg: i32, tag: lua.Type) void { + LtypeerrorL(L, narg, lapi.typename(tag)); +} + +pub fn Lwhere(L: *lua.State, level: i32) Errorset.Table!void { + var info: lua.Debug = .{ .ssbuf = undefined }; + if (L.getinfo(level, "sl", &info)) { + if (info.currentline) |line| { + try L.pushfstring("{s}:{d}: ", .{ info.short_src.?, line }); + return; + } + } + try L.rawcheckstack(1); + try L.pushstring(""); +} + +pub fn LerrorL(L: *lua.State, comptime fmt: []const u8, args: anytype) Errorset.Table!noreturn { + try Lwhere(L, 1); + try L.pushvfstring(fmt, args); + try L.concat(2); + L.raiseerror(); +} + +pub inline fn Lcheckoption(L: *lua.State, comptime T: type, narg: i32, def: ?T) T { + const name = blk: { + if (def) |d| + break :blk Loptstring(narg, @tagName(d)) + else + break :blk L.checkstring(narg); + }; + + inline for (std.meta.fields(T)) |field| { + if (std.mem.eql(u8, field.name, name)) + return @enumFromInt(field.value); + } + + var buf: [128]u8 = undefined; + return LargerrorL(L, narg, std.fmt.bufPrintZ(&buf, "invalid option '{s}'", .{name}) catch ""); +} + +/// Returns true if metatable was created, false if it already exists. +pub fn Lnewmetatable(L: *lua.State, tname: [:0]const u8) !bool { + if (try L.getfield(lua.REGISTRYINDEX, tname) != .Nil) // get registry.name, name already in use? + return false; // leave previous value on top, but return false + L.pop(1); + try L.newtable(); // create metatable + L.pushvalue(-1); + try L.setfield(lua.REGISTRYINDEX, tname); // registry.name = metatable + return true; +} + +pub inline fn Lgetmetatable(L: *lua.State, tname: [:0]const u8) !lua.Type { + return L.getfield(lua.REGISTRYINDEX, tname); +} + +pub inline fn Lcheckudata(L: *lua.State, comptime T: type, ud: i32, tname: [:0]const u8) ?*T { + return @ptrCast(c.luaL_checkudata(@ptrCast(L), ud, tname).?); +} + +pub fn Lcheckbuffer(L: *lua.State, idx: i32) []u8 { + if (L.tobuffer(idx)) |b| + return b + else + return tag_error(L, idx, .Buffer); +} + +pub fn Lcheckstack(L: *lua.State, space: usize, msg: ?[]const u8) Errorset.Table!void { + if (!try L.checkstack(space)) + if (msg) |m| + try LerrorL(L, "stack overflow ({s})", .{m}) + else + try LerrorL(L, "stack overflow", .{}); +} + +pub fn Lchecktype(L: *lua.State, narg: i32, t: lua.Type) void { + if (L.typeOf(narg) != t) + tag_error(L, narg, t); +} + +pub fn Lcheckany(L: *lua.State, narg: i32) !void { + if (L.typeOf(narg) == .None) + try LerrorL(L, "missing argument #{d}", .{narg}); +} + +pub fn Lchecklstring(L: *lua.State, narg: i32) []const u8 { + if (L.tolstring(narg)) |s| + return s + else + tag_error(L, narg, .String); +} +pub fn Lcheckstring(L: *lua.State, narg: i32) [:0]const u8 { + if (L.tolstring(narg)) |s| + return s + else + tag_error(L, narg, .String); +} + +pub fn Loptlstring(L: *lua.State, narg: i32, d: []const u8) []const u8 { + return OptionalValue([]const u8, L, Lchecklstring, narg, d); +} +pub fn Loptstring(L: *lua.State, narg: i32, d: [:0]const u8) [:0]const u8 { + return OptionalValue([:0]const u8, L, Lcheckstring, narg, d); +} + +pub fn Lchecknumber(L: *lua.State, narg: i32) f64 { + return L.tonumberx(narg) orelse tag_error(L, narg, .Number); +} + +pub fn Loptnumber(L: *lua.State, narg: i32, d: f64) f64 { + return OptionalValue(f64, L, Lchecknumber, narg, d); +} + +pub fn Lcheckboolean(L: *lua.State, narg: i32) bool { + if (L.isboolean(narg)) + return L.toboolean(narg) + else + tag_error(L, narg, .Boolean); +} + +pub fn Loptboolean(L: *lua.State, narg: i32, d: bool) bool { + return OptionalValue(bool, L, Lcheckboolean, narg, d); +} + +pub fn Lcheckinteger(L: *lua.State, narg: i32) i32 { + return L.tointegerx(narg) orelse tag_error(L, narg, .Number); +} + +pub fn Lcheckinteger64(L: *lua.State, narg: i32) i64 { + return L.tointeger64(narg) orelse tag_error(L, narg, .Number); +} + +pub fn Loptinteger(L: *lua.State, narg: i32, d: i32) i32 { + return OptionalValue(i32, L, Lcheckinteger, narg, d); +} + +pub fn Loptinteger64(L: *lua.State, narg: i32, d: i64) i64 { + return OptionalValue(i64, L, Lcheckinteger64, narg, d); +} + +pub fn Lcheckunsigned(L: *lua.State, narg: i32) u32 { + return L.tounsignedx(narg) orelse tag_error(L, narg, .Number); +} + +pub fn Loptunsigned(L: *lua.State, narg: i32, d: u32) u32 { + return OptionalValue(u32, L, Lcheckunsigned, narg, d); +} + +pub fn Lcheckvector(L: *lua.State, narg: i32) []const f32 { + return L.tovector(narg) orelse tag_error(L, narg, .Vector); +} + +pub fn Loptvector(L: *lua.State, narg: i32, d: []const f32) []const f32 { + return OptionalValue([]const f32, L, Lcheckvector, narg, d); +} + +pub fn Lgetmetafield(L: *lua.State, obj: i32, event: [:0]const u8) Errorset.Table!bool { + if (!L.getmetatable(obj)) // no metatable? + return false; + try L.pushstring(event); + _ = L.rawget(-2); + if (L.isnil(-1)) { + L.pop(2); // remove metatable and metafield + return false; + } + L.remove(-2); // remove only metatable + return true; +} + +/// `unsafe` throws exceptions. Use `Zcallmeta`. +pub fn Lcallmeta(L: *lua.State, obj: i32, event: [:0]const u8) bool { + return c.luaL_callmeta(@ptrCast(L), obj, event) != 0; +} + +pub fn Lregister(L: *lua.State, libname: ?[:0]const u8, funcs: []const Reg) Errorset.Table!void { + if (libname) |name| { + _ = try Lfindtable(L, lua.REGISTRYINDEX, "_LOADED", 1); + _ = try L.getfield(-1, name); + if (!L.istable(-1)) { + L.pop(1); + if (try Lfindtable(L, lua.GLOBALSINDEX, name, funcs.len)) |_| + try LerrorL(L, "name conflict for module '{s}'", .{name}); + L.pushvalue(-1); + try L.setfield(-3, name); + } + L.remove(-2); + L.insert(-1); + } + for (funcs) |f| { + if (f.func) |func| { + try L.pushcfunction(func, f.name); + try L.setfield(-2, f.name); + } + } +} + +pub inline fn Lfindtable(L: *lua.State, idx: i32, fname: [:0]const u8, szhint: usize) !?[]const u8 { + const p = c.luaL_findtable(@ptrCast(L), idx, fname, @truncate(@as(isize, @intCast(szhint)))); + if (p != null) + return std.mem.span(p) + else + return null; +} + +pub inline fn Ltypename(L: *lua.State, idx: i32) [:0]const u8 { + return std.mem.span(c.luaL_typename(@ptrCast(L), idx)); +} + +pub inline fn Lcallyieldable(L: *lua.State, nargs: i32, nresults: i32) i32 { + return c.luaL_callyieldable(@ptrCast(L), nargs, nresults); +} + +/// `unsafe` throws exceptions. Use `Ztolstring`. +pub inline fn Ltolstring(L: *lua.State, idx: i32) [:0]const u8 { + var len: usize = undefined; + if (c.luaL_tolstring(@ptrCast(L), idx, &len)) |str| + return str[0..len :0] + else + unreachable; +} diff --git a/deps/luau/src/VM/lbaselib.zig b/deps/luau/src/VM/lbaselib.zig new file mode 100644 index 0000000..7daab9c --- /dev/null +++ b/deps/luau/src/VM/lbaselib.zig @@ -0,0 +1,7 @@ +const c = @import("c"); + +const lua = @import("lua.zig"); + +pub inline fn open(L: *lua.State) void { + _ = c.luaopen_base(@ptrCast(L)); +} diff --git a/deps/luau/src/VM/lbitlib.zig b/deps/luau/src/VM/lbitlib.zig new file mode 100644 index 0000000..beb67e8 --- /dev/null +++ b/deps/luau/src/VM/lbitlib.zig @@ -0,0 +1,7 @@ +const c = @import("c"); + +const lua = @import("lua.zig"); + +pub inline fn open(L: *lua.State) void { + _ = c.luaopen_bit32(@ptrCast(L)); +} diff --git a/deps/luau/src/VM/lbuffer.zig b/deps/luau/src/VM/lbuffer.zig new file mode 100644 index 0000000..4e10202 --- /dev/null +++ b/deps/luau/src/VM/lbuffer.zig @@ -0,0 +1,33 @@ +const std = @import("std"); + +const lua = @import("lua.zig"); + +const lobject = @import("lobject.zig"); + +const lgc = @import("lgc.zig"); +const lmem = @import("lmem.zig"); + +const Errorset = @import("errorset.zig"); + +// buffer size limit +pub const MAX_BUFFER_SIZE = 1 << 30; + +// GCObject size has to be at least 16 bytes, so a minimum of 8 bytes is always reserved +pub inline fn sizebuffer(len: usize) usize { + return @offsetOf(lobject.Buffer, "data") + (if (len < 8) 8 else len); +} + +pub fn Bnewbuffer(L: *lua.State, s: usize) Errorset.Memory!*lobject.Buffer { + if (s > MAX_BUFFER_SIZE) + return error.BlockTooBig; + + const b = try lmem.Mnewgco(L, lobject.Buffer, sizebuffer(s), L.activememcat); + lgc.Cinit(L, @ptrCast(@alignCast(b)), @intFromEnum(lua.Type.Buffer)); + b.len = @intCast(s); + @memset(@as([*]u8, @ptrCast(@alignCast(&b.data)))[0..s], 0); + return b; +} + +pub fn Bfreebuffer(L: *lua.State, b: *lobject.Buffer, page: *lmem.lua_Page) void { + lmem.Mfreegco(L, b.obj2gco(), sizebuffer(b.len), b.header.memcat, page); +} diff --git a/deps/luau/src/VM/lbuflib.zig b/deps/luau/src/VM/lbuflib.zig new file mode 100644 index 0000000..41b3937 --- /dev/null +++ b/deps/luau/src/VM/lbuflib.zig @@ -0,0 +1,7 @@ +const c = @import("c"); + +const lua = @import("lua.zig"); + +pub inline fn open(L: *lua.State) void { + _ = c.luaopen_buffer(@ptrCast(L)); +} diff --git a/deps/luau/src/VM/lclass.zig b/deps/luau/src/VM/lclass.zig new file mode 100644 index 0000000..4861234 --- /dev/null +++ b/deps/luau/src/VM/lclass.zig @@ -0,0 +1,90 @@ +const std = @import("std"); + +const lua = @import("lua.zig"); + +const lobject = @import("lobject.zig"); + +const lgc = @import("lgc.zig"); +const ltm = @import("ltm.zig"); +const lmem = @import("lmem.zig"); +const ltable = @import("ltable.zig"); +const lstring = @import("lstring.zig"); +const lfunc = @import("lfunc.zig"); + +const Errorset = @import("errorset.zig"); + +pub fn Rnewclass( + L: *lua.State, + name: *lobject.TString, + memberstooffset: *lobject.LuaTable, + offsettomember: [*]*lobject.TString, + numberofinstancemembers: u32, + numberofstaticmembers: u32, +) !*lobject.LuauClass { + std.debug.assert(L.global.GCthreshold == std.math.maxInt(usize)); // GC must be paused + const classobject = try lmem.Mnewgco(L, lobject.LuauClass, @sizeOf(lobject.LuauClass), L.activememcat); + lgc.Cinit(L, @ptrCast(@alignCast(classobject)), @intFromEnum(lua.Type.Class)); + classobject.name = name; + + classobject.staticmembers = try lmem.Mnewarray(L, lobject.TValue, numberofstaticmembers, classobject.header.memcat); + for (0..numberofstaticmembers) |i| + classobject.staticmembers[i].setnilvalue(); + + classobject.memberstooffset = memberstooffset; + classobject.offsettomember = offsettomember; + + classobject.metatable = try ltable.Hnew(L, 0, 1); + + const constructor = try lfunc.FnewCclosure(L, 0, L.gt.?); + constructor.d.c.f = zig_luaR_createobject; + constructor.d.c.debugname = "luaR_createobject"; + constructor.d.c.cont = null; + const dest = try ltable.Hsetstr(L, classobject.metatable, L.global.tmname[@intFromEnum(ltm.TMS.TM_CALL)]); + std.debug.assert(dest.ttisnil()); + dest.setclvalue(L, constructor); + classobject.metatable.readonly = 1; + classobject.instancemetatable = null; + + classobject.numberofinstancemembers = numberofinstancemembers; + classobject.numberofallmembers = numberofinstancemembers + numberofstaticmembers; + + return classobject; +} + +pub fn Raddclassmember(L: *lua.State, classobject: *lobject.LuauClass, name: *lobject.TString, value: *const lobject.TValue) !void { + std.debug.assert(@as(?*anyopaque, @ptrCast(@alignCast(classobject.staticmembers))) != null); + const offset = ltable.Hgetstr(classobject.memberstooffset, name); + const offsetint: u32 = @intFromFloat(offset.nvalue()); + std.debug.assert(offsetint >= classobject.numberofinstancemembers and offsetint < classobject.numberofallmembers); + std.debug.assert(value.ttisfunction() and value.value.gc.?.gch.ttype() == @intFromEnum(lua.Type.Function)); + classobject.staticmembers[offsetint - classobject.numberofinstancemembers].setobj(L, value); + lgc.Cbarrier(L, @ptrCast(@alignCast(classobject)), value); + + var isMetamethod: bool = name == lstring.Sassumelstr(L, "__tostring"); + var i: u32 = 0; + while (!isMetamethod and i < ltm.N) : (i += 1) + isMetamethod = name == L.global.tmname[i]; + + if (isMetamethod) { + if (classobject.instancemetatable == null) { + classobject.instancemetatable = try ltable.Hnew(L, 0, 1); + lgc.Cobjbarrier(L, @ptrCast(@alignCast(classobject)), @ptrCast(@alignCast(classobject.instancemetatable.?))); + } + const dest = try ltable.Hsetstr(L, classobject.instancemetatable.?, name); + dest.setobj(L, value); + lgc.Cbarrier(L, @ptrCast(@alignCast(classobject.instancemetatable.?)), value); + } +} + +extern "c" fn zig_luaR_createobject(L: *lua.State) c_int; + +pub fn Rfreeclass(L: *lua.State, classobject: *lobject.LuauClass, page: *lmem.lua_Page) void { + lmem.Mfreearray(L, lobject.TValue, classobject.staticmembers, classobject.numberofallmembers - classobject.numberofinstancemembers, classobject.header.memcat); + lmem.Mfreearray(L, *lobject.TString, classobject.offsettomember, classobject.numberofallmembers, classobject.header.memcat); + lmem.Mfreegco(L, @ptrCast(@alignCast(classobject)), @sizeOf(lobject.LuauClass), classobject.header.memcat, page); +} + +pub fn Rfreeobject(L: *lua.State, classinstance: *lobject.LuauObject, page: *lmem.lua_Page) void { + lmem.Mfreearray(L, lobject.TValue, classinstance.members, classinstance.numberofmembers, classinstance.header.memcat); + lmem.Mfreegco(L, @ptrCast(@alignCast(classinstance)), @sizeOf(lobject.LuauObject), classinstance.header.memcat, page); +} diff --git a/deps/luau/src/VM/lcommon.zig b/deps/luau/src/VM/lcommon.zig new file mode 100644 index 0000000..76b1e66 --- /dev/null +++ b/deps/luau/src/VM/lcommon.zig @@ -0,0 +1,7 @@ +const config = @import("luaconf.zig"); + +/// +/// type for virtual-machine instructions +/// must be an unsigned with (at least) 4 bytes (see details in lopcodes.h) +/// +pub const Instruction = u32; diff --git a/deps/luau/src/VM/lcorolib.zig b/deps/luau/src/VM/lcorolib.zig new file mode 100644 index 0000000..c8e35aa --- /dev/null +++ b/deps/luau/src/VM/lcorolib.zig @@ -0,0 +1,7 @@ +const c = @import("c"); + +const lua = @import("lua.zig"); + +pub inline fn open(L: *lua.State) void { + _ = c.luaopen_coroutine(@ptrCast(L)); +} diff --git a/deps/luau/src/VM/ldblib.zig b/deps/luau/src/VM/ldblib.zig new file mode 100644 index 0000000..f028c04 --- /dev/null +++ b/deps/luau/src/VM/ldblib.zig @@ -0,0 +1,7 @@ +const c = @import("c"); + +const lua = @import("lua.zig"); + +pub inline fn open(L: *lua.State) void { + _ = c.luaopen_debug(@ptrCast(L)); +} diff --git a/deps/luau/src/VM/ldebug.zig b/deps/luau/src/VM/ldebug.zig new file mode 100644 index 0000000..2c6aaea --- /dev/null +++ b/deps/luau/src/VM/ldebug.zig @@ -0,0 +1,146 @@ +const c = @import("c"); +const std = @import("std"); + +const build_config = @import("config"); + +const lua = @import("lua.zig"); +const ltm = @import("ltm.zig"); +const lstate = @import("lstate.zig"); +const lobject = @import("lobject.zig"); +const Errorset = @import("errorset.zig"); + +pub const MEMERRMSG = "not enough memory"; +pub const ERRERRMSG = "error in error handling"; + +pub fn currentpc(ci: *lstate.CallInfo) usize { + if (ci.savedpc.inst) |pc| { + return (@divExact(@intFromPtr(pc) - @intFromPtr(ci.ci_func().d.l.p.code), @sizeOf(u32))) - 1; + } else return 0; +} + +pub fn currentline(ci: *lstate.CallInfo) i32 { + std.debug.assert(ci.isLua()); + return Ggetline(ci.ci_func().d.l.p, currentpc(ci)); +} + +pub fn getluaproto(ci: *lstate.CallInfo) ?*lobject.Proto { + return if (ci.isLua()) + ci.ci_func().d.l.p + else + null; +} + +pub inline fn getargument(L: *lua.State, level: i32, n: i32) bool { + return c.lua_getargument(@ptrCast(L), level, n) != 0; +} + +pub inline fn getlocal(L: *lua.State, level: i32, n: i32) ?[:0]const u8 { + const name = c.lua_getlocal(@ptrCast(L), level, n); + if (name != null) + return std.mem.span(name); + return null; +} + +pub inline fn setlocal(L: *lua.State, level: i32, n: i32) ?[:0]const u8 { + const name = c.lua_setlocal(@ptrCast(L), level, n); + if (name != null) + return std.mem.span(name); + return null; +} + +pub inline fn stackdepth(L: *lua.State) usize { + return L.ci.? - L.base_ci.?; +} + +pub fn getinfo(L: *lua.State, level: i32, what: [:0]const u8, ar: *lua.Debug) bool { + var info: c.lua_Debug = undefined; + if (c.lua_getinfo(@ptrCast(L), level, what.ptr, &info) == 0) + return false; + ar.fromLua(info, what); + return true; +} + +fn pusherror(L: *lua.State, msg: [:0]const u8) Errorset.Memory!void { + const ci = L.ci.?; + if (ci.isLua()) { + const source = getluaproto(ci).?.source; + // var chunkbuf: [lua.config.IDSIZE]u8 = undefined; + const line = currentline(ci); + if (source) |src| { + try L.pushfstring("{s}:{d}: {s}", .{ std.mem.span(src.getstr()), line, msg }); + } else { + try L.pushfstring(":{d}: {s}", .{ line, msg }); + } + } else { + try L.pushstring(msg); + } +} + +pub fn GrunerrorL(L: *lua.State, comptime fmt: []const u8, args: anytype) Errorset.Table!noreturn { + try L.pushvfstring(fmt, args); + try L.rawcheckstack(1); + L.raiseerror(); +} + +pub fn Ggetline(p: *lobject.Proto, pc: usize) i32 { + std.debug.assert(pc >= 0 and pc < p.sizecode); + + if (p.lineinfo) |lineinfo| { + return p.abslineinfo.?[pc >> @intCast(p.linegaplog2)] + lineinfo[pc]; + } else return 0; +} + +pub fn Gisnative(L: *lua.State, level: usize) bool { + if (level >= L.ci.?[0].sub(@ptrCast(L.base_ci.?))) + return false; + const ci = L.ci.? - level; + return (ci[0].flags & lstate.CALLINFO_NATIVE) != 0; +} + +pub fn singlestep(L: *lua.State, enabled: bool) void { + if (comptime !build_config.use_zig_backend) { + return c.lua_singlestep(@ptrCast(L), if (enabled) 1 else 0); + } + L.singlestep_on = enabled; +} + +pub inline fn breakpoint(L: *lua.State, funcindex: i32, line: i32, enabled: bool) i32 { + return c.lua_breakpoint(@ptrCast(L), funcindex, line, if (enabled) 1 else 0); +} + +pub fn getcoverage( + L: *lua.State, + comptime T: type, + context: *T, + funcindex: i32, + comptime callback: *const fn ( + ctx: *T, + func: ?[:0]const u8, + line: i32, + depth: i32, + hits: []const i32, + ) void, +) void { + c.lua_getcoverage(@ptrCast(L), funcindex, context, struct { + fn inner( + ctx: ?*anyopaque, + func: [*c]const u8, + line: c_int, + depth: c_int, + hits: [*c]const c_int, + size: usize, + ) callconv(.c) void { + @call(.always_inline, callback, .{ + @as(*T, @ptrCast(@alignCast(ctx.?))), + if (func != null) std.mem.span(func) else null, + line, + depth, + hits[0..size], + }); + } + }.inner); +} + +pub inline fn debugtrace(L: *lua.State) [:0]const u8 { + return std.mem.span(c.lua_debugtrace(@ptrCast(L))); +} diff --git a/deps/luau/src/VM/ldo.zig b/deps/luau/src/VM/ldo.zig new file mode 100644 index 0000000..d34001a --- /dev/null +++ b/deps/luau/src/VM/ldo.zig @@ -0,0 +1,138 @@ +const c = @import("c"); +const std = @import("std"); + +const lmem = @import("lmem.zig"); +const lstate = @import("lstate.zig"); + +const lua = @import("lua.zig"); +const ldebug = @import("ldebug.zig"); +const lobject = @import("lobject.zig"); + +const Errorset = @import("errorset.zig"); + +extern "c" fn zig_luau_luaD_throw(L: *lua.State, errcode: i32) noreturn; + +pub const MAX_STACK_SIZE = (1024 / @sizeOf(lobject.TValue)) * 1024 * 1024; + +pub inline fn throw(L: *lua.State, errcode: lua.Status) noreturn { + zig_luau_luaD_throw(L, @intFromEnum(errcode)); +} + +pub inline fn getgrownstacksize(L: *lua.State, n: usize) usize { + return if (n <= L.stacksize) 2 * @as(u32, @intCast(L.stacksize)) else @as(u32, @intCast(L.stacksize)) + n; +} +pub inline fn stacklimitreached(L: *lua.State, n: usize) bool { + return @intFromPtr(L.stack_last) - @intFromPtr(L.top) <= n * @sizeOf(lobject.TValue); +} + +pub inline fn Dcheckstackfornewci(L: *lua.State, n: usize) Errorset.Memory!void { + if (@intFromPtr(L.stack_last) - @intFromPtr(L.top) < n * @sizeOf(lobject.TValue)) + try Dreallocstack(L, getgrownstacksize(L, n), true) + else + try Dreallocstack(L, @as(u32, @intCast(L.stacksize - lstate.EXTRA_STACK)), true); +} + +pub inline fn Dcheckstack(L: *lua.State, n: usize) Errorset.Memory!void { + if (@intFromPtr(L.stack_last) - @intFromPtr(L.top) < n * @sizeOf(lobject.TValue)) + try Dgrowstack(L, n) + else + try Dreallocstack(L, @as(u32, @intCast(L.stacksize - lstate.EXTRA_STACK)), false); +} + +pub inline fn incr_top(L: *lua.State) Errorset.Memory!void { + try Dcheckstack(L, 1); + L.top += 1; +} + +pub inline fn expandstacklimit(L: *lua.State, p: *lobject.TValue) void { + std.debug.assert(@intFromPtr(p) <= @intFromPtr(L.stack_last)); + if (@intFromPtr(L.ci.?[0].top) < @intFromPtr(p)) + L.ci.?[0].top = @ptrCast(p); +} + +fn correctstack(L: *lua.State, oldstack: [*]lobject.TValue) void { + L.top = L.stack + (L.top - oldstack); + var up: ?*lobject.UpVal = L.openupval; + while (up) |uv| : (up = uv.u.open.threadnext) + uv.v = @ptrCast(L.stack + (@as([*]lobject.TValue, @ptrCast(uv.v)) - oldstack)); + var ci = L.base_ci.?; + const top_bound = @intFromPtr(L.ci); + while (@intFromPtr(ci) <= top_bound) : (ci += 1) { + ci[0].top = L.stack + (ci[0].top - oldstack); + ci[0].base = L.stack + (ci[0].base - oldstack); + ci[0].func = L.stack + (ci[0].func - oldstack); + } + L.base = L.stack + (L.base - oldstack); +} + +pub fn Dreallocstack(L: *lua.State, newsize: usize, fornewci: bool) Errorset.Memory!void { + // throw 'out of memory' error because space for a custom error message cannot be guaranteed here + if (newsize > MAX_STACK_SIZE) { + // reallocation was performed to setup a new CallInfo frame, which we have to remove + if (fornewci) { + const cip = L.ci.? - 1; + + L.ci = cip; + L.base = cip[0].base; + L.top = cip[0].top; + } + + return error.OutOfMemory; + } + + const realsize = newsize + lstate.EXTRA_STACK; + if (L.stacksize == realsize) { + // fast path: skip reallocation + return; + } + + const oldstack = L.stack; + std.debug.assert(L.stack_last - L.stack == L.stacksize - lstate.EXTRA_STACK); + L.stack = try lmem.Mreallocarray(L, lobject.TValue, L.stack, @intCast(L.stacksize), realsize, L.header.memcat) orelse unreachable; + const newstack = L.stack; + var i: usize = @intCast(L.stacksize); + while (i < realsize) : (i += 1) + newstack[i].setnilvalue(); + L.stacksize = @intCast(realsize); + L.stack_last = newstack + newsize; + correctstack(L, oldstack); +} + +pub fn DreallocCI(L: *lua.State, newsize: usize) Errorset.Memory!void { + const oldci = L.base_ci; + L.base_ci = try lmem.Mreallocarray(L, lstate.CallInfo, L.base_ci, @intCast(L.size_ci), newsize, L.header.memcat); + L.size_ci = @intCast(newsize); + L.ci = @ptrFromInt((@intFromPtr(L.ci) - @intFromPtr(oldci)) + @intFromPtr(L.base_ci)); + L.end_ci = L.base_ci.? + newsize - 1; +} + +pub fn Dgrowstack(L: *lua.State, n: usize) Errorset.Memory!void { + try Dreallocstack(L, getgrownstacksize(L, n), false); +} + +pub inline fn @"resume"(L: *lua.State, from: ?*lua.State, narg: i32) lua.Status { + return @enumFromInt(c.lua_resume(@ptrCast(L), @ptrCast(from), narg)); +} + +pub inline fn resumeerror(L: *lua.State, from: ?*lua.State) lua.Status { + return @enumFromInt(c.lua_resumeerror(@ptrCast(L), @ptrCast(from))); +} + +pub fn yield(L: *lua.State, nresults: u32) !i32 { + if (L.nCcalls > L.baseCcalls) + try ldebug.GrunerrorL(L, "attempt to yield across metamethod/C-call boundary", .{}); + L.base = L.top - nresults; + L.curr_status = @intFromEnum(lua.Status.Yield); + return -1; +} + +pub fn @"break"(L: *lua.State) !i32 { + if (L.nCcalls > L.baseCcalls) + try ldebug.GrunerrorL(L, "attempt to yield across metamethod/C-call boundary", .{}); + L.curr_status = @intFromEnum(lua.Status.Break); + return -1; +} + +pub fn isyieldable(L: *lua.State) bool { + return L.nCcalls <= L.baseCcalls; +} diff --git a/deps/luau/src/VM/lfunc.zig b/deps/luau/src/VM/lfunc.zig new file mode 100644 index 0000000..dbb157e --- /dev/null +++ b/deps/luau/src/VM/lfunc.zig @@ -0,0 +1,164 @@ +const std = @import("std"); + +const lua = @import("lua.zig"); + +const lgc = @import("lgc.zig"); +const lmem = @import("lmem.zig"); +const lstate = @import("lstate.zig"); +const lcommon = @import("lcommon.zig"); +const lobject = @import("lobject.zig"); + +pub inline fn sizeCclosure(n: u8) usize { + return @offsetOf(lobject.Closure, "d") + @offsetOf(lobject.Closure.ValueUnion.C, "upvals") + (@sizeOf(lobject.TValue) * @as(usize, @intCast(n))); +} +pub inline fn sizeLclosure(n: u8) usize { + return @offsetOf(lobject.Closure, "d") + @offsetOf(lobject.Closure.ValueUnion.L, "uprefs") + (@sizeOf(lobject.TValue) * @as(usize, @intCast(n))); +} +pub inline fn getproto(cl: *lobject.Closure) *lobject.Proto { + return cl.d.l.p; +} + +pub fn Fnewproto(L: *lua.State) !*lobject.Proto { + const f = try lmem.Mnewgco(L, lobject.Proto, @sizeOf(lobject.Proto), L.activememcat); + lgc.Cinit(L, @ptrCast(@alignCast(f)), @intFromEnum(lua.Type.Proto)); + + f.nups = 0; + f.numparams = 0; + f.is_vararg = 0; + f.maxstacksize = 0; + f.flags = 0; + + f.k = null; + f.code = null; + f.p = null; + f.codeentry = null; + + f.execdata = null; + f.exectarget = 0; + + f.lineinfo = null; + f.abslineinfo = null; + f.locvars = null; + f.upvalues = null; + f.source = null; + + f.debugname = null; + f.debuginsn = null; + + f.typeinfo = null; + + f.userdata = null; + + f.gclist = null; + + f.sizecode = 0; + f.sizep = 0; + f.sizelocvars = 0; + f.sizeupvalues = 0; + f.sizek = 0; + f.sizelineinfo = 0; + f.linegaplog2 = 0; + f.linedefined = 0; + f.bytecodeid = 0; + f.sizetypeinfo = 0; + + f.feedbackvec = null; + f.feedbackvecsize = 0; + f.funid = 0; + f.optimized = null; + f.deoptimized = null; + f.cost = 0; + + return f; +} + +pub fn FnewLclosure(L: *lua.State, nelems: u8, e: *lobject.LuaTable, p: *lobject.Proto) !*lobject.Closure { + const c = try lmem.Mnewgco(L, lobject.Closure, sizeCclosure(nelems), L.activememcat); + lgc.Cinit(L, @ptrCast(@alignCast(c)), @intFromEnum(lua.Type.Function)); + c.isC = 0; + c.env = e; + c.nupvalues = nelems; + c.stacksize = p.maxstacksize; + c.preload = 0; + c.d.l.p = p; + for (0..nelems) |i| + c.d.l.upreferences()[i].setnilvalue(); + return c; +} + +pub fn FnewCclosure(L: *lua.State, nelems: u8, e: *lobject.LuaTable) !*lobject.Closure { + const c = try lmem.Mnewgco(L, lobject.Closure, sizeCclosure(nelems), L.activememcat); + lgc.Cinit(L, @ptrCast(@alignCast(c)), @intFromEnum(lua.Type.Function)); + c.isC = 1; + c.env = e; + c.nupvalues = nelems; + c.stacksize = lua.config.MINSTACK; + c.preload = 0; + c.d.c.f = null; + c.d.c.cont = null; + c.d.c.debugname = null; + return c; +} + +pub fn Ffreeupval(L: *lua.State, uv: *lobject.UpVal, page: *lmem.lua_Page) void { + lmem.Mfreegco(L, uv.obj2gco(), @sizeOf(lobject.UpVal), uv.header.memcat, page); // free upvalue +} + +pub fn Fclose(L: *lua.State, level: *lobject.TValue) void { + const g = L.global; + var uv: ?*lobject.UpVal = L.openupval; + const lvl_num = @intFromPtr(level); + while (uv != null and @intFromPtr(uv.?.v) >= lvl_num) : (uv = L.openupval) { + const u = uv.?; + const o: *lstate.GCObject = u.obj2gco(); + std.debug.assert(!lgc.isblack(o) and u.upisopen()); + std.debug.assert(!lgc.isdead(g, o)); + + // unlink value *before* closing it since value storage overlaps + L.openupval = u.u.open.threadnext; + + Fcloseupval(L, u, false); + } +} + +pub fn Fcloseupval(L: *lua.State, uv: *lobject.UpVal, dead: bool) void { + // unlink value from all lists *before* closing it since value storage overlaps + std.debug.assert(uv.u.open.next.?.u.open.prev == uv and uv.u.open.prev.?.u.open.next == uv); + uv.u.open.next.?.u.open.prev = uv.u.open.prev; + uv.u.open.prev.?.u.open.next = uv.u.open.next; + + if (dead) + return; + + uv.u.value.setobj(L, uv.v); + uv.v = &uv.u.value; + lgc.Cupvalclosed(L, uv); +} + +pub fn Ffreeproto(L: *lua.State, f: *lobject.Proto, page: *lmem.lua_Page) void { + lmem.Mfreearray(L, lcommon.Instruction, f.code, @intCast(f.sizecode), f.header.memcat); + lmem.Mfreearray(L, ?*lobject.Proto, f.p, @intCast(f.sizep), f.header.memcat); + lmem.Mfreearray(L, lobject.TValue, f.k, @intCast(f.sizek), f.header.memcat); + if (f.lineinfo) |li| + lmem.Mfreearray(L, u8, li, @intCast(f.sizelineinfo), f.header.memcat); + lmem.Mfreearray(L, lobject.LocVar, f.locvars, @intCast(f.sizelocvars), f.header.memcat); + lmem.Mfreearray(L, ?*lobject.TString, f.upvalues, @intCast(f.sizeupvalues), f.header.memcat); + if (f.debuginsn) |di| + lmem.Mfreearray(L, u8, di, @intCast(f.sizecode), f.header.memcat); + + if (f.execdata) |_| + L.global.ecb.destroy.?(L, @ptrCast(f)); + + if (f.typeinfo) |ti| + lmem.Mfreearray(L, u8, ti, @intCast(f.sizetypeinfo), f.header.memcat); + + if (f.feedbackvec) |fv| + lmem.Mfreearray(L, lobject.FeedbackVectorSlot, fv, f.feedbackvecsize, f.header.memcat); + + lmem.Mfreegco(L, f.obj2gco(), @sizeOf(lobject.Proto), f.header.memcat, page); +} + +pub fn Ffreeclosure(L: *lua.State, c: *lobject.Closure, page: *lmem.lua_Page) void { + const size = if (c.isC != 0) sizeCclosure(c.nupvalues) else sizeLclosure(c.nupvalues); + lmem.Mfreegco(L, c.obj2gco(), size, c.header.memcat, page); +} diff --git a/deps/luau/src/VM/lgc.zig b/deps/luau/src/VM/lgc.zig new file mode 100644 index 0000000..1d6aadd --- /dev/null +++ b/deps/luau/src/VM/lgc.zig @@ -0,0 +1,1129 @@ +const std = @import("std"); + +const build_config = @import("config"); + +const ltm = @import("ltm.zig"); +const lmem = @import("lmem.zig"); +const lperf = @import("lperf.zig"); +const lfunc = @import("lfunc.zig"); +const ltable = @import("ltable.zig"); +const ludata = @import("ludata.zig"); +const lclass = @import("lclass.zig"); +const lstring = @import("lstring.zig"); +const lbuffer = @import("lbuffer.zig"); +const lcommon = @import("lcommon.zig"); +const lgcdebug = @import("lgcdebug.zig"); + +const lua = @import("lua.zig"); +const ldo = @import("ldo.zig"); +const lstate = @import("lstate.zig"); +const lobject = @import("lobject.zig"); + +const Errorset = @import("errorset.zig"); + +// +// Luau uses an incremental non-generational non-moving mark&sweep garbage collector. +// +// The collector runs in three stages: mark, atomic and sweep. Mark and sweep are incremental and try to do a limited amount +// of work every GC step; atomic is ran once per the GC cycle and is indivisible. In either case, the work happens during GC +// steps that are "scheduled" by the GC pacing algorithm - the steps happen either from explicit calls to lua_gc, or after +// the mutator (aka application) allocates some amount of memory, which is known as "GC assist". In either case, GC steps +// can't happen concurrently with other access to VM state. +// +// Current GC stage is stored in global_State::gcstate, and has two additional stages for pause and second-phase mark, explained below. +// +// GC pacer is an algorithm that tries to ensure that GC can always catch up to the application allocating garbage, but do this +// with minimal amount of effort. To configure the pacer Luau provides control over three variables: GC goal, defined as the +// target heap size during atomic phase in relation to live heap size (e.g. 200% goal means the heap's worst case size is double +// the total size of alive objects), step size (how many kilobytes should the application allocate for GC step to trigger), and +// GC multiplier (how much should the GC try to mark relative to how much the application allocated). It's critical that step +// multiplier is significantly above 1, as this is what allows the GC to catch up to the application's allocation rate, and +// GC goal and GC multiplier are linked in subtle ways, described in lua.h comments for LUA_GCSETGOAL. +// +// During mark, GC tries to identify all reachable objects and mark them as reachable, while keeping unreachable objects unmarked. +// During sweep, GC tries to sweep all objects that were not reachable at the end of mark. The atomic phase is needed to ensure +// that all pending marking has completed and all objects that are still marked as unreachable are, in fact, unreachable. +// +// Notably, during mark GC doesn't free any objects, and so the heap size constantly grows; during sweep, GC doesn't do any marking +// work, so it can't immediately free objects that became unreachable after sweeping started. +// +// Every collectable object has one of three colors at any given point in time: white, gray or black. This coloring scheme +// is necessary to implement incremental marking: white objects have not been marked and may be unreachable, black objects +// have been marked and will not be marked again if they stay black, and gray objects have been marked but may contain unmarked +// references. +// +// Objects are allocated as white; however, during sweep, we need to differentiate between objects that remained white in the mark +// phase (these are not reachable and can be freed) and objects that were allocated after the mark phase ended. Because of this, the +// colors are encoded using three bits inside GCheader::marked: white0, white1 and black (so technically we use a four-color scheme: +// any object can be white0, white1, gray or black). All bits are exclusive, and gray objects have all three bits unset. This allows +// us to have the "current" white bit, which is flipped during atomic stage - during sweeping, objects that have the white color from +// the previous mark may be deleted, and all other objects may or may not be reachable, and will be changed to the current white color, +// so that the next mark can start coloring objects from scratch again. +// +// Crucially, the coloring scheme comes with what's known as a tri-color invariant: a black object may never point to a white object. +// +// At the end of atomic stage, the expectation is that there are no gray objects anymore, which means all objects are either black +// (reachable) or white (unreachable = dead). Tri-color invariant is maintained throughout mark and atomic phase. To uphold this +// invariant, every modification of an object needs to check if the object is black and the new referent is white; if so, we +// need to either mark the referent, making it non-white (known as a forward barrier), or mark the object as gray and queue it +// for additional marking (known as a backward barrier). +// +// Luau uses both types of barriers. Forward barriers advance GC progress, since they don't create new outstanding work for GC, +// but they may be expensive when an object is modified many times in succession. Backward barriers are cheaper, as they defer +// most of the work until "later", but they require queueing the object for a rescan which isn't always possible. Table writes usually +// use backward barriers (but switch to forward barriers during second-phase mark), whereas upvalue writes and setmetatable use forward +// barriers. +// +// Since marking is incremental, it needs a way to track progress, which is implemented as a gray set: at any point, objects that +// are gray need to mark their white references, objects that are black have no pending work, and objects that are white have not yet +// been reached. Once the gray set is empty, the work completes; as such, incremental marking is as simple as removing an object from +// the gray set, and turning it to black (which requires turning all its white references to gray). The gray set is implemented as +// an intrusive singly linked list, using `gclist` field in multiple objects (functions, tables, threads and protos). When an object +// doesn't have gclist field, the marking of that object needs to be "immediate", changing the colors of all references in one go. +// +// When a black object is modified, it needs to become gray again. Objects like this are placed on a separate `grayagain` list by a +// barrier - this is important because it allows us to have a mark stage that terminates when the gray set is empty even if the mutator +// is constantly changing existing objects to gray. After mark stage finishes traversing `gray` list, we copy `grayagain` list to `gray` +// once and incrementally mark it again. During this phase of marking, we may get more objects marked as `grayagain`, so after we finish +// emptying out the `gray` list the second time, we finish the mark stage and do final marking of `grayagain` during atomic phase. +// GC works correctly without this second-phase mark (called GCSpropagateagain), but it reduces the time spent during atomic phase. +// +// Sweeping is also incremental, but instead of working at a granularity of an object, it works at a granularity of a page: all GC +// objects are allocated in special pages (see lmem.cpp for details), and sweeper traverses all objects in one page in one incremental +// step, freeing objects that aren't reachable (old white), and recoloring all other objects with the new white to prepare them for next +// mark. During sweeping we don't need to maintain the GC invariant, because our goal is to paint all objects with current white - +// however, some barriers will still trigger (because some reachable objects are still black as sweeping didn't get to them yet), and +// some barriers will proactively mark black objects as white to avoid extra barriers from triggering excessively. +// +// Most references that GC deals with are strong, and as such they fit neatly into the incremental marking scheme. Some, however, are +// weak - notably, tables can be marked as having weak keys/values (using __mode metafield). During incremental marking, we don't know +// for certain if a given object is alive - if it's marked as black, it definitely was reachable during marking, but if it's marked as +// white, we don't know if it's actually unreachable. Because of this, we need to defer weak table handling to the atomic phase; after +// all objects are marked, we traverse all weak tables (that are linked into special weak table lists using `gclist` during marking), +// and remove all entries that have white keys or values. If keys or values are strong, they are marked normally. +// +// The simplified scheme described above isn't fully accurate because of threads, upvalues and strings. +// +// Strings are semantically black (they are initially white, and when the mark stage reaches a string, it changes its color and never +// touches the object again), but they are technically marked as gray - the black bit is never set on a string object. This behavior +// is inherited from Lua 5.1 GC, but doesn't have a clear rationale - effectively, strings are marked as gray but are never part of +// a gray list. +// +// Threads are hard to deal with because for them to fit into the white-gray-black scheme, writes to thread stacks need to have barriers +// that turn the thread from black (already scanned) to gray - but this is very expensive because stack writes are very common. To +// get around this problem, threads have an "active" state which means that a thread is actively executing code. When GC reaches an active +// thread, it keeps it as gray, and rescans it during atomic phase. When a thread is inactive, GC instead paints the thread black. All +// API calls that can write to thread stacks outside of execution (which implies active) uses a thread barrier that checks if the thread is +// black, and if it is it marks it as gray and puts it on a gray list to be rescanned during atomic phase. +// +// Upvalues are special objects that can be closed, in which case they contain the value (acting as a reference cell) and can be dealt +// with using the regular algorithm, or open, in which case they refer to a stack slot in some other thread. These are difficult to deal +// with because the stack writes are not monitored. Because of this open upvalues are treated in a somewhat special way: they are never marked +// as black (doing so would violate the GC invariant), and they are kept in a special global list (global_State::uvhead) which is traversed +// during atomic phase. This is needed because an open upvalue might point to a stack location in a dead thread that never marked the stack +// slot - upvalues like this are identified since they don't have `markedopen` bit set during thread traversal and closed in `clearupvals`. +// + +// +// Default settings for GC tunables (settable via lua_gc) +// +pub const I_GCGOAL = 200; // 200% (allow heap to double compared to live heap size) +pub const I_GCSTEPMUL = 200; // GC runs 'twice the speed' of memory allocation +pub const I_GCSTEPSIZE = 1; // GC runs every KB of memory allocation + +// +// Possible states of the Garbage Collector +// +pub const GCSpause = 0; +pub const GCSpropagate = 1; +pub const GCSpropagateagain = 2; +pub const GCSatomic = 3; +pub const GCSsweep = 4; + +pub inline fn keepinvariant(g: *const lstate.global_State) bool { + return g.gcstate == GCSpropagate or g.gcstate == GCSpropagateagain or g.gcstate == GCSatomic; +} + +pub inline fn testbits(x: u8, m: u8) u8 { + return x & m; +} +pub inline fn bitmask(b: u8) u8 { + return 1 << b; +} +pub inline fn bit2mask(b1: u8, b2: u8) u8 { + return (1 << b1) | (1 << b2); +} +pub inline fn testbit(x: u8, b: u8) u8 { + return testbits(x, bitmask(b)); +} +pub inline fn test2bits(x: u8, b1: u8, b2: u8) u8 { + return testbits(x, bit2mask(b1, b2)); +} + +/// +/// Layout for bit use in `marked' field: +/// bit 0 - object is white (type 0) +/// bit 1 - object is white (type 1) +/// bit 2 - object is black +/// bit 3 - object is fixed (should not be collected) +/// +pub const WHITE0BIT = 0; +pub const WHITE1BIT = 1; +pub const BLACKBIT = 2; +pub const FIXEDBIT = 3; +pub const WHITEBITS = bit2mask(WHITE0BIT, WHITE1BIT); + +pub inline fn iswhite(x: *lstate.GCObject) bool { + return test2bits(x.gch.header.marked, WHITE0BIT, WHITE1BIT) != 0; +} +pub inline fn isblack(x: *lstate.GCObject) bool { + return testbit(x.gch.header.marked, BLACKBIT) != 0; +} +pub inline fn isgray(x: *lstate.GCObject) bool { + return testbits(x.gch.header.marked, WHITEBITS | bitmask(BLACKBIT)) == 0; +} +pub inline fn isfixed(x: *lstate.GCObject) bool { + return testbit(x.gch.header.marked, FIXEDBIT) != 0; +} + +pub inline fn otherwhite(g: *const lstate.global_State) u8 { + return g.currentwhite ^ WHITEBITS; +} +pub inline fn isdead(g: *const lstate.global_State, v: *const lstate.GCObject) bool { + return (v.gch.header.marked & (WHITEBITS | bitmask(FIXEDBIT))) == (otherwhite(g) & WHITEBITS); +} + +pub inline fn changewhite(x: *lstate.GCObject) void { + x.gch.header.marked ^= WHITEBITS; +} +pub inline fn gray2black(x: *lstate.GCObject) void { + x.gch.header.marked |= bitmask(BLACKBIT); +} + +pub const GC_SWEEPPAGESTEPCOST = 16; + +pub inline fn GC_INTERRUPT(g: *lstate.global_State, L: *lua.State, state: c_int) void { + if (g.cb.interrupt) |interrupt| { + @branchHint(.unlikely); + interrupt(L, state); + } +} + +pub const maskmarks: u8 = ~(bitmask(BLACKBIT) | WHITEBITS); + +pub inline fn makewhite(g: *lstate.global_State, x: *lstate.GCObject) void { + x.gch.header.marked = (x.gch.header.marked & maskmarks) | Cwhite(g); +} + +pub inline fn white2gray(x: *lstate.GCObject) void { + x.gch.header.marked &= ~(bitmask(WHITE0BIT) | bitmask(WHITE1BIT)); +} +pub inline fn black2gray(x: *lstate.GCObject) void { + x.gch.header.marked &= ~(bitmask(BLACKBIT)); +} + +pub inline fn stringmark(s: *lobject.TString) void { + s.header.marked &= ~(bitmask(WHITE0BIT) | bitmask(WHITE1BIT)); +} + +pub inline fn markvalue(g: *lstate.global_State, o: anytype) void { + std.debug.assert(!o.iscollectable() or o.ttype() == o.value.gc.?.gch.header.tt); + if (o.iscollectable() and iswhite(o.gcvalue())) + reallymarkobject(g, o.gcvalue()); +} + +pub inline fn markobject(g: *lstate.global_State, t: *lstate.GCObject) void { + if (iswhite(t)) + reallymarkobject(g, t); +} + +pub inline fn Cwhite(g: *const lstate.global_State) u8 { + return g.currentwhite & WHITEBITS; +} + +pub inline fn CneedsGC(L: *const lua.State) bool { + return L.global.totalbytes >= L.global.GCthreshold; +} + +pub inline fn CcheckGC(L: *lua.State) Errorset.Table!void { + if (comptime build_config.hard_stack_tests) + try ldo.Dreallocstack(L, @intCast(L.stacksize - lstate.EXTRA_STACK), false); + if (CneedsGC(L)) { + if (comptime build_config.hard_mem_tests >= 1) + lgcdebug.Cvalidate(L); + _ = try Cstep(L, true); + } else { + if (comptime build_config.hard_mem_tests >= 2) + lgcdebug.Cvalidate(L); + } +} + +pub inline fn Cbarrier(L: *lua.State, p: *lstate.GCObject, v: *const lobject.TValue) void { + if (v.iscollectable() and isblack(p) and iswhite(v.gcvalue())) { + Cbarrierf(L, p, v.gcvalue()); + } +} + +pub inline fn Cbarriert(L: *lua.State, t: *lobject.LuaTable, v: *const lobject.TValue) void { + if (v.iscollectable() and isblack(t.obj2gco()) and iswhite(v.gcvalue())) { + Cbarriertable(L, t, v.gcvalue()); + } +} + +pub inline fn Cbarrierfast(L: *lua.State, t: *lstate.GCObject) void { + if (isblack(t)) + Cbarrierback(L, t, &L.gclist); +} + +pub inline fn Cobjbarrier(L: *lua.State, p: *lstate.GCObject, o: *lstate.GCObject) void { + if (isblack(p) and iswhite(o)) + Cbarrierf(L, p, o); +} + +pub inline fn Cthreadbarrier(L: *lua.State) void { + if (isblack(@ptrCast(@alignCast(L)))) { + Cbarrierback(L, @ptrCast(@alignCast(L)), &L.gclist); + } +} + +pub inline fn Cobjectbarrier(L: *lua.State) void { + if (isblack(@ptrCast(@alignCast(L)))) { + Cbarrierback(L, @ptrCast(@alignCast(L)), &L.gclist); + } +} + +pub inline fn Cinit(L: *lua.State, o: *lstate.GCObject, tt: u8) void { + o.gch.header.marked = Cwhite(L.global); + o.gch.header.tt = tt; + o.gch.header.memcat = L.activememcat; +} + +fn removeentry(n: *lobject.LuaNode) void { + std.debug.assert(n.gval().ttisnil()); + if (n.gkey().iscollectable()) + n.gkey().setttype(.Deadkey); // dead key; remove it +} + +fn reallymarkobject(g: *lstate.global_State, o: *lstate.GCObject) void { + std.debug.assert(iswhite(o) and !isdead(g, o)); + white2gray(o); + switch (o.gch.ttype()) { + @intFromEnum(lua.Type.String) => return, + @intFromEnum(lua.Type.Userdata) => { + const mt = o.tou().metatable; + gray2black(o); // udata are never gray + if (mt) |t| + markobject(g, @ptrCast(@alignCast(t))); + }, + @intFromEnum(lua.Type.UpVal) => { + const uv = o.touv(); + markvalue(g, uv.v); + if (!uv.upisopen()) // closed? + gray2black(o); // open upvalues are never black + return; + }, + @intFromEnum(lua.Type.Function) => { + o.tocl().gclist = g.gray; + g.gray = o; + return; + }, + @intFromEnum(lua.Type.Table) => { + o.toh().gclist = g.gray; + g.gray = o; + return; + }, + @intFromEnum(lua.Type.Thread) => { + o.toth().gclist = g.gray; + g.gray = o; + return; + }, + @intFromEnum(lua.Type.Buffer) => { + gray2black(o); // buffers are never gray + return; + }, + @intFromEnum(lua.Type.Proto) => { + o.top().gclist = g.gray; + g.gray = o; + return; + }, + else => unreachable, + } +} + +fn gettablemode(g: *lstate.global_State, h: *lobject.LuaTable) ?[:0]const u8 { + const mode = ltm.gfasttm(g, h.metatable, .TM_MODE); + if (mode) |m| + if (m.ttisstring()) + return m.tsvalue().toSlice(); + return null; // no weak mode +} + +fn traversetable(g: *lstate.global_State, h: *lobject.LuaTable) bool { + var i: usize = 0; + var weakkey: bool = false; + var weakvalue: bool = false; + if (h.metatable) |mt| + markobject(g, @ptrCast(@alignCast(mt))); + + // is there a weak mode? + if (gettablemode(g, h)) |modev| { + weakkey = (std.mem.indexOfScalar(u8, modev, 'k') != null); + weakvalue = (std.mem.indexOfScalar(u8, modev, 'v') != null); + if (weakkey or weakvalue) { // is really weak? + h.gclist = g.weak; // must be cleared after GC, ... + g.weak = h.obj2gco(); // ... so put in the appropriate list + } + } + + if (weakkey and weakvalue) + return true; + if (!weakvalue) { + i = @intCast(h.sizearray); + while (i > 0) : (i -= 1) + markvalue(g, &h.array.?[i - 1]); + } + i = lobject.sizenode(h); + while (i > 0) : (i -= 1) { + const n: *lobject.LuaNode = @ptrCast(h.gnode(i - 1)); + std.debug.assert(n.gkey().ttype() != @intFromEnum(lua.Type.Deadkey) or n.gval().ttisnil()); + if (n.gval().ttisnil()) + removeentry(@ptrCast(n)) // remove empty entries + else { + std.debug.assert(!n.gkey().ttisnil()); + if (!weakkey) + markvalue(g, n.gkey()); + if (!weakvalue) + markvalue(g, n.gval()); + } + } + return weakkey or weakvalue; +} + +/// All marks are conditional because a GC may happen while the +/// prototype is still being created +fn traverseproto(g: *lstate.global_State, f: *lobject.Proto) void { + if (f.source) |s| + stringmark(s); + if (f.debugname) |d| + stringmark(d); + for (0..@intCast(f.sizek)) |i| // mark literals + markvalue(g, &f.k.?[i]); + for (0..@intCast(f.sizeupvalues)) |i| { // mark upvalue names + if (f.upvalues.?[i]) |n| + stringmark(n); + } + for (0..@intCast(f.sizep)) |i| { // mark nested protos + if (f.p.?[i]) |proto| + markobject(g, @ptrCast(@alignCast(proto))); + } + for (0..@intCast(f.sizelocvars)) |i| { // mark local-variable names + if (f.locvars.?[i].varname) |varname| + stringmark(varname); + } +} + +fn traverseclosure(g: *lstate.global_State, cl: *lobject.Closure) void { + markobject(g, @ptrCast(@alignCast(cl.env))); + if (cl.isC != 0) { + for (cl.d.c.upvalues()[0..cl.nupvalues]) |*upval| // mark its upvalues + markvalue(g, upval); + } else { + std.debug.assert(cl.nupvalues == cl.d.l.p.nups); + markobject(g, @ptrCast(@alignCast(cl.d.l.p))); + for (cl.d.l.upreferences()[0..cl.nupvalues]) |*upref| // mark its upvalues + markvalue(g, upref); + } +} + +fn traversestack(g: *lstate.global_State, L: *lua.State) void { + markobject(g, @ptrCast(@alignCast(L.gt))); + if (L.namecall) |nc| + stringmark(nc); + for (L.stack[0..(L.top - L.stack)]) |*o| + markvalue(g, o); + var uv: ?*lobject.UpVal = L.openupval; + while (uv) |u| : (uv = u.u.open.threadnext) { + std.debug.assert(u.upisopen()); + u.markedopen = 1; + markobject(g, @ptrCast(@alignCast(u))); + } +} + +fn traverseclass(g: *lstate.global_State, classobject: *lobject.LuauClass) void { + markobject(g, @ptrCast(@alignCast(classobject.name))); + markobject(g, @ptrCast(@alignCast(classobject.memberstooffset))); + for (0..classobject.numberofallmembers) |i| + markobject(g, @ptrCast(@alignCast(classobject.offsettomember[i]))); + for (0..classobject.numberofallmembers - classobject.numberofinstancemembers) |i| + markobject(g, @ptrCast(@alignCast(&classobject.staticmembers[i]))); + markobject(g, @ptrCast(@alignCast(classobject.metatable))); + if (classobject.instancemetatable) |mt| + markobject(g, @ptrCast(@alignCast(mt))); +} + +fn traverseobject(g: *lstate.global_State, classinst: *lobject.LuauObject) void { + markobject(g, @ptrCast(@alignCast(classinst.lclass))); + for (0..classinst.numberofmembers) |i| + markobject(g, @ptrCast(@alignCast(&classinst.members[i]))); +} + +fn clearstack(L: *lua.State) void { + const stack_end = L.stack + @as(usize, @intCast(L.stacksize)); + for (L.top[0..(stack_end - L.top)]) |*o| // clear not-marked stack slice + o.setnilvalue(); +} + +fn shrinkstack(L: *lua.State) Errorset.Memory!void { + // compute used stack - note that we can't use th->top if we're in the middle of vararg call + var lim = L.top; + for (L.base_ci.?[0 .. (L.ci.? - L.base_ci.?) + 1]) |*ci| { // iterate through callinfo + std.debug.assert(@intFromPtr(ci.top) <= @intFromPtr(L.stack_last)); + if (@intFromPtr(lim) < @intFromPtr(ci.top)) + lim = ci.top; + } + + // shrink stack and callinfo arrays if we aren't using most of the space + const ci_used = L.ci.? - L.base_ci.?; // number of `ci' in use + const s_used = lim - L.stack; // part of stack in use + if (L.size_ci > lua.config.I_MAXCALLS) // handling overflow? + return; // do not touch the stacks + + if (3 * ci_used < L.size_ci and 2 * lstate.BASIC_CI_SIZE < L.size_ci) + try ldo.DreallocCI(L, @divTrunc(@as(usize, @intCast(L.size_ci)), 2)); // still big enough... + if (comptime build_config.hard_stack_tests) + try ldo.DreallocCI(L, ci_used + 1); + + if (3 * s_used < L.stacksize and 2 * (lstate.BASIC_STACK_SIZE + lstate.EXTRA_STACK) < L.stacksize) + try ldo.Dreallocstack(L, @divTrunc(@as(usize, @intCast(L.stacksize)), 2), false); // still big enough... + if (comptime build_config.hard_stack_tests) + try ldo.Dreallocstack(L, s_used, false); +} + +fn propagatemark(g: *lstate.global_State) Errorset.Memory!usize { + const o = g.gray.?; + std.debug.assert(isgray(o)); + gray2black(o); + switch (o.gch.ttype()) { + @intFromEnum(lua.Type.Table) => { + const h = o.toh(); + g.gray = h.gclist; + if (traversetable(g, h)) // table is weak? + black2gray(o); // keep it gray + return @sizeOf(lobject.LuaTable) + @sizeOf(lobject.TValue) * @as(u32, @intCast(h.sizearray)) + @sizeOf(lobject.LuaNode) * lobject.sizenode(h); + }, + @intFromEnum(lua.Type.Function) => { + const cl = o.tocl(); + g.gray = cl.gclist; + traverseclosure(g, cl); + return if (cl.isC != 0) lfunc.sizeCclosure(cl.nupvalues) else lfunc.sizeLclosure(cl.nupvalues); + }, + @intFromEnum(lua.Type.Thread) => { + const th = o.toth(); + g.gray = th.gclist; + const active = th.isactive or th == th.global.mainthread; + + traversestack(g, th); + + // active threads will need to be rescanned later to mark new stack writes so we mark them gray again + if (active) { + th.gclist = g.grayagain; + g.grayagain = o; + + black2gray(o); + } + + // the stack needs to be cleared after the last modification of the thread state before sweep begins + // if the thread is inactive, we might not see the thread in this cycle so we must clear it now + if (!active or g.gcstate == GCSatomic) + clearstack(th); + + // we could shrink stack at any time but we opt to do it during initial mark to do that just once per cycle + if (g.gcstate == GCSpropagate) + try shrinkstack(th); + + return @sizeOf(lua.State) + + (@sizeOf(lobject.TValue) * @as(u32, @intCast(th.stacksize))) + + (@sizeOf(lstate.CallInfo) * @as(u32, @intCast(th.size_ci))); + }, + @intFromEnum(lua.Type.Proto) => { + const p = o.top(); + g.gray = p.gclist; + traverseproto(g, p); + + return @sizeOf(lobject.Proto) + (@sizeOf(lcommon.Instruction) * @as(u32, @intCast(p.sizecode))) + + (@sizeOf(*lobject.Proto) * @as(u32, @intCast(p.sizep))) + + (@sizeOf(lobject.TValue) * @as(u32, @intCast(p.sizek))) + + @as(u32, @intCast(p.sizelineinfo)) + + (@sizeOf(lobject.LocVar) * @as(u32, @intCast(p.sizelocvars))) + + (@sizeOf(lobject.UpVal) * @as(u32, @intCast(p.sizeupvalues))) + + @as(u32, @intCast(p.sizetypeinfo)); + }, + @intFromEnum(lua.Type.Class) => { + const classobject = o.toclass(); + g.gray = classobject.gclist; + traverseclass(g, classobject); + return @sizeOf(lobject.LuauClass) + + ((classobject.numberofallmembers - classobject.numberofinstancemembers) * @sizeOf(lobject.TValue)) + + (classobject.numberofallmembers * @sizeOf(*lobject.TString)); + }, + @intFromEnum(lua.Type.Object) => { + const classinst = o.toobject(); + g.gray = classinst.gclist; + traverseobject(g, classinst); + return @sizeOf(lobject.LuauObject) + + (classinst.numberofmembers * @sizeOf(lobject.TValue)); + }, + else => unreachable, + } + return 0; +} + +fn propagateall(g: *lstate.global_State) Errorset.Memory!usize { + var work: usize = 0; + while (g.gray != null) + work += try propagatemark(g); + return work; +} + +/// +/// The next function tells whether a key or value can be cleared from +/// a weak table. Non-collectable objects are never removed from weak +/// tables. Strings behave as `values', so are never removed too. for +/// other objects: if really collected, cannot keep them. +/// +fn isobjcleared(o: *lstate.GCObject) bool { + if (o.gch.ttype() == @intFromEnum(lua.Type.String)) { + stringmark(&o.ts); // strings are `values', so are never weak + return false; + } + return iswhite(o); +} + +pub inline fn iscleared(o: anytype) bool { + return o.iscollectable() and isobjcleared(o.gcvalue()); +} + +/// clear collected entries from weaktables +fn cleartable(L: *lua.State, il: ?*lstate.GCObject) Errorset.Table!usize { + var work: usize = 0; + var ol: ?*lstate.GCObject = il; + while (ol) |l| { + const h = l.toh(); + work += @sizeOf(lobject.LuaTable) + (@sizeOf(lobject.TValue) * @as(u32, @intCast(h.sizearray))) + (@sizeOf(lobject.LuaNode) * lobject.sizenode(h)); + + var i: usize = @intCast(h.sizearray); + while (i > 0) : (i -= 1) { + const o = &h.array.?[i - 1]; + if (iscleared(o)) // value was collected? + o.setnilvalue(); // remove value + } + i = lobject.sizenode(h); + var activevalues: usize = 0; + while (i > 0) : (i -= 1) { + const n: *lobject.LuaNode = @ptrCast(h.gnode(i - 1)); + + // non-empty entry? + if (!n.gval().ttisnil()) { + // can we clear key or value? + if (iscleared(n.gkey()) or iscleared(n.gval())) { + n.gval().setnilvalue(); // remove value ... + removeentry(n); // remove entry from table + } else { + activevalues += 1; + } + } + } + + if (gettablemode(L.global, h)) |mode| { + // are we allowed to shrink this weak table? + if (std.mem.indexOfScalar(u8, mode, 's') != null) { + // shrink at 37.5% occupancy + if (activevalues < @divTrunc(lobject.sizenode(h) * 3, 8)) + try ltable.Hresizehash(L, h, activevalues); + } + } + + ol = h.gclist; + } + return work; +} + +fn freeobj(L: *lua.State, o: *lstate.GCObject, page: *lmem.lua_Page) void { + switch (o.gch.header.tt) { + @intFromEnum(lua.Type.Proto) => lfunc.Ffreeproto(L, o.top(), page), + @intFromEnum(lua.Type.Function) => lfunc.Ffreeclosure(L, o.tocl(), page), + @intFromEnum(lua.Type.UpVal) => lfunc.Ffreeupval(L, o.touv(), page), + @intFromEnum(lua.Type.Table) => ltable.Hfree(L, o.toh(), page), + @intFromEnum(lua.Type.Thread) => { + std.debug.assert(o.toth() != L and o.toth() != L.global.mainthread); + lstate.Efreethread(L, o.toth(), page); + }, + @intFromEnum(lua.Type.String) => lstring.Sfree(L, o.tots(), page), + @intFromEnum(lua.Type.Userdata) => ludata.Ufreeudata(L, o.tou(), page), + @intFromEnum(lua.Type.Buffer) => lbuffer.Bfreebuffer(L, o.tobuf(), page), + @intFromEnum(lua.Type.Class) => lclass.Rfreeclass(L, o.toclass(), page), + @intFromEnum(lua.Type.Object) => lclass.Rfreeobject(L, o.toobject(), page), + else => unreachable, + } +} + +fn shrinkbuffers(L: *lua.State) Errorset.Memory!void { + const g = L.global; + // check size of string hash + if (g.strt.nuse < @divTrunc(g.strt.size, 4) and g.strt.size > lua.config.MINSTRTABSIZE * 2) + try lstring.Sresize(L, @intCast(@divTrunc(g.strt.size, 2))); // table is too big +} + +fn shrinkbuffersfull(L: *lua.State) Errorset.Memory!void { + const g = L.global; + // check size of string hash + var hashsize = g.strt.size; + while (g.strt.nuse < @divTrunc(hashsize, 4) and hashsize > lua.config.MINSTRTABSIZE * 2) + hashsize = @divTrunc(hashsize, 2); + if (hashsize != g.strt.size) + try lstring.Sresize(L, hashsize); // table is too big +} + +fn deletegco(L: *lua.State, page: *lmem.lua_Page, gco: *lstate.GCObject) bool { + freeobj(L, gco, page); + return true; +} + +pub fn Cfreeall(L: *lua.State) void { + const g = L.global; + std.debug.assert(L == g.mainthread); + + lmem.Mvisitgco(L, *lua.State, L, deletegco); + + for (0..@intCast(g.strt.size)) |i| // free all string lists + std.debug.assert(g.strt.hash.?[i] == null); + + std.debug.assert(L.global.strt.nuse == 0); +} + +fn markudatadirectaccess(g: *lstate.global_State) void { + for (0..ludata.UTAG_INTERNAL_LIMIT) |i| { + const udatadirect = &g.udatadirect[i]; + + markvalue(g, &udatadirect.indextm); + markvalue(g, &udatadirect.newindextm); + markvalue(g, &udatadirect.namecalltm); + } +} + +fn markudatadirectfields(g: *lstate.global_State) void { + for (0..ludata.UTAG_INTERNAL_LIMIT) |i| { + if (g.udatadirectfields[i]) |f| + markobject(g, f.obj2gco()); + } +} + +fn markmt(g: *lstate.global_State) void { + for (0..lua.Type.T_COUNT) |i| { + if (g.mt[i]) |mt| + markobject(g, @ptrCast(@alignCast(mt))); + } +} + +fn marktaggedmt(g: *lstate.global_State) void { + for (0..lua.config.UTAG_LIMIT) |i| { + if (g.udatamt[i]) |mt| + markobject(g, @ptrCast(@alignCast(mt))); + } +} + +fn markroot(L: *lua.State) void { + const g = L.global; + g.gray = null; + g.grayagain = null; + g.weak = null; + markobject(g, @ptrCast(@alignCast(g.mainthread))); + // make global table be traversed before main stack + markobject(g, @ptrCast(@alignCast(g.mainthread.gt))); + markvalue(g, L.registry()); + + markudatadirectaccess(g); + markudatadirectfields(g); + + markmt(g); + marktaggedmt(g); + + g.gcstate = GCSpropagate; +} + +fn remarkupvals(g: *lstate.global_State) usize { + var work: usize = 0; + + var uv: *lobject.UpVal = g.uvhead.u.open.next orelse return 0; + while (uv != &g.uvhead) : (uv = uv.u.open.next.?) { + work += @sizeOf(lobject.UpVal); + + std.debug.assert(uv.upisopen()); + std.debug.assert(uv.u.open.next.?.u.open.prev == uv and uv.u.open.prev.?.u.open.next == uv); + std.debug.assert(!isblack(uv.obj2gco())); // open upvalues are never black + + if (isgray(uv.obj2gco())) + markvalue(g, uv.v); + } + + return work; +} + +fn clearupvals(L: *lua.State) usize { + const g = L.global; + + var work: usize = 0; + + var count: usize = 0; + var uv: *lobject.UpVal = g.uvhead.u.open.next orelse return 0; + while (uv != &g.uvhead) { + defer count += 1; + work += @sizeOf(lobject.UpVal); + + std.debug.assert(uv.upisopen()); + std.debug.assert(uv.u.open.next.?.u.open.prev == uv and uv.u.open.prev.?.u.open.next == uv); + std.debug.assert(!isblack(uv.obj2gco())); // open upvalues are never black + std.debug.assert(iswhite(uv.obj2gco()) or !uv.v.iscollectable() or !iswhite(uv.v.gcvalue())); + + if (uv.markedopen != 0) { + // upvalue is still open (belongs to alive thread) + std.debug.assert(isgray(uv.obj2gco())); + uv.markedopen = 0; // for next cycle + uv = uv.u.open.next.?; + } else { + // upvalue is either dead, or alive but the thread is dead; unlink and close + const next = uv.u.open.next.?; + lfunc.Fcloseupval(L, uv, iswhite(uv.obj2gco())); + uv = next; + } + } + + return work; +} + +fn atomic(L: *lua.State) Errorset.Table!usize { + const g = L.global; + std.debug.assert(g.gcstate == GCSatomic); + + var work: usize = 0; + + // TODO: LUAI_GCMETRICS + + // remark occasional upvalues of (maybe) dead threads + work += remarkupvals(g); + // traverse objects caught by write barrier and by 'remarkupvals' + work += try propagateall(g); + + // TODO: LUAI_GCMETRICS + + // remark weak tables + g.gray = g.weak; + g.weak = null; + std.debug.assert(!iswhite(@ptrCast(@alignCast(g.mainthread)))); + markobject(g, @ptrCast(@alignCast(L))); // mark running thread + markmt(g); // mark basic metatables (again) + + marktaggedmt(g); // mark tagged userdata metatables (again) + + markudatadirectaccess(g); // mark tagged userdata direct access functions (again) + + markudatadirectfields(g); // mark direct field dispatch tables (again) + + work += try propagateall(g); + + // TODO: LUAI_GCMETRICS + + // remark gray again + g.gray = g.grayagain; + g.grayagain = null; + work += try propagateall(g); + + // TODO: LUAI_GCMETRICS + + // remove collected objects from weak tables + work += try cleartable(L, g.weak); + g.weak = null; + + // TODO: LUAI_GCMETRICS + + // close orphaned live upvalues of dead threads and clear dead upvalues + work += clearupvals(L); + + // TODO: LUAI_GCMETRICS + + // flip current white + g.currentwhite = otherwhite(g); + g.sweepgcopage = g.allgcopages; + g.gcstate = GCSsweep; + + return work; +} + +// a version of generic luaM_visitpage specialized for the main sweep stage +fn sweepgcopage(L: *lua.State, page: *lmem.lua_Page) usize { + var start: [*]u8 = undefined; + var end: [*]u8 = undefined; + var busyBlocks: c_int = 0; + var blockSize: c_int = 0; + lmem.Mgetpagewalkinfo(page, &start, &end, &busyBlocks, &blockSize); + + std.debug.assert(busyBlocks > 0); + + const g = L.global; + + const deadmask = otherwhite(g); + std.debug.assert(testbit(deadmask, FIXEDBIT) != 0); // make sure we never sweep fixed objects + + const newwhite = Cwhite(g); + + var pos: [*]u8 = start; + while (pos != end) : (pos += @as(usize, @intCast(blockSize))) { + const gco: *lstate.GCObject = @ptrCast(@alignCast(pos)); + + // skip memory blocks that are already freed + if (gco.gch.header.tt == @intFromEnum(lua.Type.Nil)) + continue; + + // is the object alive? + if ((gco.gch.header.marked ^ WHITEBITS) & deadmask != 0) { + std.debug.assert(!isdead(g, gco)); + // make it white (for next cycle) + gco.gch.header.marked = (gco.gch.header.marked & maskmarks) | newwhite; + } else { + std.debug.assert(isdead(g, gco)); + freeobj(L, gco, page); + + // if the last block was removed, page would be removed as well + busyBlocks -= 1; + if (busyBlocks == 0) + return @divTrunc(@intFromPtr(end) - @intFromPtr(start), @as(usize, @intCast(blockSize))) + 1; + } + } + + return @divTrunc(@intFromPtr(end) - @intFromPtr(start), @as(usize, @intCast(blockSize))); +} + +fn gcstep(L: *lua.State, limit: usize) Errorset.Table!usize { + var cost: usize = 0; + const g = L.global; + switch (g.gcstate) { + GCSpause => { + markroot(L); // start a new collection + std.debug.assert(g.gcstate == GCSpropagate); + }, + GCSpropagate => { + while (g.gray != null and cost < limit) + cost += try propagatemark(g); + + if (g.gray == null) { + // TODO: LUAI_GCMETRICS + + // perform one iteration over 'gray again' list + g.gray = g.grayagain; + g.grayagain = null; + + g.gcstate = GCSpropagateagain; + } + }, + GCSpropagateagain => { + while (g.gray != null and cost < limit) + cost += try propagatemark(g); + + if (g.gray == null) { // no more `gray' objects + // TODO: LUAI_GCMETRICS + + g.gcstate = GCSatomic; + } + }, + GCSatomic => { + // TODO: LUAI_GCMETRICS + + g.gcstats.atomicstarttimestamp = lperf.clock(); + g.gcstats.atomicstarttotalsizebytes = g.totalbytes; + + cost = try atomic(L); // finish mark phase + + std.debug.assert(g.gcstate == GCSsweep); + }, + GCSsweep => { + while (g.sweepgcopage != null and cost < limit) { + const next = lmem.Mgetnextpage(g.sweepgcopage.?); // page sweep might destroy the page + + const steps = sweepgcopage(L, g.sweepgcopage.?); + + g.sweepgcopage = next; + cost += steps * GC_SWEEPPAGESTEPCOST; + } + + // nothing more to sweep? + if (g.sweepgcopage == null) { + // don't forget to visit main thread, it's the only object not allocated in GCO pages + std.debug.assert(!isdead(g, @ptrCast(@alignCast(g.mainthread)))); + makewhite(g, @ptrCast(@alignCast(g.mainthread))); // make it white (for next cycle) + + try shrinkbuffers(L); + + g.gcstate = GCSpause; // end collection + } + }, + else => unreachable, // Unexpected GC state + } + return cost; +} + +fn getheaptriggererroroffset(g: *lstate.global_State) i64 { + // adjust for error using Proportional-Integral controller + // https://en.wikipedia.org/wiki/PID_controller + const errorKb: i32 = @divTrunc((@as(i32, @intCast(g.gcstats.atomicstarttotalsizebytes)) - @as(i32, @intCast(g.gcstats.heapgoalsizebytes))), 1024); + + // we use sliding window for the error integral to avoid error sum 'windup' when the desired target cannot be reached + const triggertermcount: i32 = @divTrunc(@sizeOf(@TypeOf(g.gcstats.triggerterms)), @sizeOf(@TypeOf(g.gcstats.triggerterms[0]))); + + const slot = &g.gcstats.triggerterms[g.gcstats.triggertermpos % triggertermcount]; + const prev = slot.*; + slot.* = errorKb; + g.gcstats.triggerintegral += errorKb - prev; + g.gcstats.triggertermpos += 1; + + // controller tuning + // https://en.wikipedia.org/wiki/Ziegler%E2%80%93Nichols_method + const Ku = 0.9; // ultimate gain (measured) + const Tu = 2.5; // oscillation period (measured) + + const Kp = 0.45 * Ku; // proportional gain + const Ti = 0.8 * Tu; + const Ki = 0.54 * Ku / Ti; // integral gain + + const proportionalTerm = Kp * @as(f64, @floatFromInt(errorKb)); + const integralTerm = Ki * @as(f64, @floatFromInt(g.gcstats.triggerintegral)); + + const totalTerm = proportionalTerm + integralTerm; + + return @as(i64, @intFromFloat(totalTerm * 1024)); +} + +fn getheaptrigger(g: *lstate.global_State, heapgoal: usize) usize { + @setRuntimeSafety(false); + // adjust threshold based on a guess of how many bytes will be allocated between the cycle start and sweep phase + // our goal is to begin the sweep when used memory has reached the heap goal + const durationthreshold = 1e-3; + const allocationduration = g.gcstats.atomicstarttimestamp - g.gcstats.endtimestamp; + + // avoid measuring intervals smaller than 1ms + if (allocationduration < durationthreshold) + return heapgoal; + + const allocationrate = @as(f64, @floatFromInt(g.gcstats.atomicstarttotalsizebytes -% g.gcstats.endtotalsizebytes)) / allocationduration; + const markduration = g.gcstats.atomicstarttimestamp - g.gcstats.starttimestamp; + + const expectedgrowth = @as(i64, @intFromFloat(markduration * allocationrate)); + const offset = getheaptriggererroroffset(g); + const heaptrigger: i64 = @as(i64, @intCast(heapgoal)) - @as(i64, @intCast(expectedgrowth + offset)); + + // clamp the trigger between memory use at the end of the cycle and the heap goal + return if (heaptrigger < g.totalbytes) g.totalbytes else if (heaptrigger > heapgoal) heapgoal else @intCast(heaptrigger); +} + +pub fn Cstep(L: *lua.State, assist: bool) Errorset.Table!usize { + const g = L.global; + + const lim = g.gcstepsize * @divTrunc(g.gcstepmul, 100); + std.debug.assert(g.totalbytes >= g.GCthreshold); + const debt = g.totalbytes - g.GCthreshold; + + GC_INTERRUPT(g, L, 0); + + if (g.gcstate == GCSpause) + g.gcstats.starttimestamp = lperf.clock(); + + // TODO: LUAI_GCMETRICS + + const lastgcstate = g.gcstate; + + const work = try gcstep(L, @intCast(lim)); + + // TODO: LUAI_GCMETRICS + _ = assist; + + const actualstepsize = @divTrunc(work * 100, @as(usize, @intCast(g.gcstepmul))); + // at the end of the last cycle + if (g.gcstate == GCSpause) { + // at the end of a collection cycle, set goal based on gcgoal setting + const heapgoal = @divTrunc(g.totalbytes, 100) * @as(usize, @intCast(g.gcgoal)); + const heaptrigger = getheaptrigger(g, heapgoal); + + g.GCthreshold = heaptrigger; + + g.gcstats.heapgoalsizebytes = heapgoal; + g.gcstats.endtimestamp = lperf.clock(); + g.gcstats.endtotalsizebytes = g.totalbytes; + + // TODO: LUAI_GCMETRICS + } else { + g.GCthreshold = g.totalbytes + actualstepsize; + + // compensate if GC is "behind schedule" (has some debt to pay) + if (g.GCthreshold >= debt) + g.GCthreshold -= debt; + } + + GC_INTERRUPT(g, L, lastgcstate); + + return actualstepsize; +} + +pub fn Cbarrierf(L: *lua.State, o: *lstate.GCObject, v: *lstate.GCObject) void { + const g = L.global; + std.debug.assert(isblack(o) and iswhite(v) and !isdead(g, v) and !isdead(g, o)); + std.debug.assert(g.gcstate != GCSpause); + // must keep invariant? + if (keepinvariant(g)) + reallymarkobject(g, v) // restore invariant + else // don't mind + makewhite(g, o); // mark as white just to avoid other barriers +} + +pub fn Cbarriertable(L: *lua.State, t: *lobject.LuaTable, v: *lstate.GCObject) void { + const g = L.global; + const o = t.obj2gco(); + + // in the second propagation stage, table assignment barrier works as a forward barrier + if (g.gcstate == GCSpropagateagain) { + std.debug.assert(isblack(o) and iswhite(v) and !isdead(g, v) and !isdead(g, o)); + reallymarkobject(g, v); + return; + } + + std.debug.assert(isblack(o) and !isdead(g, o)); + std.debug.assert(g.gcstate != GCSpause); + black2gray(o); // make table gray (again) + t.gclist = g.grayagain; + g.grayagain = o; +} + +pub fn Cbarrierback(L: *lua.State, o: *lstate.GCObject, gclist: *?*lstate.GCObject) void { + const g = L.global; + std.debug.assert(isblack(o) and !isdead(g, o)); + std.debug.assert(g.gcstate != GCSpause); + + black2gray(o); + gclist.* = g.grayagain; + g.grayagain = o; +} + +pub fn Cupvalclosed(L: *lua.State, uv: *lobject.UpVal) void { + const g = L.global; + const o: *lstate.GCObject = uv.obj2gco(); + + std.debug.assert(!uv.upisopen()); // upvalue was closed but needs GC state fixup + + if (isgray(o)) { + if (keepinvariant(g)) { + gray2black(o); // closed upvalues need barrier + Cbarrier(L, @ptrCast(@alignCast(uv)), uv.v); + } else { // sweep phase: sweep it (turning it into white) + makewhite(g, o); + std.debug.assert(g.gcstate != GCSpause); + } + } +} diff --git a/deps/luau/src/VM/lgcdebug.zig b/deps/luau/src/VM/lgcdebug.zig new file mode 100644 index 0000000..acaca07 --- /dev/null +++ b/deps/luau/src/VM/lgcdebug.zig @@ -0,0 +1,223 @@ +const std = @import("std"); + +const lgc = @import("lgc.zig"); +const lua = @import("lua.zig"); +const lmem = @import("lmem.zig"); +const lstate = @import("lstate.zig"); +const ludata = @import("ludata.zig"); +const lobject = @import("lobject.zig"); + +fn validateobjref(g: *const lstate.global_State, f: *lstate.GCObject, t: *lstate.GCObject) void { + std.debug.assert(!lgc.isdead(g, t)); + if (lgc.keepinvariant(g)) { + // basic incremental invariant: black can't point to white + std.debug.assert(!(lgc.isblack(f) and lgc.iswhite(t))); + } +} + +fn validateref(g: *const lstate.global_State, f: *lstate.GCObject, v: *lobject.TValue) void { + if (v.iscollectable()) { + std.debug.assert(v.ttype() == v.gcvalue().gch.header.tt); + validateobjref(g, f, v.gcvalue()); + } +} + +fn validatetable(g: *const lstate.global_State, h: *lobject.LuaTable) void { + const sizenode = @as(u32, 1) << @intCast(h.lsizenode); + + std.debug.assert(h.bound.lastfree <= sizenode); + + if (h.metatable) |mt| + validateobjref(g, h.obj2gco(), mt.obj2gco()); + + for (0..@intCast(h.sizearray)) |i| + validateref(g, h.obj2gco(), &h.array.?[i]); + + for (0..sizenode) |i| { + const n = &h.node[i]; + + std.debug.assert(n.gkey().ttype() != @intFromEnum(lua.Type.Deadkey) or n.gval().ttisnil()); + std.debug.assert(@as(isize, @intCast(i)) + n.gnext() >= 0 and @as(isize, @intCast(i)) + n.gnext() < sizenode); + + if (!n.gval().ttisnil()) { + var k: lobject.TValue = undefined; + k.tt = n.gkey().ttype(); + k.value = n.gkey().value; + + validateref(g, h.obj2gco(), &k); + validateref(g, h.obj2gco(), n.gval()); + } + } +} + +fn validateclosure(g: *const lstate.global_State, cl: *lobject.Closure) void { + validateobjref(g, cl.obj2gco(), cl.env.obj2gco()); + + if (cl.isC != 0) { + for (cl.d.c.upvalues()[0..cl.nupvalues]) |*upval| + validateref(g, cl.obj2gco(), upval); + } else { + std.debug.assert(cl.nupvalues == cl.d.l.p.nups); + + validateobjref(g, cl.obj2gco(), cl.d.l.p.obj2gco()); + + for (cl.d.l.upreferences()[0..cl.nupvalues]) |*upref| + validateref(g, cl.obj2gco(), upref); + } +} + +fn validatestack(g: *const lstate.global_State, l: *lua.State) void { + validateobjref(g, @ptrCast(@alignCast(l)), l.gt.?.obj2gco()); + + for (l.base_ci.?[0 .. (l.ci.? - l.base_ci.?) + 1]) |*ci| { + std.debug.assert(@intFromPtr(l.stack) <= @intFromPtr(ci.base)); + std.debug.assert(@intFromPtr(ci.func) <= @intFromPtr(ci.base) and @intFromPtr(ci.base) <= @intFromPtr(ci.top)); + std.debug.assert(@intFromPtr(ci.top) <= @intFromPtr(l.stack_last)); + } + + // note: stack refs can violate gc invariant so we only check for liveness + for (l.stack[0..(l.top - l.stack)]) |*o| + o.checkliveness(g); + + if (l.namecall) |nc| + validateobjref(g, @ptrCast(@alignCast(l)), nc.obj2gco()); + + var upval: ?*lobject.UpVal = l.openupval; + while (upval) |uv| : (upval = uv.u.open.threadnext) { + std.debug.assert(uv.header.tt == @intFromEnum(lua.Type.UpVal)); + std.debug.assert(uv.upisopen()); + std.debug.assert(uv.u.open.next.?.u.open.prev == uv and uv.u.open.prev.?.u.open.next == uv); + std.debug.assert(!lgc.isblack(uv.obj2gco())); + } +} + +fn validateproto(g: *const lstate.global_State, f: *lobject.Proto) void { + if (f.source) |src| + validateobjref(g, f.obj2gco(), src.obj2gco()); + + if (f.debugname) |name| + validateobjref(g, f.obj2gco(), name.obj2gco()); + + for (0..@intCast(f.sizek)) |i| + validateref(g, f.obj2gco(), &f.k.?[i]); + + for (0..@intCast(f.sizeupvalues)) |i| + if (f.upvalues.?[i]) |uv| + validateobjref(g, f.obj2gco(), uv.obj2gco()); + + for (0..@intCast(f.sizep)) |i| + if (f.p.?[i]) |proto| + validateobjref(g, f.obj2gco(), proto.obj2gco()); + + for (0..@intCast(f.sizelocvars)) |i| + if (f.locvars.?[i].varname) |varname| + validateobjref(g, f.obj2gco(), varname.obj2gco()); +} + +fn validateclass(g: *const lstate.global_State, lco: *lobject.LuauClass) void { + const obj = lco.obj2gco(); + validateobjref(g, obj, lco.name.obj2gco()); + validateobjref(g, obj, lco.memberstooffset.obj2gco()); + for (0..lco.numberofallmembers) |i| { + validateobjref(g, obj, lco.offsettomember[i].obj2gco()); + if (i >= lco.numberofinstancemembers) + validateref(g, obj, &lco.staticmembers[i - @as(u32, @intCast(lco.numberofinstancemembers))]); + } + validateobjref(g, obj, lco.metatable.obj2gco()); + if (lco.instancemetatable) |mt| + validateobjref(g, obj, mt.obj2gco()); +} + +fn validateobject(g: *const lstate.global_State, inst: *lobject.LuauObject) void { + const obj = inst.obj2gco(); + validateobjref(g, obj, inst.lclass.obj2gco()); + for (0..inst.numberofmembers) |i| + validateref(g, obj, &inst.members[i]); +} + +fn validateobj(g: *const lstate.global_State, o: *lstate.GCObject) void { + if (lgc.isdead(g, o)) { + std.debug.assert(g.gcstate == lgc.GCSsweep); + return; + } + + switch (o.gch.header.tt) { + @intFromEnum(lua.Type.String), @intFromEnum(lua.Type.Buffer) => {}, + @intFromEnum(lua.Type.Table) => validatetable(g, o.toh()), + @intFromEnum(lua.Type.Function) => validateclosure(g, o.tocl()), + @intFromEnum(lua.Type.Userdata) => if (o.tou().metatable) |mt| + validateobjref(g, o, mt.obj2gco()), + @intFromEnum(lua.Type.Thread) => validatestack(g, o.toth()), + @intFromEnum(lua.Type.Proto) => validateproto(g, o.top()), + @intFromEnum(lua.Type.UpVal) => validateref(g, o, o.touv().v), + @intFromEnum(lua.Type.Class) => validateclass(g, o.toclass()), + @intFromEnum(lua.Type.Object) => validateobject(g, o.toobject()), + else => unreachable, + } +} + +fn validategraylist(g: *const lstate.global_State, obj: ?*lstate.GCObject) void { + if (!lgc.keepinvariant(g)) + return; + + var so: ?*lstate.GCObject = obj; + while (so) |o| { + std.debug.assert(lgc.isgray(o)); + switch (o.gch.header.tt) { + @intFromEnum(lua.Type.Table) => so = o.toh().gclist, + @intFromEnum(lua.Type.Function) => so = o.tocl().gclist, + @intFromEnum(lua.Type.Thread) => so = o.toth().gclist, + @intFromEnum(lua.Type.Class) => so = o.toclass().gclist, + @intFromEnum(lua.Type.Object) => so = o.toobject().gclist, + @intFromEnum(lua.Type.Proto) => so = o.top().gclist, + else => unreachable, + } + } +} + +fn validategco(L: *lua.State, _: ?*lmem.lua_Page, gco: *lstate.GCObject) bool { + const g = L.global; + + validateobj(g, gco); + return false; +} + +pub fn Cvalidate(L: *lua.State) void { + const g = L.global; + + std.debug.assert(!lgc.isdead(g, @ptrCast(@alignCast(g.mainthread)))); + g.registry.checkliveness(g); + + for (0..lua.Type.T_COUNT) |i| + if (g.mt[i]) |mt| + std.debug.assert(!lgc.isdead(g, mt.obj2gco())); + + for (0..lua.config.UTAG_LIMIT) |i| + if (g.udatamt[i]) |mt| + std.debug.assert(!lgc.isdead(g, mt.obj2gco())); + + for (0..ludata.UTAG_INTERNAL_LIMIT) |i| { + g.udatadirect[i].indextm.checkliveness(g); + g.udatadirect[i].newindextm.checkliveness(g); + g.udatadirect[i].namecalltm.checkliveness(g); + + if (g.udatadirectfields[i]) |f| + std.debug.assert(!lgc.isdead(g, f.obj2gco())); + } + + validategraylist(g, g.weak); + validategraylist(g, g.gray); + validategraylist(g, g.grayagain); + + _ = validategco(@ptrCast(L), null, @ptrCast(@alignCast(g.mainthread))); + + lmem.Mvisitgco(L, *lua.State, L, validategco); + + var upval: ?*lobject.UpVal = g.uvhead.u.open.next.?; + while (upval != &g.uvhead) : (upval = upval.?.u.open.next) { + std.debug.assert(upval.?.header.tt == @intFromEnum(lua.Type.UpVal)); + std.debug.assert(upval.?.upisopen()); + std.debug.assert(upval.?.u.open.next.?.u.open.prev == upval and upval.?.u.open.prev.?.u.open.next == upval); + std.debug.assert(!lgc.isblack(upval.?.obj2gco())); + } +} diff --git a/deps/luau/src/VM/linit.zig b/deps/luau/src/VM/linit.zig new file mode 100644 index 0000000..7864b23 --- /dev/null +++ b/deps/luau/src/VM/linit.zig @@ -0,0 +1,16 @@ +const c = @import("c"); +const std = @import("std"); + +const lua = @import("lua.zig"); + +pub fn Lopenlibs(L: *lua.State) !void { + c.luaL_openlibs(@ptrCast(L)); +} + +pub fn Lsandbox(L: *lua.State) !void { + c.luaL_sandbox(@ptrCast(L)); +} + +pub fn Lsandboxthread(L: *lua.State) !void { + c.luaL_sandboxthread(@ptrCast(L)); +} diff --git a/deps/luau/src/VM/lmathlib.zig b/deps/luau/src/VM/lmathlib.zig new file mode 100644 index 0000000..f806642 --- /dev/null +++ b/deps/luau/src/VM/lmathlib.zig @@ -0,0 +1,7 @@ +const c = @import("c"); + +const lua = @import("lua.zig"); + +pub inline fn open(L: *lua.State) void { + _ = c.luaopen_math(@ptrCast(L)); +} diff --git a/deps/luau/src/VM/lmem.zig b/deps/luau/src/VM/lmem.zig new file mode 100644 index 0000000..9561519 --- /dev/null +++ b/deps/luau/src/VM/lmem.zig @@ -0,0 +1,720 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details + +const std = @import("std"); +const builtin = @import("builtin"); + +const lua = @import("lua.zig"); +const lobject = @import("lobject.zig"); + +const lstate = @import("lstate.zig"); +const ldo = @import("ldo.zig"); +const ldebug = @import("ldebug.zig"); + +const Errorset = @import("errorset.zig"); + +const Error = Errorset.Memory; + +// +// Luau heap uses a size-segregated page structure, with individual pages and large allocations +// allocated using system heap (via frealloc callback). +// +// frealloc callback serves as a general, if slow, allocation callback that can allocate, free or +// resize allocations: +// +// void* frealloc(void* ud, void* ptr, size_t oldsize, size_t newsize); +// +// frealloc(ud, NULL, 0, x) creates a new block of size x +// frealloc(ud, p, x, 0) frees the block p (must return NULL) +// frealloc(ud, NULL, 0, 0) does nothing, equivalent to free(NULL) +// +// frealloc returns NULL if it cannot create or reallocate the area +// (any reallocation to an equal or smaller size cannot fail!) +// +// On top of this, Luau implements heap storage which is split into two types of allocations: +// +// - GCO, short for "garbage collected objects" +// - other objects (for example, arrays stored inside table objects) +// +// The heap layout for these two allocation types is a bit different. +// +// All GCO are allocated in pages, which is a block of memory of ~16K in size that has a page header +// (lua_Page). Each page contains 1..N blocks of the same size, where N is selected to fill the page +// completely. This amortizes the allocation cost and increases locality. Each GCO block starts with +// the GC header (GCheader) which contains the object type, mark bits and other GC metadata. If the +// GCO block is free (not used), then it must have the type set to TNIL; in this case the block can +// be part of the per-page free list, the link for that list is stored after the header (freegcolink). +// +// Importantly, the GCO block doesn't have any back references to the page it's allocated in, so it's +// impossible to free it in isolation - GCO blocks are freed by sweeping the pages they belong to, +// using luaM_freegco which must specify the page; this is called by page sweeper that traverses the +// entire page's worth of objects. For this reason it's also important that freed GCO blocks keep the +// GC header intact and accessible (with type = NIL) so that the sweeper can access it. +// +// Some GCOs are too large to fit in a 16K page without excessive fragmentation (the size threshold is +// currently 512 bytes); in this case, we allocate a dedicated small page with just a single block's worth +// storage space, but that requires allocating an extra page header. In effect large GCOs are a little bit +// less memory efficient, but this allows us to uniformly sweep small and large GCOs using page lists. +// +// All GCO pages are linked in a large intrusive linked list (global_State::allgcopages). Additionally, +// for each block size there's a page free list that contains pages that have at least one free block +// (global_State::freegcopages). This free list is used to make sure object allocation is O(1). +// +// When LUAU_ASSERTENABLED is enabled, all non-GCO pages are also linked in a list (global_State::allpages). +// Because this list is not strictly required for runtime operations, it is only tracked for the purposes of +// debugging. While overhead of linking those pages together is very small, unnecessary operations are avoided. +// +// Compared to GCOs, regular allocations have two important differences: they can be freed in isolation, +// and they don't start with a GC header. Because of this, each allocation is prefixed with block metadata, +// which contains the pointer to the page for allocated blocks, and the pointer to the next free block +// inside the page for freed blocks. +// For regular allocations that are too large to fit in a page (using the same threshold of 512 bytes), +// we don't allocate a separate page, instead simply using frealloc to allocate a vanilla block of memory. +// +// Just like GCO pages, we store a page free list (global_State::freepages) that allows O(1) allocation; +// there is no global list for non-GCO pages since we never need to traverse them directly. +// +// In both cases, we pick the page by computing the size class from the block size which rounds the block +// size up to reduce the chance that we'll allocate pages that have very few allocated blocks. The size +// class strategy is determined by SizeClassConfig constructor. +// +// Note that when the last block in a page is freed, we immediately free the page with frealloc - the +// memory manager doesn't currently attempt to keep unused memory around. This can result in excessive +// allocation traffic and can be mitigated by adding a page cache in the future. +// +// For both GCO and non-GCO pages, the per-page block allocation combines bump pointer style allocation +// (lua_Page::freeNext) and per-page free list (lua_Page::freeList). We use the bump allocator to allocate +// the contents of the page, and the free list for further reuse; this allows shorter page setup times +// which results in less variance between allocation cost, as well as tighter sweep bounds for newly +// allocated pages. +// + +// +// The sizes of most Luau objects aren't crucial for code correctness, but they are crucial for memory efficiency +// To prevent some of them accidentally growing and us losing memory without realizing it, we're going to lock +// the sizes of all critical structures down. +// +fn ABISWITCH(b64: comptime_int, b32: comptime_int) comptime_int { + if (@sizeOf(*anyopaque) == 8) + return b64 + else + return b32; +} + +comptime { + if (lua.config.VECTOR_SIZE == 4) { + std.debug.assert(@sizeOf(lobject.TValue) == ABISWITCH(24, 24)); // size mismatch for value + std.debug.assert(@sizeOf(lobject.LuaNode) == ABISWITCH(48, 48)); // size mismatch for table entry + } else { + std.debug.assert(@sizeOf(lobject.TValue) == ABISWITCH(16, 16)); // size mismatch for value + std.debug.assert(@sizeOf(lobject.LuaNode) == ABISWITCH(32, 32)); // size mismatch for table entry + } + + std.debug.assert(@offsetOf(lobject.TString, "data") == ABISWITCH(24, 20)); // size mismatch for string header + std.debug.assert(@sizeOf(lobject.LuaTable) == ABISWITCH(48, 32)); // size mismatch for table header + std.debug.assert(@offsetOf(lobject.Buffer, "data") == ABISWITCH(8, 8)); // size mismatch for buffer header + + // The userdata is designed to provide 16 byte alignment for 16 byte and larger userdata sizes + std.debug.assert(@offsetOf(lobject.Udata, "data") == 16); // data must be at precise offset provide proper alignment +} + +const kSizeClasses = lua.config.SIZECLASSES; + +// Controls the number of entries in SizeClassConfig and define the maximum possible paged allocation size +// Modifications require updates the SizeClassConfig initialization +const kMaxSmallSize = 1024; + +// Effective limit on object size to use paged allocation +// Can be modified without additional changes to code, provided it is smaller or equal to kMaxSmallSize +const kMaxSmallSizeUsed = 1024; + +const kLargePageThreshold = 512; // larger pages are used for objects larger than this size to fit more of them into a page + +// constant factor to reduce our page sizes by, to increase the chances that pages we allocate will +// allow external allocators to allocate them without wasting space due to rounding introduced by their heap meta data +const kExternalAllocatorMetaDataReduction = 24; + +const kSmallPageSize = 16 * 1024 - kExternalAllocatorMetaDataReduction; +const kLargePageSize = 32 * 1024 - kExternalAllocatorMetaDataReduction; + +const kBlockHeader = if (@sizeOf(f64) > @sizeOf(*anyopaque)) @sizeOf(f64) else @sizeOf(*anyopaque); // suitable for aligning double & void* on all platforms +const kGCOLinkOffset = (@sizeOf(lobject.GCheader) + @sizeOf(*anyopaque) - 1) & ~@as(usize, @sizeOf(*anyopaque) - 1); // GCO pages contain freelist links after the GC header + +const SizeClassConfig = extern struct { + const values = generateSizeOfClass(); + + sizeOfClass: [kSizeClasses]c_int = values[0], + classForSize: [kMaxSmallSize + 1]i8 = values[1], + classCount: c_int = values[2], + + fn generateSizeOfClass() struct { [kSizeClasses]c_int, [kMaxSmallSize + 1]i8, comptime_int } { + var classCount: comptime_int = 0; + var sizeOfClass: [kSizeClasses]c_int = [_]c_int{0} ** kSizeClasses; + var classForSize: [kMaxSmallSize + 1]i8 = [_]i8{-1} ** (kMaxSmallSize + 1); + + // we use a progressive size class scheme: + // - all size classes are aligned by 8b to satisfy pointer alignment requirements + // - we first allocate sizes classes in multiples of 8 + // - after the first cutoff we allocate size classes in multiples of 16 + // - after the second cutoff we allocate size classes in multiples of 32 + // - after the third cutoff we allocate size classes in multiples of 64 + // this balances internal fragmentation vs external fragmentation + + for ((8 / 8)..(64 / 8)) |i| { + sizeOfClass[classCount] = i * 8; + classCount += 1; + } + for ((64 / 16)..(256 / 16)) |i| { + sizeOfClass[classCount] = i * 16; + classCount += 1; + } + for ((256 / 32)..(512 / 32)) |i| { + sizeOfClass[classCount] = i * 32; + classCount += 1; + } + for ((512 / 64)..(1024 / 64) + 1) |i| { + sizeOfClass[classCount] = i * 64; + classCount += 1; + } + + std.debug.assert(classCount <= kSizeClasses); + + // fill the lookup table for all classes + for (0..classCount) |klass| + classForSize[sizeOfClass[klass]] = @as(i8, @intCast(klass)); + + // fill the gaps in lookup table + { + var size = kMaxSmallSize - 1; + @setEvalBranchQuota(kMaxSmallSize + 128); + while (size >= 0) : (size -= 1) { + if (classForSize[size] < 0) + classForSize[size] = classForSize[size + 1]; + } + } + + return .{ sizeOfClass, classForSize, classCount }; + } +}; + +const kSizeClassConfig: SizeClassConfig = .{}; + +// size class for a block of size sz; returns -1 for size=0 because empty allocations take no space +inline fn sizeclass(sz: usize) i8 { + return if (sz -% 1 < kMaxSmallSizeUsed) kSizeClassConfig.classForSize[sz] else -1; +} + +inline fn debugpageset(set: *?*lua_Page) ?*?*lua_Page { + switch (comptime builtin.mode) { + .ReleaseFast, .ReleaseSmall => return null, // ReleaseFast and ReleaseSmall defines NDEBUG + else => return set, // ReleaseSafe and Debug does not define NDEBUG + } +} + +// metadata for a block is stored in the first pointer of the block +inline fn metadata(block: *anyopaque) *?*anyopaque { + return @as(*?*anyopaque, @ptrCast(@alignCast(block))); +} +inline fn freegcolink(block: *anyopaque) *?*anyopaque { + return @as(*?*anyopaque, @ptrFromInt(@intFromPtr(block) + kGCOLinkOffset)); +} + +pub const lua_Page = extern struct { + // list of pages with free blocks + prev: ?*lua_Page = null, + next: ?*lua_Page = null, + + // list of all pages + listprev: ?*lua_Page = null, + listnext: ?*lua_Page = null, + + pageSize: c_int = 0, // page size in bytes, including page header + blockSize: c_int = 0, // block size in bytes, including block header + + freeList: ?*anyopaque = null, // next free block in this page; linked with metadata()/freegcolink() + freeNext: c_int = 0, // next free block offset in this page + busyBlocks: c_int = 0, // number of blocks allocated out of this page + + // provide additional padding based on current object size to provide 16 byte alignment of data + // later static_assert checks that this requirement is held + padding: [if (@sizeOf(*anyopaque) == 8) 8 else 12]u8 = undefined, + + data: [1]u8, +}; + +comptime { + std.debug.assert(@offsetOf(lua_Page, "data") % 16 == 0); // data must be 16 byte aligned to provide properly aligned allocation of userdata objects +} + +fn Mtoobig(L: *lua.State) noreturn { + ldebug.GrunerrorL(L, "memory allocation error: block too big"); +} + +fn newpage(L: *lua.State, pageset: ?*?*lua_Page, pageSize: usize, blockSize: c_int, blockCount: c_int) Error!*lua_Page { + const g = L.global; + + std.debug.assert(pageSize - @offsetOf(lua_Page, "data") >= blockSize * blockCount); + + const page = @as(*lua_Page, @ptrCast(@alignCast( + (g.frealloc.?)(g.ud, null, 0, pageSize) orelse return Error.OutOfMemory, + ))); + + // ASAN_POISON_MEMORY_REGION(...); // TODO: ASAN support + + // setup page header + page.* = .{ + .prev = null, + .next = null, + + .listprev = null, + .listnext = null, + + .pageSize = @intCast(pageSize), + .blockSize = blockSize, + + // note: we start with the last block in the page and move downward + // either order would work, but that way we don't need to store the block count in the page + // additionally, GC stores objects in singly linked lists, and this way the GC lists end up in increasing pointer order + .freeList = null, + .freeNext = (blockCount - 1) * blockSize, + .busyBlocks = 0, + + .data = undefined, + }; + + if (pageset) |set| { + page.listnext = set.*; + if (page.listnext) |next| + next.listprev = page; + set.* = page; + } + + return page; +} + +// this is part of a cold path in newblock and newgcoblock +// it is marked as noinline to prevent it from being inlined into those functions +// if it is inlined, then the compiler may determine those functions are "too big" to be profitably inlined, which results in reduced performance +noinline fn newclasspage(L: *lua.State, freepageset: [*]?*lua_Page, pageset: ?*?*lua_Page, sizeClass: u8, storeMetadata: bool) Error!*lua_Page { + const sizeOfClass = kSizeClassConfig.sizeOfClass[sizeClass]; + const pageSize: usize = if (sizeOfClass > @as(c_int, kLargePageThreshold)) kLargePageSize else kSmallPageSize; + const blockSize = sizeOfClass + @as(c_int, if (storeMetadata) kBlockHeader else 0); + const blockCount = @divTrunc(pageSize - @offsetOf(lua_Page, "data"), @as(usize, @intCast(blockSize))); + + const page = try newpage(L, pageset, pageSize, blockSize, @intCast(blockCount)); + + // prepend a page to page freelist (which is empty because we only ever allocate a new page when it is!) + std.debug.assert(freepageset[sizeClass] == null); + freepageset[sizeClass] = page; + + return page; +} + +fn freepage(L: *lua.State, pageset: ?*?*lua_Page, page: *lua_Page) void { + const g = L.global; + + if (pageset) |set| { + // remove page from alllist + if (page.listnext) |next| + next.listprev = page.listprev; + + if (page.listprev) |prev| + prev.listnext = page.listnext + else if (set.* == page) + set.* = page.listnext; + } + + // so long + _ = (g.frealloc.?)(g.ud, @ptrCast(page), @intCast(page.pageSize), 0); +} + +fn freeclasspage(L: *lua.State, freepageset: [*]?*lua_Page, pageset: ?*?*lua_Page, page: *lua_Page, sizeClass: u8) void { + // remove page from freelist + if (page.next) |next| + next.prev = page.prev; + + if (page.prev) |prev| + prev.next = page.next + else if (freepageset[sizeClass] == page) + freepageset[sizeClass] = page.next; + + freepage(L, pageset, page); +} + +fn newblock(L: *lua.State, sizeClass: u8) Error!*anyopaque { + const g = L.global; + const page = g.freepages[sizeClass] orelse blk: { + // slow path: no page in the freelist, allocate a new one + break :blk try newclasspage(L, &g.freepages, debugpageset(&g.allpages), sizeClass, true); + }; + + std.debug.assert(page.prev == null); + std.debug.assert(page.freeList != null or page.freeNext >= 0); + std.debug.assert(page.blockSize == kSizeClassConfig.sizeOfClass[sizeClass] + kBlockHeader); + + var block: *anyopaque = undefined; + + if (page.freeNext >= 0) { + block = @ptrFromInt(@intFromPtr(&page.data) + @as(usize, @intCast(page.freeNext))); + // ASAN_UNPOISON_MEMORY_REGION(...); // TODO: ASAN support + + page.freeNext -= page.blockSize; + page.busyBlocks += 1; + } else { + block = page.freeList.?; + // ASAN_UNPOISON_MEMORY_REGION(...); // TODO: ASAN support + + page.freeList = metadata(block).*; + page.busyBlocks += 1; + } + + // the first word in a block point back to the page + metadata(block).* = @ptrCast(@alignCast(page)); + + // if we allocate the last block out of a page, we need to remove it from free list + if (page.freeList == null and page.freeNext < 0) { + g.freepages[sizeClass] = page.next; + if (page.next) |next| + next.prev = null; + page.next = null; + } + + // the user data is right after the metadata + return @ptrFromInt(@intFromPtr(block) + kBlockHeader); +} + +fn newgcoblock(L: *lua.State, sizeClass: u8) Error!*anyopaque { + const g = L.global; + const page = g.freegcopages[sizeClass] orelse blk: { + // slow path: no page in the freelist, allocate a new one + break :blk try newclasspage(L, &g.freegcopages, &g.allgcopages, sizeClass, false); + }; + + std.debug.assert(page.prev == null); + std.debug.assert(page.freeList != null or page.freeNext >= 0); + std.debug.assert(page.blockSize == kSizeClassConfig.sizeOfClass[sizeClass]); + + var block: *anyopaque = undefined; + + if (page.freeNext >= 0) { + block = @ptrFromInt(@intFromPtr(&page.data) + @as(usize, @intCast(page.freeNext))); + // ASAN_UNPOISON_MEMORY_REGION(...); // TODO: ASAN support + + page.freeNext -= page.blockSize; + page.busyBlocks += 1; + } else { + block = page.freeList.?; + // ASAN_UNPOISON_MEMORY_REGION(...); // TODO: ASAN support + + page.freeList = freegcolink(block).*; + page.busyBlocks += 1; + } + + // if we allocate the last block out of a page, we need to remove it from free list + if (page.freeList == null and page.freeNext < 0) { + g.freegcopages[sizeClass] = page.next; + if (page.next) |next| + next.prev = null; + page.next = null; + } + + return block; +} + +fn freeblock(L: *lua.State, sizeClass: u8, iblock: *anyopaque) void { + const g = L.global; + + // the user data is right after the metadata + const block: *anyopaque = @ptrFromInt(@intFromPtr(iblock) - kBlockHeader); + + const page = @as(*lua_Page, @ptrCast(@alignCast(metadata(block).*))); + std.debug.assert(page.busyBlocks > 0); + std.debug.assert(page.blockSize == kSizeClassConfig.sizeOfClass[sizeClass] + kBlockHeader); + std.debug.assert(@intFromPtr(block) >= @intFromPtr(&page.data) and @intFromPtr(block) < @intFromPtr(page) + @as(usize, @intCast(page.pageSize))); + + // if the page wasn't in the page free list, it should be now since it got a block! + if (page.freeList == null and page.freeNext < 0) { + std.debug.assert(page.prev == null); + std.debug.assert(page.next == null); + + page.next = g.freepages[sizeClass]; + if (page.next) |next| + next.prev = page; + g.freepages[sizeClass] = page; + } + + // add the block to the free list inside the page + metadata(block).* = page.freeList; + page.freeList = block; + + // ASAN_POISON_MEMORY_REGION(...); // TODO: ASAN support + + page.busyBlocks -= 1; + + // if it's the last block in the page, we don't need the page + if (page.busyBlocks == 0) + freeclasspage(L, &g.freepages, debugpageset(&g.allpages), page, sizeClass); +} + +fn freegcoblock(L: *lua.State, sizeClass: u8, block: *anyopaque, page: *lua_Page) void { + std.debug.assert(page.busyBlocks > 0); + std.debug.assert(page.blockSize == kSizeClassConfig.sizeOfClass[sizeClass]); + std.debug.assert(@intFromPtr(block) >= @intFromPtr(&page.data) and @intFromPtr(block) < @intFromPtr(page) + @as(usize, @intCast(page.pageSize))); + + const g = L.global; + + // if the page wasn't in the page free list, it should be now since it got a block! + if (page.freeList == null and page.freeNext < 0) { + std.debug.assert(page.prev == null); + std.debug.assert(page.next == null); + + page.next = g.freegcopages[sizeClass]; + if (page.next) |next| + next.prev = page; + g.freegcopages[sizeClass] = page; + } + + // when separate block metadata is not used, free list link is stored inside the block data itself + freegcolink(block).* = page.freeList; + page.freeList = block; + + // ASAN_POISON_MEMORY_REGION(...); // TODO: ASAN support + + page.busyBlocks -= 1; + + // if it's the last block in the page, we don't need the page + if (page.busyBlocks == 0) + freeclasspage(L, &g.freegcopages, &g.allgcopages, page, sizeClass); +} + +pub fn Mnew_(L: *lua.State, nsize: usize, memcat: u8) Error!*anyopaque { + const g = L.global; + + const nclass = sizeclass(nsize); + + const block = if (nclass >= 0) + newblock(L, @intCast(nclass)) + else + (g.frealloc.?)(g.ud, null, 0, nsize) orelse return Error.OutOfMemory; + + g.totalbytes += nsize; + g.memcatbytes[memcat] += nsize; + + if (g.cb.onallocate) |onallocate| { + @branchHint(.unlikely); + onallocate(L, 0, nsize); + } + + return block; +} + +pub fn Mnewgco_(L: *lua.State, nsize: usize, memcat: u8) Error!*lstate.GCObject { + // we need to accommodate space for link for free blocks (freegcolink) + std.debug.assert(nsize >= kGCOLinkOffset + @sizeOf(*anyopaque)); + + const g = L.global; + + const nclass = sizeclass(nsize); + + var block: *anyopaque = undefined; + + if (nclass >= 0) { + block = try newgcoblock(L, @intCast(nclass)); + } else { + const page = try newpage(L, &g.allgcopages, @offsetOf(lua_Page, "data") + nsize, @intCast(nsize), 1); + + block = @ptrCast(@alignCast(&page.data)); + // ASAN_UNPOISON_MEMORY_REGION(...); // TODO: ASAN support + + page.freeNext -= page.blockSize; + page.busyBlocks += 1; + } + + g.totalbytes += nsize; + g.memcatbytes[memcat] += nsize; + + if (g.cb.onallocate) |onallocate| { + @branchHint(.unlikely); + onallocate(L, 0, nsize); + } + + return @ptrCast(@alignCast(block)); +} +pub inline fn Mnewgco(L: *lua.State, comptime T: type, nsize: usize, memcat: u8) !*T { + return @ptrCast(@alignCast(try Mnewgco_(L, nsize, memcat))); +} + +pub fn Mfree_(L: *lua.State, block: ?*anyopaque, osize: usize, memcat: u8) void { + const g = L.global; + std.debug.assert((osize == 0) == (block == null)); + + const oclass = sizeclass(osize); + + if (oclass >= 0) + freeblock(L, @intCast(oclass), block.?) + else + _ = (g.frealloc.?)(g.ud, @ptrCast(block), osize, 0); + + g.totalbytes -= osize; + g.memcatbytes[memcat] -= osize; +} + +pub fn Mfreegco_(L: *lua.State, block: ?*lstate.GCObject, osize: usize, memcat: u8, page: *lua_Page) void { + const g = L.global; + std.debug.assert((osize == 0) == (block == null)); + + const oclass = sizeclass(osize); + + if (oclass >= 0) { + block.?.gch.header.tt = @intFromEnum(lua.Type.Nil); + + freegcoblock(L, @intCast(oclass), @ptrCast(@alignCast(block.?)), page); + } else { + std.debug.assert(page.busyBlocks == 1); + std.debug.assert(page.blockSize == osize); + std.debug.assert(@intFromPtr(block.?) == @intFromPtr(&page.data)); + + freepage(L, &g.allgcopages, page); + } + + g.totalbytes -= osize; + g.memcatbytes[memcat] -= osize; +} +pub inline fn Mfreegco(L: *lua.State, p: *lstate.GCObject, size: usize, memcat: u8, page: *lua_Page) void { + std.debug.assert(p.gch.header.tt >= @intFromEnum(lua.Type.String)); + Mfreegco_(L, p, size, memcat, page); +} + +pub fn Mrealloc_(L: *lua.State, block: ?*anyopaque, osize: usize, nsize: usize, memcat: u8) Error!?*anyopaque { + const g = L.global; + std.debug.assert((osize == 0) == (block == null)); + + const nclass = sizeclass(nsize); + const oclass = sizeclass(osize); + var result: ?*anyopaque = undefined; + + // if either block needs to be allocated using a block allocator, we can't use realloc directly + if (nclass >= 0 or oclass >= 0) { + result = if (nclass >= 0) + try newblock(L, @intCast(nclass)) + else + (g.frealloc.?)(g.ud, null, 0, nsize) orelse if (nsize > 0) return Error.OutOfMemory else null; + + if (osize > 0 and nsize > 0) { + const tsize = @min(osize, nsize); + @memcpy( + @as([*]u8, @ptrCast(@alignCast(result)))[0..tsize], + @as([*]u8, @ptrCast(@alignCast(block.?)))[0..tsize], + ); + } + + if (oclass >= 0) + freeblock(L, @intCast(oclass), block.?) + else + _ = (g.frealloc.?)(g.ud, block, osize, 0); + } else { + result = (g.frealloc.?)(g.ud, block, osize, nsize) orelse if (nsize > 0) return Error.OutOfMemory else null; + } + + std.debug.assert((nsize == 0) == (result == null)); + g.totalbytes = (g.totalbytes - osize) + nsize; + if (nsize < osize) + g.memcatbytes[memcat] -= osize - nsize + else + g.memcatbytes[memcat] += nsize - osize; + + if (g.cb.onallocate) |onallocate| { + @branchHint(.unlikely); + onallocate(L, osize, nsize); + } + + return result; +} + +pub inline fn Marraysize_(n: usize, e: usize) Error!usize { + if (n <= @divTrunc(std.math.maxInt(usize), e)) return n * e else return Error.BlockTooBig; +} +pub inline fn Mnewarray(L: *lua.State, comptime T: type, n: usize, memcat: u8) Error![*]T { + return @ptrCast(@alignCast(try Mnew_(L, try Marraysize_(n, @sizeOf(T)), memcat))); +} +pub inline fn Mfreearray(L: *lua.State, comptime T: type, b: ?[*]T, n: usize, memcat: u8) void { + Mfree_(L, @ptrCast(@alignCast(b)), n * @sizeOf(T), memcat); +} +pub inline fn Mreallocarray(L: *lua.State, comptime T: type, v: ?[*]T, oldn: usize, n: usize, memcat: u8) Error!?[*]T { + return @ptrCast(@alignCast((try Mrealloc_(L, @ptrCast(@alignCast(v)), oldn * @sizeOf(T), try Marraysize_(n, @sizeOf(T)), memcat)))); +} + +pub fn Mgetpagewalkinfo(page: *lua_Page, start: *[*]u8, end: *[*]u8, busyBlocks: *c_int, blockSize: *c_int) void { + const blockCount = @divTrunc(page.pageSize - @offsetOf(lua_Page, "data"), page.blockSize); + + std.debug.assert(page.freeNext >= -page.blockSize and page.freeNext <= (blockCount - 1) * page.blockSize); + + const data = @as([*]u8, @ptrCast(@alignCast(&page.data))); // silences ubsan when indexing page->data + + start.* = data[@intCast(page.freeNext + page.blockSize)..]; + end.* = data[@intCast(blockCount * page.blockSize)..]; + busyBlocks.* = page.busyBlocks; + blockSize.* = page.blockSize; +} + +pub fn Mgetpageinfo(page: *lua_Page, pageBlocks: *c_int, busyBlocks: *c_int, blockSize: *c_int, pageSize: *c_int) void { + pageBlocks.* = @divTrunc(page.pageSize - @offsetOf(lua_Page, "data"), page.blockSize); + busyBlocks.* = page.busyBlocks; + blockSize.* = page.blockSize; + pageSize.* = page.pageSize; +} + +pub fn Mgetnextpage(page: *lua_Page) ?*lua_Page { + return page.listnext; +} + +pub fn Mvisitpage( + page: *lua_Page, + comptime T: type, + context: T, + comptime visitor: fn (context: T, page: *lua_Page, gco: *lstate.GCObject) bool, +) void { + var start: [*]u8 = undefined; + var end: [*]u8 = undefined; + var busyBlocks: c_int = 0; + var blockSize: c_int = 0; + + Mgetpagewalkinfo(page, &start, &end, &busyBlocks, &blockSize); + + var pos: [*]u8 = start; + while (pos != end) : (pos += @as(u32, @intCast(blockSize))) { + const gco: *lstate.GCObject = @ptrCast(@alignCast(pos)); + + // skip memory blocks that are already freed + if (gco.gch.header.tt == @intFromEnum(lua.Type.Nil)) + continue; + + // when true is returned it means that the element was deleted + if (visitor(context, page, gco)) { + std.debug.assert(busyBlocks > 0); + busyBlocks -= 1; + + // if the last block was removed, page would be removed as well + if (busyBlocks == 0) + break; + } + } +} + +pub fn Mvisitgco( + L: *lua.State, + comptime T: type, + context: T, + comptime visitor: fn (context: T, page: *lua_Page, gco: *lstate.GCObject) bool, +) void { + const g = L.global; + + var curr: ?*lua_Page = g.allgcopages; + while (curr) |page| { + const next = page.listnext; // block visit might destroy the page + + Mvisitpage(page, T, context, visitor); + + curr = next; + } +} diff --git a/deps/luau/src/VM/lnumutils.zig b/deps/luau/src/VM/lnumutils.zig new file mode 100644 index 0000000..f310249 --- /dev/null +++ b/deps/luau/src/VM/lnumutils.zig @@ -0,0 +1,58 @@ +const std = @import("std"); +const c = @import("c"); + +const lua = @import("lua.zig"); + +pub inline fn inumisnan(x: anytype) bool { + comptime switch (@typeInfo(@TypeOf(x))) { + .comptime_float, .float => {}, + .comptime_int, .int => {}, + else => @compileError("Unsupported type"), + }; + return x != x; +} + +pub inline fn inumeq(a: anytype, b: f64) bool { + comptime switch (@typeInfo(@TypeOf(a))) { + .comptime_float, .comptime_int => return @as(f64, a) == b, + .float => return @as(f64, @floatCast(a)) == b, + .int => return @as(f64, @floatFromInt(a)) == b, + else => @compileError("Unsupported type"), + }; +} + +pub inline fn iveceq(a: []const f32, b: []const f32) bool { + if (comptime lua.config.VECTOR_SIZE == 4) + return a[0] == b[0] and a[1] == b[1] and a[2] == b[2] and a[3] == b[3] + else + return a[0] == b[0] and a[1] == b[1] and a[2] == b[2]; +} + +pub inline fn ivecisnan(x: []const f32) bool { + if (comptime lua.config.VECTOR_SIZE == 4) + return x[0] != x[0] or x[1] != x[1] or x[2] != x[2] or x[3] != x[3] + else + return x[0] != x[0] or x[1] != x[1] or x[2] != x[2]; +} + +pub inline fn inum2int(x: f64) i32 { + return @truncate(@as(i53, @intFromFloat(x))); +} + +pub const I_MAXNUM2STR = 48; + +pub fn printspecial(buf: []u8, sign: u1, fraction: u64) []u8 { + if (fraction == 0) { + const _inf = "-inf"; + const len = _inf.len - (1 - sign); + @memcpy(buf[0..len], _inf[1 - sign ..]); + return buf[0..len]; + } else { + @memcpy(buf[0..3], "nan"); + return buf[0..3]; + } +} + +pub fn inum2str(buf: []u8, x: f64) []u8 { + return std.fmt.bufPrint(buf, "{d}", .{x}) catch unreachable; +} diff --git a/deps/luau/src/VM/lobject.zig b/deps/luau/src/VM/lobject.zig new file mode 100644 index 0000000..81ad9d5 --- /dev/null +++ b/deps/luau/src/VM/lobject.zig @@ -0,0 +1,943 @@ +const c = @import("c"); +const std = @import("std"); + +const lgc = @import("lgc.zig"); +const lua = @import("lua.zig"); +const lstate = @import("lstate.zig"); +const lcommon = @import("lcommon.zig"); +const lnumutils = @import("lnumutils.zig"); + +const Errorset = @import("errorset.zig"); + +pub const CommonHeader = extern struct { + tt: u8, + marked: u8, + memcat: u8, +}; + +pub const GCheader = extern struct { + header: CommonHeader, + + pub inline fn ttype(this: *const GCheader) c_int { + return this.header.tt; + } +}; + +pub const Value = extern union { + gc: ?*lstate.GCObject, + p: ?*anyopaque, + n: f64, + b: c_int, + l: i64, + /// v[0], v[1] live here; v[2] lives in TValue::extra + v: [2]f32, +}; + +/// +/// Tagged Values +/// +pub const TValue = extern struct { + value: Value, + extra: [lua.config.EXTRA_SIZE]c_int = undefined, + tt: c_int, + + pub inline fn ttype(this: *const TValue) c_int { + return this.tt; + } + + pub inline fn typeOf(obj: *const TValue) lua.Type { + return @enumFromInt(obj.ttype()); + } + + pub inline fn ttisnil(obj: *const TValue) bool { + return obj.ttype() == @intFromEnum(lua.Type.Nil); + } + pub inline fn ttisnumber(obj: *const TValue) bool { + return obj.ttype() == @intFromEnum(lua.Type.Number); + } + pub inline fn ttisinteger(obj: *const TValue) bool { + return obj.ttype() == @intFromEnum(lua.Type.Integer); + } + pub inline fn ttisstring(obj: *const TValue) bool { + return obj.ttype() == @intFromEnum(lua.Type.String); + } + pub inline fn ttistable(obj: *const TValue) bool { + return obj.ttype() == @intFromEnum(lua.Type.Table); + } + pub inline fn ttisfunction(obj: *const TValue) bool { + return obj.ttype() == @intFromEnum(lua.Type.Function); + } + pub inline fn ttisboolean(obj: *const TValue) bool { + return obj.ttype() == @intFromEnum(lua.Type.Boolean); + } + pub inline fn ttisuserdata(obj: *const TValue) bool { + return obj.ttype() == @intFromEnum(lua.Type.Userdata); + } + pub inline fn ttisthread(obj: *const TValue) bool { + return obj.ttype() == @intFromEnum(lua.Type.Thread); + } + pub inline fn ttisbuffer(obj: *const TValue) bool { + return obj.ttype() == @intFromEnum(lua.Type.Buffer); + } + pub inline fn ttislightuserdata(obj: *const TValue) bool { + return obj.ttype() == @intFromEnum(lua.Type.LightUserdata); + } + pub inline fn ttisvector(obj: *const TValue) bool { + return obj.ttype() == @intFromEnum(lua.Type.Vector); + } + pub inline fn ttisupval(obj: *const TValue) bool { + return obj.ttype() == @intFromEnum(lua.Type.UpVal); + } + pub inline fn ttisclass(obj: *const TValue) bool { + return obj.ttype() == @intFromEnum(lua.Type.Class); + } + pub inline fn ttisobject(obj: *const TValue) bool { + return obj.ttype() == @intFromEnum(lua.Type.Object); + } + + pub inline fn obj2gco(obj: *TValue) *lstate.GCObject { + std.debug.assert(obj.iscollectable()); + return @ptrCast(@alignCast(obj)); + } + + pub inline fn gcvalue(obj: *const TValue) *lstate.GCObject { + std.debug.assert(obj.iscollectable()); + return obj.value.gc.?; + } + pub inline fn pvalue(obj: *const TValue) ?*anyopaque { + std.debug.assert(obj.ttislightuserdata()); + return obj.value.p; + } + pub inline fn nvalue(obj: *const TValue) f64 { + std.debug.assert(obj.ttisnumber()); + return obj.value.n; + } + pub inline fn lvalue(obj: *const TValue) i64 { + std.debug.assert(obj.ttisinteger()); + return obj.value.l; + } + pub inline fn vvalue(obj: *const TValue) []const f32 { + std.debug.assert(obj.ttisvector()); + return @as([*]const f32, @ptrCast(&obj.value.v))[0..lua.config.VECTOR_SIZE]; + } + pub inline fn tsvalue(obj: *const TValue) *TString { + std.debug.assert(obj.ttisstring()); + return &obj.value.gc.?.ts; + } + pub inline fn uvalue(obj: *const TValue) *Udata { + std.debug.assert(obj.ttisuserdata()); + return &obj.value.gc.?.u; + } + pub inline fn clvalue(obj: *const TValue) *Closure { + std.debug.assert(obj.ttisfunction()); + return &obj.value.gc.?.cl; + } + pub inline fn hvalue(obj: *const TValue) *LuaTable { + std.debug.assert(obj.ttistable()); + return &obj.value.gc.?.h; + } + pub inline fn bvalue(obj: *const TValue) bool { + std.debug.assert(obj.ttisboolean()); + return obj.value.b != 0; + } + pub inline fn thvalue(obj: *const TValue) *lstate.lua_State { + std.debug.assert(obj.ttisthread()); + return &obj.value.gc.?.th; + } + pub inline fn bufvalue(obj: *const TValue) *Buffer { + std.debug.assert(obj.ttisbuffer()); + return &obj.value.gc.?.buf; + } + pub inline fn upvalue(obj: *TValue) *UpVal { + std.debug.assert(obj.ttisupval()); + return &obj.value.gc.?.uv; + } + pub inline fn classvalue(obj: *const TValue) *LuauClass { + std.debug.assert(obj.ttisclass()); + return &obj.value.gc.?.lclass; + } + pub inline fn objectvalue(obj: *const TValue) *LuauObject { + std.debug.assert(obj.ttisobject()); + return &obj.value.gc.?.lobject; + } + pub inline fn svalue(obj: *const TValue) [*c]const u8 { + return obj.tsvalue().getstr(); + } + + pub inline fn l_isfalse(obj: *const TValue) bool { + return obj.ttisnil() or (obj.ttisboolean() and !obj.bvalue()); + } + + pub inline fn lightuserdatatag(obj: *const TValue) u8 { + std.debug.assert(obj.ttislightuserdata()); + return @intCast(obj.extra[0]); + } + + pub inline fn checkliveness(obj: *const TValue, g: *const lstate.global_State) void { + std.debug.assert(!obj.iscollectable() or ((obj.ttype() == obj.value.gc.?.gch.header.tt) and !lgc.isdead(g, obj.value.gc.?))); + } + + pub inline fn setnilvalue(obj: *TValue) void { + obj.settype(.Nil); + } + pub inline fn setnvalue(obj: *TValue, x: f64) void { + obj.value.n = x; + obj.settype(.Number); + } + pub inline fn setlvalue(obj: *TValue, x: i64) void { + obj.value.l = x; + obj.settype(.Integer); + } + pub inline fn setvvalue(obj: *TValue, x: f32, y: f32, z: f32, w: ?f32) void { + const i_v: [*]f32 = @ptrCast(&obj.value.v); + i_v[0] = x; + i_v[1] = y; + i_v[2] = z; + if (comptime lua.config.VECTOR_SIZE == 4) + i_v[3] = w orelse 0; + obj.settype(.Vector); + } + pub inline fn setpvalue(obj: *TValue, x: ?*anyopaque, tag: u32) void { + obj.value.p = x; + obj.extra[0] = @intCast(tag); + obj.settype(.LightUserdata); + } + pub inline fn setbvalue(obj: *TValue, x: bool) void { + obj.value.b = if (x) 1 else 0; + obj.settype(.Boolean); + } + pub inline fn setsvalue(obj: *TValue, L: *lstate.lua_State, x: *TString) void { + obj.value.gc = @ptrCast(@alignCast(x)); + obj.settype(.String); + obj.checkliveness(L.global); + } + pub inline fn setuvalue(obj: *TValue, L: *lstate.lua_State, x: *Udata) void { + obj.value.gc = @ptrCast(@alignCast(x)); + obj.settype(.Userdata); + obj.checkliveness(L.global); + } + pub inline fn setthvalue(obj: *TValue, L: *lstate.lua_State, x: *lstate.lua_State) void { + obj.value.gc = @ptrCast(@alignCast(x)); + obj.settype(.Thread); + obj.checkliveness(L.global); + } + pub inline fn setbufvalue(obj: *TValue, L: *lstate.lua_State, x: *Buffer) void { + obj.value.gc = @ptrCast(@alignCast(x)); + obj.settype(.Buffer); + obj.checkliveness(L.global); + } + pub inline fn setclvalue(obj: *TValue, L: *lstate.lua_State, x: *Closure) void { + obj.value.gc = @ptrCast(@alignCast(x)); + obj.settype(.Function); + obj.checkliveness(L.global); + } + pub inline fn sethvalue(obj: *TValue, L: *lstate.lua_State, x: *LuaTable) void { + obj.value.gc = @ptrCast(@alignCast(x)); + obj.settype(.Table); + obj.checkliveness(L.global); + } + pub inline fn setptvalue(obj: *TValue, L: *lstate.lua_State, x: *Proto) void { + obj.value.gc = @ptrCast(@alignCast(x)); + obj.settype(.Proto); + obj.checkliveness(L.global); + } + pub inline fn setupvalue(obj: *TValue, L: *lstate.lua_State, x: *UpVal) void { + obj.value.gc = @ptrCast(@alignCast(x)); + obj.settype(.UpVal); + obj.checkliveness(L.global); + } + pub inline fn setobj(obj: *TValue, L: *lstate.lua_State, o2: *const TValue) void { + obj.* = o2.*; + obj.checkliveness(L.global); + } + pub inline fn setclassvalue(obj: *TValue, L: *lstate.lua_State, x: *LuauClass) void { + obj.value.gc = @ptrCast(@alignCast(x)); + obj.settype(.Class); + obj.checkliveness(L.global); + } + pub inline fn setobjectvalue(obj: *TValue, L: *lstate.lua_State, x: *LuauObject) void { + obj.value.gc = @ptrCast(@alignCast(x)); + obj.settype(.Object); + obj.checkliveness(L.global); + } + + pub inline fn settype(obj: *TValue, t: lua.Type) void { + obj.tt = @intFromEnum(t); + } + pub inline fn iscollectable(o: *const TValue) bool { + return o.ttype() >= @intFromEnum(lua.Type.String); + } + + pub inline fn iscfunction(o: *const TValue) bool { + return o.ttype() == @intFromEnum(lua.Type.Function) and o.clvalue().isC != 0; + } + pub inline fn isLfunction(o: *const TValue) bool { + return o.ttype() == @intFromEnum(lua.Type.Function) and o.clvalue().isC == 0; + } +}; + +pub const LU_TAG_ITERATOR = lua.config.UTAG_LIMIT; + +pub inline fn checkliveness() void {} + +pub const StkId = [*]TValue; + +pub const TString = extern struct { + header: CommonHeader, + + // 1 byte padding + + atom: i16, + + // 2 byte padding + + /// next string in the hash table bucket + next: ?*TString, + + hash: c_uint, + len: c_uint, + + /// string data is allocated right after the header + data: [1]u8, + + pub inline fn obj2gco(obj: *TString) *lstate.GCObject { + return @ptrCast(@alignCast(obj)); + } + + pub inline fn gdata(s: *TString) [*]u8 { + return @ptrCast(@alignCast(&s.data)); + } + + pub inline fn getstr(s: *const TString) [*c]const u8 { + return @ptrCast(@alignCast(&s.data)); + } + + pub inline fn toSlice(s: *const TString) [:0]const u8 { + return s.getstr()[0..s.len :0]; + } +}; + +pub const Udata = extern struct { + header: CommonHeader, + + tag: u8, + + len: c_int, + + metatable: ?*LuaTable, + + data: [1]u8 align(8), + + pub inline fn obj2gco(obj: *Udata) *lstate.GCObject { + return @ptrCast(@alignCast(obj)); + } +}; + +pub const Buffer = extern struct { + header: CommonHeader, + + len: c_uint, + + data: [1]u8 align(8), + + pub inline fn obj2gco(obj: *Buffer) *lstate.GCObject { + return @ptrCast(@alignCast(obj)); + } +}; + +pub const FeedbackVectorSlotKind = enum(u8) { + CallTarget, +}; + +pub const FeedbackVectorSlot = extern struct { + kind: FeedbackVectorSlotKind, + data: extern union { + call_target: extern struct { + pc: u32, + proto: u32, + hits: u32, + }, + }, +}; + +/// +/// Function Prototypes +/// +pub const Proto = extern struct { + header: CommonHeader, + + /// number of upvalues + nups: u8, + numparams: u8, + is_vararg: u8, + maxstacksize: u8, + flags: u8, + + /// constants used by the function + k: ?[*]TValue, + /// function bytecode + code: ?[*]lcommon.Instruction, + /// functions defined inside the function + p: ?[*]?*Proto, + codeentry: ?*const lcommon.Instruction, + + execdata: ?*anyopaque, + exectarget: usize, + + lineinfo: ?[*]u8, // for each instruction, line number as a delta from baseline + abslineinfo: ?[*]u8, // baseline line info, one entry for each 1<= @intFromEnum(lua.Type.String); + } + + pub inline fn setnilvalue(obj: *TKey) void { + obj.pi.tt = @intFromEnum(lua.Type.Nil); + } + + pub inline fn next(this: *TKey) i28 { + return this.pi.next; + } +}; + +pub const LuaNode = extern struct { + val: TValue, + key: TKey, + + pub inline fn gkey(this: *LuaNode) *TKey { + return &this.key; + } + pub inline fn gval(this: *LuaNode) *TValue { + return &this.val; + } + pub inline fn gnext(this: *LuaNode) i28 { + return this.key.pi.next; + } + + pub inline fn add_num(this: *LuaNode, n: anytype) *LuaNode { + switch (@typeInfo(@TypeOf(n))) { + .comptime_int => return @ptrCast(@as([*]LuaNode, @ptrCast(this)) + @as(usize, @intCast(n))), + .int => |i| { + if (i.signedness == .unsigned) + return @ptrCast(@as([*]LuaNode, @ptrCast(this)) + @as(usize, @intCast(n))) + else { + if (n < 0) + return @ptrCast(@as([*]LuaNode, @ptrCast(this)) - @as(usize, @intCast(-n))) + else + return @ptrCast(@as([*]LuaNode, @ptrCast(this)) + @as(usize, @intCast(n))); + } + }, + else => @compileError("n must be an integer"), + } + } + + pub fn sub(this: *LuaNode, ptr: *LuaNode) isize { + if (@intFromPtr(ptr) > @intFromPtr(this)) + return -@as(isize, @intCast(@as([*]LuaNode, @ptrCast(ptr)) - @as([*]LuaNode, @ptrCast(this)))); + return @intCast(@as([*]LuaNode, @ptrCast(this)) - @as([*]LuaNode, @ptrCast(ptr))); + } +}; + +pub inline fn setnodekey(L: *lstate.lua_State, node: *LuaNode, obj: *const TValue) void { + node.key.value = obj.value; + @memcpy(node.key.extra[0..lua.config.EXTRA_SIZE], obj.extra[0..lua.config.EXTRA_SIZE]); + node.key.pi.tt = @intCast(obj.tt); + obj.checkliveness(L.global); +} + +pub inline fn getnodekey(L: *lstate.lua_State, obj: *TValue, node: *const LuaNode) void { + obj.value = node.key.value; + @memcpy(obj.extra[0..lua.config.EXTRA_SIZE], node.key.extra[0..lua.config.EXTRA_SIZE]); + obj.tt = @intCast(node.key.pi.tt); + obj.checkliveness(L.global); +} + +pub const LuaTable = extern struct { + header: CommonHeader, + + /// 1<

= 256) { + l += 8; + x >>= 8; + } + return l + log_2[x]; +} + +pub fn OrawequalObj(t1: *const TValue, t2: *const TValue) bool { + if (t1.ttype() != t2.ttype()) + return false; + + switch (t1.typeOf()) { + .None => unreachable, + .Nil => return true, + .Number => return t1.nvalue() == t2.nvalue(), + .Integer => return t1.lvalue() == t2.lvalue(), + .Vector => return lnumutils.iveceq(t1.vvalue(), t2.vvalue()), + .Boolean => return t1.bvalue() == t2.bvalue(), + .LightUserdata => return t1.pvalue() == t2.pvalue() and t1.lightuserdatatag() == t2.lightuserdatatag(), + inline else => |t| { + comptime std.debug.assert(t.istypecollectable()); + return t1.gcvalue() == t2.gcvalue(); + }, + } +} + +pub fn OrawequalKey(t1: *const TKey, t2: *const TValue) bool { + if (t1.ttype() != t2.ttype()) + return false; + + switch (t1.typeOf()) { + .None => unreachable, + .Nil => return true, + .Number => return t1.nvalue() == t2.nvalue(), + .Integer => return t1.lvalue() == t2.lvalue(), + .Vector => return lnumutils.iveceq(t1.vvalue(), t2.vvalue()), + .Boolean => return t1.bvalue() == t2.bvalue(), + .LightUserdata => return t1.pvalue() == t2.pvalue() and t1.lightuserdatatag() == t2.lightuserdatatag(), + inline else => |t| { + comptime std.debug.assert(t.istypecollectable()); + return t1.gcvalue() == t2.gcvalue(); + }, + } +} + +pub fn Opushvfstring(L: *lua.State, comptime fmt: []const u8, args: anytype) Errorset.Table!void { + var buf: [lua.config.BUFFERSIZE]u8 = undefined; + const fstr = std.fmt.bufPrint(&buf, fmt, args) catch |err| @panic(@errorName(err)); + try L.pushlstring(fstr); +} + +pub inline fn Opushfstring(L: *lua.State, comptime fmt: []const u8, args: anytype) Errorset.Table!void { + try Opushvfstring(L, fmt, args); +} + +// pub fn Ochunkid(out: []u8, comptime source: []const u8) []u8 { +// c.luaO +// } + +test "size match" { + const Sizes = struct { + extern "c" const GCObject_size: u8; + extern "c" const GCheader_size: u8; + extern "c" const Value_size: u8; + extern "c" const TValue_size: u8; + extern "c" const TString_size: u8; + extern "c" const Udata_size: u8; + extern "c" const LuauBuffer_size: u8; + extern "c" const Proto_size: u8; + extern "c" const LocVar_size: u8; + extern "c" const UpVal_size: u8; + extern "c" const Closure_size: u8; + extern "c" const TKey_size: u8; + extern "c" const LuaNode_size: u8; + extern "c" const LuaTable_size: u8; + extern "c" const LuauClass_size: u8; + extern "c" const LuauObject_size: u8; + + extern "c" const TString_data_offset: u8; + extern "c" const Udata_data_offset: u8; + extern "c" const LuauBuffer_data_offset: u8; + }; + + try std.testing.expect(Sizes.GCObject_size == @sizeOf(lstate.GCObject)); + try std.testing.expect(Sizes.GCheader_size == @sizeOf(GCheader)); + try std.testing.expect(Sizes.Value_size == @sizeOf(Value)); + try std.testing.expect(Sizes.TValue_size == @sizeOf(TValue)); + try std.testing.expect(Sizes.TString_size == @sizeOf(TString)); + try std.testing.expect(Sizes.Udata_size == @sizeOf(Udata)); + try std.testing.expect(Sizes.LuauBuffer_size == @sizeOf(Buffer)); + try std.testing.expect(Sizes.Proto_size == @sizeOf(Proto)); + try std.testing.expect(Sizes.LocVar_size == @sizeOf(LocVar)); + try std.testing.expect(Sizes.UpVal_size == @sizeOf(UpVal)); + try std.testing.expect(Sizes.Closure_size == @sizeOf(Closure)); + try std.testing.expect(Sizes.TKey_size == @sizeOf(TKey)); + try std.testing.expect(Sizes.LuaNode_size == @sizeOf(LuaNode)); + try std.testing.expect(Sizes.LuaTable_size == @sizeOf(LuaTable)); + try std.testing.expect(Sizes.LuauClass_size == @sizeOf(LuauClass)); + try std.testing.expect(Sizes.LuauObject_size == @sizeOf(LuauObject)); + + try std.testing.expect(Sizes.TString_data_offset == @offsetOf(TString, "data")); + try std.testing.expect(Sizes.Udata_data_offset == @offsetOf(Udata, "data")); + try std.testing.expect(Sizes.LuauBuffer_data_offset == @offsetOf(Buffer, "data")); +} diff --git a/deps/luau/src/VM/loslib.zig b/deps/luau/src/VM/loslib.zig new file mode 100644 index 0000000..bb87bc9 --- /dev/null +++ b/deps/luau/src/VM/loslib.zig @@ -0,0 +1,7 @@ +const c = @import("c"); + +const lua = @import("lua.zig"); + +pub inline fn open(L: *lua.State) void { + _ = c.luaopen_os(@ptrCast(L)); +} diff --git a/deps/luau/src/VM/lperf.zig b/deps/luau/src/VM/lperf.zig new file mode 100644 index 0000000..1a7bb91 --- /dev/null +++ b/deps/luau/src/VM/lperf.zig @@ -0,0 +1,9 @@ +const c = @import("c"); + +const lua = @import("lua.zig"); + +extern "c" fn lua_clock() f64; + +pub fn clock() f64 { + return lua_clock(); +} diff --git a/deps/luau/src/VM/lstate.zig b/deps/luau/src/VM/lstate.zig new file mode 100644 index 0000000..16add29 --- /dev/null +++ b/deps/luau/src/VM/lstate.zig @@ -0,0 +1,1011 @@ +const c = @import("c"); +const std = @import("std"); + +const build_config = @import("config"); + +const zapi = @import("zapi.zig"); + +const ldo = @import("ldo.zig"); +const lgc = @import("lgc.zig"); +const ltm = @import("ltm.zig"); +const lua = @import("lua.zig"); +const lapi = @import("lapi.zig"); +const laux = @import("laux.zig"); +const linit = @import("linit.zig"); +const lmem = @import("lmem.zig"); +const lfunc = @import("lfunc.zig"); +const ltable = @import("ltable.zig"); +const ludata = @import("ludata.zig"); +const ldebug = @import("ldebug.zig"); +const lstring = @import("lstring.zig"); +const lcommon = @import("lcommon.zig"); +const lobject = @import("lobject.zig"); +const lvmload = @import("lvmload.zig"); + +const lbaselib = @import("lbaselib.zig"); +const lcorolib = @import("lcorolib.zig"); +const ltablib = @import("ltablib.zig"); +const loslib = @import("loslib.zig"); +const lstrlib = @import("lstrlib.zig"); +const lmathlib = @import("lmathlib.zig"); +const ldblib = @import("ldblib.zig"); +const lutf8lib = @import("lutf8lib.zig"); +const lbitlib = @import("lbitlib.zig"); +const lbuflib = @import("lbuflib.zig"); +const lveclib = @import("lveclib.zig"); + +const Errorset = @import("errorset.zig"); + +const state = @This(); + +// extra stack space to handle TM calls and some other extras +pub const EXTRA_STACK = 5; + +pub const BASIC_CI_SIZE = 8; + +pub const BASIC_STACK_SIZE = 2 * lua.config.MINSTACK; + +/// +/// Main thread combines a thread state and the global state +/// +pub const LG = extern struct { + l: lua_State, + g: global_State, +}; + +const stringtable = extern struct { + hash: ?[*]?*lobject.TString, + /// number of elements + nuse: u32, + size: c_int, +}; + +/// +/// informations about a call +/// +/// the general Lua stack frame structure is as follows: +/// - each function gets a stack frame, with function "registers" being stack slots on the frame +/// - function arguments are associated with registers 0+ +/// - function locals and temporaries follow after; usually locals are a consecutive block per scope, and temporaries are allocated after this, but +/// this is up to the compiler +/// +/// when function doesn't have varargs, the stack layout is as follows: +/// ^ (func) ^^ [fixed args] [locals + temporaries] +/// where ^ is the 'func' pointer in CallInfo struct, and ^^ is the 'base' pointer (which is what registers are relative to) +/// +/// when function *does* have varargs, the stack layout is more complex - the runtime has to copy the fixed arguments so that the 0+ addressing still +/// works as follows: +/// ^ (func) [fixed args] [varargs] ^^ [fixed args] [locals + temporaries] +/// +/// computing the sizes of these individual blocks works as follows: +/// - the number of fixed args is always matching the `numparams` in a function's Proto lobject; runtime adds `nil` during the call execution as +/// necessary +/// - the number of variadic args can be computed by evaluating (ci->base - ci->func - 1 - numparams) +/// +/// the CallInfo structures are allocated as an array, with each subsequent call being *appended* to this array (so if f calls g, CallInfo for g +/// immediately follows CallInfo for f) +/// the `nresults` field in CallInfo is set by the caller to tell the function how many arguments the caller is expecting on the stack after the +/// function returns +/// the `flags` field in CallInfo contains internal execution flags that are important for pcall/etc, see LUA_CALLINFO_* +/// +pub const CallInfo = extern struct { + /// base for this function + base: lobject.StkId, + /// function index in the stack + func: lobject.StkId, + /// top for this function + top: lobject.StkId, + p: ?*lobject.Proto, + savedpc: extern union { + inst: ?*const lcommon.Instruction, + errfunc: i32, + }, + + /// expected number of results from this function + nresults: c_int, + /// call frame flags, see LUA_CALLINFO_* + flags: c_uint, + + pub inline fn sub(this: *CallInfo, ptr: *CallInfo) usize { + return @divExact(@intFromPtr(this) - @intFromPtr(ptr), @sizeOf(CallInfo)); + } + + pub inline fn ci_func(this: *CallInfo) *lobject.Closure { + return this.func[0].clvalue(); + } + + pub inline fn isLua(this: *CallInfo) bool { + return this.func[0].ttisfunction() and this.ci_func().isC != 1; + } +}; + +// should the interpreter return after returning from this callinfo? first frame must have this set +pub const CALLINFO_RETURN = 1 << 0; +// should the error thrown during execution get handled by continuation from this callinfo? func must be C +pub const CALLINFO_HANDLE = 1 << 1; +// should this function be executed using execution callback for native code +pub const CALLINFO_NATIVE = 1 << 2; + +const GCStats = extern struct { + // data for proportional-integral controller of heap trigger value + triggerterms: [32]i32 = std.mem.zeroes([32]i32), + triggertermpos: u32 = 0, + triggerintegral: i32 = 0, + + atomicstarttotalsizebytes: usize = 0, + endtotalsizebytes: usize = 0, + heapgoalsizebytes: usize = 0, + + starttimestamp: f64 = 0, + atomicstarttimestamp: f64 = 0, + endtimestamp: f64 = 0, +}; + +const GCCycleMetrics = extern struct { + starttotalsizebytes: usize = 0, + heaptriggersizebytes: usize = 0, + + pausetime: f64 = 0.0, // time from end of the last cycle to the start of a new one + + starttimestamp: f64 = 0.0, + endtimestamp: f64 = 0.0, + + marktime: f64 = 0.0, + markassisttime: f64 = 0.0, + markmaxexplicittime: f64 = 0.0, + markexplicitsteps: usize = 0, + markwork: usize = 0, + + atomicstarttimestamp: f64 = 0.0, + atomicstarttotalsizebytes: usize = 0, + atomictime: f64 = 0.0, + + // specific atomic stage parts + atomictimeupval: f64 = 0.0, + atomictimeweak: f64 = 0.0, + atomictimegray: f64 = 0.0, + atomictimeclear: f64 = 0.0, + + sweeptime: f64 = 0.0, + sweepassisttime: f64 = 0.0, + sweepmaxexplicittime: f64 = 0.0, + sweepexplicitsteps: usize = 0, + sweepwork: usize = 0, + + assistwork: usize = 0, + explicitwork: usize = 0, + + propagatework: usize = 0, + propagateagainwork: usize = 0, + + endtotalsizebytes: usize = 0, +}; + +const GCMetrics = extern struct { + stepexplicittimeacc: f64 = 0.0, + stepassisttimeacc: f64 = 0.0, + + /// when cycle is completed, last cycle values are updated + completedcycles: u64 = 0, + + lastcycle: GCCycleMetrics, + currcycle: GCCycleMetrics, +}; + +const ExecutionCallbacks = extern struct { + context: ?*anyopaque = null, + /// gets called when a function is created + close: ?*const fn (L: *lua_State) callconv(.c) void = null, + /// gets called when a function is destroyed + destroy: ?*const fn (L: *lua_State, proto: *anyopaque) callconv(.c) void = null, + /// gets called when a function is about to start/resume (when execdata is present), return 0 to exit VM + enter: ?*const fn (L: *lua_State, proto: *anyopaque) callconv(.c) c_int = null, + /// gets called when a function has to be switched from native to bytecode in the debugger + disable: ?*const fn (L: *lua_State, proto: *anyopaque) callconv(.c) void = null, + /// gets called to request the size of memory associated with native part of the Proto + getmemorysize: ?*const fn (L: *lua_State, proto: *anyopaque) callconv(.c) usize = null, + /// gets called to get the userdata type index + gettypemapping: ?*const fn (L: *lua_State, str: [*c]const u8, len: usize) callconv(.c) u8 = null, + /// called to get the execution counter data and count {uint32_t, uint32_t, uint64_t} + getcounterdata: ?*const fn (L: *lua_State, proto: *anyopaque, count: *usize) callconv(.c) [*]const u8 = null, + /// called when inlining threshold is reached + inlinefunction: ?*const fn (L: *lua_State, caller: *lobject.Closure, target: *lobject.Closure, pc: u32) callconv(.c) *lobject.Proto = null, +}; + +const UdataDirectAccessData = extern struct { + // NOTE: experimental API and is subject to breaking changes + // registration of callbacks for direct userdata __index, __newindex and __namecall access with string keys assigned with an atom + // cachedslot is initially 0 and can be set to a custom value to help with data lookup inside the userdata + // IMPORTANT: cachedslot values are shared between all userdata, callbacks function of one userdata tag has to correctly handle values set by another + pub const Access = fn (L: *lua_State, data: *anyopaque, atom: c_int, cachedslot: *u16, utag: c_int) callconv(.c) void; + pub const Namecall = fn (L: *lua_State, data: *anyopaque, atom: c_int, cachedslot: *u16, utag: c_int) callconv(.c) c_int; + + indextm: lobject.TValue, + newindextm: lobject.TValue, + namecalltm: lobject.TValue, + index: ?*const Access, + newindex: ?*const Access, + namecall: ?*const Namecall, +}; + +pub const global_State = extern struct { + /// hash table for strings + strt: stringtable, + + /// function to reallocate memory + frealloc: ?lua.Alloc, + /// auxiliary data to `frealloc' + ud: ?*anyopaque, + + currentwhite: u8, + /// state of garbage collector + gcstate: u8, + + /// list of gray objects + gray: ?*GCObject, + /// list of objects to be traversed atomically + grayagain: ?*GCObject, + /// list of weak tables (to be cleared) + weak: ?*GCObject, + + /// when totalbytes >= GCthreshold, run GC step + GCthreshold: usize, + /// number of bytes currently allocated + totalbytes: usize, + /// see LUAI_GCGOAL + gcgoal: c_int, + /// see LUAI_GCSTEPMUL + gcstepmul: c_int, + /// see LUAI_GCSTEPSIZE + gcstepsize: c_int, + + /// free page linked list for each size class for non-collectable objects + freepages: [lua.config.SIZECLASSES]?*lmem.lua_Page, + /// free page linked list for each size class for collectable objects + freegcopages: [lua.config.SIZECLASSES]?*lmem.lua_Page, + /// page linked list with all pages for all non-collectable lobject classes (available with LUAU_ASSERTENABLED) + allpages: ?*lmem.lua_Page, + /// page linked list with all pages for all collectable lobject classes + allgcopages: ?*lmem.lua_Page, + /// position of the sweep in `allgcopages' + sweepgcopage: ?*lmem.lua_Page, + + mainthread: *lua_State, + /// head of double-linked list of all open upvalues + uvhead: lobject.UpVal, + /// metatables for basic types + mt: [lua.Type.T_COUNT]?*lobject.LuaTable, + /// names for basic types + ttname: [lua.Type.T_COUNT]*lobject.TString, + /// array with tag-method names + tmname: [ltm.N]*lobject.TString, + + /// storage for temporary values used in pseudo2addr + pseudotemp: lobject.TValue, + + /// registry table, used by lua_ref and LUA_REGISTRYINDEX + registry: lobject.TValue, + /// next free slot in registry + registryfree: c_int, + + /// jump buffer data for longjmp-style error handling + errorjmp: ?*anyopaque, + + /// PCG random number generator state + rngstate: u64, + /// pointer encoding key for display + ptrenckey: [4]u64, + + cb: lua.Callbacks, + + ecb: ExecutionCallbacks, + + ecbdata: [lua.config.EXECUTION_CALLBACK_STORAGE]u8 align(16), + + /// Set of userdata __index/__newindex/__namecall metamethods for a direct access + udatadirect: [ludata.UTAG_INTERNAL_LIMIT]UdataDirectAccessData, + + /// total amount of memory used by each memory category + memcatbytes: [lua.config.MEMORY_CATEGORIES]usize, + + /// for each userdata tag, a gc callback to be called immediately before freeing memory + udatagc: [lua.config.UTAG_LIMIT]?*const fn (*lua_State, ?*anyopaque) callconv(.c) void, + /// metatables for tagged userdata + udatamt: [lua.config.UTAG_LIMIT]?*lobject.LuaTable, + + /// names for tagged lightuserdata + lightuserdataname: [lua.config.LUTAG_LIMIT]?*lobject.TString, + + // per-tag direct field dispatch tables; NULL until first field is registered for that tag + udatadirectfields: [ludata.UTAG_INTERNAL_LIMIT]?*lobject.LuaTable, + + gcstats: GCStats, + lastprotoid: u32, + + /// TODO: change `false` to be based on configuration (LUAI_GCMETRICS) + gcmetrics: if (false) GCMetrics else void, +}; + +pub const lua_State = extern struct { + header: lobject.CommonHeader, + + curr_status: u8, + + /// memory category that is used for new GC lobject allocations + activememcat: u8, + + /// thread is currently executing, stack may be mutated without barriers + isactive: bool, + /// call debugstep hook after each instruction + singlestep_on: bool, + + /// first free slot in the stack + top: lobject.StkId, + /// base of current function + base: lobject.StkId, + global: *global_State, + /// call info for current function + ci: ?[*]CallInfo, + /// last free slot in the stack + stack_last: lobject.StkId, + /// stack base + stack: [*]lobject.TValue, + + /// points after end of ci array + end_ci: ?[*]CallInfo, + /// array of CallInfo's + base_ci: ?[*]CallInfo, + + stacksize: c_int, + /// size of array `base_ci' + size_ci: c_int, + + /// number of nested C calls + nCcalls: u16, + /// nested C calls when resuming coroutine + baseCcalls: u16, + + /// when table operations or INDEX/NEWINDEX is invoked from Luau, what is the expected slot for lookup? + cachedslot: c_int, + + /// table of globals + gt: ?*lobject.LuaTable, + /// list of open upvalues in this stack + openupval: ?*lobject.UpVal, + gclist: ?*GCObject, + + /// when invoked from Luau using NAMECALL, what method do we need to invoke? + namecall: ?*lobject.TString, + + userdata: ?*anyopaque, + + pub inline fn registry(L: *lua_State) *lobject.TValue { + return &L.global.registry; + } + + pub inline fn curr_func(L: *lua_State) *lobject.Closure { + return L.ci.?[0].func[0].clvalue(); + } + + // pub const api_incr_top = lapi.api_incr_top; + // pub const api_check = lapi.check; + // pub const api_checknelems = lapi.checknelems; + + // + // state manipulation + // + pub const close = state.close; + pub const newthread = lapi.newthread; + pub const mainthread = lapi.mainthread; + pub const resetthread = state.resetthread; + pub const isthreadreset = state.isthreadreset; + + // + // basic stack manipulation + // + pub const absindex = lapi.absindex; + pub const gettop = lapi.gettop; + pub const settop = lapi.settop; + pub const pop = lapi.pop; + pub const pushvalue = lapi.pushvalue; + pub const remove = lapi.remove; + pub const insert = lapi.insert; + pub const replace = lapi.replace; + pub const checkstack = lapi.checkstack; + pub const rawcheckstack = lapi.rawcheckstack; + + pub const xmove = lapi.xmove; + pub const xpush = lapi.xpush; + + // + // access functions (stack -> C) + // + pub const isnumber = lapi.isnumber; + pub const isstring = lapi.isstring; + pub const isinteger64 = lapi.isinteger64; + pub const iscfunction = lapi.iscfunction; + pub const isLfunction = lapi.isLfunction; + pub const isuserdata = lapi.isuserdata; + pub const @"type" = lapi.type; + pub const isfunction = lapi.isfunction; + pub const istable = lapi.istable; + pub const islightuserdata = lapi.islightuserdata; + pub const isnone = lapi.isnone; + pub const isnil = lapi.isnil; + pub const isboolean = lapi.isboolean; + pub const isvector = lapi.isvector; + pub const isthread = lapi.isthread; + pub const isbuffer = lapi.isbuffer; + pub const isnoneornil = lapi.isnoneornil; + pub const isclass = lapi.isclass; + pub const isobject = lapi.isobject; + pub const typeOf = lapi.typeOf; + pub const typename = lapi.typename; + + pub const equal = lapi.equal; + pub const rawequal = lapi.rawequal; + pub const lessthan = lapi.lessthan; + + pub const tonumberx = lapi.tonumberx; + pub const tonumber = lapi.tonumber; + pub const tointegerx = lapi.tointegerx; + pub const tointeger = lapi.tointeger; + pub const tounsignedx = lapi.tounsignedx; + pub const tounsigned = lapi.tounsigned; + pub const tovector = lapi.tovector; + pub const toboolean = lapi.toboolean; + pub const tointeger64 = lapi.tointeger64; + pub const tolstring = lapi.tolstring; + pub const tostring = lapi.tostring; + pub const namecallatom = lapi.namecallatom; + pub const namecallstr = lapi.namecallstr; + pub const objlen = lapi.objlen; + pub const strlen = lapi.strlen; + pub const tocfunction = lapi.tocfunction; + pub const tolightuserdata = lapi.tolightuserdata; + pub const tolightuserdatatagged = lapi.tolightuserdatatagged; + pub const touserdata = lapi.touserdata; + pub const touserdatatagged = lapi.touserdatatagged; + pub const userdatatag = lapi.userdatatag; + pub const lightuserdatatag = lapi.lightuserdatatag; + pub const tothread = lapi.tothread; + pub const tobuffer = lapi.tobuffer; + pub const topointer = lapi.topointer; + + // + // push functions (C -> stack) + // + pub const pushnil = lapi.pushnil; + pub const pushnumber = lapi.pushnumber; + pub const pushinteger = lapi.pushinteger; + pub const pushinteger64 = lapi.pushinteger64; + pub const pushunsigned = lapi.pushunsigned; + pub const pushvector = lapi.pushvector; + pub const pushlstring = lapi.pushlstring; + pub const pushstring = lapi.pushstring; + pub const pushvfstring = lapi.pushvfstring; + pub const pushfstring = lapi.pushfstring; + pub const pushcclosurek = lapi.pushcclosurek; + pub const pushcfunction = lapi.pushcfunction; + pub const pushcclosure = lapi.pushcclosure; + pub const pushboolean = lapi.pushboolean; + pub const pushthread = lapi.pushthread; + + pub const pushlightuserdatatagged = lapi.pushlightuserdatatagged; + pub const pushlightuserdata = lapi.pushlightuserdata; + pub const newuserdatatagged = lapi.newuserdatatagged; + pub const newuserdata = lapi.newuserdata; + pub const newuserdatataggedwithmetatable = lapi.newuserdatataggedwithmetatable; + pub const newuserdatadtor = lapi.newuserdatadtor; + + pub const newbuffer = lapi.newbuffer; + + // + // get functions (Lua -> stack) + // + pub const gettable = lapi.gettable; + pub const getfield = lapi.getfield; + pub const getglobal = lapi.getglobal; + pub const rawgetfield = lapi.rawgetfield; + pub const rawgetglobal = lapi.rawgetglobal; + pub const rawget = lapi.rawget; + pub const rawgeti = lapi.rawgeti; + pub const getref = lapi.getref; + pub const createtable = lapi.createtable; + pub const newtable = lapi.newtable; + + pub const setreadonly = lapi.setreadonly; + pub const getreadonly = lapi.getreadonly; + pub const setsafeenv = lapi.setsafeenv; + + pub const getmetatable = lapi.getmetatable; + pub const getfenv = lapi.getfenv; + + // + // set functions (stack -> Lua) + // + pub const settable = lapi.settable; + pub const setfield = lapi.setfield; + pub const setglobal = lapi.setglobal; + pub const rawsetfield = lapi.rawsetfield; + pub const rawsetglobal = lapi.rawsetglobal; + pub const rawset = lapi.rawset; + pub const rawseti = lapi.rawseti; + pub const setmetatable = lapi.setmetatable; + pub const setfenv = lapi.setfenv; + + // + // `load' and `call' functions (load and run Luau bytecode) + // + pub const call = lapi.call; + pub const pcall = lapi.pcall; + pub const cpcall = lapi.cpcall; + + // + // coroutine functions + // + pub const yield = ldo.yield; + pub const @"break" = ldo.@"break"; + pub const resumethread = ldo.@"resume"; + pub const resumeerror = ldo.resumeerror; + pub const status = lapi.status; + pub const isyieldable = ldo.isyieldable; + pub const getthreaddata = lapi.getthreaddata; + pub const setthreaddata = lapi.setthreaddata; + pub const costatus = lapi.costatus; + + // + // garbage-collection function and options + // + pub const gc = lapi.gc; + + // + // memory statistics + // all allocated bytes are attributed to the memory category of the running thread (0..LUA_MEMORY_CATEGORIES-1) + // + pub const setmemcat = lapi.setmemcat; + pub const totalbytes = lapi.totalbytes; + + // + // miscellaneous functions + // + pub const raiseerror = lapi.@"error"; + + pub const next = lapi.next; + pub const rawiter = lapi.rawiter; + + pub const concat = lapi.concat; + + pub const getupvalue = lapi.getupvalue; + pub const setupvalue = lapi.setupvalue; + pub const ref = lapi.ref; + pub const unref = lapi.unref; + pub const setuserdatatag = lapi.setuserdatatag; + pub const setuserdatadtor = lapi.setuserdatadtor; + pub const getuserdatadtor = lapi.getuserdatadtor; + pub const setuserdatametatable = lapi.setuserdatametatable; + pub const getuserdatametatable = lapi.getuserdatametatable; + pub const setlightuserdataname = lapi.setlightuserdataname; + pub const getlightuserdataname = lapi.getlightuserdataname; + pub const clonefunction = lapi.clonefunction; + pub const cleartable = lapi.cleartable; + pub const clonetable = lapi.clonetable; + pub const callbacks = lapi.callbacks; + pub const getallocf = lapi.getallocf; + + // lapi + pub const Atoobject = lapi.Atoobject; + pub const Apushobject = lapi.Apushobject; + + // laux + pub const LargerrorL = laux.LargerrorL; + pub const Largerror = laux.Largerror; + pub const Largcheck = laux.Largcheck; + pub const LtypeerrorL = laux.LtypeerrorL; + pub const Lwhere = laux.Lwhere; + pub const LerrorL = laux.LerrorL; + pub const Lcheckoption = laux.Lcheckoption; + pub const Lnewmetatable = laux.Lnewmetatable; + pub const Lgetmetatable = laux.Lgetmetatable; + pub const Lcheckudata = laux.Lcheckudata; + pub const Lcheckbuffer = laux.Lcheckbuffer; + pub const Lcheckstack = laux.Lcheckstack; + pub const Lchecktype = laux.Lchecktype; + pub const Lcheckany = laux.Lcheckany; + pub const Lchecklstring = laux.Lchecklstring; + pub const Lcheckstring = laux.Lcheckstring; + pub const Loptlstring = laux.Loptlstring; + pub const Loptstring = laux.Loptstring; + pub const Lchecknumber = laux.Lchecknumber; + pub const Loptnumber = laux.Loptnumber; + pub const Lcheckboolean = laux.Lcheckboolean; + pub const Loptboolean = laux.Loptboolean; + pub const Lcheckinteger = laux.Lcheckinteger; + pub const Loptinteger = laux.Loptinteger; + pub const Lcheckinteger64 = laux.Lcheckinteger64; + pub const Loptinteger64 = laux.Loptinteger64; + pub const Lcheckunsigned = laux.Lcheckunsigned; + pub const Loptunsigned = laux.Loptunsigned; + pub const Lcheckvector = laux.Lcheckvector; + pub const Loptvector = laux.Loptvector; + pub const Lgetmetafield = laux.Lgetmetafield; + pub const Lcallmeta = laux.Lcallmeta; + pub const Lregister = laux.Lregister; + pub const Lfindtable = laux.Lfindtable; + pub const Ltypename = laux.Ltypename; + pub const Lcallyieldable = laux.Lcallyieldable; + pub const Ltolstring = laux.Ltolstring; + + // ldebug + pub const getargument = ldebug.getargument; + pub const getlocal = ldebug.getlocal; + pub const setlocal = ldebug.setlocal; + pub const stackdepth = ldebug.stackdepth; + pub const getinfo = ldebug.getinfo; + pub const Gisnative = ldebug.Gisnative; + pub const singlestep = ldebug.singlestep; + pub const breakpoint = ldebug.breakpoint; + pub const getcoverage = ldebug.getcoverage; + pub const debugtrace = ldebug.debugtrace; + + // linit + pub const Lopenlibs = linit.Lopenlibs; + pub const Lsandbox = linit.Lsandbox; + pub const Lsandboxthread = linit.Lsandboxthread; + + // lobject + pub const Opushfstring = lobject.pushfstring; + + // lvmload + pub const load = lvmload.load; + + // libraries + pub const openbase = lbaselib.open; + pub const opencoroutine = lcorolib.open; + pub const opentable = ltablib.open; + pub const openos = loslib.open; + pub const openstring = lstrlib.open; + pub const openmath = lmathlib.open; + pub const opendebug = ldblib.open; + pub const openutf8 = lutf8lib.open; + pub const openbit32 = lbitlib.open; + pub const openbuffer = lbuflib.open; + pub const openvector = lveclib.open; + + // zig api + pub const Zpushfunction = zapi.Zpushfunction; + pub const Zpushclosure = zapi.Zpushclosure; + pub const Zpushclosurek = zapi.Zpushclosurek; + pub const ZpushfunctionV = zapi.ZpushfunctionV; + pub const Zpushvalue = zapi.Zpushvalue; + pub const Zsetfield = zapi.Zsetfield; + pub const Zsetfieldfn = zapi.Zsetfieldfn; + pub const ZsetfieldfnV = zapi.ZsetfieldfnV; + pub const Zsetglobal = zapi.Zsetglobal; + pub const Zsetglobalfn = zapi.Zsetglobalfn; + pub const ZsetglobalfnV = zapi.ZsetglobalfnV; + pub const Zpushbuffer = zapi.Zpushbuffer; + pub const Zresumeerror = zapi.Zresumeerror; + pub const Zresumeferror = zapi.Zresumeferror; + pub const Zerror = zapi.Zerror; + pub const Zerrorf = zapi.Zerrorf; + pub const Ztolstring = zapi.Ztolstring; + pub const Ztolstringk = zapi.Ztolstringk; + pub const Zcallmeta = zapi.Zcallmeta; + pub const Zchecktype = zapi.Zchecktype; + pub const Zcheckvalue = zapi.Zcheckvalue; + pub const Zcheckfield = zapi.Zcheckfield; + pub const Zcheckstack = zapi.Zcheckstack; + pub const Zyielderror = zapi.Zyielderror; + pub const Znewmetatable = zapi.Znewmetatable; + + pub inline fn deinit(L: *lua_State) void { + L.close(); + } +}; + +pub const GCObject = extern union { + gch: lobject.GCheader, + ts: lobject.TString, + u: lobject.Udata, + cl: lobject.Closure, + h: lobject.LuaTable, + p: lobject.Proto, + uv: lobject.UpVal, + th: lua_State, // thread + buf: lobject.Buffer, + lclass: lobject.LuauClass, + lobject: lobject.LuauObject, + + pub inline fn tots(o: *GCObject) *lobject.TString { + std.debug.assert(o.gch.ttype() == @intFromEnum(lua.Type.String)); + return &o.ts; + } + pub inline fn tou(o: *GCObject) *lobject.Udata { + std.debug.assert(o.gch.ttype() == @intFromEnum(lua.Type.Userdata)); + return &o.u; + } + pub inline fn tocl(o: *GCObject) *lobject.Closure { + std.debug.assert(o.gch.ttype() == @intFromEnum(lua.Type.Function)); + return &o.cl; + } + pub inline fn toh(o: *GCObject) *lobject.LuaTable { + std.debug.assert(o.gch.ttype() == @intFromEnum(lua.Type.Table)); + return &o.h; + } + pub inline fn top(o: *GCObject) *lobject.Proto { + std.debug.assert(o.gch.ttype() == @intFromEnum(lua.Type.Proto)); + return &o.p; + } + pub inline fn touv(o: *GCObject) *lobject.UpVal { + std.debug.assert(o.gch.ttype() == @intFromEnum(lua.Type.UpVal)); + return &o.uv; + } + pub inline fn toth(o: *GCObject) *state.lua_State { + std.debug.assert(o.gch.ttype() == @intFromEnum(lua.Type.Thread)); + return &o.th; + } + pub inline fn tobuf(o: *GCObject) *lobject.Buffer { + std.debug.assert(o.gch.ttype() == @intFromEnum(lua.Type.Buffer)); + return &o.buf; + } + pub inline fn toclass(o: *GCObject) *lobject.LuauClass { + std.debug.assert(o.gch.ttype() == @intFromEnum(lua.Type.Class)); + return &o.lclass; + } + pub inline fn toobject(o: *GCObject) *lobject.LuauObject { + std.debug.assert(o.gch.ttype() == @intFromEnum(lua.Type.Object)); + return &o.lobject; + } +}; + +pub inline fn Lnewstate() !*lua.State { + if (c.luaL_newstate()) |s| + return @ptrCast(@alignCast(s)) + else + return error.OutOfMemory; +} + +pub fn close(L: *lua_State) void { + if (comptime !build_config.use_zig_backend) { + return c.lua_close(@ptrCast(L)); + } + const GL = L.global.mainthread; // only the main thread can be closed + lfunc.Fclose(GL, @ptrCast(GL.stack)); // close all upvalues for this thread + close_state(GL); +} + +fn stack_init(L1: *lua.State, L: *lua_State) Errorset.Memory!void { + // initialize CallInfo array + L1.base_ci = try lmem.Mnewarray(L, CallInfo, BASIC_CI_SIZE, L1.header.memcat); + L1.ci = L1.base_ci.?; + L1.size_ci = BASIC_CI_SIZE; + L1.end_ci = L1.base_ci.? + @as(usize, @intCast(L1.size_ci - 1)); + // initialize stack array + L1.stack = try lmem.Mnewarray(L, lobject.TValue, BASIC_STACK_SIZE + EXTRA_STACK, L1.header.memcat); + L1.stacksize = BASIC_STACK_SIZE + EXTRA_STACK; + const stack = L1.stack; + for (0..BASIC_STACK_SIZE + EXTRA_STACK) |i| + stack[i].setnilvalue(); // erase new stack + L1.top = stack; + L1.stack_last = stack[@intCast(L1.stacksize - EXTRA_STACK)..]; + // initialize first ci + L1.ci.?[0].func = L1.top; + L1.top += 1; + L1.top[0].setnilvalue(); // `function' entry for this `ci' + L1.base = L1.top; + L1.ci.?[0].base = L1.top; + L1.ci.?[0].top = L1.top + @as(usize, @intCast(lua.config.MINSTACK)); +} + +fn freestack(L: *lua_State, L1: *lua.State) void { + lmem.Mfreearray(L, CallInfo, L1.base_ci, @intCast(L1.size_ci), L1.header.memcat); + lmem.Mfreearray(L, lobject.TValue, L1.stack, @intCast(L1.stacksize), L1.header.memcat); +} + +fn f_luaopen(L: *lua_State) Errorset.Table!void { + const g = L.global; + try stack_init(L, L); + L.gt = try ltable.Hnew(L, 0, 2); // table of globals + L.registry().sethvalue(L, try ltable.Hnew(L, 0, 2)); // registry + try lstring.Sresize(L, @intCast(lua.config.MINSTRTABSIZE)); // initial size of string table + try ltm.Tinit(L); + lstring.Sfix(try lstring.Snew(L, ldebug.MEMERRMSG)); // pin to make sure we can always throw this error + lstring.Sfix(try lstring.Snew(L, ldebug.ERRERRMSG)); // pin to make sure we can always throw this error + g.GCthreshold = 4 * g.totalbytes; +} + +fn preinit_state(L: *lua_State, g: *global_State) void { + L.global = g; + L.stack = undefined; // TODO: null + L.stacksize = 0; + L.gt = null; + L.openupval = null; + L.size_ci = 0; + L.nCcalls = 0; + L.baseCcalls = 0; + L.curr_status = 0; + L.base_ci = null; + L.ci = null; + L.namecall = null; + L.cachedslot = 0; + L.singlestep_on = false; + L.isactive = false; + L.activememcat = 0; + L.userdata = null; +} + +fn close_state(L: *lua_State) void { + const g = L.global; + lfunc.Fclose(L, @ptrCast(L.stack)); // close all upvalues for this thread + lgc.Cfreeall(L); // collect all objects + std.debug.assert(g.strt.nuse == 0); + lmem.Mfreearray(L, ?*lobject.TString, L.global.strt.hash.?, @intCast(L.global.strt.size), 0); + freestack(L, L); + for (0..@intCast(lua.config.SIZECLASSES)) |i| { + std.debug.assert(g.freepages[i] == null); + std.debug.assert(g.freegcopages[i] == null); + } + std.debug.assert(g.allgcopages == null); + std.debug.assert(g.totalbytes == @sizeOf(LG)); + std.debug.assert(g.memcatbytes[0] == @sizeOf(LG)); + for (1..@intCast(lua.config.MEMORY_CATEGORIES)) |i| + std.debug.assert(g.memcatbytes[i] == 0); + + if (L.global.ecb.close) |close_fn| + close_fn(L); + + _ = (g.frealloc.?)(g.ud, L, @sizeOf(LG), 0); +} + +pub fn Enewthread(L: *lua_State) Errorset.Table!*lua_State { + const L1 = try lmem.Mnewgco(L, lua_State, @sizeOf(lua_State), L.activememcat); + lgc.Cinit(L, @ptrCast(@alignCast(L1)), @intFromEnum(lua.Type.Thread)); + preinit_state(L1, L.global); + L1.activememcat = L.activememcat; // inherit the active memory category + try stack_init(L1, L); // init stack + L1.gt = L.gt; // share table of globals + L1.singlestep_on = L.singlestep_on; + std.debug.assert(lgc.iswhite(@ptrCast(@alignCast(L1)))); + return L1; +} + +pub fn Efreethread(L: *lua_State, L1: *lua.State, page: *lmem.lua_Page) void { + const g = L.global; + if (g.cb.userthread) |ut| + ut(null, L1); + + freestack(L, L1); + lmem.Mfreegco(L, @ptrCast(@alignCast(L1)), @sizeOf(lua.State), L1.header.memcat, page); +} + +pub fn resetthread(L: *lua_State) Errorset.Memory!void { + if (comptime !build_config.use_zig_backend) { + return c.lua_resetthread(@ptrCast(L)); + } + lapi.api_check(L, !L.isactive); + lapi.api_check(L, L.status() != .Ok or L.ci == L.base_ci); + + // close upvalues before clearing anything + lfunc.Fclose(L, @ptrCast(L.stack)); + + // clear call frames + const ci = &L.base_ci.?[0]; + ci.p = null; + ci.func = @ptrCast(L.stack); + ci.base = ci.func[1..]; + ci.top = ci.base[lua.config.MINSTACK..]; + ci.func[0].setnilvalue(); + L.ci = @ptrCast(ci); + if (L.size_ci != BASIC_CI_SIZE) + try ldo.DreallocCI(L, BASIC_CI_SIZE); + // clear thread state + L.curr_status = @intFromEnum(lua.Status.Ok); + L.base = L.ci.?[0].base; + L.top = L.ci.?[0].base; + L.nCcalls = 0; + L.baseCcalls = 0; + // clear thread stack + if (L.stacksize != BASIC_STACK_SIZE + EXTRA_STACK) + try ldo.Dreallocstack(L, @intCast(BASIC_STACK_SIZE), false); + for (0..@intCast(L.stacksize)) |i| + L.stack[i].setnilvalue(); +} + +pub fn isthreadreset(L: *lua_State) bool { + if (comptime !build_config.use_zig_backend) { + return c.lua_isthreadreset(@ptrCast(L)) != 0; + } + return L.ci == L.base_ci and L.base == L.top and L.curr_status == @intFromEnum(lua.Status.Ok); +} + +pub fn newstate(f: lua.Alloc, ud: ?*anyopaque) Errorset.Table!*lua_State { + if (comptime !build_config.use_zig_backend) { + return @ptrCast(@alignCast(c.lua_newstate(@ptrCast(@alignCast(f)), ud) orelse return error.OutOfMemory)); + } + const l = f(ud, null, 0, @sizeOf(LG)) orelse return error.OutOfMemory; + const L: *lua_State = @ptrCast(@alignCast(l)); + const g: *global_State = &(@as(*LG, @ptrCast(@alignCast(L)))).g; + L.header.tt = @intFromEnum(lua.Type.Thread); + L.header.marked = lgc.bit2mask(lgc.WHITE0BIT, lgc.FIXEDBIT); + g.currentwhite = L.header.marked; + L.header.memcat = 0; + preinit_state(L, g); + g.frealloc = f; + g.ud = ud; + g.mainthread = L; + g.uvhead.u.open.prev = &g.uvhead; + g.uvhead.u.open.next = &g.uvhead; + g.GCthreshold = 0; // mark it as unfinished state + g.registryfree = 0; + g.errorjmp = null; + g.rngstate = 0; + g.ptrenckey[0] = 1; + g.ptrenckey[1] = 0; + g.ptrenckey[2] = 0; + g.ptrenckey[3] = 0; + g.strt.size = 0; + g.strt.nuse = 0; + g.strt.hash = null; + g.pseudotemp.setnilvalue(); + L.registry().setnilvalue(); + g.gcstate = lgc.GCSpause; + g.gray = null; + g.grayagain = null; + g.weak = null; + g.totalbytes = @sizeOf(LG); + g.gcgoal = lgc.I_GCGOAL; + g.gcstepmul = lgc.I_GCSTEPMUL; + g.gcstepsize = @as(c_int, lgc.I_GCSTEPSIZE) << 10; + + for (0..@intCast(lua.config.SIZECLASSES)) |i| { + g.freepages[i] = null; + g.freegcopages[i] = null; + } + + g.allpages = null; + g.allgcopages = null; + g.sweepgcopage = null; + + @memset(g.mt[0..], null); + + for (0..lua.Type.T_COUNT) |i| + g.mt[i] = null; + + @memset(g.udatagc[0..], null); + @memset(g.udatamt[0..], null); + + for (0..ludata.UTAG_INTERNAL_LIMIT) |i| { + const udatadirect = &g.udatadirect[i]; + + udatadirect.indextm.setnilvalue(); + udatadirect.newindextm.setnilvalue(); + udatadirect.namecalltm.setnilvalue(); + udatadirect.index = null; + udatadirect.newindex = null; + udatadirect.namecall = null; + } + + @memset(g.udatadirectfields[0..], null); + + @memset(g.lightuserdataname[0..], null); + @memset(g.memcatbytes[0..], 0); + + g.memcatbytes[0] = @sizeOf(LG); + + g.cb = .{}; + + g.ecb = .{}; + + g.ecbdata = std.mem.zeroes([lua.config.EXECUTION_CALLBACK_STORAGE]u8); + + g.gcstats = .{}; + g.lastprotoid = 1; + + // TODO: LUAI_GCMETRICS + + errdefer L.close(); + try f_luaopen(L); + + return L; +} diff --git a/deps/luau/src/VM/lstring.zig b/deps/luau/src/VM/lstring.zig new file mode 100644 index 0000000..6da8a47 --- /dev/null +++ b/deps/luau/src/VM/lstring.zig @@ -0,0 +1,213 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +const lua = @import("lua.zig"); +const lobject = @import("lobject.zig"); + +const lgc = @import("lgc.zig"); +const lmem = @import("lmem.zig"); + +const Errorset = @import("errorset.zig"); + +/// string size limit +pub const MAXSSIZE = (1 << 30); + +/// string atoms are not defined by default; the storage is 16-bit integer +pub const ATOM_UNDEF = -32768; + +inline fn sizestring(len: usize) usize { + return @offsetOf(lobject.TString, "data") + len + 1; +} + +pub inline fn Snew(L: *lua.State, s: []const u8) Errorset.Memory!*lobject.TString { + return Snewlstr(L, s); +} + +pub inline fn Sfix(s: *lobject.TString) void { + s.header.marked |= lgc.bitmask(lgc.FIXEDBIT); +} + +pub inline fn Supdateatom(L: *lua.State, ts: *lobject.TString) void { + if (ts.atom == ATOM_UNDEF) + ts.atom = if (L.global.cb.useratom) |useratom| useratom(L, @ptrCast(@alignCast(&ts.data)), @intCast(ts.len)) else -1; +} + +pub fn Shash(str: []const u8) u32 { + // Note that this hashing algorithm is replicated in BytecodeBuilder.cpp, BytecodeBuilder::getStringHash + var src = str; + var len: usize = str.len; + + var a: u32 = 0; + var b: u32 = 0; + var h: u32 = @truncate(len); + + // hash prefix in 12b chunks (using aligned reads) with ARX based hash (LuaJIT v2.1, lookup3) + // note that we stop at length<32 to maintain compatibility with Lua 5.1 + while (len >= 32) : (len -= 12) { + a +%= std.mem.readInt(u32, src[0..4], builtin.cpu.arch.endian()); + b +%= std.mem.readInt(u32, src[4..8], builtin.cpu.arch.endian()); + h +%= std.mem.readInt(u32, src[8..12], builtin.cpu.arch.endian()); + + // mix + a ^= h; + a -%= ((h >> 14) | (h << (32 - 14))); + b ^= a; + b -%= ((a >> 11) | (a << (32 - 11))); + h ^= b; + h -%= ((b >> 25) | (b << (32 - 25))); + + src = src[12..]; + } + + // original Lua 5.1 hash for compatibility (exact match when len<32) + var i: usize = len; + while (i > 0) : (i -= 1) + h ^= (h << 5) +% (h >> 2) +% src[i - 1]; + + return h; +} + +pub fn Sresize(L: *lua.State, newsize: usize) Errorset.Memory!void { + const newhash = try lmem.Mnewarray(L, ?*lobject.TString, newsize, 0); + const tb = &L.global.strt; + for (0..newsize) |i| + newhash[i] = null; + // rehash + for (0..@intCast(tb.size)) |i| { + var p: ?*lobject.TString = tb.hash.?[i]; + while (p) |node| { // for each node in the list + const next = node.next; // save next + const h = node.hash; + const h1 = lobject.lmod(usize, h, newsize); // new position + std.debug.assert(h % newsize == h1); + node.next = newhash[h1]; // chain it + newhash[h1] = node; + p = next; + } + } + lmem.Mfreearray(L, ?*lobject.TString, tb.hash, @intCast(tb.size), 0); + tb.size = @intCast(newsize); + tb.hash = newhash; +} + +fn newlstr(L: *lua.State, str: []const u8, hash: u32) Errorset.Memory!*lobject.TString { + const l = str.len; + if (l > MAXSSIZE) + return error.BlockTooBig; + + const ts = try lmem.Mnewgco(L, lobject.TString, sizestring(l), L.activememcat); + lgc.Cinit(L, @ptrCast(@alignCast(ts)), @intFromEnum(lua.Type.String)); + ts.atom = ATOM_UNDEF; + ts.hash = hash; + ts.len = @intCast(l); + + @memcpy(ts.gdata()[0..l], str[0..l]); + ts.gdata()[l] = 0; // ending 0 + + const tb = &L.global.strt; + const h: u32 = lobject.lmod(u32, hash, @intCast(tb.size)); + ts.next = tb.hash.?[h]; // chain new entry + tb.hash.?[h] = ts; + + tb.nuse += 1; + if (tb.nuse > tb.size and tb.size <= @divTrunc(std.math.maxInt(i32), 2)) + try Sresize(L, @intCast(tb.size * 2)); // too crowded + + return ts; +} + +pub fn Sbufstart(L: *lua.State, size: usize) Errorset.Memory!*lobject.TString { + if (size > MAXSSIZE) + return error.BlockTooBig; + + const ts = try lmem.Mnewgco(L, lobject.TString, sizestring(size), L.activememcat); + lgc.Cinit(L, @ptrCast(@alignCast(ts)), @intFromEnum(lua.Type.String)); + ts.atom = ATOM_UNDEF; + ts.hash = 0; // computed in Sbuffinish + ts.len = @intCast(size); + + ts.next = null; + + return ts; +} + +pub fn Sbuffinish(L: *lua.State, ts: *lobject.TString) Errorset.Memory!*lobject.TString { + const h = Shash(ts.gdata()[0..ts.len]); + const tb = &L.global.strt; + const bucket: u32 = lobject.lmod(u32, h, @intCast(tb.size)); + + // search if we already have this string in the hash table + var el: ?*lobject.TString = tb.hash.?[bucket]; + while (el) |node| : (el = node.next) { + if (node.len == ts.len and std.mem.eql(u8, node.gdata()[0..ts.len], ts.gdata()[0..ts.len])) { + // string may be dead + if (lgc.isdead(L.global, @ptrCast(@alignCast(node)))) + lgc.changewhite(@ptrCast(@alignCast(node))); + return node; + } + } + + std.debug.assert(ts.next == null); + + ts.hash = h; + ts.gdata()[ts.len] = 0; // ending 0 + ts.next = tb.hash.?[bucket]; // chain new entry + tb.hash.?[bucket] = ts; + + tb.nuse += 1; + if (tb.nuse > tb.size and tb.size <= @divTrunc(std.math.maxInt(i32), 2)) + try Sresize(L, @intCast(tb.size * 2)); // too crowded + + return ts; +} + +fn findstrnode(L: *lua.State, str: []const u8, h: u32) ?*lobject.TString { + var el = L.global.strt.hash.?[lobject.lmod(u32, h, @intCast(L.global.strt.size))]; + while (el) |node| : (el = node.next) { + if (node.len == str.len and std.mem.eql(u8, node.gdata()[0..node.len], str[0..str.len])) { + // string may be dead + if (lgc.isdead(L.global, @ptrCast(@alignCast(node)))) + lgc.changewhite(@ptrCast(@alignCast(node))); + return node; + } + } + return null; // not found +} + +pub fn Snewlstr(L: *lua.State, str: []const u8) Errorset.Memory!*lobject.TString { + const h = Shash(str); + if (findstrnode(L, str, h)) |el| + return el; + return newlstr(L, str, h); // not found +} + +pub fn Sassumelstr(L: *lua.State, str: []const u8) ?*lobject.TString { + const h = Shash(str); + return findstrnode(L, str, h); +} + +fn unlinkstr(L: *lua.State, ts: *lobject.TString) bool { + const g = L.global; + + var p = &g.strt.hash.?[lobject.lmod(u32, ts.hash, @intCast(g.strt.size))]; + + while (p.*) |node| { + if (node == ts) { + p.* = node.next; + return true; + } else { + p = &node.next; + } + } + + return false; +} + +pub fn Sfree(L: *lua.State, ts: *lobject.TString, page: *lmem.lua_Page) void { + if (unlinkstr(L, ts)) + L.global.strt.nuse -= 1 + else + std.debug.assert(ts.next == null); // orphaned string buffer + + lmem.Mfreegco(L, ts.obj2gco(), sizestring(ts.len), ts.header.memcat, page); +} diff --git a/deps/luau/src/VM/lstrlib.zig b/deps/luau/src/VM/lstrlib.zig new file mode 100644 index 0000000..11ea9cd --- /dev/null +++ b/deps/luau/src/VM/lstrlib.zig @@ -0,0 +1,7 @@ +const c = @import("c"); + +const lua = @import("lua.zig"); + +pub inline fn open(L: *lua.State) void { + _ = c.luaopen_string(@ptrCast(L)); +} diff --git a/deps/luau/src/VM/ltable.zig b/deps/luau/src/VM/ltable.zig new file mode 100644 index 0000000..262847d --- /dev/null +++ b/deps/luau/src/VM/ltable.zig @@ -0,0 +1,700 @@ +const c = @import("c"); +const std = @import("std"); + +const lua = @import("lua.zig"); +const lobject = @import("lobject.zig"); + +const lstate = @import("lstate.zig"); +const ldebug = @import("ldebug.zig"); +const lgc = @import("lgc.zig"); +const lmem = @import("lmem.zig"); +const lnumutils = @import("lnumutils.zig"); + +const Errorset = @import("errorset.zig"); + +const Error = Errorset.Table; + +const MAXBITS = 26; +const MAXSIZE = 1 << MAXBITS; + +const TValue = lobject.TValue; +const LuaNode = lobject.LuaNode; +const LuaTable = lobject.LuaTable; + +const LUA_VECTOR_SIZE = lua.config.VECTOR_SIZE; + +// const Hdummynode: LuaNode = .{ +// .val = .{ .extra = undefined, .tt = @intFromEnum(lua.Type.Nil), .value = undefined }, +// .key = .{ .extra = undefined, .pi = .{ .tt = @intFromEnum(lua.Type.Nil), .next = 0 }, .value = undefined }, +// }; +extern "c" const luaH_dummynode: LuaNode; +pub const dummynode = &luaH_dummynode; + +pub inline fn invalidateTMcache(t: *LuaTable) void { + t.tmcache = 0; +} + +pub inline fn hashpow2(t: *const LuaTable, n: u32) [*]LuaNode { + return t.gnode(lobject.lmod(usize, n, lobject.sizenode(t))); +} +pub inline fn hashstr(t: *const LuaTable, str: *const lobject.TString) [*]LuaNode { + return hashpow2(t, str.hash); +} +pub inline fn hashboolean(t: *const LuaTable, b: bool) [*]LuaNode { + return hashpow2(t, if (b) 1 else 0); +} + +pub fn hashpointer(t: *const LuaTable, p: ?*const anyopaque) [*]LuaNode { + // we discard the high 32-bit portion of the pointer on 64-bit platforms as it doesn't carry much entropy anyway + var h: u32 = if (p) |ptr| @truncate(@intFromPtr(ptr)) else 0; + + // MurmurHash3 32-bit finalizer + h ^= h >> 16; + h *%= 0x85ebca6b; + h ^= h >> 13; + h *%= 0xc2b2ae35; + h ^= h >> 16; + + return hashpow2(t, h); +} + +fn hashnum(t: *const LuaTable, n: f64) [*]LuaNode { + comptime std.debug.assert(@sizeOf(f64) == @sizeOf(u32) * 2); // expected a 8-byte double; + var i: [2]u32 = undefined; + @memcpy(i[0..], &@as([2]u32, @bitCast(n))); + + // mask out sign bit to make sure -0 and 0 hash to the same value + var h1: u32 = i[0]; + var h2: u32 = i[1] & 0x7fffffff; + + // finalizer from MurmurHash64B + const m: u32 = 0x5bd1e995; + + h1 ^= h2 >> 18; + h1 *%= m; + h2 ^= h1 >> 22; + h2 *%= m; + h1 ^= h2 >> 17; + h1 *%= m; + h2 ^= h1 >> 19; + h2 *%= m; + + // ... truncated to 32-bit output (normally hash is equal to (uint64_t(h1) << 32) | h2, but we only really need the lower 32-bit half) + return hashpow2(t, h2); +} + +fn hashint(t: *const LuaTable, n: i64) [*]LuaNode { + comptime std.debug.assert(@sizeOf(i64) == @sizeOf(u32) * 2); // expected a 8-byte integer; + var i: [2]u32 = undefined; + @memcpy(i[0..], &@as([2]u32, @bitCast(n))); + + var h1: u32 = i[0]; + var h2: u32 = i[1]; + + // finalizer from MurmurHash64B + const m: u32 = 0x5bd1e995; + + h1 ^= h2 >> 18; + h1 *%= m; + h2 ^= h1 >> 22; + h2 *%= m; + h1 ^= h2 >> 17; + h1 *%= m; + h2 ^= h1 >> 19; + h2 *%= m; + + // ... truncated to 32-bit output (normally hash is equal to (uint64_t(h1) << 32) | h2, but we only really need the lower 32-bit half) + return hashpow2(t, h2); +} + +fn hashvec(t: *const LuaTable, v: []const f32) [*]LuaNode { + var i: [LUA_VECTOR_SIZE]u32 = undefined; + @memcpy(i[0..], (@as([*]const u32, @ptrCast(@alignCast(v.ptr))))[0..LUA_VECTOR_SIZE]); + + // convert -0 to 0 to make sure they hash to the same value + i[0] = if (i[0] == 0x80000000) 0 else i[0]; + i[1] = if (i[1] == 0x80000000) 0 else i[1]; + i[2] = if (i[2] == 0x80000000) 0 else i[2]; + + // scramble bits to make sure that integer coordinates have entropy in lower bits + i[0] ^= i[0] >> 17; + i[1] ^= i[1] >> 17; + i[2] ^= i[2] >> 17; + + // Optimized Spatial Hashing for Collision Detection of Deformable Objects + var h: u32 = (i[0] * 73856093) ^ (i[1] * 19349663) ^ (i[2] * 83492791); + + if (comptime LUA_VECTOR_SIZE == 4) { + i[3] = if (i[3] == 0x80000000) 0 else i[3]; + i[3] ^= i[3] >> 17; + h ^= i[3] * 39916801; + } + + return hashpow2(t, h); +} + +fn mainposition(t: *const LuaTable, key: *const TValue) [*]LuaNode { + comptime std.debug.assert(@sizeOf(LuaNode) == @sizeOf(TValue) * 2); + comptime std.debug.assert(@alignOf(LuaNode) == @alignOf(TValue)); + return switch (key.typeOf()) { + .Number => hashnum(t, key.nvalue()), + .Integer => hashint(t, key.lvalue()), + .Vector => hashvec(t, key.vvalue()), + .String => hashstr(t, key.tsvalue()), + .Boolean => hashboolean(t, key.bvalue()), + .LightUserdata => hashpointer(t, key.pvalue()), + else => hashpointer(t, @ptrCast(@alignCast(key.gcvalue()))), + }; +} + +/// +/// returns the index for `key` if `key` is an appropriate key to live in +/// the array part of the table, -1 otherwise. +/// +fn arrayindex(key: f64) i32 { + const i: i32 = lnumutils.inum2int(key); + + return if (@as(f64, @floatFromInt(i)) == key) i else -1; +} + +// {============================================================= +// Rehash +// ============================================================== + +pub inline fn maybesetaboundary(t: *LuaTable, boundary: i32) void { + if (t.bound.aboundary <= 0) + t.bound.aboundary = -boundary; +} + +pub inline fn getaboundary(t: *LuaTable) c_int { + return if (t.bound.aboundary < 0) -t.bound.aboundary else t.sizearray; +} + +fn computesizes(nums: []const u32, narray: *usize) usize { + var i: usize = 0; + var twotoi: usize = 1; // 2^i + var a: usize = 0; // number of elements smaller than 2^i + var na: usize = 0; // number of elements to go to array part + var n: usize = 0; // optimal size for array part + while (@divTrunc(twotoi, 2) < narray.*) : (i += 1) { + defer twotoi *= 2; + if (nums[i] > 0) { + a += nums[i]; + if (a > @divTrunc(twotoi, 2)) { // more than half elements present? + n = twotoi; // optimal size (till now) + na = a; // all elements smaller than n will go to array part + } + } + if (a == narray.*) + break; // all elements already counted + } + narray.* = n; + std.debug.assert(@divTrunc(narray.*, 2) <= na and na <= narray.*); + return na; +} + +fn countint(key: f64, nums: []u32) u1 { + const k = arrayindex(key); + if (0 < k and k <= MAXSIZE) { + // is `key' an appropriate array index? + nums[@intCast(lobject.ceillog2(@intCast(k)))] += 1; // count as such + return 1; + } + return 0; +} + +fn numusearray(t: *const LuaTable, nums: []u32) usize { + var lg: u8 = 0; + var ttlg: i32 = 1; // 2^lg + var ause: usize = 0; // summation of `nums' + var i: u32 = 1; // count to traverse all array keys + while (lg <= MAXBITS) : (lg += 1) { // for each slice + defer ttlg *= 2; + var lc: u32 = 0; // counter + var lim: i32 = ttlg; + if (lim > t.sizearray) { + lim = t.sizearray; // adjust upper limit + if (i > lim) + break; // no more elements to count + } + while (i <= lim) : (i += 1) { + if (!t.array.?[i - 1].ttisnil()) + lc += 1; + } + nums[lg] = lc; + ause += lc; + } + return ause; +} + +fn numusehash(t: *const LuaTable, nums: []u32, pnasize: *usize) u32 { + var totaluse: u32 = 0; // total number of elements + var ause: usize = 0; // summation of `nums' + var i: usize = lobject.sizenode(t); + while (i > 0) : (i -= 1) { + const n: *LuaNode = @ptrCast(t.gnode(i - 1)); + if (!n.gval().ttisnil()) { + if (n.gkey().ttisnumber()) + ause += countint(n.gkey().nvalue(), nums); + totaluse += 1; + } + } + pnasize.* += ause; + return totaluse; +} + +fn setarrayvector(L: *lua.State, t: *LuaTable, size: usize) Error!void { + if (size > MAXSIZE) + return error.@"table overflow"; + t.array = try lmem.Mreallocarray(L, TValue, t.array, @intCast(t.sizearray), size, t.header.memcat); + var i: usize = @intCast(t.sizearray); + while (i < size) : (i += 1) + t.array.?[i].setnilvalue(); + t.sizearray = @intCast(size); +} + +fn setnodevector(L: *lua.State, t: *LuaTable, newsize: usize) Error!void { + var size: usize = newsize; + var lsize: u8 = 0; + if (size == 0) { // no elements to hash part? + t.node = @ptrCast(@alignCast(@constCast(dummynode))); // use common `dummynode' + } else { + lsize = @intCast(lobject.ceillog2(@truncate(newsize))); + if (lsize > MAXBITS) + return error.@"table overflow"; + size = lobject.twoto(@intCast(lsize)); + t.node = try lmem.Mnewarray(L, LuaNode, size, t.header.memcat); + for (0..size) |i| { + const n: *LuaNode = @ptrCast(t.gnode(i)); + n.key.pi.next = 0; + n.gkey().setnilvalue(); + n.gval().setnilvalue(); + } + } + t.lsizenode = lsize; + t.nodemask8 = @truncate((@as(usize, 1) << @truncate(lsize)) - 1); + t.bound.lastfree = @intCast(size); // all positions are free +} + +fn arrayornewkey(L: *lua.State, t: *LuaTable, key: *const TValue) Error!*TValue { + if (key.ttisnumber()) { + const n = key.nvalue(); + const k = lnumutils.inum2int(n); + if (@as(f64, @floatFromInt(k)) == n and k - 1 < t.sizearray) + return &t.array.?[@intCast(k - 1)]; + } + + return newkey(L, t, key); +} + +fn resize(L: *lua.State, t: *LuaTable, nasize: usize, nhsize: usize) Error!void { + if (nasize > MAXSIZE or nhsize > MAXSIZE) + return error.@"table overflow"; + + const oldasize: i32 = t.sizearray; + const oldhsize: u8 = t.lsizenode; + const nold = t.node; // save old hash ... + if (nasize > oldasize) // array part must grow? + try setarrayvector(L, t, nasize); + + // create new hash part with appropriate size + try setnodevector(L, t, nhsize); + // used for the migration check at the end + const nnew = t.node; + + if (nasize < oldasize) { // array part must shrink? + t.sizearray = @intCast(nasize); + // re-insert elements from vanishing slice + var i: usize = nasize; + while (i < oldasize) : (i += 1) { + if (!t.array.?[i].ttisnil()) { + var ok: TValue = undefined; + ok.setnvalue(@floatFromInt(i + 1)); + (try newkey(L, t, &ok)).setobj(L, &t.array.?[i]); + } + } + // shrink array + t.array = try lmem.Mreallocarray(L, TValue, t.array, @intCast(oldasize), nasize, t.header.memcat); + } + + // used for the migration check at the end + const anew = t.array; + + // re-insert elements from hash part + var i: usize = lobject.twoto(@truncate(oldhsize)); + while (i > 0) : (i -= 1) { + const old: *LuaNode = @ptrCast(nold + i - 1); + if (!old.gval().ttisnil()) { + var ok: TValue = undefined; + lobject.getnodekey(L, &ok, old); + (try arrayornewkey(L, t, &ok)).setobj(L, old.gval()); + } + } + + // make sure we haven't recursively rehashed during element migration + std.debug.assert(nnew == t.node); + std.debug.assert(anew == t.array); + + if (@as(*LuaNode, @ptrCast(nold)) != dummynode) + lmem.Mfreearray(L, LuaNode, nold, lobject.twoto(@truncate(oldhsize)), t.header.memcat); // free old array +} + +fn adjustasize(t: *LuaTable, size: usize, ek: ?*const TValue) usize { + const tbound: bool = @as(*LuaNode, @ptrCast(t.node)) != dummynode or size < t.sizearray; + const ekindex: i32 = if (ek != null and ek.?.ttisnumber()) arrayindex(ek.?.nvalue()) else -1; + // move the array size up until the boundary is guaranteed to be inside the array part + var adjusted_size = size; + while (adjusted_size + 1 == ekindex or (tbound and !Hgetnum(t, @intCast(adjusted_size + 1)).ttisnil())) + adjusted_size += 1; + return adjusted_size; +} + +pub fn Hresizearray(L: *lua.State, t: *LuaTable, nasize: usize) Error!void { + const nsize = if (@as(*LuaNode, @ptrCast(t.node)) == dummynode) 0 else lobject.sizenode(t); + const asize = adjustasize(t, nasize, null); + try resize(L, t, asize, nsize); +} + +pub fn Hresizehash(L: *lua.State, t: *LuaTable, nhsize: usize) Error!void { + try resize(L, t, @intCast(t.sizearray), nhsize); +} + +fn rehash(L: *lua.State, t: *LuaTable, ek: *const TValue) Error!void { + var nums: [MAXBITS + 1]u32 = [_]u32{0} ** (MAXBITS + 1); + var nasize = numusearray(t, nums[0..]); // count keys in array part + var totaluse: usize = nasize; // all those keys are integer keys + totaluse += numusehash(t, nums[0..], &nasize); // count keys in hash part + + // count extra key + if (ek.ttisnumber()) + nasize += countint(ek.nvalue(), nums[0..]); + totaluse += 1; + + // compute new size for array part + const na = computesizes(nums[0..], &nasize); + var nh = totaluse - na; + + // enforce the boundary invariant; for performance, only do hash lookups if we must + const nadjusted = adjustasize(t, nasize, ek); + + // count how many extra elements belong to array part instead of hash part + const aextra = nadjusted - nasize; + + if (aextra != 0) { + // we no longer need to store those extra array elements in hash part + nh -= aextra; + + // because hash nodes are twice as large as array nodes, the memory we saved for hash parts can be used by array part + // this follows the general sparse array part optimization where array is allocated when 50% occupation is reached + nasize = nadjusted + aextra; + + // since the size was changed, it's again important to enforce the boundary invariant at the new size + nasize = adjustasize(t, nasize, ek); + } + + // resize the table to new computed sizes + try resize(L, t, nasize, nh); +} + +pub fn Hnew(L: *lua.State, narray: u32, nhash: u32) Error!*LuaTable { + const t = try lmem.Mnewgco(L, LuaTable, @sizeOf(LuaTable), L.header.memcat); + lgc.Cinit(L, @ptrCast(@alignCast(t)), @intFromEnum(lua.Type.Table)); + t.metatable = null; + t.tmcache = ~(@as(u8, 0)); + t.array = null; + t.sizearray = 0; + t.bound.lastfree = 0; + t.lsizenode = 0; + t.readonly = 0; + t.safeenv = 0; + t.nodemask8 = 0; + t.node = @ptrCast(@alignCast(@constCast(dummynode))); + if (narray > 0) + try setarrayvector(L, t, narray); + if (nhash > 0) + try setnodevector(L, t, nhash); + return t; +} + +pub fn Hfree(L: *lua.State, t: *LuaTable, page: *lmem.lua_Page) void { + if (@as(*LuaNode, @ptrCast(t.node)) != dummynode) + lmem.Mfreearray(L, LuaNode, t.node, lobject.sizenode(t), t.header.memcat); + if (t.array) |arr| + lmem.Mfreearray(L, TValue, arr, @intCast(t.sizearray), t.header.memcat); + lmem.Mfreegco(L, t.obj2gco(), @sizeOf(LuaTable), t.header.memcat, page); +} + +fn getfreepos(t: *LuaTable) ?*LuaNode { + while (t.bound.lastfree > 0) { + t.bound.lastfree -= 1; + + const n: *LuaNode = @ptrCast(t.gnode(@intCast(t.bound.lastfree))); + if (n.gkey().ttisnil()) + return n; + } + return null; // could not find a free place +} + +// +// inserts a new key into a hash table; first, check whether key's main +// position is free. If not, check whether colliding node is in its main +// position or not: if it is not, move colliding node to an empty place and +// put new key in its main position; otherwise (colliding node is in its main +// position), new key goes to an empty position. +// +fn newkey(L: *lua.State, t: *LuaTable, key: *const TValue) Error!*TValue { + // enforce boundary invariant + if (key.ttisnumber() and key.nvalue() == @as(f64, @floatFromInt(t.sizearray + 1))) { + try rehash(L, t, key); // grow table + + // after rehash, numeric keys might be located in the new array part, but won't be found in the node part + return arrayornewkey(L, t, key); + } + + var mp: *LuaNode = @ptrCast(mainposition(t, key)); + if (!mp.gval().ttisnil() or mp == dummynode) { + const n = getfreepos(t) orelse { + // cannot find a free place? + try rehash(L, t, key); // grow table + + // after rehash, numeric keys might be located in the new array part, but won't be found in the node part + return arrayornewkey(L, t, key); + }; // get a free place + std.debug.assert(@as(*LuaNode, @ptrCast(n)) != dummynode); + var mk: TValue = undefined; + lobject.getnodekey(L, &mk, mp); + var othern: *LuaNode = @ptrCast(mainposition(t, &mk)); + if (othern != mp) { // is colliding node out of its main position? + // yes; move colliding node into free position + while (othern.add_num(othern.gnext()) != mp) + othern = othern.add_num(othern.gnext()); // find previous + othern.key.pi.next = @truncate(n.sub(othern)); // redo the chain with `n' in place of `mp' + n.* = mp.*; // copy colliding node into free pos. (mp->next also goes) + if (mp.gnext() != 0) { + n.key.pi.next += @truncate(mp.sub(n)); // correct 'next' + mp.key.pi.next = 0; // now 'mp' is free + } + mp.gval().setnilvalue(); + } else { // colliding node is in its own main position + // new node will go into free position + if (mp.gnext() != 0) + n.key.pi.next = @truncate((mp.add_num(mp.gnext())).sub(n)) // chain new position + else + std.debug.assert(n.gnext() == 0); + mp.key.pi.next = @truncate(n.sub(mp)); + mp = n; + } + } + lobject.setnodekey(L, mp, key); + lgc.Cbarriert(L, t, key); + std.debug.assert(mp.gval().ttisnil()); + return mp.gval(); +} + +// +// search function for integers +// +pub fn Hgetnum(t: *LuaTable, key: i32) *const TValue { + // (1 <= key && key <= t->sizearray) + if (@as(u32, @intCast(key - 1)) < @as(u32, @intCast(t.sizearray))) + return &t.array.?[@as(u32, @intCast(key - 1))] + else if (@as(*LuaNode, @ptrCast(t.node)) != dummynode) { + // hash fallback + const nk: f64 = @floatFromInt(key); + var n = hashnum(t, nk); + while (true) { // check whether `key' is somewhere in the chain + if (n[0].gkey().ttisnumber() and n[0].gkey().nvalue() == nk) + return n[0].gval(); // that's it + if (n[0].gnext() == 0) + break; + n = @ptrCast(n[0].add_num(n[0].gnext())); + } + } + return lobject.Onilobject; +} + +pub fn Hgetstr(t: *LuaTable, key: *lobject.TString) *const TValue { + var n: *LuaNode = @ptrCast(hashstr(t, key)); + while (true) { // check whether `key' is somewhere in the chain + if (n.gkey().ttisstring() and n.gkey().tsvalue() == key) + return n.gval(); // that's it + if (n.gnext() == 0) + break; + n = n.add_num(n.gnext()); + } + return lobject.Onilobject; +} + +pub fn Hget(t: *LuaTable, key: *const TValue) *const TValue { + switch (key.typeOf()) { + .Nil => return lobject.Onilobject, + .String => return Hgetstr(t, key.tsvalue()), + .Number => { + const k = lnumutils.inum2int(key.nvalue()); + if (@as(f64, @floatFromInt(k)) == key.nvalue()) // index is int? + return Hgetnum(t, k); // use specialized version + // else go through + }, + else => {}, + } + var n = mainposition(t, key); + while (true) { // check whether `key' is somewhere in the chain + if (lobject.OrawequalKey(n[0].gkey(), key)) + return n[0].gval(); // that's it + if (n[0].gnext() == 0) + break; + n = @ptrCast(n[0].add_num(n[0].gnext())); + } + return lobject.Onilobject; // not found +} + +pub fn Hset(L: *lua.State, t: *LuaTable, key: *const TValue) Error!*TValue { + const p = Hget(t, key); + invalidateTMcache(t); + if (p != lobject.Onilobject) + return @constCast(p) + else + return try Hnewkey(L, t, key); +} + +pub fn Hnewkey(L: *lua.State, t: *LuaTable, key: *const TValue) Error!*TValue { + if (key.ttisnil()) + return error.@"table index is nil"; + if (key.ttisnumber() and lnumutils.inumisnan(key.nvalue())) + return error.@"table index is nan"; + if (key.ttisvector() and lnumutils.ivecisnan(key.vvalue())) + return error.@"table index contains nan"; + return newkey(L, t, key); +} + +pub fn Hsetnum(L: *lua.State, t: *LuaTable, key: i32) Error!*TValue { + // (1 <= key && key <= t->sizearray) + if (key - 1 < t.sizearray) + return &t.array.?[@intCast(key - 1)]; + // hash fallback + const p = Hgetnum(t, key); + if (p != lobject.Onilobject) + return @constCast(p) + else { + var k: TValue = undefined; + k.setnvalue(@floatFromInt(key)); + return newkey(L, t, &k); + } +} + +pub fn Hsetstr(L: *lua.State, t: *LuaTable, key: *lobject.TString) Error!*TValue { + const p = Hgetstr(t, key); + invalidateTMcache(t); + if (p != lobject.Onilobject) + return @constCast(p) + else { + var k: TValue = undefined; + k.setsvalue(L, key); + return newkey(L, t, &k); + } +} + +fn updateaboundary(t: *LuaTable, boundary: u32) u32 { + if (boundary < t.sizearray and t.array.?[boundary - 1].ttisnil()) { + if (boundary >= 2 and !t.array.?[boundary - 2].ttisnil()) { + maybesetaboundary(t, @intCast(boundary - 1)); + return boundary - 1; + } + } else if (boundary + 1 < t.sizearray and !t.array.?[boundary].ttisnil() and t.array.?[boundary + 1].ttisnil()) { + maybesetaboundary(t, @intCast(boundary + 1)); + return boundary + 1; + } + return 0; +} + +/// Try to find a boundary in table `t'. A `boundary' is an integer index +/// such that t[i] is non-nil and t[i+1] is nil (and 0 if t[1] is nil). +pub fn Hgetn(t: *LuaTable) usize { + const boundary = getaboundary(t); + const array_size: usize = @intCast(t.sizearray); + if (boundary > 0) { + if (!t.array.?[array_size - 1].ttisnil() and @as(*lobject.LuaNode, @ptrCast(t.node)) == dummynode) + return @intCast(array_size); // fast-path: the end of the array in `t' already refers to a boundary + if (boundary < array_size and !t.array.?[@as(u32, @intCast(boundary)) - 1].ttisnil() and t.array.?[@intCast(boundary)].ttisnil()) + return @intCast(boundary); // fast-path: boundary already refers to a boundary in `t' + + const foundboundary = updateaboundary(t, @intCast(boundary)); + if (foundboundary > 0) + return @intCast(foundboundary); + } + if (array_size > 0 and t.array.?[array_size - 1].ttisnil()) { + // "branchless" binary search from Array Layouts for Comparison-Based Searching, Paul Khuong, Pat Morin, 2017. + // note that clang is cmov-shy on cmovs around memory operands, so it will compile this to a branchy loop. + var base = t.array.?; + var rest = array_size; + var half = rest >> 1; + while (half > 0) : (half = rest >> 1) { + base = if (base[half].ttisnil()) base else base[half..]; + rest -= half; + } + const _boundary = @as(usize, if (!base[0].ttisnil()) 1 else 0) + (base - t.array.?); + maybesetaboundary(t, @intCast(_boundary)); + return _boundary; + } else { + // validate boundary invariant + std.debug.assert(@as(*lobject.LuaNode, @ptrCast(t.node)) == dummynode or Hgetnum(t, @intCast(array_size + 1)).ttisnil()); + return array_size; + } +} + +pub fn Hclone(L: *lua.State, tt: *LuaTable) Error!*LuaTable { + const t = try lmem.Mnewgco(L, LuaTable, @sizeOf(LuaTable), L.activememcat); + lgc.Cinit(L, @ptrCast(@alignCast(t)), @intFromEnum(lua.Type.Table)); + t.metatable = tt.metatable; + t.tmcache = tt.tmcache; + t.array = null; + t.sizearray = 0; + t.lsizenode = 0; + t.nodemask8 = 0; + t.readonly = 0; + t.safeenv = 0; + t.node = @ptrCast(@constCast(dummynode)); + t.bound.lastfree = 0; + + if (tt.sizearray > 0) { + t.array = try lmem.Mnewarray(L, TValue, @intCast(tt.sizearray), tt.header.memcat); + maybesetaboundary(t, getaboundary(tt)); + t.sizearray = tt.sizearray; + + @memcpy(t.array.?[0..@intCast(tt.sizearray)], tt.array.?[0..@intCast(tt.sizearray)]); + } + + if (@as(*LuaNode, @ptrCast(tt.node)) != dummynode) { + const size = @as(usize, 1) << @as(if (@sizeOf(usize) == 8) u6 else u5, @truncate(tt.lsizenode)); + t.node = try lmem.Mnewarray(L, LuaNode, size, tt.header.memcat); + t.lsizenode = tt.lsizenode; + t.nodemask8 = tt.nodemask8; + @memcpy(t.node[0..@intCast(size)], tt.node[0..@intCast(size)]); + t.bound.lastfree = tt.bound.lastfree; + } + + return t; +} + +pub fn Hclear(tt: *LuaTable) void { + // clear array part + for (0..@intCast(tt.sizearray)) |i| + tt.array.?[i].setnilvalue(); + + maybesetaboundary(tt, 0); + + // clear hash part + if (@as(*LuaNode, @ptrCast(tt.node)) != dummynode) { + const size = lobject.sizenode(tt); + tt.bound.lastfree = @intCast(size); + for (0..@intCast(size)) |i| { + const n = tt.gnode(i); + n[0].gkey().setttype(.Nil); + n[0].gval().setnilvalue(); + n[0].key.pi.next = 0; + } + } + + // back to empty -> no tag methods present + tt.tmcache = ~@as(u8, 0); +} diff --git a/deps/luau/src/VM/ltablib.zig b/deps/luau/src/VM/ltablib.zig new file mode 100644 index 0000000..117cfa5 --- /dev/null +++ b/deps/luau/src/VM/ltablib.zig @@ -0,0 +1,7 @@ +const c = @import("c"); + +const lua = @import("lua.zig"); + +pub inline fn open(L: *lua.State) void { + _ = c.luaopen_table(@ptrCast(L)); +} diff --git a/deps/luau/src/VM/ltm.zig b/deps/luau/src/VM/ltm.zig new file mode 100644 index 0000000..8c306d9 --- /dev/null +++ b/deps/luau/src/VM/ltm.zig @@ -0,0 +1,132 @@ +const std = @import("std"); + +const lua = @import("lua.zig"); + +const lobject = @import("lobject.zig"); +const lstate = @import("lstate.zig"); +const ltable = @import("ltable.zig"); +const lstring = @import("lstring.zig"); + +const Errorset = @import("errorset.zig"); + +pub const TMS = enum { + TM_INDEX, + TM_NEWINDEX, + TM_MODE, + TM_NAMECALL, + TM_CALL, + TM_ITER, + TM_LEN, + TM_EQ, // last tag method with `fast' access + TM_ADD, + TM_SUB, + TM_MUL, + TM_DIV, + TM_IDIV, + TM_MOD, + TM_POW, + TM_UNM, + TM_LT, + TM_LE, + TM_CONCAT, + TM_TYPE, + TM_METATABLE, + TM_N, // number of elements in the enum +}; + +pub const N: comptime_int = @intFromEnum(TMS.TM_N); + +pub const typenames = [_][:0]const u8{ + // ORDER TYPE + "nil", + "boolean", + + "userdata", + "number", + "integer", + "vector", + + "string", + + "table", + "function", + "userdata", + "thread", + "buffer", + "class", + "object", +}; + +pub const eventname = [_][:0]const u8{ + // ORDER TM + + "__index", + "__newindex", + "__mode", + "__namecall", + "__call", + "__iter", + "__len", + + "__eq", + + "__add", + "__sub", + "__mul", + "__div", + "__idiv", + "__mod", + "__pow", + "__unm", + + "__lt", + "__le", + "__concat", + "__type", + "__metatable", +}; + +comptime { + if (typenames.len != lua.Type.T_COUNT) + @compileError("typenames size mismatch"); + if (eventname.len != N) + @compileError("eventname size mismatch"); + if (@intFromEnum(TMS.TM_EQ) >= 8) + @compileError("fasttm optimization stores a bitfield with metamethods in a byte"); +} + +pub fn Tinit(L: *lua.State) Errorset.Memory!void { + for (0..@intCast(lua.Type.T_COUNT)) |i| { + L.global.ttname[i] = try lstring.Snew(L, typenames[i]); + lstring.Sfix(L.global.ttname[i]); // never collect these names + } + for (0..N) |i| { + L.global.tmname[i] = try lstring.Snew(L, eventname[i]); + lstring.Sfix(L.global.tmname[i]); // never collect these names + } +} + +pub const LONGEST_TYPENAME_SIZE = res: { + var large = 0; + for (typenames) |name| + large = @max(large, name.len); + break :res large; +}; + +pub fn gfasttm(g: *lstate.global_State, et: ?*lobject.LuaTable, event: TMS) ?*const lobject.TValue { + const mt = et orelse return null; + if (mt.tmcache & (@as(usize, 1) << @intFromEnum(event)) > 0) + return null; + + return Tgettm(mt, event, g.tmname[@intFromEnum(event)]); +} + +pub fn Tgettm(events: *lobject.LuaTable, e: TMS, ename: *lobject.TString) ?*const lobject.TValue { + const tm = ltable.Hgetstr(events, ename); + if (tm.ttisnil()) { + // no tag method? + events.tmcache |= @truncate(@as(usize, 1) << @intFromEnum(e)); // cache this fact + return null; + } + return tm; +} diff --git a/deps/luau/src/VM/lua.zig b/deps/luau/src/VM/lua.zig new file mode 100644 index 0000000..9a115b5 --- /dev/null +++ b/deps/luau/src/VM/lua.zig @@ -0,0 +1,268 @@ +const std = @import("std"); +pub const c = @import("c"); + +const lstate = @import("lstate.zig"); +pub const config = @import("luaconf.zig"); + +pub const MULTRET = c.LUA_MULTRET; + +// pseudo-indices +pub const REGISTRYINDEX = c.LUA_REGISTRYINDEX; +pub const GLOBALSINDEX = c.LUA_GLOBALSINDEX; +pub const ENVIRONINDEX = c.LUA_ENVIRONINDEX; + +pub fn upvalueindex(i: i32) i32 { + return GLOBALSINDEX - i; +} + +pub fn ispseudo(i: i32) bool { + return i <= REGISTRYINDEX; +} + +// thread status; 0 is OK +pub const Status = enum(u3) { + Ok = 0, + Yield, + ErrRun, + /// legacy error code, preserved for compatibility + ErrSyntax, + ErrMem, + ErrErr, + /// yielded for a debug breakpoint + Break, + + pub fn check(s: Status) !Status { + switch (s) { + .ErrErr, .ErrRun => return error.Runtime, + .ErrMem => return error.OutOfMemory, + .ErrSyntax => return error.BadSyntax, + else => return s, + } + } +}; + +pub const CoStatus = enum(u3) { + /// running + Running = 0, + /// suspended + Suspended, + /// 'normal' (it resumed another coroutine) + Normal, + /// finished + Finished, + /// finished with error + FinishedErr, +}; + +pub const State = lstate.lua_State; + +pub const CFunction = *const fn (L: *State) callconv(.c) c_int; +pub const Continuation = *const fn (L: *State, status: c_int) callconv(.c) c_int; +pub const Destructor = *const fn (L: *State, ?*anyopaque) callconv(.c) void; +pub const Coverage = *const fn (?*anyopaque, [*c]const u8, c_int, c_int, [*c]const c_int, usize) callconv(.c) void; + +/// +/// prototype for memory-allocation functions +/// +pub const Alloc = *const fn (ud: ?*anyopaque, ptr: ?*anyopaque, osize: usize, nsize: usize) callconv(.c) ?*anyopaque; + +/// +/// basic types +/// +pub const TNONE = c.LUA_TNONE; + +/// Must be a signed integer because LuaType.none is -1 +pub const Type = enum(i6) { + None = TNONE, + Nil = c.LUA_TNIL, // must be 0 due to lua_isnoneornil + Boolean = c.LUA_TBOOLEAN, // must be 1 due to l_isfalse + LightUserdata = c.LUA_TLIGHTUSERDATA, + Number = c.LUA_TNUMBER, + Integer = c.LUA_TINTEGER, + Vector = c.LUA_TVECTOR, + String = c.LUA_TSTRING, // all types above this must be value types, all types below this must be GC types - see iscollectable + Table = c.LUA_TTABLE, + Function = c.LUA_TFUNCTION, + Userdata = c.LUA_TUSERDATA, + Thread = c.LUA_TTHREAD, + Buffer = c.LUA_TBUFFER, + Class = c.LUA_TCLASS, + Object = c.LUA_TOBJECT, + + // values below this line are used in GCObject tags but may never show up in TValue type tags + + /// LUA_TDEADKEY is used in TKey to identify Luau table entries that have the value set to nil, + /// so that we can remove the strong reference to the key. + Deadkey = c.LUA_TDEADKEY, + + // These values should never show up in TValue tag types. + Proto = c.LUA_TPROTO, + UpVal = c.LUA_TUPVAL, + + // the count of TValue type tags + pub const T_COUNT = c.LUA_T_COUNT; + + pub inline fn isnoneornil(t: Type) bool { + return t == .None or t == .Nil; + } + pub inline fn istypecollectable(comptime t: Type) bool { + return @intFromEnum(t) >= @intFromEnum(Type.String); + } +}; + +// type of numbers in Luau +pub const Number = c.lua_Number; + +// type for integer functions +pub const Integer = c.lua_Integer; + +// unsigned integer type +pub const Unsigned = c.lua_Unsigned; + +/// +/// garbage-collection function and options +/// +pub const GCOp = enum(u4) { + // stop and resume incremental garbage collection + Stop = c.LUA_GCSTOP, + Restart = c.LUA_GCRESTART, + + // run a full GC cycle; not recommended for latency sensitive applications + Collect = c.LUA_GCCOLLECT, + + // return the heap size in KB and the remainder in bytes + Count = c.LUA_GCCOUNT, + CountB = c.LUA_GCCOUNTB, + + // return 1 if GC is active (not stopped); note that GC may not be actively collecting even if it's running + IsRunning = c.LUA_GCISRUNNING, + + /// + /// perform an explicit GC step, with the step size specified in KB + /// + /// garbage collection is handled by 'assists' that perform some amount of GC work matching pace of allocation + /// explicit GC steps allow to perform some amount of work at custom points to offset the need for GC assists + /// note that GC might also be paused for some duration (until bytes allocated meet the threshold) + /// if an explicit step is performed during this pause, it will trigger the start of the next collection cycle + /// + Step = c.LUA_GCSTEP, + + /// + /// tune GC parameters G (goal), S (step multiplier) and step size (usually best left ignored) + /// + /// garbage collection is incremental and tries to maintain the heap size to balance memory and performance overhead + /// this overhead is determined by G (goal) which is the ratio between total heap size and the amount of live data in it + /// G is specified in percentages; by default G=200% which means that the heap is allowed to grow to ~2x the size of live data. + /// + /// collector tries to collect S% of allocated bytes by interrupting the application after step size bytes were allocated. + /// when S is too small, collector may not be able to catch up and the effective goal that can be reached will be larger. + /// S is specified in percentages; by default S=200% which means that collector will run at ~2x the pace of allocations. + /// + /// it is recommended to set S in the interval [100 / (G - 100), 100 + 100 / (G - 100))] with a minimum value of 150%; for example: + /// - for G=200%, S should be in the interval [150%, 200%] + /// - for G=150%, S should be in the interval [200%, 300%] + /// - for G=125%, S should be in the interval [400%, 500%] + /// + SetGoal = c.LUA_GCSETGOAL, + SetStepMul = c.LUA_GCSETSTEPMUL, + SetStepSize = c.LUA_GCSETSTEPSIZE, +}; + +/// +/// reference system, can be used to pin objects +/// +pub const NOREF = c.LUA_NOREF; +pub const REFNIL = c.LUA_REFNIL; + +pub const Hook = *const fn (?*State, [*c]c.lua_Debug) callconv(.c) void; + +pub const Debug = struct { + what: Context = .lua, + name: ?[:0]const u8 = null, + source: ?[:0]const u8 = null, + short_src: ?[]u8 = null, + linedefined: ?u32 = null, + currentline: ?u32 = null, + nupvals: u8 = 0, + nparams: u8 = 0, + isvararg: u8 = 0, + ssbuf: [config.IDSIZE:0]u8, + + pub const Context = enum { + lua, + c, + main, + tail, + }; + + pub fn fromLua(self: *Debug, ar: c.lua_Debug, options: []const u8) void { + if (std.mem.indexOf(u8, options, "n")) |_| { + if (ar.name != null) + self.name = std.mem.span(ar.name); + } + + if (std.mem.indexOf(u8, options, "s")) |_| { + self.source = std.mem.span(ar.source); + + const short_src: [:0]const u8 = std.mem.span(ar.short_src); + @memcpy(self.ssbuf[0..short_src.len], short_src[0.. :0]); + self.short_src = self.ssbuf[0..short_src.len]; + + if (ar.linedefined >= 0) + self.linedefined = @intCast(ar.linedefined); + self.what = blk: { + const what = std.mem.span(ar.what); + if (std.mem.eql(u8, "Lua", what)) break :blk .lua; + if (std.mem.eql(u8, "C", what)) break :blk .c; + if (std.mem.eql(u8, "main", what)) break :blk .main; + if (std.mem.eql(u8, "tail", what)) break :blk .tail; + unreachable; + }; + } + + if (std.mem.indexOf(u8, options, "l")) |_| { + if (ar.currentline >= 0) + self.currentline = @intCast(ar.currentline); + } + + if (std.mem.indexOf(u8, options, "u")) |_| + self.nupvals = ar.nupvals; + + if (std.mem.indexOf(u8, options, "a")) |_| { + self.nparams = ar.nparams; + self.isvararg = ar.isvararg; + } + } +}; + +/// Callbacks that can be used to reconfigure behavior of the VM dynamically. +/// These are shared between all coroutines. +/// +/// Note: interrupt is safe to set from an arbitrary thread but all other callbacks +/// can only be changed when the VM is not running any code +pub const Callbacks = extern struct { + /// arbitrary userdata pointer that is never overwritten by Luau + userdata: ?*anyopaque = null, + + /// gets called at safepoints (loop back edges, call/ret, gc) if set + interrupt: ?*const fn (L: *State, gc: c_int) callconv(.c) void = null, + /// gets called when an unprotected error is raised (if longjmp is used) + panic: ?*const fn (L: *State, errcode: c_int) callconv(.c) void = null, + + /// gets called when L is created (LP == parent) or destroyed (LP == NULL) + userthread: ?*const fn (LP: ?*State, L: *State) callconv(.c) void = null, + /// gets called when a string is created; returned atom can be retrieved via tostringatom + useratom: ?*const fn (L: *State, s: [*c]const u8, l: usize) callconv(.c) i16 = null, + + /// gets called when BREAK instruction is encountered + debugbreak: ?*const fn (L: *State, ar: *c.lua_Debug) callconv(.c) void = null, + /// gets called after each instruction in single step mode + debugstep: ?*const fn (L: *State, ar: *c.lua_Debug) callconv(.c) void = null, + /// gets called when thread execution is interrupted by break in another thread + debuginterrupt: ?*const fn (L: *State, ar: *c.lua_Debug) callconv(.c) void = null, + /// gets called when protected call results in an error + debugprotectederror: ?*const fn (L: *State) callconv(.c) void = null, + + /// gets called when memory is allocated + onallocate: ?*const fn (L: *State, osize: usize, nsize: usize) callconv(.c) void = null, +}; diff --git a/deps/luau/src/VM/luaconf.zig b/deps/luau/src/VM/luaconf.zig new file mode 100644 index 0000000..2ff0a39 --- /dev/null +++ b/deps/luau/src/VM/luaconf.zig @@ -0,0 +1,57 @@ +const c = @import("c"); +const config = @import("config"); + +pub const LUAU_VERSION = config.luau_version; + +/// Can be used to reconfigure internal error handling to use longjmp instead of C++ EH +pub const USE_LONGJMP = c.LUA_USE_LONGJMP; + +/// LUA_IDSIZE gives the maximum size for the description of the source +pub const IDSIZE = c.LUA_IDSIZE; + +/// LUA_MINSTACK is the guaranteed number of Lua stack slots available to a C function +pub const MINSTACK = c.LUA_MINSTACK; + +/// LUAI_MAXCSTACK limits the number of Lua stack slots that a C function can use +pub const I_MAXCSTACK = c.LUAI_MAXCSTACK; + +/// LUAI_MAXCALLS limits the number of nested calls +pub const I_MAXCALLS = c.LUAI_MAXCALLS; + +/// LUAI_MAXCCALLS is the maximum depth for nested C calls; this limit depends on native stack size +pub const I_MAXCCALLS = c.LUAI_MAXCCALLS; + +/// buffer size used for on-stack string operations; this limit depends on native stack size +pub const BUFFERSIZE = c.LUA_BUFFERSIZE; + +/// number of valid Lua userdata tags +pub const UTAG_LIMIT = c.LUA_UTAG_LIMIT; + +/// number of valid Lua lightuserdata tags +pub const LUTAG_LIMIT = c.LUA_LUTAG_LIMIT; + +/// upper bound for number of size classes used by page allocator +pub const SIZECLASSES = c.LUA_SIZECLASSES; + +/// available number of separate memory categories +pub const MEMORY_CATEGORIES = c.LUA_MEMORY_CATEGORIES; + +/// extra storage for execution callbacks in global state +pub const EXECUTION_CALLBACK_STORAGE = c.LUA_EXECUTION_CALLBACK_STORAGE; + +/// minimum size for the string table (must be power of 2) +pub const MINSTRTABSIZE = c.LUA_MINSTRTABSIZE; + +/// maximum number of captures supported by pattern matching +pub const MAXCAPTURES = c.LUA_MAXCAPTURES; + +pub const I_USER_ALIGNMENT_T = extern union { + u: f64, + s: *anyopaque, + l: c_long, +}; + +/// The length of Luau vector values, either 3 or 4. +pub const VECTOR_SIZE = if (config.use_4_vector) 4 else 3; + +pub const EXTRA_SIZE = (VECTOR_SIZE - 2); diff --git a/deps/luau/src/VM/ludata.zig b/deps/luau/src/VM/ludata.zig new file mode 100644 index 0000000..06b8877 --- /dev/null +++ b/deps/luau/src/VM/ludata.zig @@ -0,0 +1,57 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +const lua = @import("lua.zig"); + +const lobject = @import("lobject.zig"); + +const lgc = @import("lgc.zig"); +const lmem = @import("lmem.zig"); + +const Errorset = @import("errorset.zig"); + +/// special tag value is used for user data with inline dtors +pub const UTAG_IDTOR = lua.config.UTAG_LIMIT; + +/// special tag value is used for newproxy-created user data (all other user data objects are host-exposed) +pub const UTAG_PROXY = (lua.config.UTAG_LIMIT + 1); + +/// must be updated if more internal tags are added +pub const UTAG_INTERNAL_LIMIT = UTAG_PROXY + 1; + +pub inline fn sizeudata(len: usize) usize { + return @offsetOf(lobject.Udata, "data") + (if (len > 16) ((len + 15) & ~@as(usize, 15)) else len); +} + +pub fn Unewudata(L: *lua.State, s: usize, tag: u8) Errorset.Memory!*lobject.Udata { + if (s > std.math.maxInt(i32) - @sizeOf(lobject.Udata)) + return error.BlockTooBig; + + const u = try lmem.Mnewgco(L, lobject.Udata, sizeudata(s), L.activememcat); + lgc.Cinit(L, @ptrCast(@alignCast(u)), @intFromEnum(lua.Type.Userdata)); + u.metatable = null; + u.len = @intCast(s); + u.tag = tag; + return u; +} + +pub fn Ufreeudata(L: *lua.State, u: *lobject.Udata, page: *lmem.lua_Page) void { + if (u.tag < lua.config.UTAG_LIMIT) { + // TODO: access to L here is highly unsafe since this is called during internal GC traversal + // certain operations such as lua_getthreaddata are okay, but by and large this risks crashes on improper use + if (L.global.udatagc[u.tag]) |dtor| + dtor(L, @ptrCast(@alignCast(&u.data))); + } else if (u.tag == UTAG_IDTOR) { + const InlineDtor = *const fn (data: ?*anyopaque) callconv(.c) void; + var dtor: ?InlineDtor = null; + dtor = @ptrFromInt(std.mem.readVarInt( + usize, + (@as([*]u8, @ptrCast(&u.data)) + @as(u32, @intCast(u.len)) - @sizeOf(InlineDtor))[0..@sizeOf(InlineDtor)], + builtin.cpu.arch.endian(), + )); + if (dtor) |d| + d(@ptrCast(@alignCast(&u.data))); + } + + lmem.Mfreegco(L, u.obj2gco(), sizeudata(@intCast(u.len)), u.header.memcat, page); +} diff --git a/deps/luau/src/VM/lutf8lib.zig b/deps/luau/src/VM/lutf8lib.zig new file mode 100644 index 0000000..a7dd6a7 --- /dev/null +++ b/deps/luau/src/VM/lutf8lib.zig @@ -0,0 +1,7 @@ +const c = @import("c"); + +const lua = @import("lua.zig"); + +pub inline fn open(L: *lua.State) void { + _ = c.luaopen_utf8(@ptrCast(L)); +} diff --git a/deps/luau/src/VM/lveclib.zig b/deps/luau/src/VM/lveclib.zig new file mode 100644 index 0000000..4f5e054 --- /dev/null +++ b/deps/luau/src/VM/lveclib.zig @@ -0,0 +1,7 @@ +const c = @import("c"); + +const lua = @import("lua.zig"); + +pub inline fn open(L: *lua.State) void { + _ = c.luaopen_vector(@ptrCast(L)); +} diff --git a/deps/luau/src/VM/lvm.zig b/deps/luau/src/VM/lvm.zig new file mode 100644 index 0000000..657852b --- /dev/null +++ b/deps/luau/src/VM/lvm.zig @@ -0,0 +1,7 @@ +const lua = @import("lua.zig"); +const lobject = @import("lobject.zig"); +const lvmutils = @import("lvmutils.zig"); + +pub inline fn equalobj(L: *lua.State, o1: *const lobject.TValue, o2: *const lobject.TValue) bool { + return o1.ttype() == o2.ttype() and lvmutils.Vequalval(L, o1, o2); +} diff --git a/deps/luau/src/VM/lvmload.zig b/deps/luau/src/VM/lvmload.zig new file mode 100644 index 0000000..53ba8e7 --- /dev/null +++ b/deps/luau/src/VM/lvmload.zig @@ -0,0 +1,9 @@ +const c = @import("c"); +const std = @import("std"); + +const lua = @import("lua.zig"); + +pub inline fn load(L: *lua.State, chunkname: [:0]const u8, bytecode: []const u8, env: i32) !void { + if (c.luau_load(@ptrCast(L), chunkname.ptr, bytecode.ptr, bytecode.len, env) != 0) + return error.Fail; +} diff --git a/deps/luau/src/VM/lvmutils.zig b/deps/luau/src/VM/lvmutils.zig new file mode 100644 index 0000000..b240f1d --- /dev/null +++ b/deps/luau/src/VM/lvmutils.zig @@ -0,0 +1,128 @@ +const std = @import("std"); + +const lua = @import("lua.zig"); +const ltm = @import("ltm.zig"); +const ldebug = @import("ldebug.zig"); +const lobject = @import("lobject.zig"); +const lstring = @import("lstring.zig"); +const lnumutils = @import("lnumutils.zig"); + +pub fn Vtonumber(obj: *const lobject.TValue, n: *lobject.TValue) ?*const lobject.TValue { + if (obj.ttisnumber()) + return obj; + if (obj.ttisstring()) { + const num = std.fmt.parseFloat(f64, std.mem.span(obj.svalue())) catch return null; + n.setnvalue(num); + return n; + } + return null; +} + +pub fn Vtostring(L: *lua.State, obj: *lobject.TValue) bool { + if (!obj.ttisnumber()) + return false + else { + var s: [lnumutils.I_MAXNUM2STR]u8 = undefined; + const n = obj.nvalue(); + const e = lnumutils.inum2str(&s, n); + obj.setsvalue(L, lstring.Snewlstr(L, e) catch return false); + return true; + } +} + +// pub fn Vlessthan(L: *lua.State, l: *const lobject.TValue, r: *const lobject.TValue) bool { +// if (l.ttype() != r.ttype()) { +// ldebug.Gordererror(); +// } +// } + +fn get_compTM(L: *lua.State, mt1: *lobject.LuaTable, mt2: *lobject.LuaTable, event: ltm.TMS) ?*const lobject.TValue { + const tm1 = ltm.gfasttm(L.global, mt1, event) orelse return null; + if (mt1 == mt2) + return tm1; + const tm2 = ltm.gfasttm(L.global, mt2, event) orelse return null; + if (lobject.OrawequalObj(tm1, tm2)) + return tm1; + return null; +} + +// pub fn Vequalval(L: *lua.State, t1: *const lobject.TValue, t2: *const lobject.TValue) bool { +// std.debug.assert(t1.ttype() == t2.ttype()); +// switch (t1.ttype()) { +// .Nil => return true, +// .Number => return t1.nvalue() == t2.nvalue(), +// .Vector => lnumutils.iveceq(t1.vvalue(), t2.vvalue()), +// .Boolean => return t1.bvalue() == t2.bvalue(), +// .LightUserdata => return t1.pvalue() == t2.pvalue() and t1.lightuserdatatag() == t2.lightuserdatatag(), +// .Userdata => { +// const tm = get_compTM(L, t1.u.table, t2.u.table, ltm.TMS.TM_EQ) orelse return t1.hvalue() == t2.hvalue(); +// callTMres(L, L.top, tm, t1, t2); +// return !(L.top.ttisnil() or L.top.ttisboolean() and !L.top.bvalue()); +// }, +// } +// } + +test Vtonumber { + const allocator = std.testing.allocator; + + { + var n = lobject.TValue{ .tt = @intFromEnum(lua.Type.None), .value = undefined }; + const obj = lobject.TValue{ .tt = @intFromEnum(lua.Type.Number), .value = .{ .n = 1.0 } }; + try std.testing.expect(Vtonumber(&obj, &n) == &obj); + } + { + var n = lobject.TValue{ .tt = @intFromEnum(lua.Type.None), .value = undefined }; + const obj = lobject.TValue{ .tt = @intFromEnum(lua.Type.Boolean), .value = .{ .b = 1 } }; + try std.testing.expect(Vtonumber(&obj, &n) == null); + } + { + const GCObject = @import("lstate.zig").GCObject; + const gc_buf = try allocator.alloc(u8, @sizeOf(GCObject) + 4); + defer allocator.free(gc_buf); + const gc: *GCObject = @ptrCast(@alignCast(gc_buf[0..@sizeOf(GCObject)])); + gc.* = .{ + .gch = .{ + .header = .{ + .tt = @intFromEnum(lua.Type.String), + .marked = 0, + .memcat = 0, + }, + }, + }; + + const data = gc_buf[@offsetOf(lobject.TString, "data")..]; + data[0] = '1'; + data[1] = '2'; + data[2] = '3'; + data[3] = 0; + + var n = lobject.TValue{ .tt = @intFromEnum(lua.Type.None), .value = undefined }; + const obj = lobject.TValue{ .tt = @intFromEnum(lua.Type.String), .value = .{ .gc = gc } }; + try std.testing.expect(Vtonumber(&obj, &n) == &n); + try std.testing.expect(n.ttisnumber()); + try std.testing.expect(n.nvalue() == 123.0); + } + { + const GCObject = @import("lstate.zig").GCObject; + const gc_buf = try allocator.alloc(u8, @sizeOf(GCObject) + 2); + defer allocator.free(gc_buf); + const gc: *GCObject = @ptrCast(@alignCast(gc_buf[0..@sizeOf(GCObject)])); + gc.* = .{ + .gch = .{ + .header = .{ + .tt = @intFromEnum(lua.Type.String), + .marked = 0, + .memcat = 0, + }, + }, + }; + + const data = gc_buf[@offsetOf(lobject.TString, "data")..]; + data[0] = 'b'; + data[1] = 0; + + var n = lobject.TValue{ .tt = @intFromEnum(lua.Type.None), .value = undefined }; + const obj = lobject.TValue{ .tt = @intFromEnum(lua.Type.String), .value = .{ .gc = gc } }; + try std.testing.expect(Vtonumber(&obj, &n) == null); + } +} diff --git a/deps/luau/src/VM/zapi.zig b/deps/luau/src/VM/zapi.zig new file mode 100644 index 0000000..e364142 --- /dev/null +++ b/deps/luau/src/VM/zapi.zig @@ -0,0 +1,1591 @@ +const std = @import("std"); + +const ldo = @import("ldo.zig"); + +const lua = @import("lua.zig"); +const ltm = @import("ltm.zig"); +const lapi = @import("lapi.zig"); +const laux = @import("laux.zig"); + +const Errorset = @import("errorset.zig"); + +pub fn LuaZigFn(comptime ReturnType: type) type { + switch (@typeInfo(ReturnType)) { + .int => { + if (ReturnType != i32) + @compileError("Unsupported Fn Return type, must be i32"); + }, + else => {}, + } + return fn (state: *lua.State) ReturnType; +} + +fn handleError(L: *lua.State, err: anyerror) noreturn { + switch (err) { + // else => @panic("Unknown error"), + error.RaiseLuauError => L.raiseerror(), + error.OutOfMemory => ldo.throw(L, .ErrMem), + else => L.LerrorL("{s}", .{@errorName(err)}) catch ldo.throw(L, .ErrMem), + } +} + +pub fn ZigToCFn(comptime fnType: std.builtin.Type.Fn, comptime f: anytype) lua.CFunction { + if (fnType.params.len != 1) + @compileError("Fn must have exactly 1 parameter"); + const param_type = fnType.params[0].type orelse @compileError("Param must have a type"); + if (param_type != *lua.State) + @compileError("Fn parameter must be *lua.State"); + switch (@typeInfo(fnType.return_type orelse @compileError("Fn must return something"))) { + .int => { + if (fnType.return_type != i32) + @compileError("Unsupported Fn Return type, must be i32"); + return struct { + fn inner(s: *lua.State) callconv(.c) c_int { + return @call(.always_inline, f, .{s}); + } + }.inner; + }, + .void => { + return struct { + fn inner(s: *lua.State) callconv(.c) c_int { + @call(.always_inline, f, .{s}); + return 0; + } + }.inner; + }, + .error_union => |error_union| { + switch (@typeInfo(error_union.payload)) { + .int => { + if (error_union.payload != i32) + @compileError("Unsupported Fn Return type, must be i32"); + return struct { + fn inner(s: *lua.State) callconv(.c) c_int { + if (@call(.always_inline, f, .{s})) |res| + return res + else |err| + handleError(s, err); + } + }.inner; + }, + .void => { + return struct { + fn inner(s: *lua.State) callconv(.c) c_int { + if (@call(.always_inline, f, .{s})) + return 0 + else |err| + handleError(s, err); + } + }.inner; + }, + else => |t| @compileError("Unsupported Fn Return type " ++ @tagName(t)), + } + }, + else => |t| @compileError("Unsupported Fn Return type " ++ @tagName(t)), + } +} + +pub fn ZigToCFnV(comptime fnType: std.builtin.Type.Fn, comptime f: anytype) lua.CFunction { + if (fnType.params.len != 1) + @compileError("Fn must have exactly 1 parameter"); + const param_type = fnType.params[0].type orelse @compileError("Param must have a type"); + if (param_type != *lua.State) + @compileError("Fn parameter must be *lua.State"); + switch (@typeInfo(fnType.return_type orelse @compileError("Fn must return something"))) { + .void, .noreturn => { + return struct { + fn inner(L: *lua.State) callconv(.c) c_int { + @call(.always_inline, f, .{L}); + return 0; + } + }.inner; + }, + .comptime_int, .comptime_float, .bool, .int, .float, .@"struct", .array, .@"enum", .null, .optional => { + return struct { + fn inner(L: *lua.State) callconv(.c) c_int { + Zpushvalue(L, @call(.always_inline, f, .{L})) catch |err| switch (@as(anyerror, err)) { + error.RaiseLuauError => L.raiseerror(), + error.OutOfMemory => ldo.throw(L, .ErrMem), + else => L.LerrorL("{s}", .{@errorName(err)}) catch ldo.throw(L, .ErrMem), + }; + return 1; + } + }.inner; + }, + .error_union => |error_union| { + switch (@typeInfo(error_union.payload)) { + .void, .noreturn => { + return struct { + fn inner(L: *lua.State) callconv(.c) c_int { + if (@call(.always_inline, f, .{L})) + return 0 + else |err| + handleError(L, err); + } + }.inner; + }, + .comptime_int, .comptime_float, .bool, .int, .float, .@"struct", .array, .@"enum", .null, .optional => { + return struct { + fn inner(L: *lua.State) callconv(.c) c_int { + if (@call(.always_inline, f, .{L})) |res| { + Zpushvalue(L, res) catch |err| switch (err) { + error.RaiseLuauError => L.raiseerror(), + error.OutOfMemory => ldo.throw(L, .ErrMem), + else => L.LerrorL("{s}", .{@errorName(err)}) catch ldo.throw(L, .ErrMem), + }; + return 1; + } else |err| handleError(L, err); + } + }.inner; + }, + else => |t| @compileError("Unsupported Fn Return type " ++ @tagName(t)), + } + }, + .error_set => { + return struct { + fn inner(L: *lua.State) callconv(.c) c_int { + const err = @call(.always_inline, f, .{L}); + return handleError(L, err); + } + }.inner; + }, + else => |t| @compileError("Unsupported Fn Return type " ++ @tagName(t)), + } +} + +pub fn toCFn(comptime f: anytype) lua.CFunction { + const t = @TypeOf(f); + const ti = @typeInfo(t); + switch (ti) { + .@"fn" => |Fn| return ZigToCFn(Fn, f), + .pointer => |ptr| { + // *const fn ... + if (!ptr.is_const) + @compileError("Pointer must be constant"); + const pi = @typeInfo(ptr.child); + switch (pi) { + .@"fn" => |Fn| return ZigToCFn(Fn, f), + else => @compileError("Pointer must be a pointer to a function"), + } + }, + else => @compileError("zig_fn must be a Fn or a Fn Pointer"), + } + @compileError("Could not determine zig_fn type"); +} + +pub fn toCFnV(comptime f: anytype) lua.CFunction { + const t = @TypeOf(f); + const ti = @typeInfo(t); + switch (ti) { + .@"fn" => |Fn| return ZigToCFnV(Fn, f), + .pointer => |ptr| { + // *const fn ... + if (!ptr.is_const) + @compileError("Pointer must be constant"); + const pi = @typeInfo(ptr.child); + switch (pi) { + .@"fn" => |Fn| return ZigToCFnV(Fn, f), + else => @compileError("Pointer must be a pointer to a function"), + } + }, + else => @compileError("zig_fn must be a Fn or a Fn Pointer"), + } + @compileError("Could not determine zig_fn type"); +} + +pub inline fn Zpushfunction(L: *lua.State, comptime f: anytype, name: [:0]const u8) !void { + try L.pushcfunction(toCFn(f), name); +} + +pub inline fn Zpushclosure(L: *lua.State, comptime f: anytype, name: [:0]const u8, nup: i32) !void { + try L.pushcclosure(toCFn(f), name, nup); +} + +pub fn Zpushclosurek(L: *lua.State, comptime f: anytype, name: [:0]const u8, nup: i32, comptime cont: ?fn (L: *lua.State, status: i32) i32) void { + L.pushcclosurek(toCFn(f), name, nup, if (cont) |cont_f| + struct { + fn inner(l: *lua.State, status: c_int) callconv(.c) c_int { + return @call(.always_inline, cont_f, .{ l, status }); + } + }.inner + else + null); +} + +pub inline fn ZpushfunctionV(L: *lua.State, comptime f: anytype, name: [:0]const u8) void { + L.pushcfunction(toCFnV(f), name); +} + +pub fn Zyielderror(L: *lua.State) anyerror { + try L.pushlstring("attempt to yield across metamethod/C-call boundary"); + return error.RaiseLuauError; +} + +pub fn Zpushvalue(L: *lua.State, value: anytype) !void { + switch (@typeInfo(@TypeOf(value))) { + .bool => L.pushboolean(value), + .comptime_int => L.pushinteger(@intCast(value)), + .comptime_float => L.pushnumber(@floatCast(value)), + .int => |int| { + const pushfn = if (int.signedness == .signed) lapi.pushinteger else lapi.pushunsigned; + if (int.bits <= 32) + pushfn(L, @intCast(value)) + else + pushfn(L, @truncate(value)); + }, + .float => |float| { + if (float.bits <= 64) + L.pushnumber(@floatCast(value)) + else + @compileError("float size too large"); + }, + .pointer => |pointer| { + if (pointer.size == .one) + switch (@typeInfo(pointer.child)) { + .array => |a| { + if (a.child == u8) + try L.pushlstring(value) + else + @compileError("Unsupported pointer array type"); + }, + else => |t| @compileError("Unsupported pointer type " ++ @tagName(t)), + } + else if (pointer.size == .slice and pointer.child == u8) { + if (pointer.sentinel_ptr) |sentinel| { + const s: *const pointer.child = @ptrCast(sentinel); + if (s.* == 0) + try L.pushlstring(value) + else + @compileError("Unsupported pointer sentinel [:?]" ++ @typeName(pointer.child)); + } else try L.pushlstring(value); + } else if (pointer.size == .slice) { + if (pointer.sentinel_ptr) |_| + @compileError("Unsupported pointer sentinel " ++ @typeName(pointer.child)); + var write = value; + if (value.len > std.math.maxInt(i32)) + write = value[0..std.math.maxInt(i32)]; + try L.createtable(@intCast(value.len), 0); + for (write, 1..) |v, i| { + try Zpushvalue(L, v); + try L.rawseti(-2, @intCast(i)); + } + } + }, + .array => |a| { + if (comptime a.len > std.math.maxInt(i32)) + @compileError("Array too large"); + try L.createtable(a.len, 0); + for (value, 1..) |v, i| { + try Zpushvalue(L, v); + try L.rawseti(-2, @intCast(i)); + } + }, + .vector => |info| { + if (info.len != lua.config.VECTOR_SIZE) + @compileError("Vector size mismatch"); + switch (info.len) { + 3 => L.pushvector(value[0], value[1], value[2], 0), + 4 => L.pushvector(value[0], value[1], value[2], value[3]), + else => @compileError("Unsupported vector size"), + } + }, + .@"enum" => |e| { + switch (@typeInfo(e.tag_type)) { + .int => |int| { + if (int.signedness == .unsigned) + L.pushunsigned(@intFromEnum(value)) + else + L.pushinteger(@intFromEnum(value)); + }, + else => @compileError("Unsupported enum type " ++ @tagName(e.tag_type)), + } + }, + .@"struct" => |s| { + try L.createtable(0, s.fields.len); + inline for (s.fields) |field| { + switch (@typeInfo(field.type)) { + .@"fn" => try Zsetfieldfn(L, -1, field.name, @field(value, field.name)), + else => try Zsetfield(L, -1, field.name, @field(value, field.name)), + } + } + }, + .null => L.pushnil(), + .optional => { + if (value) |v| + try Zpushvalue(L, v) + else + L.pushnil(); + }, + .void => {}, + else => |t| @compileError("Unsupported type " ++ @tagName(t)), + } +} + +pub fn Zsetfield(L: *lua.State, comptime index: i32, k: [:0]const u8, value: anytype) !void { + const idx = comptime if (index != lua.GLOBALSINDEX and index != lua.REGISTRYINDEX and index < 0) index - 1 else index; + try Zpushvalue(L, value); + try L.rawsetfield(idx, k); +} +pub fn Zsetfieldfn(L: *lua.State, comptime index: i32, comptime k: [:0]const u8, comptime f: anytype) !void { + const idx = comptime if (index != lua.GLOBALSINDEX and index != lua.REGISTRYINDEX and index < 0) index - 1 else index; + try Zpushfunction(L, f, k); + try L.rawsetfield(idx, k); +} +pub fn ZsetfieldfnV(L: *lua.State, comptime index: i32, comptime k: [:0]const u8, comptime f: anytype) !void { + const idx = comptime if (index != lua.GLOBALSINDEX and index != lua.REGISTRYINDEX and index < 0) index - 1 else index; + try ZpushfunctionV(L, f, k); + try L.rawsetfield(idx, k); +} + +pub fn Zsetglobal(L: *lua.State, name: [:0]const u8, value: anytype) !void { + try Zpushvalue(L, value); + try L.setglobal(name); +} +pub fn Zsetglobalfn(L: *lua.State, comptime name: [:0]const u8, comptime f: anytype) !void { + try Zpushfunction(L, f, name); + try L.setglobal(name); +} +pub fn ZsetglobalfnV(L: *lua.State, comptime name: [:0]const u8, comptime f: anytype) !void { + try ZpushfunctionV(L, f, name); + try L.setglobal(name); +} + +pub fn Zpushbuffer(L: *lua.State, bytes: []const u8) !void { + const buf = try L.newbuffer(bytes.len); + @memcpy(buf, bytes); +} + +pub fn Zresumeerror(L: *lua.State, from: ?*lua.State, msg: []const u8) !lua.Status { + try L.pushlstring(msg); + return L.resumeerror(from); +} + +pub fn Zresumeferror(L: *lua.State, from: ?*lua.State, comptime fmt: []const u8, args: anytype) !lua.Status { + try L.pushfstring(fmt, args); + return L.resumeerror(from); +} + +pub fn Zerror(L: *lua.State, msg: []const u8) anyerror { + try L.pushlstring(msg); + return error.RaiseLuauError; +} + +pub fn Zerrorf(L: *lua.State, comptime fmt: []const u8, args: anytype) anyerror { + try L.pushfstring(fmt, args); + return error.RaiseLuauError; +} + +/// Calls a metamethod and pushes the result on the stack. +/// If the metamethod fails, it returns an error & error value on stack. +pub fn Zcallmeta(L: *lua.State, obj: i32, event: [:0]const u8) !bool { + const idx = lapi.absindex(L, obj); + if (!try L.Lgetmetafield(idx, event)) + return false; + L.pushvalue(idx); + _ = try L.pcall(1, 1, 0).check(); + return true; +} + +/// Converts value to string & pushes to stack. +pub fn Ztolstringk(L: *lua.State, idx: i32) ![]const u8 { + const MAX_NUM_BUF = std.fmt.float.bufferSize(.decimal, f64); + const VEC_SIZE = lua.config.VECTOR_SIZE; + switch (L.typeOf(idx)) { + .Nil => try L.pushlstring("nil"), + .Boolean => try L.pushlstring(if (L.toboolean(idx)) "true" else "false"), + .Number => { + const number = L.tonumber(idx).?; + var s: [MAX_NUM_BUF]u8 = undefined; + const buf = std.fmt.bufPrint(&s, "{d}", .{number}) catch unreachable; // should be able to fit + try L.pushlstring(buf); + }, + .Vector => { + const vec = L.tovector(idx).?; + var s: [(MAX_NUM_BUF * VEC_SIZE) + ((VEC_SIZE - 1) * 2)]u8 = undefined; + const buf = if (VEC_SIZE == 3) + std.fmt.bufPrint(&s, "{d}, {d}, {d}", .{ vec[0], vec[1], vec[2] }) catch unreachable // should be able to fit + else + std.fmt.bufPrint(&s, "{d}, {d}, {d}, {d}", .{ vec[0], vec[1], vec[2], vec[3] }) catch unreachable; // should be able to fit + try L.pushlstring(buf); + }, + .String => L.pushvalue(idx), + .Integer => { + const l = L.tointeger64(idx).?; + var s: [MAX_NUM_BUF]u8 = undefined; + const buf = std.fmt.bufPrint(&s, "{d}", .{l}) catch unreachable; // should be able to fit + try L.pushlstring(buf); + }, + else => { + const ptr = L.topointer(idx); + var s: [20 + ltm.LONGEST_TYPENAME_SIZE]u8 = undefined; // 16 + 2 + 2(extra) + size + const buf = std.fmt.bufPrint(&s, "{s}: 0x{x:016}", .{ lapi.typename(L.typeOf(idx)), @intFromPtr(ptr) }) catch unreachable; // should be able to fit + try L.pushlstring(buf); + }, + } + return L.tolstring(-1) orelse unreachable; +} + +/// Converts value to string & pushes to stack. Calls a __tostring metamethod when it exists. +/// If the metamethod fails, it returns an error & error value on stack. +pub fn Ztolstring(L: *lua.State, idx: i32) ![]const u8 { + if (try Zcallmeta(L, idx, "__tostring")) { + return L.tolstring(-1) orelse return error.BadReturnType; + } + return Ztolstringk(L, idx); +} + +fn tag_error(L: *lua.State, narg: i32, tag: lua.Type, comptime msg: ?[]const u8) anyerror { + const curr_type = L.typeOf(narg); + const fname = laux.currfuncname(L); + const obj = lapi.Atoobject(L, narg); + + if (narg > 0) { + if (msg) |m| { + if (fname) |name| + return Zerrorf(L, "{s}, argument #{d} to '{s}' ({s} expected, got {s})", .{ m, narg, name, lapi.typename(tag), lapi.typename(curr_type) }) + else + return Zerrorf(L, "{s}, argument #{d} ({s} expected, got {s})", .{ m, narg, lapi.typename(tag), lapi.typename(curr_type) }); + } else { + if (obj != null) { + if (fname) |name| + return Zerrorf(L, "invalid argument #{d} to '{s}' ({s} expected, got {s})", .{ narg, name, lapi.typename(tag), lapi.typename(curr_type) }) + else + return Zerrorf(L, "invalid argument #{d} ({s} expected, got {s})", .{ narg, lapi.typename(tag), lapi.typename(curr_type) }); + } else { + if (fname) |name| + return Zerrorf(L, "missing argument #{d} to '{s}' ({s} expected)", .{ narg, name, lapi.typename(tag) }) + else + return Zerrorf(L, "missing argument #{d} ({s} expected)", .{ narg, lapi.typename(tag) }); + } + } + } else { + return Zerrorf(L, "{s} ({s} expected, got {s})", .{ msg orelse "invalid value", lapi.typename(tag), lapi.typename(curr_type) }); + } +} + +pub fn Zcheckstack(L: *lua.State, space: usize, msg: ?[]const u8) !void { + if (!try L.checkstack(space)) + if (msg) |m| + return Zerrorf(L, "stack overflow ({s})", .{m}) + else + return Zerrorf(L, "stack overflow", .{}); +} + +pub fn Zchecktype(L: *lua.State, narg: i32, t: lua.Type) !void { + if (L.typeOf(narg) != t) + return tag_error(L, narg, t, null); +} + +pub fn Zcheckvalue(L: *lua.State, comptime T: type, narg: i32, comptime msg: ?[]const u8) !T { + switch (@typeInfo(T)) { + .bool => if (L.isboolean(narg)) + return L.toboolean(narg) + else + return tag_error(L, narg, .Boolean, msg), + .int => |int| { + if (!L.isnumber(narg)) + return tag_error(L, narg, .Number, msg); + if (int.bits < 32) { + const value = lapi.tointeger(L, narg) orelse unreachable; + const max = std.math.maxInt(T); + const min = std.math.minInt(T); + if (value > max or value < min) { + if (narg > 0) { + return L.Zerrorf("invalid argument #{d} (number expected between {d} and {d}, got {d})", .{ narg, min, max, value }); + } else { + return L.Zerrorf("{s} (number expected between {d} and {d}, got {d})", .{ msg orelse "invalid value", min, max, value }); + } + } + return @intCast(value); + } else if (int.bits == 32) { + const getfn = if (int.signedness == .signed) lapi.tointeger else lapi.tounsigned; + return getfn(L, narg) orelse unreachable; + } else @compileError("int size too large, 32 bits or lower is supported"); + }, + .float => |float| { + if (!L.isnumber(narg)) + return tag_error(L, narg, .Number, msg); + if (float.bits <= 64) + return @floatCast(L.tonumber(narg) orelse unreachable) + else + @compileError("float size too large"); + }, + .pointer => |pointer| { + if (pointer.size == .one) + switch (L.typeOf(narg)) { + .LightUserdata, .Userdata => return L.touserdata(pointer.child, narg) orelse unreachable, + else => return tag_error(L, narg, .Userdata, msg), + } + else if (pointer.size == .slice) { + if (pointer.child == u8) { + if (pointer.sentinel_ptr) |sentinel| { + const s: *const pointer.child = @ptrCast(sentinel); + if (s.* == 0) { + if (!pointer.is_const) + @compileError("Pointer must be [:0]const u8 when using a sentinel, string only"); + if (L.typeOf(narg) != .String) + return tag_error(L, narg, .String, msg); + return L.tostring(narg) orelse unreachable; + } else @compileError("Unsupported pointer sentinel [:?]" ++ @typeName(pointer.child)); + } else { + if (pointer.is_const) + switch (L.typeOf(narg)) { + .String => return L.tolstring(narg) orelse unreachable, + .Buffer => return L.tobuffer(narg) orelse unreachable, + else => return tag_error(L, narg, .String, msg), + } + else switch (L.typeOf(narg)) { + .Buffer => return L.tobuffer(narg) orelse unreachable, + else => return tag_error(L, narg, .Buffer, msg), + } + } + } else if (pointer.child == f32) { + if (pointer.is_const and pointer.sentinel_ptr == null) { + if (L.isvector(narg)) + return L.tovector(narg) orelse unreachable + else + return tag_error(L, narg, .Vector, msg); + } else @compileError("Unsupported pointer type, you would need to make []f32 const and exclude sentinel" ++ @typeName(T)); + } else @compileError("Unsupported pointer type " ++ @typeName(T)); + } else @compileError("Unsupported pointer type " ++ @typeName(T)); + }, + .@"enum" => |e| { + switch (@typeInfo(e.tag_type)) { + .int => |int| { + if (int.bits > 32) + @compileError("int size too large, 32 bits or lower is supported"); + if (!L.isnumber(narg)) + return tag_error(L, narg, .Number, msg); + if (int.signedness == .unsigned) { + const value = L.tounsigned(narg) orelse unreachable; + comptime var can_cast = true; + comptime for (e.fields, 0..) |field, order| { + if (field.value != order) { + can_cast = false; + break; + } + }; + if (comptime can_cast) { + if (value < e.fields.len) + return @enumFromInt(value); + } else { + inline for (e.fields) |field| { + if (field.value == value) + return @enumFromInt(value); + } + } + return Zerror(L, "Invalid enum value"); + } else { + const value = L.tointeger(narg) orelse unreachable; + inline for (e.fields) |field| { + if (field.value == value) + return @enumFromInt(value); + } + return Zerror(L, "Invalid enum value"); + } + }, + else => @compileError("Unsupported enum type " ++ @tagName(e.tag_type)), + } + }, + .@"struct" => |s| { + if (!L.istable(narg)) + return tag_error(L, narg, .Table, msg); + var val: T = std.mem.zeroes(T); + inline for (s.fields) |field| { + @field(val, field.name) = if (comptime field.defaultValue()) |default| + try Zcheckfield(L, ?field.type, narg, field.name) orelse default + else + try Zcheckfield(L, field.type, narg, field.name); + L.pop(1); + } + return val; + }, + .null => if (L.typeOf(narg) == .Nil) + return null + else + return tag_error(L, narg, .Nil, msg), + .optional => |optional| { + if (L.isnoneornil(narg)) + return null; + return try Zcheckvalue(L, optional.child, narg, msg); + }, + .void => if (L.typeOf(narg) == .None) + return + else { + return tag_error(L, narg, .None, msg); + }, + else => |t| @compileError("Unsupported type " ++ @tagName(t)), + } +} + +pub fn Zcheckfield(L: *lua.State, comptime T: type, idx: i32, comptime field: [:0]const u8) !T { + _ = try L.getfield(idx, field); + errdefer L.remove(-2); + return try Zcheckvalue(L, T, -1, "invalid field '" ++ field ++ "'"); +} + +/// Returns true if metatable was created, false if it already exists. +pub fn Znewmetatable(L: *lua.State, tname: [:0]const u8, value: anytype) !bool { + if (try L.getfield(lua.REGISTRYINDEX, tname) != .Nil) + return false; + L.pop(1); + if (@typeInfo(@TypeOf(value)) != .@"struct") + @compileError("value must be a struct"); + try L.Zpushvalue(value); + L.pushvalue(-1); + try L.setfield(lua.REGISTRYINDEX, tname); + return true; +} + +const EXCEPTIONS_ENABLED = !@import("builtin").cpu.arch.isWasm(); + +test "toCFn + Zchecktype" { + const L = try @import("lstate.zig").Lnewstate(); + defer L.deinit(); + errdefer std.debug.print("{s}\n", .{L.tostring(-1) orelse "No lua error"}); + + { + const foo = struct { + fn inner(l: *lua.State) !i32 { + try Zchecktype(l, 1, .Number); + std.testing.expectEqual(6, l.tonumber(1).?) catch @panic("failed"); + l.pushnumber(2); + return 1; + } + }.inner; + + try L.pushcclosure(toCFn(foo), "foo", 0); + L.pushnumber(6); + L.call(1, 1); + defer L.pop(1); + try std.testing.expectEqual(2, L.tonumber(-1).?); + } + if (comptime EXCEPTIONS_ENABLED) { + const foo = struct { + fn inner(l: *lua.State) !i32 { + try Zchecktype(l, 1, .Number); + return 0; + } + }.inner; + + try L.pushcclosure(toCFn(foo), "foo", 0); + try std.testing.expectEqual(error.Runtime, L.pcall(0, 0, 0).check()); + try std.testing.expectEqualStrings("missing argument #1 to 'foo' (number expected)", L.tostring(-1).?); + defer L.pop(1); + } + if (comptime EXCEPTIONS_ENABLED) { + const foo = struct { + fn inner(_: *lua.State) !i32 { + return error.TestError; + } + }.inner; + + try L.pushcclosure(toCFn(foo), "foo", 0); + try std.testing.expectEqual(error.Runtime, L.pcall(0, 0, 0).check()); + try std.testing.expectEqualStrings("TestError", L.tostring(-1).?); + defer L.pop(1); + } + { + const foo = struct { + fn inner(l: *lua.State) void { + std.testing.expectEqual(9, l.tonumber(1).?) catch @panic("failed"); + } + }.inner; + + try L.pushcclosure(toCFn(foo), "foo", 0); + L.pushnumber(9); + L.call(1, 0); + } +} + +test toCFnV { + const L = try @import("lstate.zig").Lnewstate(); + defer L.deinit(); + errdefer std.debug.print("{s}\n", .{L.tostring(-1) orelse "No lua error"}); + + { + const foo = struct { + fn inner(l: *lua.State) i32 { + std.testing.expectEqual(6, l.tonumber(1).?) catch @panic("failed"); + return 2; + } + }.inner; + + try L.pushcclosure(toCFnV(foo), "foo", 0); + L.pushnumber(6); + L.call(1, 1); + defer L.pop(1); + try std.testing.expectEqual(2, L.tonumber(-1).?); + } + { + const foo = struct { + fn inner(_: *lua.State) comptime_int { + return 8; + } + }.inner; + + try L.pushcclosure(toCFnV(foo), "foo", 0); + L.call(0, 1); + defer L.pop(1); + try std.testing.expectEqual(8, L.tonumber(-1).?); + } + { + const foo = struct { + fn inner(_: *lua.State) comptime_float { + return 123.456; + } + }.inner; + + try L.pushcclosure(toCFnV(foo), "foo", 0); + L.call(0, 1); + defer L.pop(1); + try std.testing.expectEqual(123.456, L.tonumber(-1).?); + } + { + const foo = struct { + fn inner(_: *lua.State) f64 { + return 234.567; + } + }.inner; + + try L.pushcclosure(toCFnV(foo), "foo", 0); + L.call(0, 1); + defer L.pop(1); + try std.testing.expectEqual(234.567, L.tonumber(-1).?); + } + { + const Dummy = struct { + a: f64, + b: enum { A, B, C }, + c: []const u8, + d: i4, + }; + const foo = struct { + fn inner(_: *lua.State) Dummy { + return Dummy{ + .a = 3.14, + .b = .A, + .c = "Test", + .d = 6, + }; + } + }.inner; + + try L.pushcclosure(toCFnV(foo), "foo", 0); + L.call(0, 1); + defer L.pop(1); + try std.testing.expectEqual(.Number, L.getfield(-1, "a")); + try std.testing.expectEqual(3.14, L.tonumber(-1).?); + L.pop(1); + try std.testing.expectEqual(.Number, L.getfield(-1, "b")); + try std.testing.expectEqual(0, L.tointeger(-1).?); + L.pop(1); + try std.testing.expectEqual(.String, L.getfield(-1, "c")); + try std.testing.expectEqualStrings("Test", L.tostring(-1).?); + L.pop(1); + try std.testing.expectEqual(.Number, L.getfield(-1, "d")); + try std.testing.expectEqual(6, L.tointeger(-1).?); + L.pop(1); + } + { + const foo = struct { + fn inner(_: *lua.State) ?i32 { + return 123; + } + }.inner; + + try L.pushcclosure(toCFnV(foo), "foo", 0); + L.call(0, 1); + defer L.pop(1); + try std.testing.expectEqual(123, L.tonumber(-1).?); + } + { + const foo = struct { + fn inner(_: *lua.State) !?i32 { + return 123; + } + }.inner; + + try L.pushcclosure(toCFnV(foo), "foo", 0); + L.call(0, 1); + defer L.pop(1); + try std.testing.expectEqual(123, L.tonumber(-1).?); + } + { + const foo = struct { + fn inner(_: *lua.State) enum { A, B, C } { + return .C; + } + }.inner; + + try L.pushcclosure(toCFnV(foo), "foo", 0); + L.call(0, 1); + defer L.pop(1); + try std.testing.expectEqual(2, L.tointeger(-1).?); + } + if (comptime EXCEPTIONS_ENABLED) { + const foo = struct { + fn inner(_: *lua.State) !?i32 { + return error.Failed; + } + }.inner; + + try L.pushcclosure(toCFnV(foo), "foo", 0); + try std.testing.expectEqual(error.Runtime, L.pcall(0, 0, 0).check()); + try std.testing.expectEqualStrings("Failed", L.tostring(-1).?); + defer L.pop(1); + } + if (comptime EXCEPTIONS_ENABLED) { + const foo = struct { + fn inner(_: *lua.State) !i32 { + return error.TestError; + } + }.inner; + + try L.pushcclosure(toCFnV(foo), "foo", 0); + try std.testing.expectEqual(error.Runtime, L.pcall(0, 0, 0).check()); + try std.testing.expectEqualStrings("TestError", L.tostring(-1).?); + defer L.pop(1); + } + { + const foo = struct { + fn inner(l: *lua.State) void { + std.testing.expectEqual(9, l.tonumber(1).?) catch @panic("failed"); + } + }.inner; + + try L.pushcclosure(toCFnV(foo), "foo", 0); + L.pushnumber(9); + L.call(1, 0); + } +} + +test Zpushfunction { + const L = try @import("lstate.zig").Lnewstate(); + defer L.deinit(); + errdefer std.debug.print("{s}\n", .{L.tostring(-1) orelse "No lua error"}); + + { + const foo = struct { + fn inner(l: *lua.State) i32 { + std.testing.expectEqual(6, l.tonumber(1).?) catch @panic("failed"); + l.pushnumber(2); + return 1; + } + }.inner; + + try Zpushfunction(L, foo, "foo"); + L.pushnumber(6); + L.call(1, 1); + try std.testing.expectEqual(2, L.tonumber(-1).?); + } +} + +test Zpushvalue { + const L = try @import("lstate.zig").Lnewstate(); + defer L.deinit(); + errdefer std.debug.print("{s}\n", .{L.tostring(-1) orelse "No lua error"}); + + try Zpushvalue(L, 455); + try std.testing.expectEqual(.Number, L.typeOf(-1)); + try std.testing.expectEqual(455, L.tointeger(-1).?); + L.pop(1); + + try Zpushvalue(L, @as(u8, 255)); + try std.testing.expectEqual(.Number, L.typeOf(-1)); + try std.testing.expectEqual(255, L.tounsigned(-1).?); + L.pop(1); + + try Zpushvalue(L, @as(i10, std.math.maxInt(i10))); + try std.testing.expectEqual(.Number, L.typeOf(-1)); + try std.testing.expectEqual(std.math.maxInt(i10), L.tointeger(-1).?); + L.pop(1); + + try Zpushvalue(L, 1.24); + try std.testing.expectEqual(.Number, L.typeOf(-1)); + try std.testing.expectEqual(1.24, L.tonumber(-1).?); + L.pop(1); + + try Zpushvalue(L, @as(f32, 1.24)); + try std.testing.expectEqual(.Number, L.typeOf(-1)); + try std.testing.expectApproxEqRel(1.24, L.tonumber(-1).?, 0.001); + L.pop(1); + + try Zpushvalue(L, @as(f64, 1.24)); + try std.testing.expectEqual(.Number, L.typeOf(-1)); + try std.testing.expectEqual(1.24, L.tonumber(-1).?); + L.pop(1); + + try Zpushvalue(L, "Test"); + try std.testing.expectEqual(.String, L.typeOf(-1)); + try std.testing.expectEqualStrings("Test", L.tostring(-1).?); + L.pop(1); + + try Zpushvalue(L, @as([]const u8, "Test2")); + try std.testing.expectEqual(.String, L.typeOf(-1)); + try std.testing.expectEqualStrings("Test2", L.tostring(-1).?); + L.pop(1); + + try Zpushvalue(L, @as([:0]const u8, "Test3")); + try std.testing.expectEqual(.String, L.typeOf(-1)); + try std.testing.expectEqualStrings("Test3", L.tostring(-1).?); + L.pop(1); + + try Zpushvalue(L, true); + try std.testing.expectEqual(.Boolean, L.typeOf(-1)); + try std.testing.expectEqual(true, L.toboolean(-1)); + L.pop(1); + + try Zpushvalue(L, null); + try std.testing.expectEqual(.Nil, L.typeOf(-1)); + try std.testing.expectEqual(false, L.toboolean(-1)); + L.pop(1); + + try Zpushvalue(L, .{}); // empty struct + try std.testing.expectEqual(.Table, L.typeOf(-1)); + L.pushnil(); + try std.testing.expectEqual(false, L.next(-2)); + L.pop(1); + + try Zpushvalue(L, .{ .x = 1, .y = 2 }); + { + defer L.pop(1); + try std.testing.expectEqual(.Table, L.typeOf(-1)); + L.pushnil(); + try std.testing.expectEqual(true, L.next(-2)); + try std.testing.expectEqual(.String, L.typeOf(-2)); + try std.testing.expectEqual(.Number, L.typeOf(-1)); + L.pop(1); + try std.testing.expectEqual(true, L.next(-2)); + try std.testing.expectEqual(.String, L.typeOf(-2)); + try std.testing.expectEqual(.Number, L.typeOf(-1)); + L.pop(1); + try std.testing.expectEqual(false, L.next(-2)); + + try std.testing.expectEqual(.Number, L.getfield(-1, "x")); + try std.testing.expectEqual(1, L.tointeger(-1).?); + L.pop(1); + try std.testing.expectEqual(.Number, L.getfield(-1, "y")); + try std.testing.expectEqual(2, L.tointeger(-1).?); + L.pop(1); + } + + { + const foo = struct { + fn inner(l: *lua.State) i32 { + std.testing.expectEqual(6, l.tonumber(1).?) catch @panic("failed"); + l.pushnumber(2); + return 1; + } + }.inner; + + try Zpushvalue(L, .{ + .foo = foo, + }); + + try std.testing.expectEqual(.Function, L.getfield(-1, "foo")); + L.pushnumber(6); + L.call(1, 1); + try std.testing.expectEqual(2, L.tonumber(-1).?); + } + + if (comptime lua.config.VECTOR_SIZE == 3) { + try Zpushvalue(L, @Vector(3, f32){ 1.0, 2.0, 3.0 }); + } else { + try Zpushvalue(L, @Vector(4, f32){ 1.0, 2.0, 3.0, 4.0 }); + } + { + defer L.pop(1); + try std.testing.expectEqual(.Vector, L.typeOf(-1)); + const vec = L.tovector(-1).?; + try std.testing.expectEqual(lua.config.VECTOR_SIZE, vec.len); + try std.testing.expectEqual(1.0, vec[0]); + try std.testing.expectEqual(2.0, vec[1]); + try std.testing.expectEqual(3.0, vec[2]); + if (comptime lua.config.VECTOR_SIZE == 4) + try std.testing.expectEqual(4.0, vec[3]); + } + + { + var array: [3]i32 = undefined; + array[0] = 1; + array[1] = 2; + array[2] = 3; + + try Zpushvalue(L, array); + defer L.pop(1); + { + try std.testing.expectEqual(.Table, L.typeOf(-1)); + L.pushnil(); + try std.testing.expectEqual(true, L.next(-2)); + try std.testing.expectEqual(.Number, L.typeOf(-2)); + try std.testing.expectEqual(.Number, L.typeOf(-1)); + L.pop(1); + try std.testing.expectEqual(true, L.next(-2)); + try std.testing.expectEqual(.Number, L.typeOf(-2)); + try std.testing.expectEqual(.Number, L.typeOf(-1)); + L.pop(1); + try std.testing.expectEqual(true, L.next(-2)); + try std.testing.expectEqual(.Number, L.typeOf(-2)); + try std.testing.expectEqual(.Number, L.typeOf(-1)); + L.pop(1); + try std.testing.expectEqual(false, L.next(-2)); + + try std.testing.expectEqual(.Number, L.rawgeti(-1, 1)); + try std.testing.expectEqual(1, L.tointeger(-1).?); + L.pop(1); + try std.testing.expectEqual(.Number, L.rawgeti(-1, 2)); + try std.testing.expectEqual(2, L.tointeger(-1).?); + L.pop(1); + try std.testing.expectEqual(.Number, L.rawgeti(-1, 3)); + try std.testing.expectEqual(3, L.tointeger(-1).?); + L.pop(1); + } + } + + { + var array: []i32 = try std.testing.allocator.alloc(i32, 3); + defer std.testing.allocator.free(array); + array[0] = 4; + array[1] = 5; + array[2] = 6; + + try Zpushvalue(L, array); + defer L.pop(1); + { + try std.testing.expectEqual(.Table, L.typeOf(-1)); + L.pushnil(); + try std.testing.expectEqual(true, L.next(-2)); + try std.testing.expectEqual(.Number, L.typeOf(-2)); + try std.testing.expectEqual(.Number, L.typeOf(-1)); + L.pop(1); + try std.testing.expectEqual(true, L.next(-2)); + try std.testing.expectEqual(.Number, L.typeOf(-2)); + try std.testing.expectEqual(.Number, L.typeOf(-1)); + L.pop(1); + try std.testing.expectEqual(true, L.next(-2)); + try std.testing.expectEqual(.Number, L.typeOf(-2)); + try std.testing.expectEqual(.Number, L.typeOf(-1)); + L.pop(1); + try std.testing.expectEqual(false, L.next(-2)); + + try std.testing.expectEqual(.Number, L.rawgeti(-1, 1)); + try std.testing.expectEqual(4, L.tointeger(-1).?); + L.pop(1); + try std.testing.expectEqual(.Number, L.rawgeti(-1, 2)); + try std.testing.expectEqual(5, L.tointeger(-1).?); + L.pop(1); + try std.testing.expectEqual(.Number, L.rawgeti(-1, 3)); + try std.testing.expectEqual(6, L.tointeger(-1).?); + L.pop(1); + } + } + + try Zpushvalue(L, @as(?u8, 255)); + try std.testing.expectEqual(.Number, L.typeOf(-1)); + try std.testing.expectEqual(255, L.tounsigned(-1).?); + L.pop(1); + + try Zpushvalue(L, @as(?u8, null)); + try std.testing.expectEqual(.Nil, L.typeOf(-1)); + try std.testing.expectEqual(false, L.toboolean(-1)); + L.pop(1); + + { + try Zpushvalue(L, .{ + .x = 1, + .y = 2, + }); + try std.testing.expectEqual(.Table, L.typeOf(-1)); + L.pushnil(); + try std.testing.expectEqual(true, L.next(-2)); + try std.testing.expectEqual(.String, L.typeOf(-2)); + try std.testing.expectEqual(.Number, L.typeOf(-1)); + L.pop(1); + try std.testing.expectEqual(true, L.next(-2)); + try std.testing.expectEqual(.String, L.typeOf(-2)); + try std.testing.expectEqual(.Number, L.typeOf(-1)); + L.pop(1); + try std.testing.expectEqual(false, L.next(-2)); + + try std.testing.expectEqual(.Number, L.getfield(-1, "x")); + try std.testing.expectEqual(1, L.tointeger(-1).?); + L.pop(1); + try std.testing.expectEqual(.Number, L.getfield(-1, "y")); + try std.testing.expectEqual(2, L.tointeger(-1).?); + L.pop(1); + } +} + +test Zcheckvalue { + const L = try @import("lstate.zig").Lnewstate(); + defer L.deinit(); + errdefer std.debug.print("{s}\n", .{L.tostring(-1) orelse "No lua error"}); + + try Zpushvalue(L, 455); + try std.testing.expectEqual(455, try Zcheckvalue(L, i32, -1, null)); + L.pop(1); + + try Zpushvalue(L, 1.24); + try std.testing.expectEqual(1.24, try Zcheckvalue(L, f64, -1, null)); + L.pop(1); + + try Zpushvalue(L, "Test"); + try std.testing.expectEqualStrings("Test", try Zcheckvalue(L, []const u8, -1, null)); + L.pop(1); + + try Zpushvalue(L, true); + try std.testing.expectEqual(true, try Zcheckvalue(L, bool, -1, null)); + L.pop(1); + + try Zpushvalue(L, null); + try std.testing.expectEqual(null, try Zcheckvalue(L, ?i32, -1, null)); + L.pop(1); + try Zpushvalue(L, 2); + try std.testing.expectEqual(2, try Zcheckvalue(L, ?i32, -1, null)); + L.pop(1); + + const e = enum { A, B, C }; + try Zpushvalue(L, e.A); + try std.testing.expectEqual(e.A, try Zcheckvalue(L, e, -1, null)); + L.pop(1); + + const odd_e = enum(u4) { A = 2, B = 3, C = 4 }; + try Zpushvalue(L, odd_e.A); + try std.testing.expectEqual(odd_e.A, try Zcheckvalue(L, odd_e, -1, null)); + L.pop(1); + + const signed_e = enum(i32) { A = -1, B = 2, C = 4 }; + try Zpushvalue(L, signed_e.B); + try std.testing.expectEqual(signed_e.B, try Zcheckvalue(L, signed_e, -1, null)); + L.pop(1); + + const structA = struct { x: i32, y: i32 }; + try Zpushvalue(L, @as(structA, .{ .x = 1, .y = 2 })); + const val = try Zcheckvalue(L, structA, -1, null); + try std.testing.expectEqual(1, val.x); + try std.testing.expectEqual(2, val.y); + L.pop(1); + + const structB = struct { top: structA, bottom: structA }; + try Zpushvalue(L, @as(structB, .{ + .top = .{ .x = 1, .y = 2 }, + .bottom = .{ .x = 3, .y = 4 }, + })); + const val2 = try Zcheckvalue(L, structB, -1, null); + try std.testing.expectEqual(1, val2.top.x); + try std.testing.expectEqual(2, val2.top.y); + try std.testing.expectEqual(3, val2.bottom.x); + try std.testing.expectEqual(4, val2.bottom.y); + L.pop(1); + + const structDefaults = struct { x: i32, y: i32, z: ?i32 = 5 }; + try Zpushvalue(L, @as(structDefaults, .{ .x = 1, .y = 2 })); + const val3 = try Zcheckvalue(L, structDefaults, -1, null); + try std.testing.expectEqual(1, val3.x); + try std.testing.expectEqual(2, val3.y); + try std.testing.expectEqual(5, val3.z); + L.pop(1); + + const structDefaults2 = struct { x: i32, y: i32, z: i32 = 5 }; + try Zpushvalue(L, @as(structDefaults2, .{ .x = 1, .y = 2 })); + const val4 = try Zcheckvalue(L, structDefaults2, -1, null); + try std.testing.expectEqual(1, val4.x); + try std.testing.expectEqual(2, val4.y); + try std.testing.expectEqual(5, val4.z); + L.pop(1); + + { + const ud = struct { b: i32, c: i32 }; + const ptr = try L.newuserdata(ud); + ptr.* = .{ .b = 1, .c = 2 }; + const checked_ud = try Zcheckvalue(L, *ud, -1, null); + try std.testing.expectEqual(1, checked_ud.b); + try std.testing.expectEqual(2, checked_ud.c); + } + + { + const vec = if (lua.config.VECTOR_SIZE == 3) + @Vector(3, f32){ 1.0, 2.0, 3.0 } + else + @Vector(4, f32){ 1.0, 2.0, 3.0, 4.0 }; + try Zpushvalue(L, vec); + const checked_vec = try Zcheckvalue(L, []const f32, -1, null); + try std.testing.expectEqual(1.0, checked_vec[0]); + try std.testing.expectEqual(2.0, checked_vec[1]); + try std.testing.expectEqual(3.0, checked_vec[2]); + if (comptime lua.config.VECTOR_SIZE == 4) + try std.testing.expectEqual(4.0, checked_vec[3]); + L.pop(1); + } + + { + try Zpushbuffer(L, "Test"); + const checked_buf = try Zcheckvalue(L, []const u8, -1, null); + try std.testing.expectEqualSlices(u8, "Test", checked_buf); + const checked_buf2 = try Zcheckvalue(L, []u8, -1, null); + try std.testing.expectEqualSlices(u8, "Test", checked_buf2); + L.pop(1); + } + + { + try L.pushlstring("Test2"); + const checked_buf = try Zcheckvalue(L, []const u8, -1, null); + try std.testing.expectEqualSlices(u8, "Test2", checked_buf); + try std.testing.expectError(error.RaiseLuauError, Zcheckvalue(L, []u8, @intCast(L.gettop()), null)); + try std.testing.expectEqualStrings("invalid argument #2 (buffer expected, got string)", L.tostring(-1).?); + L.pop(1); + try std.testing.expectError(error.RaiseLuauError, Zcheckvalue(L, []u8, @intCast(L.gettop()), "custom")); + try std.testing.expectEqualStrings("custom, argument #2 (buffer expected, got string)", L.tostring(-1).?); + L.pop(1); + try std.testing.expectError(error.RaiseLuauError, Zcheckvalue(L, []u8, -1, null)); + try std.testing.expectEqualStrings("invalid value (buffer expected, got string)", L.tostring(-1).?); + L.pop(1); + try std.testing.expectError(error.RaiseLuauError, Zcheckvalue(L, []u8, -1, "custom")); + try std.testing.expectEqualStrings("custom (buffer expected, got string)", L.tostring(-1).?); + L.pop(2); + } + + { + L.pushinteger(255); + const num = try Zcheckvalue(L, u8, -1, null); + try std.testing.expectEqual(255, num); + try std.testing.expectError(error.RaiseLuauError, Zcheckvalue(L, u2, @intCast(L.gettop()), null)); + try std.testing.expectEqualStrings("invalid argument #2 (number expected between 0 and 3, got 255)", L.tostring(-1).?); + L.pop(1); + try std.testing.expectError(error.RaiseLuauError, Zcheckvalue(L, i2, @intCast(L.gettop()), null)); + try std.testing.expectEqualStrings("invalid argument #2 (number expected between -2 and 1, got 255)", L.tostring(-1).?); + L.pop(1); + try std.testing.expectError(error.RaiseLuauError, Zcheckvalue(L, u2, -1, null)); + try std.testing.expectEqualStrings("invalid value (number expected between 0 and 3, got 255)", L.tostring(-1).?); + L.pop(2); + } + { + L.pushinteger(-20); + const num = try Zcheckvalue(L, i8, -1, null); + try std.testing.expectEqual(-20, num); + try std.testing.expectError(error.RaiseLuauError, Zcheckvalue(L, u2, @intCast(L.gettop()), null)); + try std.testing.expectEqualStrings("invalid argument #2 (number expected between 0 and 3, got -20)", L.tostring(-1).?); + L.pop(1); + try std.testing.expectError(error.RaiseLuauError, Zcheckvalue(L, i2, @intCast(L.gettop()), null)); + try std.testing.expectEqualStrings("invalid argument #2 (number expected between -2 and 1, got -20)", L.tostring(-1).?); + L.pop(1); + try std.testing.expectError(error.RaiseLuauError, Zcheckvalue(L, i3, -1, null)); + try std.testing.expectEqualStrings("invalid value (number expected between -4 and 3, got -20)", L.tostring(-1).?); + L.pop(2); + } + { + try Zpushvalue(L, .{ .x = 1 }); + try std.testing.expectError(error.RaiseLuauError, Zcheckvalue(L, structA, -1, null)); + try std.testing.expectEqualStrings("invalid field 'y' (number expected, got nil)", L.tostring(-1).?); + L.pop(2); + + try Zpushvalue(L, .{ + .top = .{ .x = 1, .y = 2 }, + }); + try std.testing.expectError(error.RaiseLuauError, Zcheckvalue(L, structB, -1, null)); + try std.testing.expectEqualStrings("invalid field 'bottom' (table expected, got nil)", L.tostring(-1).?); + L.pop(2); + + try Zpushvalue(L, .{ + .top = .{ .x = 1, .y = 2 }, + .bottom = .{ .x = 3 }, + }); + try std.testing.expectError(error.RaiseLuauError, Zcheckvalue(L, structB, -1, null)); + try std.testing.expectEqualStrings("invalid field 'y' (number expected, got nil)", L.tostring(-1).?); + L.pop(2); + } +} + +test Zsetfield { + const L = try @import("lstate.zig").Lnewstate(); + defer L.deinit(); + errdefer std.debug.print("{s}\n", .{L.tostring(-1) orelse "No lua error"}); + + { + try L.newtable(); + try Zsetfield(L, -1, "a", 455); + try Zsetfield(L, -1, "b", "str"); + try Zsetfield(L, -1, "c", true); + try std.testing.expectEqual(.Number, L.getfield(-1, "a")); + try std.testing.expectEqual(455, L.tointeger(-1).?); + L.pop(1); + try std.testing.expectEqual(.String, L.getfield(-1, "b")); + try std.testing.expectEqualStrings("str", L.tostring(-1).?); + L.pop(1); + try std.testing.expectEqual(.Boolean, L.getfield(-1, "c")); + try std.testing.expectEqual(true, L.toboolean(-1)); + } + { + try Zsetfield(L, lua.GLOBALSINDEX, "a", 455); + try Zsetfield(L, lua.GLOBALSINDEX, "b", "str"); + try Zsetfield(L, lua.GLOBALSINDEX, "c", true); + try std.testing.expectEqual(.Number, L.getfield(lua.GLOBALSINDEX, "a")); + try std.testing.expectEqual(455, L.tointeger(-1).?); + try std.testing.expectEqual(.String, L.getfield(lua.GLOBALSINDEX, "b")); + try std.testing.expectEqualStrings("str", L.tostring(-1).?); + try std.testing.expectEqual(.Boolean, L.getfield(lua.GLOBALSINDEX, "c")); + try std.testing.expectEqual(true, L.toboolean(-1)); + } + { + try Zsetfield(L, lua.REGISTRYINDEX, "a", 455); + try Zsetfield(L, lua.REGISTRYINDEX, "b", "str"); + try Zsetfield(L, lua.REGISTRYINDEX, "c", true); + try std.testing.expectEqual(.Number, L.getfield(lua.REGISTRYINDEX, "a")); + try std.testing.expectEqual(455, L.tointeger(-1).?); + try std.testing.expectEqual(.String, L.getfield(lua.REGISTRYINDEX, "b")); + try std.testing.expectEqualStrings("str", L.tostring(-1).?); + try std.testing.expectEqual(.Boolean, L.getfield(lua.REGISTRYINDEX, "c")); + try std.testing.expectEqual(true, L.toboolean(-1)); + } +} + +test Zsetglobal { + const L = try @import("lstate.zig").Lnewstate(); + defer L.deinit(); + errdefer std.debug.print("{s}\n", .{L.tostring(-1) orelse "No lua error"}); + + try Zsetglobal(L, "a", 455); + try Zsetglobal(L, "b", "str"); + try Zsetglobal(L, "c", true); + try std.testing.expectEqual(.Number, L.getglobal("a")); + try std.testing.expectEqual(455, L.tointeger(-1).?); + try std.testing.expectEqual(.String, L.getglobal("b")); + try std.testing.expectEqualStrings("str", L.tostring(-1).?); + try std.testing.expectEqual(.Boolean, L.getglobal("c")); + try std.testing.expectEqual(true, L.toboolean(-1)); +} + +test Zsetfieldfn { + const L = try @import("lstate.zig").Lnewstate(); + defer L.deinit(); + errdefer std.debug.print("{s}\n", .{L.tostring(-1) orelse "No lua error"}); + + { + const foo = struct { + fn inner(l: *lua.State) i32 { + std.testing.expectEqual(6, l.tonumber(1).?) catch @panic("failed"); + l.pushnumber(2); + return 1; + } + }.inner; + + try L.newtable(); + try Zsetfieldfn(L, -1, "foo", foo); + try std.testing.expectEqual(.Function, L.getfield(-1, "foo")); + L.pushnumber(6); + L.call(1, 1); + try std.testing.expectEqual(2, L.tonumber(-1).?); + } +} + +test Zsetglobalfn { + const L = try @import("lstate.zig").Lnewstate(); + defer L.deinit(); + errdefer std.debug.print("{s}\n", .{L.tostring(-1) orelse "No lua error"}); + + { + const foo = struct { + fn inner(l: *lua.State) i32 { + std.testing.expectEqual(6, l.tonumber(1).?) catch @panic("failed"); + l.pushnumber(2); + return 1; + } + }.inner; + + try Zsetglobalfn(L, "foo", foo); + try std.testing.expectEqual(.Function, L.getglobal("foo")); + L.pushnumber(6); + L.call(1, 1); + try std.testing.expectEqual(2, L.tonumber(-1).?); + } +} + +test Zpushbuffer { + const L = try @import("lstate.zig").Lnewstate(); + defer L.deinit(); + errdefer std.debug.print("{s}\n", .{L.tostring(-1) orelse "No lua error"}); + + try Zpushbuffer(L, "Test"); + try std.testing.expectEqual(.Buffer, L.typeOf(-1)); + try std.testing.expectEqualSlices(u8, "Test", L.tobuffer(-1).?); +} + +test Zresumeerror { + const L = try @import("lstate.zig").Lnewstate(); + defer L.deinit(); + errdefer std.debug.print("{s}\n", .{L.tostring(-1) orelse "No lua error"}); + + { + const foo = struct { + fn inner(l: *lua.State) !i32 { + return try l.yield(0); + } + }.inner; + + try Zpushfunction(L, foo, "foo"); + try std.testing.expectEqual(.Yield, L.resumethread(null, 0)); + try std.testing.expectEqual(.ErrRun, Zresumeerror(L, null, "Test")); + try std.testing.expectEqualStrings("Test", L.tostring(-1).?); + } +} + +test Zresumeferror { + const L = try @import("lstate.zig").Lnewstate(); + defer L.deinit(); + errdefer std.debug.print("{s}\n", .{L.tostring(-1) orelse "No lua error"}); + + { + const foo = struct { + fn inner(l: *lua.State) !i32 { + return try l.yield(0); + } + }.inner; + + try Zpushfunction(L, foo, "foo"); + try std.testing.expectEqual(.Yield, L.resumethread(null, 0)); + try std.testing.expectEqual(.ErrRun, Zresumeferror(L, null, "Test {s}", .{"Fmt"})); + try std.testing.expectEqualStrings("Test Fmt", L.tostring(-1).?); + } +} + +test Zerror { + const L = try @import("lstate.zig").Lnewstate(); + defer L.deinit(); + errdefer std.debug.print("{s}\n", .{L.tostring(-1) orelse "No lua error"}); + + try std.testing.expectEqual(error.RaiseLuauError, Zerror(L, "Test")); + try std.testing.expectEqual(.String, L.typeOf(-1)); + try std.testing.expectEqualStrings("Test", L.tostring(-1).?); +} + +test Zyielderror { + const L = try @import("lstate.zig").Lnewstate(); + defer L.deinit(); + errdefer std.debug.print("{s}\n", .{L.tostring(-1) orelse "No lua error"}); + + try std.testing.expectEqual(error.RaiseLuauError, Zyielderror(L)); + try std.testing.expectEqualStrings("attempt to yield across metamethod/C-call boundary", L.tostring(-1).?); +} + +test Zerrorf { + const L = try @import("lstate.zig").Lnewstate(); + defer L.deinit(); + errdefer std.debug.print("{s}\n", .{L.tostring(-1) orelse "No lua error"}); + + try std.testing.expectEqual(error.RaiseLuauError, Zerrorf(L, "Test {s}", .{"Fmt"})); + try std.testing.expectEqual(.String, L.typeOf(-1)); + try std.testing.expectEqualStrings("Test Fmt", L.tostring(-1).?); +} + +test Ztolstring { + const L = try @import("lstate.zig").Lnewstate(); + defer L.close(); + + { + L.pushnil(); + try std.testing.expectEqualStrings("nil", try Ztolstring(L, -1)); + L.pop(1); + } + { + L.pushboolean(true); + try std.testing.expectEqualStrings("true", try Ztolstring(L, -1)); + L.pop(1); + } + { + if (lua.config.VECTOR_SIZE == 3) { + L.pushvector(1.2, 44.0, 123.0, 0.0); + try std.testing.expectEqualStrings("1.2, 44, 123", try Ztolstring(L, -1)); + } else { + L.pushvector(1.2, 44.0, 123.0, 1205.0); + try std.testing.expectEqualStrings("1.2, 44, 123, 1205", try Ztolstring(L, -1)); + } + L.pop(1); + } + { + try L.pushstring("hello"); + try std.testing.expectEqualStrings("hello", try Ztolstring(L, -1)); + L.pop(1); + } + { + L.pushinteger(-123); + try std.testing.expectEqualStrings("-123", try Ztolstring(L, -1)); + L.pop(1); + } + { + L.pushunsigned(123); + try std.testing.expectEqualStrings("123", try Ztolstring(L, -1)); + L.pop(1); + } + { + L.pushnumber(123.0); + try std.testing.expectEqualStrings("123", try Ztolstring(L, -1)); + L.pop(1); + } + { + L.pushlightuserdata(@ptrFromInt(0)); + try std.testing.expectEqualStrings("userdata: 0x0000000000000000", try Ztolstring(L, -1)); + L.pop(1); + } + { + _ = try L.newuserdata(struct {}); + try L.newtable(); + try L.Zpushfunction(struct { + fn inner(l: *lua.State) !i32 { + try l.pushlstring("meta_test"); + return 1; + } + }.inner, "__tostring"); + try L.setfield(-2, "__tostring"); + _ = try L.setmetatable(-2); + try std.testing.expectEqualStrings("meta_test", try Ztolstring(L, -1)); + L.pop(2); + } + if (comptime EXCEPTIONS_ENABLED) { + _ = try L.newuserdata(struct {}); + try L.newtable(); + try L.Zpushfunction(struct { + fn inner(l: *lua.State) !i32 { + try l.pushstring("error"); + l.raiseerror(); + } + }.inner, "__tostring"); + try L.setfield(-2, "__tostring"); + _ = try L.setmetatable(-2); + try std.testing.expectEqual(error.Runtime, Ztolstring(L, -1)); + try std.testing.expectEqual(.String, L.typeOf(-1)); + try std.testing.expectEqualStrings("error", L.tostring(-1).?); + L.pop(2); + } + { + _ = try L.newuserdata(struct {}); + try L.newtable(); + try L.Zpushfunction(struct { + fn inner(l: *lua.State) !i32 { + try l.newtable(); + return 1; + } + }.inner, "__tostring"); + try L.setfield(-2, "__tostring"); + _ = try L.setmetatable(-2); + try std.testing.expectEqual(error.BadReturnType, Ztolstring(L, -1)); + try std.testing.expectEqual(.Table, L.typeOf(-1)); + L.pop(2); + } +} + +test Znewmetatable { + const L = try @import("lstate.zig").Lnewstate(); + defer L.deinit(); + errdefer std.debug.print("{s}\n", .{L.tostring(-1) orelse "No lua error"}); + + { + try std.testing.expect(try Znewmetatable(L, "MAIN", .{ .a = 2, .b = 3 })); + try std.testing.expectEqual(.Table, L.typeOf(-1)); + try std.testing.expectEqual(.Number, L.getfield(-1, "a")); + try std.testing.expectEqual(2, L.tointeger(-1).?); + L.pop(1); + try std.testing.expectEqual(.Number, L.getfield(-1, "b")); + try std.testing.expectEqual(3, L.tointeger(-1).?); + L.pop(2); + } +} diff --git a/deps/luau/src/bridge.cpp b/deps/luau/src/bridge.cpp new file mode 100644 index 0000000..cad336d --- /dev/null +++ b/deps/luau/src/bridge.cpp @@ -0,0 +1,125 @@ +#include + +#include "Luau/Common.h" +#include "ldo.h" +#include "lclass.h" + +#include +#include +#include +#include + +LUAU_FASTFLAG(DebugLuauUserDefinedClasses); +LUAU_FASTFLAG(DebugLuauUserDefinedClassesRuntime); +LUAU_FASTFLAG(LuauAllowGlobalDeclarationToBeCalledClass); +LUAU_FASTFLAG(LuauIntegerType2); + +ZIG_EXPORT void xsh_set_luau_flags() +{ + std::printf("SETTING LUAU FLAGS\n"); + + FFlag::DebugLuauUserDefinedClasses.value = true; + FFlag::DebugLuauUserDefinedClassesRuntime.value = true; + FFlag::LuauAllowGlobalDeclarationToBeCalledClass.value = true; + FFlag::LuauIntegerType2.value = true; +} + +static int assertionHandler(const char *expr, const char *file, int line, const char *function) +{ + printf("%s(%d): ASSERTION FAILED: %s\n", file, line, expr); + return 1; +} + +ZIG_EXPORT void zig_registerAssertionHandler() +{ + Luau::assertHandler() = assertionHandler; +} + +ZIG_EXPORT void ZIG_FN(luau_free)(void *ptr) +{ + free(ptr); +} + +ZIG_EXPORT void ZIG_FN(delete_any)(void* value) +{ + operator delete(value); +} + +ZIG_EXPORT void* ZIG_FN(new_any)(size_t size) +{ + return operator new(size); +} + +ZIG_EXPORT size_t ZIG_FN(string_size)(std::string *str) +{ + return str->size(); +} + +ZIG_EXPORT const char* ZIG_FN(string_c_str)(std::string *str) +{ + return str->c_str(); +} + +ZIG_EXPORT Luau::FValue* zig_luau_getFValueList_bool() +{ + return Luau::FValue::list; +} + +ZIG_EXPORT Luau::FValue* zig_luau_getFValueList_int() +{ + return Luau::FValue::list; +} + +ZIG_EXPORT l_noret zig_luau_luaD_throw(lua_State *L, int errcode) +{ + luaD_throw(L, errcode); +} + +#if defined(__wasm__) + +#include + +#define LUAU_TRY_CATCH(trying, catching) zig_luau_try_catch_js(trying, catching) +#define LUAU_THROW(e) zig_luau_throw_js(e) +#define LUAU_EXTERNAL_TRY_CATCH + +#if not defined(LUAU_WASM_ENV_NAME) +#define LUAU_WASM_ENV_NAME "env" +#endif + +struct TryCatchContext +{ + std::function trying; + std::function catching; +}; +// only clang compilers support C/C++ -> wasm so it's safe to use the attribute here +__attribute__((import_module(LUAU_WASM_ENV_NAME), import_name("try_catch"))) void zig_luau_try_catch_js_impl(TryCatchContext *context); +__attribute__((import_module(LUAU_WASM_ENV_NAME), import_name("throw"))) void zig_luau_throw_js_impl(const std::exception *e); + +void zig_luau_try_catch_js(std::function trying, std::function catching) +{ + auto context = TryCatchContext{trying, catching}; + zig_luau_try_catch_js_impl(&context); +} + +void zig_luau_throw_js(const std::exception &e) +{ + zig_luau_throw_js_impl(&e); +} + +ZIG_EXPORT void zig_luau_try_impl(TryCatchContext *context) +{ + context->trying(); +} + +ZIG_EXPORT void zig_luau_catch_impl(TryCatchContext *context, const std::exception &e) +{ + context->catching(e); +} + +#endif + +ZIG_EXPORT int ZIG_FN(luaR_createobject)(lua_State *L) +{ + return luaR_createobject(L); +} diff --git a/deps/luau/src/bridge.h b/deps/luau/src/bridge.h new file mode 100644 index 0000000..fa2034a --- /dev/null +++ b/deps/luau/src/bridge.h @@ -0,0 +1,15 @@ +#ifndef LUAU_HEADERS +#define LUAU_HEADERS + +#include "lua.h" +#include "lualib.h" +#include "luacode.h" +#if (defined(__x86_64__) || defined(__amd64__) || defined(__aarch64__) || defined(__arm64__) || defined(__ARM64__)) && !defined(__BIG_ENDIAN__) +#include "luacodegen.h" +#endif + +#define ZIG_EXPORT extern "C" + +#define ZIG_FN(name) zig_##name + +#endif // LUAU_HEADERS diff --git a/deps/luau/src/cpp_std.zig b/deps/luau/src/cpp_std.zig new file mode 100644 index 0000000..7328904 --- /dev/null +++ b/deps/luau/src/cpp_std.zig @@ -0,0 +1,173 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +extern "c" fn zig_string_size(self: *const String) usize; +extern "c" fn zig_string_c_str(self: *const String) [*c]const u8; + +pub fn BasicString(comptime value_type: type) type { + return extern struct { + const __min_cap = if ((@sizeOf(__long) - 1) / @sizeOf(value_type) > 2) + (@sizeOf(__long) - 1) / @sizeOf(value_type) + else + 2; + + const __long = extern struct { + capacity: usize, + size: usize, + ptr: [*c]const value_type, + }; + + const __short = extern struct { + len: u8, + buffer: [__min_cap - 1:0]value_type, + }; + + data: extern union { + short: __short, + long: __long, + }, + + pub inline fn isShort(self: *const String) bool { + return self.data.short.len & 1 == 0; + } + + pub fn size(self: *const String) usize { + // if (self.isShort()) + // return self.data.short.len >> 1; + // return self.data.long.size; + return zig_string_size(self); + } + + pub fn c_str(self: *const String) [*c]const u8 { + // if (self.isShort()) + // return self.data.short.buffer; + // return self.data.long.ptr; + return zig_string_c_str(self); + } + + pub fn slice(self: *const String) []const u8 { + // if (self.isShort()) { + // const len = self.data.short.len >> 1; + // return self.data.short.buffer[0..len]; + // } + // const len = self.data.long.size; + // return self.data.long.ptr[0..len]; + const len = zig_string_size(self); + return zig_string_c_str(self)[0..len]; + } + }; +} + +// LLVM: 24 +// GCC/MSVC: 32 +// data: [24]u8 align(8), +pub const String = BasicString(u8); +comptime { + switch (@sizeOf(usize)) { + 4 => { + std.testing.expectEqual(12, @sizeOf(String)) catch @panic("String must be 12 bytes"); + std.testing.expectEqual(4, @alignOf(String)) catch @panic("String must be 4-byte aligned"); + }, + 8 => { + std.testing.expectEqual(24, @sizeOf(String)) catch @panic("String must be 24 bytes"); + std.testing.expectEqual(8, @alignOf(String)) catch @panic("String must be 8-byte aligned"); + }, + else => @compileError("Unsupported pointer size"), + } +} + +pub fn Exception(comptime T: type) type { + return extern struct { + vtable: *const anyopaque, + value: T, + }; +} + +pub fn Optional(comptime T: type) type { + return extern struct { + value: T = undefined, + has: bool, + + pub fn to(self: @This()) ?T { + if (self.has) { + return self.value; + } else { + return null; + } + } + + pub const nullopt = @This(){ .has = false }; + }; +} + +pub fn Vector(comptime T: type) type { + return extern struct { + begin: [*]T, + end: [*]T, + capacity_end: [*]T, + + const This = @This(); + + pub fn iterator(self: This) Iterator { + return .{ + .current = self.begin, + .end = self.end, + }; + } + + pub fn size(self: This) usize { + // divExact would not work if the sizeOf(T) doesn't match the C++ std::vector + return @divExact(@intFromPtr(self.end) - @intFromPtr(self.begin), @sizeOf(T)); + } + + pub fn empty(self: This) bool { + return self.size() == 0; + } + + pub fn front(self: This) ?T { + if (self.begin == self.end) + return null + else + return self.begin[0]; + } + + pub fn back(self: This) ?T { + const i = self.size(); + if (i == 0) + return null; + return self.begin[i - 1]; + } + + pub fn capacity(self: This) usize { + // divExact would not work if the sizeOf(T) doesn't match the C++ std::vector + return @divExact(@intFromPtr(self.capacity_end) - @intFromPtr(self.begin), @sizeOf(T)); + } + + pub fn at(self: This, pos: usize) T { + std.debug.assert(!self.empty()); + std.debug.assert(pos < self.size()); + return self.begin[pos]; + } + + pub const Iterator = struct { + current: [*]T, + end: [*]T, + pub fn next(self: *Iterator) ?T { + if (self.current == self.end) + return null + else { + const value = self.current[0]; + self.current = self.current[1..]; + return value; + } + } + }; + }; +} + +pub fn Pair(comptime First: type, comptime Second: type) type { + return extern struct { + first: First, + second: Second, + }; +} diff --git a/deps/luau/src/lib.zig b/deps/luau/src/lib.zig new file mode 100644 index 0000000..ad40e8a --- /dev/null +++ b/deps/luau/src/lib.zig @@ -0,0 +1,346 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +const build_config = @import("config"); + +pub const codegen = @import("CodeGen/lcodegen.zig"); + +pub const Analysis = if (build_config.buildAnalysis) struct { + pub const Frontend = @import("Analysis/Frontend.zig"); + pub const FileResolver = @import("Analysis/FileResolver.zig"); + pub const AstJsonEncoder = @import("Analysis/AstJsonEncoder.zig"); + pub const GenericConfigResolver = @import("Analysis/GenericConfigResolver.zig"); + test { + inline for (@typeInfo(@This()).@"struct".decls) |decl| + std.testing.refAllDecls(@field(@This(), decl.name)); + } +} else void; + +pub const Ast = if (build_config.buildAst) struct { + pub const Ast = @import("Ast/Ast.zig"); + pub const Cst = @import("Ast/Cst.zig"); + pub const Allocator = @import("Ast/Allocator.zig"); + pub const Lexer = @import("Ast/Lexer.zig"); + pub const Parser = @import("Ast/Parser.zig"); + pub const Location = @import("Ast/Location.zig"); + test { + inline for (@typeInfo(@This()).@"struct".decls) |decl| + std.testing.refAllDecls(@field(@This(), decl.name)); + } +} else void; + +pub const Inliner = if (build_config.buildInliner) struct { + pub const luajitinliner = @import("Inliner/luajitinliner.zig"); + test { + inline for (@typeInfo(@This()).@"struct".decls) |decl| + std.testing.refAllDecls(@field(@This(), decl.name)); + } +} else void; + +pub const Common = struct { + pub const DenseHash = @import("Common/DenseHash.zig"); + pub const Bytecode = @import("Common/Bytecode.zig"); + pub const BytecodeUtils = @import("Common/BytecodeUtils.zig"); + pub const ExperimentalFlags = @import("Common/ExperimentalFlags.zig"); + pub const Variant = @import("Common/Variant.zig"); + test { + inline for (@typeInfo(@This()).@"struct".decls) |decl| + std.testing.refAllDecls(@field(@This(), decl.name)); + } +}; + +pub const Compiler = if (build_config.buildCompiler) struct { + pub const luacode = @import("Compiler/luacode.zig"); + pub const Compiler = @import("Compiler/Compiler.zig"); + test { + inline for (@typeInfo(@This()).@"struct".decls) |decl| + std.testing.refAllDecls(@field(@This(), decl.name)); + } +} else void; + +pub const VM = if (build_config.buildVM) struct { + pub const lua = @import("VM/lua.zig"); + pub const ldo = @import("VM/ldo.zig"); + pub const lgc = @import("VM/lgc.zig"); + pub const ltm = @import("VM/ltm.zig"); + pub const zapi = @import("VM/zapi.zig"); + pub const lapi = @import("VM/lapi.zig"); + pub const laux = @import("VM/laux.zig"); + pub const lperf = @import("VM/lperf.zig"); + pub const linit = @import("VM/linit.zig"); + pub const lmem = @import("VM/lmem.zig"); + pub const lstate = @import("VM/lstate.zig"); + pub const lstring = @import("VM/lstring.zig"); + pub const ltable = @import("VM/ltable.zig"); + pub const ludata = @import("VM/ludata.zig"); + pub const lbuffer = @import("VM/lbuffer.zig"); + pub const lclass = @import("VM/lclass.zig"); + pub const lfunc = @import("VM/lfunc.zig"); + pub const ldebug = @import("VM/ldebug.zig"); + pub const lobject = @import("VM/lobject.zig"); + pub const lvmload = @import("VM/lvmload.zig"); + pub const lcommon = @import("VM/lcommon.zig"); + pub const lvm = @import("VM/lvm.zig"); + pub const lvmutils = @import("VM/lvmutils.zig"); + pub const lgcdebug = @import("VM/lgcdebug.zig"); + + // libraries + pub const lbitlib = @import("VM/lbitlib.zig"); + pub const lbaselib = @import("VM/lbaselib.zig"); + pub const lcorolib = @import("VM/lcorolib.zig"); + pub const ldblib = @import("VM/ldblib.zig"); + pub const lmathlib = @import("VM/lmathlib.zig"); + pub const loslib = @import("VM/loslib.zig"); + pub const lstrlib = @import("VM/lstrlib.zig"); + pub const ltablib = @import("VM/ltablib.zig"); + pub const lutf8lib = @import("VM/lutf8lib.zig"); + pub const lveclib = @import("VM/lveclib.zig"); + + // extra + pub const Errorset = @import("VM/errorset.zig"); + test { + inline for (@typeInfo(@This()).@"struct".decls) |decl| + std.testing.refAllDecls(@field(@This(), decl.name)); + } +} else void; + +pub const cpp_std = @import("cpp_std.zig"); + +test { + _ = Analysis; + _ = Ast; + _ = Common; + _ = Compiler; + _ = VM; + _ = Inliner; + _ = cpp_std; +} + +// +// VM +// +pub const LUAU_VERSION = VM.lua.config.LUAU_VERSION; +pub const VECTOR_SIZE = VM.lua.config.VECTOR_SIZE; + +pub const State = VM.lua.State; + +// +// Compiler +// +pub const compile = Compiler.luacode.compile; +pub const CompileOptions = Compiler.Compiler.CompileOptions; + +const c_FlagGroup = extern struct { + names: [*c][*c]const u8, + types: [*c]c_int, + size: usize, +}; + +fn FValue(comptime T: type) type { + return extern struct { + const Self = @This(); + + value: T, + dynamic: bool, + name: [*c]const u8, + next: ?*Self, + + pub const Iterator = struct { + state: *Self, + consumed: bool = false, + + pub fn next(self: *Iterator) ?*Self { + const state = self.state; + if (!self.consumed) { + self.consumed = true; + return state; + } + const n = state.next orelse return null; + self.state = n; + return n; + } + }; + + pub fn iterator(self: *Self) Iterator { + return .{ + .state = self, + }; + } + }; +} + +/// This function is defined in luau.cpp and must be called to define the assertion printer +extern "c" fn zig_registerAssertionHandler() void; + +extern "c" fn zig_luau_getFValueList_bool() *FValue(bool); +extern "c" fn zig_luau_getFValueList_int() *FValue(c_int); + +// NCG Workarounds - Minimal Debug Support for NCG +/// Luau.CodeGen mock __register_frame for a workaround Luau NCG +export fn __register_frame(frame: *const u8) void { + _ = frame; +} +/// Luau.CodeGen mock __deregister_frame for a workaround Luau NCG +export fn __deregister_frame(frame: *const u8) void { + _ = frame; +} + +pub const FFlags = struct { + pub fn Get(comptime T: type) *FValue(if (T == i32) c_int else T) { + if (T == bool) + return zig_luau_getFValueList_bool() + else if (T == c_int or T == i32) + return zig_luau_getFValueList_int() + else + @compileError("Unsupported type"); + } + + pub fn SetByName(comptime T: type, name: []const u8, value: T) !void { + var iter = Get(T).iterator(); + while (iter.next()) |flag| { + if (std.mem.eql(u8, std.mem.span(flag.name), name)) { + flag.value = value; + return; + } + } + return error.UnknownFlag; + } + + pub fn GetByName(comptime T: type, name: []const u8) ?*FValue(if (T == i32) c_int else T) { + var iter = Get(T).iterator(); + while (iter.next()) |flag| { + if (std.mem.eql(u8, std.mem.span(flag.name), name)) + return flag; + } + return null; + } +}; + +pub const Metamethods = struct { + pub const index = "__index"; + pub const newindex = "__newindex"; + pub const call = "__call"; + pub const concat = "__concat"; + pub const unm = "__unm"; + pub const add = "__add"; + pub const sub = "__sub"; + pub const mul = "__mul"; + pub const div = "__div"; + pub const idiv = "__idiv"; + pub const mod = "__mod"; + pub const pow = "__pow"; + pub const tostring = "__tostring"; + pub const metatable = "__metatable"; + pub const eq = "__eq"; + pub const lt = "__lt"; + pub const le = "__le"; + pub const mode = "__mode"; + pub const len = "__len"; + pub const iter = "__iter"; + pub const typename = "__type"; + pub const namecall = "__namecall"; +}; + +pub const CodeGen = if (build_config.buildCodeGen) struct { + pub fn Supported() bool { + return codegen.supported(); + } + pub fn Create(luau: *VM.lua.State) void { + codegen.create(luau); + } + pub fn Compile(luau: *VM.lua.State, idx: i32) void { + codegen.compile(luau, @intCast(idx)); + } +} else struct { + pub fn Supported() bool { + return false; + } + pub fn Create(_: *VM.lua.State) void { + @panic("CodeGen is not supported on " ++ @tagName(builtin.target.cpu.arch)); + } + pub fn Compile(_: *VM.lua.State, _: i32) void { + @panic("CodeGen is not supported on " ++ @tagName(builtin.target.cpu.arch)); + } +}; + +const alignment = @alignOf(std.c.max_align_t); + +/// Allows Luau to allocate memory using a Zig allocator passed in via data. +fn alloc(data: ?*anyopaque, ptr: ?*anyopaque, osize: usize, nsize: usize) callconv(.c) ?*align(alignment) anyopaque { + // just like malloc() returns a pointer "which is suitably aligned for any built-in type", + // the memory allocated by this function should also be aligned for any type that Lua may + // desire to allocate. use the largest alignment for the target + const allocator_ptr: *std.mem.Allocator = @ptrCast(@alignCast(data.?)); + + if (@as(?[*]align(alignment) u8, @ptrCast(@alignCast(ptr)))) |prev_ptr| { + const prev_slice = prev_ptr[0..osize]; + + // when nsize is zero the allocator must behave like free and return null + if (nsize == 0) { + allocator_ptr.free(prev_slice); + return null; + } + + // when nsize is not zero the allocator must behave like realloc + const new_ptr = allocator_ptr.realloc(prev_slice, nsize) catch return null; + return new_ptr.ptr; + } else if (nsize == 0) { + return null; + } else { + // ptr is null, allocate a new block of memory + const new_ptr = allocator_ptr.alignedAlloc(u8, .fromByteUnits(alignment), nsize) catch return null; + return new_ptr.ptr; + } +} + +pub fn getallocator(luau: *VM.lua.State) std.mem.Allocator { + var data: ?*std.mem.Allocator = undefined; + _ = luau.getallocf(@ptrCast(&data)); + + if (data) |allocator_ptr| { + // Although the Allocator is passed to Lua as a pointer, return a + // copy to make use more convenient. + return allocator_ptr.*; + } + + @panic("Lua.allocator() invalid on Lua states created without a Zig allocator"); +} + +/// Initialize a Luau state with the given allocator +pub fn init(allocator_ptr: *const std.mem.Allocator) !*VM.lua.State { + zig_registerAssertionHandler(); + return try VM.lstate.newstate(alloc, @constCast(allocator_ptr)); +} + +comptime { + if (builtin.target.cpu.arch.isWasm() and build_config.wasm_cxa_exceptions) { + _ = struct { + var exception_buf: [4096]u8 = undefined; + var exception_fba = std.heap.FixedBufferAllocator.init(exception_buf[0..]); + export fn __cxa_allocate_exception(size: usize) callconv(.c) [*]u8 { + const data = exception_fba.allocator().alloc(u8, size + 4) catch unreachable; + std.mem.writeInt(u32, data[0..4], size, .little); + return data[4..].ptr; + } + export fn __cxa_free_exception(data: [*]const u8) callconv(.c) void { + const size = std.mem.readInt(u32, (data - 4)[0..4], .little); + exception_fba.allocator().free(data[0 .. size + 4]); + } + // should NEVER be called as we override in the luau upstream dependency + export fn __cxa_throw(thrown_exception: *u8, cpp_type_info: *anyopaque, dest: *const fn () callconv(.c) void) callconv(.c) noreturn { + _ = thrown_exception; + _ = cpp_type_info; + _ = dest; + unreachable; + } + + var threaded: ?std.Io.Threaded = null; + export fn clock() callconv(.c) i64 { + if (threaded == null) { + threaded = std.Io.Threaded.init(std.heap.c_allocator, .{}); + } + return std.Io.Timestamp.now(threaded.?.io(), .cpu_process).toMilliseconds(); + } + }; + } +} diff --git a/deps/luau/src/tests.zig b/deps/luau/src/tests.zig new file mode 100644 index 0000000..2afb361 --- /dev/null +++ b/deps/luau/src/tests.zig @@ -0,0 +1,1949 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +const testing = std.testing; + +const luau = @import("luau"); + +const AllocFn = luau.AllocFn; +const StringBuffer = luau.StringBuffer; +const DebugInfo = luau.DebugInfo; +const State = luau.VM.lua.State; + +const expect = testing.expect; +const expectEqual = testing.expectEqual; +const expectEqualStrings = testing.expectEqualStrings; +const expectError = testing.expectError; + +const EXCEPTIONS_ENABLED = !builtin.cpu.arch.isWasm(); + +fn expectStringContains(actual: []const u8, expected_contains: []const u8) !void { + if (std.mem.indexOf(u8, actual, expected_contains) == null) + return; + return error.TestExpectedStringContains; +} + +fn alloc(data: ?*anyopaque, ptr: ?*anyopaque, osize: usize, nsize: usize) callconv(.c) ?*anyopaque { + _ = data; + + const alignment = @alignOf(std.c.max_align_t); + if (@as(?[*]align(alignment) u8, @ptrCast(@alignCast(ptr)))) |prev_ptr| { + const prev_slice = prev_ptr[0..osize]; + if (nsize == 0) { + testing.allocator.free(prev_slice); + return null; + } + const new_ptr = testing.allocator.realloc(prev_slice, nsize) catch return null; + return new_ptr.ptr; + } else if (nsize == 0) { + return null; + } else { + const new_ptr = testing.allocator.alignedAlloc(u8, .fromByteUnits(alignment), nsize) catch return null; + return new_ptr.ptr; + } +} + +fn failing_alloc(data: ?*anyopaque, ptr: ?*anyopaque, osize: usize, nsize: usize) callconv(.c) ?*anyopaque { + _ = data; + _ = ptr; + _ = osize; + _ = nsize; + return null; +} + +test { + std.testing.refAllDecls(@This()); +} + +test "initialization" { + // initialize the Zig wrapper + var lua = try luau.init(&testing.allocator); + try expectEqual(luau.VM.lua.Status.Ok, lua.status()); + lua.deinit(); + + // attempt to initialize the Zig wrapper with no memory + try expectError(error.OutOfMemory, luau.init(&testing.failing_allocator)); + + // use the library directly + lua = try luau.VM.lstate.newstate(alloc, null); + lua.close(); + + // use the library with a bad AllocFn + try expectError(error.OutOfMemory, luau.VM.lstate.newstate(failing_alloc, null)); + + // use the auxiliary library (uses libc realloc and cannot be checked for leaks!) + lua = try luau.VM.lstate.Lnewstate(); + lua.close(); +} + +test "alloc functions" { + var lua = try luau.VM.lstate.newstate(alloc, null); + defer lua.deinit(); + + // get default allocator + var data: ?*anyopaque = undefined; + try expectEqual(alloc, lua.getallocf(&data)); +} + +test "Zig allocator access" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + const inner = struct { + fn inner(l: *State) i32 { + const allocator = luau.getallocator(l); + + const num = l.tointeger(1) orelse @panic("expected integer"); + + // Use the allocator + const nums = allocator.alloc(i32, @intCast(num)) catch unreachable; + defer allocator.free(nums); + + // Do something pointless to use the slice + var sum: i32 = 0; + for (nums, 0..) |*n, i| n.* = @intCast(i); + for (nums) |n| sum += n; + + l.pushinteger(sum); + return 1; + } + }.inner; + + try lua.Zpushfunction(inner, "test"); + lua.pushinteger(10); + _ = lua.pcall(1, 1, 0); + + try expectEqual(45, lua.tointeger(-1).?); +} + +test "standard library loading" { + // open all standard libraries + { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + try lua.Lopenlibs(); + } + + // open all standard libraries with individual functions + // these functions are only useful if you want to load the standard + // packages into a non-standard table + { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + lua.openbase(); + lua.openstring(); + lua.opentable(); + lua.openmath(); + lua.openos(); + lua.opendebug(); + lua.opencoroutine(); + lua.openutf8(); + lua.openbit32(); + lua.openbuffer(); + lua.openvector(); + } +} + +test "number conversion success and failure" { + const lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + _ = try lua.pushstring("1234.5678"); + try expectEqual(1234.5678, lua.tonumber(-1) orelse return error.InvalidType); + + _ = try lua.pushstring("1234"); + try expectEqual(1234, lua.tointeger(-1) orelse return error.InvalidType); + + lua.pushnil(); + try expectError(error.Fail, lua.tonumber(-1) orelse error.Fail); + try expectError(error.Fail, lua.tointeger(-1) orelse error.Fail); + + _ = try lua.pushstring("fail"); + try expectError(error.Fail, lua.tonumber(-1) orelse error.Fail); + try expectError(error.Fail, lua.tointeger(-1) orelse error.Fail); +} + +test "compare" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + lua.pushnumber(1); + lua.pushnumber(2); + + try testing.expect(!try lua.equal(1, 2)); + try testing.expect(try lua.lessthan(1, 2)); + + lua.pushinteger(2); + try testing.expect(try lua.equal(2, 3)); +} + +const add = struct { + fn addInner(l: *State) i32 { + const a = l.tointeger(1) orelse 0; + const b = l.tointeger(2) orelse 0; + l.pushinteger(a + b); + return 1; + } +}.addInner; + +test "type of and getting values" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + lua.pushnil(); + try expect(lua.isnil(1)); + try expect(lua.isnoneornil(1)); + try expect(lua.isnoneornil(2)); + try expect(lua.isnone(2)); + try expectEqual(.Nil, lua.typeOf(1)); + + lua.pushboolean(true); + try expectEqual(.Boolean, lua.typeOf(-1)); + try expect(lua.isboolean(-1)); + + try lua.newtable(); + try expectEqual(.Table, lua.typeOf(-1)); + try expect(lua.istable(-1)); + + lua.pushinteger(1); + try expectEqual(.Number, lua.typeOf(-1)); + try expect(lua.isnumber(-1)); + try expectEqual(1, lua.tointeger(-1) orelse @panic("bad")); + try expectEqualStrings("number", lua.Ltypename(-1)); + + lua.pushunsigned(4); + try expectEqual(.Number, lua.typeOf(-1)); + try expect(lua.isnumber(-1)); + try expectEqual(4, lua.tounsigned(-1) orelse @panic("bad")); + try expectEqualStrings("number", lua.Ltypename(-1)); + + var value: i32 = 0; + lua.pushlightuserdata(&value); + try expectEqual(.LightUserdata, lua.typeOf(-1)); + try expect(lua.islightuserdata(-1)); + try expect(lua.isuserdata(-1)); + + lua.pushnumber(0.1); + try expectEqual(.Number, lua.typeOf(-1)); + try expect(lua.isnumber(-1)); + try expectEqual(0.1, lua.tonumber(-1) orelse @panic("bad")); + + _ = lua.pushthread(); + try expectEqual(.Thread, lua.typeOf(-1)); + try expect(lua.isthread(-1)); + try expectEqual(lua, lua.tothread(-1) orelse @panic("bad")); + + try lua.pushstring("all your codebase are belong to us"); + try expectEqualStrings("all your codebase are belong to us", lua.tolstring(-1) orelse @panic("bad")); + try expectEqual(.String, lua.typeOf(-1)); + try expect(lua.isstring(-1)); + + try lua.Zpushfunction(add, "func"); + try expectEqual(.Function, lua.typeOf(-1)); + try expect(lua.iscfunction(-1)); + try expect(lua.isfunction(-1)); + try expectEqual(luau.VM.zapi.toCFn(add), lua.tocfunction(-1).?); + + try lua.pushstring("hello world"); + try expectEqualStrings("hello world", lua.tostring(-1) orelse @panic("bad")); + try expectEqual(.String, lua.typeOf(-1)); + try expect(lua.isstring(-1)); + + try lua.pushfstring("{s} {s} {d}", .{ "hello", "world", @as(i32, 10) }); + try expectEqual(.String, lua.typeOf(-1)); + try expect(lua.isstring(-1)); + try expectEqualStrings("hello world 10", lua.tostring(-1) orelse @panic("bad")); + + // Comptime known + try lua.pushfstring("{s} {s} {d}", .{ "hello", "world", @as(i32, 10) }); + try expectEqual(.String, lua.typeOf(-1)); + try expect(lua.isstring(-1)); + try expectEqualStrings("hello world 10", lua.tostring(-1) orelse @panic("bad")); + + // Runtime known + const arg1 = try std.testing.allocator.dupe(u8, "Hello"); + defer std.testing.allocator.free(arg1); + const arg2 = try std.testing.allocator.dupe(u8, "World"); + defer std.testing.allocator.free(arg2); + + try lua.pushfstring("{s} {s} {d}", .{ arg1, arg2, @as(i32, 10) }); + try expectEqual(.String, lua.typeOf(-1)); + try expect(lua.isstring(-1)); + try expectEqualStrings("Hello World 10", lua.tostring(-1) orelse @panic("bad")); + + lua.pushvalue(2); + try expectEqual(.Boolean, lua.typeOf(-1)); + try expect(lua.isboolean(-1)); +} + +test "typenames" { + try expectEqualStrings("no value", luau.VM.lapi.typename(.None)); + try expectEqualStrings("nil", luau.VM.lapi.typename(.Nil)); + try expectEqualStrings("boolean", luau.VM.lapi.typename(.Boolean)); + try expectEqualStrings("userdata", luau.VM.lapi.typename(.LightUserdata)); + try expectEqualStrings("number", luau.VM.lapi.typename(.Number)); + try expectEqualStrings("string", luau.VM.lapi.typename(.String)); + try expectEqualStrings("table", luau.VM.lapi.typename(.Table)); + try expectEqualStrings("function", luau.VM.lapi.typename(.Function)); + try expectEqualStrings("userdata", luau.VM.lapi.typename(.Userdata)); + try expectEqualStrings("thread", luau.VM.lapi.typename(.Thread)); + try expectEqualStrings("vector", luau.VM.lapi.typename(.Vector)); + try expectEqualStrings("buffer", luau.VM.lapi.typename(.Buffer)); +} + +// test "executing string contents" { +// var lua = try luau.init(&testing.allocator); +// defer lua.deinit(); +// lua.Lopenlibs(); + +// try lua.loadString("f = function(x) return x + 10 end"); +// _ = lua.pcall(0, 0, 0); +// try lua.loadString("a = f(2)"); +// _ = lua.pcall(0, 0, 0); + +// try expectEqual(.number, try lua.getglobal("a")); +// try expectEqual(12, try lua.toInteger(1)); + +// try expectError(error.Fail, lua.loadString("bad syntax")); +// try lua.loadString("a = g()"); +// try expectError(error.Runtime, lua.pcall(0, 0, 0)); +// } + +test "filling and checking the stack" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + try expectEqual(0, lua.gettop()); + + // We want to push 30 values onto the stack + // this should work without fail + try expectEqual(true, lua.checkstack(30)); + + var count: i32 = 0; + while (count < 30) : (count += 1) { + lua.pushnil(); + } + + try expectEqual(30, lua.gettop()); + + // this should fail (beyond max stack size) + try expectEqual(false, lua.checkstack(1_000_000)); + + // this is small enough it won't fail (would raise an error if it did) + try lua.Lcheckstack(40, null); + while (count < 40) : (count += 1) { + lua.pushnil(); + } + + try expectEqual(40, lua.gettop()); +} + +test "stack manipulation" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + var num: i32 = 1; + while (num <= 10) : (num += 1) { + lua.pushinteger(num); + } + try expectEqual(10, lua.gettop()); + + lua.settop(12); + try expectEqual(12, lua.gettop()); + try expect(lua.isnil(-1)); + + lua.remove(1); + try expect(lua.isnil(-1)); + + lua.insert(1); + try expect(lua.isnil(1)); + + lua.settop(0); + try expectEqual(0, lua.gettop()); +} + +test "calling a function" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + try lua.Zsetglobalfn("zigadd", add); + + _ = try lua.getglobal("zigadd"); + lua.pushinteger(10); + lua.pushinteger(32); + + // pcall is preferred, but we might as well test call when we know it is safe + lua.call(2, 1); + try expectEqual(42, lua.tointeger(1).?); +} + +// test "string buffers" { +// var lua = try luau.init(&testing.allocator); +// defer lua.deinit(); + +// var buffer: StringBuffer = undefined; +// buffer.init(lua); + +// buffer.addChar('z'); +// buffer.addString("igl"); + +// var str = buffer.prep(); +// str[0] = 'u'; +// str[1] = 'a'; +// str[2] = 'u'; +// buffer.addSize(3); + +// buffer.addString(" api "); +// lua.pushnumber(5.1); +// buffer.addValue(); +// buffer.pushResult(); +// try expectEqualStrings("zigluau api 5.1", try lua.toString(-1)); + +// // now test a small buffer +// buffer.init(lua); +// var b = buffer.prep(); +// b[0] = 'a'; +// b[1] = 'b'; +// b[2] = 'c'; +// buffer.addSize(3); + +// b = buffer.prep(); +// @memcpy(b[0..23], "defghijklmnopqrstuvwxyz"); +// buffer.addSize(23); +// buffer.pushResult(); +// try expectEqualStrings("abcdefghijklmnopqrstuvwxyz", try lua.toString(-1)); +// lua.pop(1); + +// buffer.init(lua); +// b = buffer.prep(); +// @memcpy(b[0..3], "abc"); +// buffer.pushResultSize(3); +// try expectEqualStrings("abc", try lua.toString(-1)); +// lua.pop(1); +// } + +const sub = struct { + fn subInner(l: *State) i32 { + const a = l.toInteger(1) catch 0; + const b = l.toInteger(2) catch 0; + l.pushinteger(a - b); + return 1; + } +}.subInner; + +test "function registration" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + const funcs = [_]luau.VM.laux.Reg{ + .{ .name = "add", .func = luau.VM.zapi.toCFn(add) }, + }; + try lua.newtable(); + try lua.Lregister(null, &funcs); + + _ = try lua.getfield(-1, "add"); + lua.pushinteger(1); + lua.pushinteger(2); + _ = lua.pcall(2, 1, 0); + try expectEqual(3, lua.tointeger(-1).?); + lua.settop(0); + + // register functions as globals in a library table + try lua.Lregister("testlib", &funcs); + + // testlib.add(1, 2) + _ = try lua.getglobal("testlib"); + _ = try lua.getfield(-1, "add"); + lua.pushinteger(1); + lua.pushinteger(2); + _ = lua.pcall(2, 1, 0); + try expectEqual(3, lua.tointeger(-1).?); +} + +test "warn fn" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + const warnFn = struct { + fn inner(L: *State) void { + const msg = L.tostring(1) orelse @panic("bad"); + if (!std.mem.eql(u8, msg, "this will be caught by the warnFn")) + std.debug.panic("test failed", .{}); + } + }.inner; + + try lua.Zpushfunction(warnFn, "newWarn"); + lua.pushvalue(-1); + try lua.setfield(luau.VM.lua.GLOBALSINDEX, "warn"); + try lua.pushstring("this will be caught by the warnFn"); + lua.call(1, 0); +} + +test "string literal" { + const allocator = testing.allocator; + var lua = try luau.init(&allocator); + defer lua.deinit(); + + const zbytes = [_:0]u8{ 'H', 'e', 'l', 'l', 'o', ' ', 0, 'W', 'o', 'r', 'l', 'd' }; + try testing.expectEqual(zbytes.len, 12); + + try lua.pushstring(&zbytes); + const str1 = lua.tostring(-1) orelse @panic("bad"); + try testing.expectEqual(6, str1.len); + try testing.expectEqualStrings("Hello ", str1); + + try lua.pushlstring(&zbytes); + const str2 = lua.tolstring(-1) orelse @panic("bad"); + try testing.expectEqual(12, str2.len); + try testing.expectEqualStrings(&zbytes, str2); +} + +test "concat" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + _ = try lua.pushstring("hello "); + lua.pushnumber(10); + _ = try lua.pushstring(" wow!"); + try lua.concat(3); + + try expectEqualStrings("hello 10 wow!", lua.tostring(-1) orelse @panic("bad")); +} + +test "garbage collector" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + // because the garbage collector is an opaque, unmanaged + // thing, it is hard to test, so just run each function + _ = lua.gc(.Stop, 0); + _ = lua.gc(.Collect, 0); + _ = lua.gc(.Restart, 0); + _ = lua.gc(.Count, 0); + _ = lua.gc(.CountB, 0); + + _ = lua.gc(.IsRunning, 0); + _ = lua.gc(.Step, 0); + + _ = lua.gc(.SetGoal, 10); + _ = lua.gc(.SetStepMul, 2); + _ = lua.gc(.SetStepSize, 1); +} + +test "threads" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + var new_thread = try lua.newthread(); + + try expectEqual(1, lua.gettop()); + try expectEqual(0, new_thread.gettop()); + + lua.pushinteger(10); + lua.pushnil(); + + try expectEqual(3, lua.gettop()); + + lua.xmove(new_thread, 2); + + try expectEqual(2, new_thread.gettop()); + try expectEqual(1, lua.gettop()); + + var new_thread2 = try lua.newthread(); + + try expectEqual(2, lua.gettop()); + try expectEqual(0, new_thread2.gettop()); + + lua.pushnil(); + + lua.xpush(new_thread2, -1); + + try expectEqual(3, lua.gettop()); + try expectEqual(1, new_thread2.gettop()); + try expectEqual(.Nil, new_thread2.typeOf(1)); +} + +test "userdata and uservalues" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + const Data = struct { + val: i32, + code: [4]u8, + }; + + // create a Luau-owned pointer to a Data with 2 associated user values + var data = try lua.newuserdata(Data); + data.val = 1; + @memcpy(&data.code, "abcd"); + + try expectEqual(data, lua.touserdata(Data, 1) orelse @panic("bad")); + try expectEqual(@as(*const anyopaque, @ptrCast(data)), lua.topointer(1) orelse @panic("bad")); +} + +test "upvalues" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + // counter from PIL + const counter = struct { + fn inner(l: *State) i32 { + var counter = l.tointeger(luau.VM.lua.upvalueindex(1)) orelse 0; + counter += 1; + l.pushinteger(counter); + l.pushinteger(counter); + l.replace(luau.VM.lua.upvalueindex(1)); + return 1; + } + }.inner; + + // Initialize the counter at 0 + lua.pushinteger(0); + try lua.pushcclosure(luau.VM.zapi.toCFn(counter), "counter", 1); + try lua.setglobal("counter"); + + // call the function repeatedly, each time ensuring the result increases by one + var expected: i32 = 1; + while (expected <= 10) : (expected += 1) { + _ = try lua.getglobal("counter"); + lua.call(0, 1); + try expectEqual(expected, lua.tointeger(-1).?); + lua.pop(1); + } +} + +test "raise error" { + if (!EXCEPTIONS_ENABLED) + return error.SkipZigTest; + + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + const makeError = struct { + fn inner(l: *State) !i32 { + _ = try l.pushstring("makeError made an error"); + l.raiseerror(); + return 0; + } + }.inner; + + try lua.Zpushfunction(makeError, "func"); + lua.pushinteger(1256); + try expectError(error.Runtime, lua.pcall(1, 0, 0).check()); + try expectEqualStrings("makeError made an error", lua.tostring(-1).?); +} + +fn continuation(l: *State, status: luau.Status, ctx: isize) i32 { + _ = status; + + if (ctx == 5) { + _ = l.pushstring("done"); + return 1; + } else { + // yield the current context value + l.pushinteger(ctx); + return l.yieldCont(1, ctx + 1, luau.wrap(continuation)); + } +} + +fn continuation52(l: *State) i32 { + const ctxOrNull = l.getContext() catch unreachable; + const ctx = ctxOrNull orelse 0; + if (ctx == 5) { + _ = l.pushstring("done"); + return 1; + } else { + // yield the current context value + l.pushinteger(ctx); + return l.yieldCont(1, ctx + 1, luau.wrap(continuation52)); + } +} + +test "yielding no continuation" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + const thread = try lua.newthread(); + const func = struct { + fn inner(l: *State) !i32 { + l.pushinteger(1); + return try l.yield(1); + } + }.inner; + try thread.Zpushfunction(func, "func"); + + try expectEqual(.Suspended, lua.costatus(thread)); + + _ = try thread.resumethread(null, 0).check(); + + try expectEqual(.Suspended, lua.costatus(thread)); + try expectEqual(1, thread.tointeger(-1).?); + try thread.resetthread(); + try expect(thread.isthreadreset()); + try expectEqual(.Finished, lua.costatus(thread)); +} + +test "aux check functions" { + if (!EXCEPTIONS_ENABLED) + return error.SkipZigTest; + + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + const function = struct { + fn inner(l: *State) !i32 { + try l.Lcheckany(1); + _ = l.Lcheckinteger(2); + _ = l.Lchecknumber(3); + _ = l.Lcheckstring(4); + l.Lchecktype(5, .Boolean); + return 0; + } + }.inner; + + try lua.Zpushfunction(function, "func"); + _ = lua.pcall(0, 0, 0).check() catch { + try expectStringContains("argument #1", lua.tostring(-1) orelse @panic("bad")); + lua.pop(-1); + }; + + try lua.Zpushfunction(function, "func"); + lua.pushnil(); + _ = lua.pcall(1, 0, 0).check() catch { + try expectStringContains("number expected", lua.tostring(-1) orelse @panic("bad")); + lua.pop(-1); + }; + + try lua.Zpushfunction(function, "func"); + lua.pushnil(); + lua.pushinteger(3); + _ = lua.pcall(2, 0, 0).check() catch { + try expectStringContains("string expected", lua.tostring(-1) orelse @panic("bad")); + lua.pop(-1); + }; + + try lua.Zpushfunction(function, "func"); + lua.pushnil(); + lua.pushinteger(3); + lua.pushnumber(4); + _ = lua.pcall(3, 0, 0).check() catch { + try expectStringContains("string expected", lua.tostring(-1) orelse @panic("bad")); + lua.pop(-1); + }; + + try lua.Zpushfunction(function, "func"); + lua.pushnil(); + lua.pushinteger(3); + lua.pushnumber(4); + _ = try lua.pushstring("hello world"); + _ = lua.pcall(4, 0, 0).check() catch { + try expectStringContains("boolean expected", lua.tostring(-1) orelse @panic("bad")); + lua.pop(-1); + }; + + try lua.Zpushfunction(function, "func"); + // test pushFail here (currently acts the same as pushnil) + lua.pushnil(); + lua.pushinteger(3); + lua.pushnumber(4); + _ = try lua.pushstring("hello world"); + lua.pushboolean(true); + _ = lua.pcall(5, 0, 0); +} + +test "aux opt functions" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + const function = struct { + fn inner(l: *State) i32 { + expectEqual(10, l.Loptinteger(1, 10)) catch unreachable; + expectEqualStrings("zig", l.Loptstring(2, "zig")) catch unreachable; + expectEqual(1.23, l.Loptnumber(3, 1.23)) catch unreachable; + expectEqualStrings("lang", l.Loptstring(4, "lang")) catch unreachable; + return 0; + } + }.inner; + + try lua.Zpushfunction(function, "func"); + _ = lua.pcall(0, 0, 0); + + try lua.Zpushfunction(function, "func"); + lua.pushinteger(10); + _ = try lua.pushstring("zig"); + lua.pushnumber(1.23); + _ = try lua.pushstring("lang"); + _ = lua.pcall(4, 0, 0); +} + +// test "checkOption" { +// if (!EXCEPTIONS_ENABLED) +// return error.SkipZigTest; + +// var lua = try luau.init(&testing.allocator); +// defer lua.deinit(); + +// const Variant = enum { +// one, +// two, +// three, +// }; + +// const function = struct { +// fn inner(l: *lua) i32 { +// const option = l.checkOption(Variant, 1, .one); +// l.pushinteger(switch (option) { +// .one => 1, +// .two => 2, +// .three => 3, +// }); +// return 1; +// } +// }.inner; + +// lua.Zpushfunction(function, "func"); +// _ = lua.pushstring("one"); +// _ = lua.pcall(1, 1, 0); +// try expectEqual(1, try lua.toInteger(-1)); +// lua.pop(1); + +// lua.Zpushfunction(function, "func"); +// _ = lua.pushstring("two"); +// _ = lua.pcall(1, 1, 0); +// try expectEqual(2, try lua.toInteger(-1)); +// lua.pop(1); + +// lua.Zpushfunction(function, "func"); +// _ = lua.pushstring("three"); +// _ = lua.pcall(1, 1, 0); +// try expectEqual(3, try lua.toInteger(-1)); +// lua.pop(1); + +// // try the default now +// lua.Zpushfunction(function, "func"); +// _ = lua.pcall(0, 1, 0); +// try expectEqual(1, try lua.toInteger(-1)); +// lua.pop(1); + +// // check the raised error +// lua.Zpushfunction(function, "func"); +// _ = lua.pushstring("unknown"); +// try expectError(error.Runtime, lua.pcall(1, 1, 0)); +// try expectStringContains("(invalid option 'unknown')", try lua.toString(-1)); +// } + +test "ref luau" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + lua.pushnil(); + try expectEqual(null, lua.ref(1)); + try expectEqual(1, lua.gettop()); + + // In luau lua.ref does not pop the item from the stack + // and the data is stored in the REGISTRYINDEX by default + _ = try lua.pushstring("Hello there"); + const ref = try lua.ref(2) orelse @panic("bad"); + + _ = lua.rawgeti(luau.VM.lua.REGISTRYINDEX, ref); + try expectEqualStrings("Hello there", lua.tostring(-1) orelse @panic("bad")); + + lua.unref(ref); +} + +test "args and errors" { + if (!EXCEPTIONS_ENABLED) + return error.SkipZigTest; + + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + const argCheck = struct { + fn inner(l: *State) !i32 { + try l.Largcheck(false, 1, "error!"); + return 0; + } + }.inner; + + try lua.Zpushfunction(argCheck, "ArgCheck"); + try expectError(error.Runtime, lua.pcall(0, 0, 0).check()); + + const raisesError = struct { + fn inner(l: *State) !i32 { + try l.LerrorL("some error {s}!", .{"zig"}); + unreachable; + } + }.inner; + + try lua.Zpushfunction(raisesError, "Error"); + try expectError(error.Runtime, lua.pcall(0, 0, 0).check()); + try expectEqualStrings("some error zig!", lua.tostring(-1) orelse @panic("bad")); + + const raisesFmtError = struct { + fn inner(l: *State) !i32 { + try l.LerrorL("some fmt error {s}!", .{"zig"}); + unreachable; + } + }.inner; + + try lua.Zpushfunction(raisesFmtError, "ErrorFmt"); + try expectError(error.Runtime, lua.pcall(0, 0, 0).check()); + try expectEqualStrings("some fmt error zig!", lua.tostring(-1) orelse @panic("bad")); + + const FmtError = struct { + fn inner(l: *State) !i32 { + return l.Zerrorf("some err fmt error {s}!", .{"zig"}); + } + }.inner; + + try lua.Zpushfunction(FmtError, "ErrorFmt"); + try expectError(error.Runtime, lua.pcall(0, 0, 0).check()); + try expectEqualStrings("some err fmt error zig!", lua.tostring(-1) orelse @panic("bad")); + + const Error = struct { + fn inner(l: *State) !i32 { + return l.Zerror("some error"); + } + }.inner; + + try lua.Zpushfunction(Error, "Error"); + try expectError(error.Runtime, lua.pcall(0, 0, 0).check()); + try expectEqualStrings("some error", lua.tostring(-1) orelse @panic("bad")); +} + +test "objectLen" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + _ = try lua.pushstring("lua"); + try testing.expectEqual(3, lua.objlen(-1)); +} + +test "compile and run bytecode" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + try lua.Lopenlibs(); + + // Load bytecode + const src = "return 133"; + const bc = try luau.compile(testing.allocator, src, luau.CompileOptions{}); + defer testing.allocator.free(bc); + + try lua.load("...", bc, 0); + _ = lua.pcall(0, 1, 0); + const v = lua.tointeger(-1) orelse @panic("bad"); + try expectEqual(133, v); + + // Try mutable globals. Calls to mutable globals should produce longer bytecode. + const src2 = "Foo.print()\nBar.print()"; + const bc1 = try luau.compile(testing.allocator, src2, luau.CompileOptions{}); + defer testing.allocator.free(bc1); + + const options = luau.CompileOptions{ + .mutableGlobals = &[_:null]?[*:0]const u8{ "Foo", "Bar" }, + }; + const bc2 = try luau.compile(testing.allocator, src2, options); + defer testing.allocator.free(bc2); + // A really crude check for changed bytecode. Better would be to match + // produced bytecode in text format, but the API doesn't support it. + try expect(bc1.len < bc2.len); +} + +const DataDtor = struct { + gc_hits_ptr: *i32, + + pub fn dtor(self: *DataDtor) void { + self.gc_hits_ptr.* = self.gc_hits_ptr.* + 1; + } +}; + +test "userdata dtor" { + var gc_hits: i32 = 0; + + // create a Luau-owned pointer to a Data, configure Data with a destructor. + { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + var data = try lua.newuserdatadtor(DataDtor, DataDtor.dtor); + data.gc_hits_ptr = &gc_hits; + try expectEqual(@as(*anyopaque, @ptrCast(data)), lua.topointer(1) orelse @panic("bad")); + try expectEqual(0, gc_hits); + lua.pop(1); // don't let the stack hold a ref to the user data + _ = lua.gc(.Collect, 0); + try expectEqual(1, gc_hits); + _ = lua.gc(.Collect, 0); + try expectEqual(1, gc_hits); + } +} + +fn vectorCtor(l: *State) i32 { + const x = l.tonumber(1) orelse 0; + const y = l.tonumber(2) orelse 0; + const z = l.tonumber(3) orelse 0; + if (luau.luau_vector_size == 4) { + const w = l.optNumber(4, 0); + l.pushVector(@floatCast(x), @floatCast(y), @floatCast(z), @floatCast(w)); + } else { + l.pushVector(@floatCast(x), @floatCast(y), @floatCast(z)); + } + return 1; +} + +fn foo(a: i32, b: i32) i32 { + return a + b; +} + +fn bar(a: i32, b: i32) !i32 { + if (a > b) return error.wrong; + return a + b; +} + +test "debug stacktrace" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + const stackTrace = struct { + fn inner(l: *State) !i32 { + try l.pushstring(l.debugtrace()); + return 1; + } + }.inner; + try lua.Zpushfunction(stackTrace, "test"); + _ = lua.pcall(0, 1, 0); + try expectEqualStrings("[C] function test\n", lua.tostring(-1) orelse @panic("bad")); +} + +test "debug stacktrace luau" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + const src = + \\function MyFunction() + \\ return stack() + \\end + \\ + \\return MyFunction() + \\ + ; + + const bc = try luau.compile(testing.allocator, src, .{ + .debugLevel = 2, + }); + defer testing.allocator.free(bc); + + const stackTrace = struct { + fn inner(l: *State) !i32 { + try l.pushstring(l.debugtrace()); + return 1; + } + }.inner; + try lua.Zpushfunction(stackTrace, "stack"); + try lua.setglobal("stack"); + + try lua.load("module", bc, 0); + _ = lua.pcall(0, 1, 0); // CALL main() + + try expectEqualStrings( + \\[C] function stack + \\[string "module"]:2 function MyFunction + \\[string "module"]:5 + \\ + , lua.tostring(-1) orelse @panic("bad")); +} + +test "buffers" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + lua.openbase(); + lua.openbuffer(); + + const buf = try lua.newbuffer(12); + try lua.Zpushbuffer("Hello, world 2"); + + try expectEqual(12, buf.len); + + @memcpy(buf, "Hello, world"); + + try expect(lua.isbuffer(-1)); + try expectEqualStrings("Hello, world", buf); + try expectEqualStrings("Hello, world", lua.tobuffer(-2) orelse @panic("bad")); + try expectEqualStrings("Hello, world 2", lua.tobuffer(-1) orelse @panic("bad")); + + const src = + \\function MyFunction(buf, buf2) + \\ assert(buffer.tostring(buf) == "Hello, world") + \\ assert(buffer.tostring(buf2) == "Hello, world 2") + \\ local newBuf = buffer.create(4); + \\ buffer.writeu8(newBuf, 0, 82) + \\ buffer.writeu8(newBuf, 1, 101) + \\ buffer.writeu8(newBuf, 2, 115) + \\ buffer.writeu8(newBuf, 3, 116) + \\ return newBuf + \\end + \\ + \\return MyFunction + \\ + ; + + const bc = try luau.compile(testing.allocator, src, .{ + .debugLevel = 2, + }); + defer testing.allocator.free(bc); + + try lua.load("module", bc, 0); + _ = lua.pcall(0, 1, 0); // CALL main() + + lua.pushvalue(-3); + lua.pushvalue(-3); + _ = lua.pcall(2, 1, 0); // CALL MyFunction(buf) + + const newBuf = lua.Lcheckbuffer(-1); + try expectEqual(4, newBuf.len); + try expectEqualStrings("Rest", newBuf); +} + +test "Set Api" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + lua.openbase(); + lua.openstring(); + + const vectorFn = struct { + fn inner(l: *State) i32 { + const x: f32 = @floatCast(l.Loptnumber(1, 0.0)); + const y: f32 = @floatCast(l.Loptnumber(2, 0.0)); + const z: f32 = @floatCast(l.Loptnumber(3, 0.0)); + + if (luau.VECTOR_SIZE == 3) { + l.pushvector(x, y, z, null); + } else { + const w: f32 = @floatCast(l.Loptnumber(4, 0.0)); + l.pushvector(x, y, z, w); + } + + return 1; + } + }.inner; + try lua.Zsetglobalfn("vector", vectorFn); + + const src = + \\function MyFunction(api) + \\ assert(type(api.a) == "function"); api.a() + \\ assert(type(api.b) == "boolean" and api.b == true); + \\ assert(type(api.c) == "number" and api.c == 1.1); + \\ assert(type(api.d) == "number" and api.d == 2); + \\ assert(type(api.e) == "string" and api.e == "Api"); + \\ assert(type(api.f) == "string" and api.f == string.char(65, 0, 66) and api.f ~= "AB" and #api.f == 3); + \\ assert(type(api.pos) == "vector" and api.pos.X == 1 and api.pos.Y == 2 and api.pos.Z == 3); + \\ + \\ assert(type(_a) == "function"); _a() + \\ assert(type(_b) == "boolean" and _b == true); + \\ assert(type(_c) == "number" and _c == 1.1); + \\ assert(type(_d) == "number" and _d == 2); + \\ assert(type(_e) == "string" and _e == "Api"); + \\ assert(type(_f) == "string" and _f == string.char(65, 0, 66) and _f ~= "AB" and #_f == 3); + \\ assert(type(_pos) == "vector" and _pos.X == 1 and _pos.Y == 2 and _pos.Z == 3); + \\ + \\ assert(type(gl_a) == "function"); gl_a() + \\ assert(type(gl_b) == "boolean" and gl_b == true); + \\ assert(type(gl_c) == "number" and gl_c == 1.1); + \\ assert(type(gl_d) == "number" and gl_d == 2); + \\ assert(type(gl_e) == "string" and gl_e == "Api"); + \\ assert(type(gl_f) == "string" and gl_f == string.char(65, 0, 66) and gl_f ~= "AB" and #gl_f == 3); + \\ assert(type(gl_pos) == "vector" and gl_pos.X == 1 and gl_pos.Y == 2 and gl_pos.Z == 3); + \\end + \\ + \\return MyFunction + \\ + ; + + const bc = try luau.compile(testing.allocator, src, .{ + .debugLevel = 2, + .optimizationLevel = 0, + .vectorCtor = "vector", + .vectorType = "vector", + }); + defer testing.allocator.free(bc); + + const tempFn = struct { + fn inner(l: *State) !i32 { + _ = try l.getglobal("count"); + l.pushinteger((l.tointeger(-1) orelse 0) + 1); + try l.setglobal("count"); + return 0; + } + }.inner; + try lua.newtable(); + try lua.Zsetfieldfn(-1, "a", tempFn); + try lua.Zsetfield(-1, "b", true); + try lua.Zsetfield(-1, "c", 1.1); + try lua.Zsetfield(-1, "d", 2); + try lua.Zsetfield(-1, "e", "Api"); + try lua.Zsetfield(-1, "f", &[_]u8{ 'A', 0, 'B' }); + if (luau.VECTOR_SIZE == 3) { + try lua.Zsetfield(-1, "pos", @Vector(3, f32){ 1.0, 2.0, 3.0 }); + } else { + try lua.Zsetfield(-1, "pos", @Vector(4, f32){ 1.0, 2.0, 3.0, 4.0 }); + } + + try lua.Zsetfieldfn(luau.VM.lua.GLOBALSINDEX, "_a", tempFn); + try lua.Zsetfield(luau.VM.lua.GLOBALSINDEX, "_b", true); + try lua.Zsetfield(luau.VM.lua.GLOBALSINDEX, "_c", @as(f64, 1.1)); + try lua.Zsetfield(luau.VM.lua.GLOBALSINDEX, "_d", @as(i32, 2)); + try lua.Zsetfield(luau.VM.lua.GLOBALSINDEX, "_e", "Api"); + try lua.Zsetfield(luau.VM.lua.GLOBALSINDEX, "_f", &[_]u8{ 'A', 0, 'B' }); + if (luau.VECTOR_SIZE == 3) { + try lua.Zsetfield(luau.VM.lua.GLOBALSINDEX, "_pos", @Vector(3, f32){ 1.0, 2.0, 3.0 }); + } else { + try lua.Zsetfield(luau.VM.lua.GLOBALSINDEX, "_pos", @Vector(4, f32){ 1.0, 2.0, 3.0, 4.0 }); + } + + try lua.Zsetglobalfn("gl_a", tempFn); + try lua.Zsetglobal("gl_b", true); + try lua.Zsetglobal("gl_c", 1.1); + try lua.Zsetglobal("gl_d", 2); + try lua.Zsetglobal("gl_e", "Api"); + try lua.Zsetglobal("gl_f", &[_]u8{ 'A', 0, 'B' }); + if (luau.VECTOR_SIZE == 3) { + try lua.Zsetglobal("gl_pos", @Vector(3, f32){ 1.0, 2.0, 3.0 }); + } else { + try lua.Zsetglobal("gl_pos", @Vector(4, f32){ 1.0, 2.0, 3.0, 4.0 }); + } + + try lua.load("module", bc, 0); + _ = lua.pcall(0, 1, 0); // CALL main() + + lua.pushvalue(-2); + switch (try lua.pcall(1, 1, 0).check()) { + .Ok => {}, + .Yield => std.debug.panic("unexpected yield\n", .{}), + .Break => std.debug.panic("unexpected break\n", .{}), + else => unreachable, + } + + _ = try lua.getglobal("count"); + try expectEqual(3, lua.tointeger(-1) orelse @panic("bad")); +} + +test "Vectors" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + lua.openbase(); + lua.openstring(); + lua.openmath(); + + const vectorFn = struct { + fn inner(l: *State) i32 { + const x: f32 = @floatCast(l.Loptnumber(1, 0.0)); + const y: f32 = @floatCast(l.Loptnumber(2, 0.0)); + const z: f32 = @floatCast(l.Loptnumber(3, 0.0)); + + if (luau.VECTOR_SIZE == 3) { + l.pushvector(x, y, z, null); + } else { + const w: f32 = @floatCast(l.Loptnumber(4, 0.0)); + l.pushvector(x, y, z, w); + } + + return 1; + } + }.inner; + + const src = + \\function MyFunction() + \\ local vec = vector(0, 1.1, 2.2); + \\ assert(type(vec) == "vector") + \\ assert(vec.X == 0); + \\ assert(math.round(vec.Y*100)/100 == 1.1); -- 1.100000023841858 + \\ assert(math.round(vec.Z*100)/100 == 2.2); -- 2.200000047683716 + \\ return vec + \\end + \\ + \\return MyFunction() + \\ + ; + + const bc = try luau.compile(testing.allocator, src, .{ + .debugLevel = 2, + .optimizationLevel = 0, + .vectorCtor = "vector", + .vectorType = "vector", + }); + defer testing.allocator.free(bc); + + try lua.Zsetglobalfn("vector", vectorFn); + + try lua.load("module", bc, 0); + _ = lua.pcall(0, 1, 0); // CALL main() + + try expect(lua.isvector(-1)); + const vec = lua.tovector(-1) orelse @panic("bad"); + try expectEqual(luau.VECTOR_SIZE, vec.len); + try expectEqual(0.0, vec[0]); + try expectEqual(1.1, vec[1]); + try expectEqual(2.2, vec[2]); + if (luau.VECTOR_SIZE == 4) { + try expectEqual(0.0, vec[3]); + } + + if (luau.VECTOR_SIZE == 3) { + lua.pushvector(0.0, 1.0, 0.0, null); + } else { + lua.pushvector(0.0, 1.0, 0.0, 0.0); + } + const vec2 = lua.Lcheckvector(-1); + try expectEqual(luau.VECTOR_SIZE, vec2.len); + try expectEqual(0.0, vec2[0]); + try expectEqual(1.0, vec2[1]); + try expectEqual(0.0, vec2[2]); + if (luau.VECTOR_SIZE == 4) { + try expectEqual(0.0, vec2[3]); + } +} + +test "Luau JIT/CodeGen" { + // Skip this test if the Luau NCG is not supported on machine + if (!luau.CodeGen.Supported()) + return error.SkipZigTest; + + var lua = try luau.init(&std.testing.allocator); + defer lua.deinit(); + luau.CodeGen.Create(lua); + + lua.openbase(); + + try lua.Zsetglobalfn("test", struct { + fn inner(L: *State) !i32 { + L.pushboolean(L.Gisnative(@intCast(L.Loptinteger(1, 0)))); + return 1; + } + }.inner); + + const src = + \\ + \\local function func(): () + \\ assert(native == test()) + \\ return + \\end + \\ + \\pcall(func) + \\ + ; + const bc = try luau.compile(testing.allocator, src, .{ + .debugLevel = 2, + .optimizationLevel = 2, + }); + defer testing.allocator.free(bc); + + try lua.load("module", bc, 0); + + luau.CodeGen.Compile(lua, -1); + + _ = lua.pcall(0, 0, 0); // CALL main() +} + +test "Luau JIT/CodeGen compileLoad" { + // Skip this test if the Luau NCG is not supported on machine + if (!luau.CodeGen.Supported()) + return error.SkipZigTest; + + var lua = try luau.init(&std.testing.allocator); + defer lua.deinit(); + luau.CodeGen.Create(lua); + + lua.openbase(); + + try lua.Zsetglobalfn("test", struct { + fn inner(L: *State) !i32 { + L.pushboolean(L.Gisnative(@intCast(L.Loptinteger(1, 0)))); + return 1; + } + }.inner); + + const src = + \\ + \\local function func(): () + \\ assert(native == test()) + \\ return + \\end + \\ + \\pcall(func) + \\ + ; + + try luau.Compiler.Compiler.compileLoad(lua, "module", src, .{ + .debugLevel = 2, + .optimizationLevel = 2, + }, 0); + + luau.CodeGen.Compile(lua, -1); + + _ = lua.pcall(0, 0, 0); // CALL main() +} + +test "Luau JIT/CodeGen ParseResult" { + // Skip this test if the Luau NCG is not supported on machine + if (!luau.CodeGen.Supported()) + return error.SkipZigTest; + + var lua = try luau.init(&std.testing.allocator); + defer lua.deinit(); + luau.CodeGen.Create(lua); + + lua.openbase(); + + try lua.Zsetglobalfn("test", struct { + fn inner(L: *State) !i32 { + L.pushboolean(L.Gisnative(@intCast(L.Loptinteger(1, 0)))); + return 1; + } + }.inner); + + const src = + \\ + \\local function func(): () + \\ assert(native == test()) + \\ return + \\end + \\ + \\pcall(func) + \\ + ; + + const luau_allocator = luau.Ast.Allocator.init(); + defer luau_allocator.deinit(); + + const astNameTable = luau.Ast.Lexer.AstNameTable.init(luau_allocator); + defer astNameTable.deinit(); + + const parseResult = luau.Ast.Parser.parse(src, astNameTable, luau_allocator, .{}); + defer parseResult.deinit(); + + try luau.Compiler.Compiler.compileLoadParseResult(lua, "module", parseResult, astNameTable, .{ + .debugLevel = 2, + .optimizationLevel = 2, + }, 0); + + luau.CodeGen.Compile(lua, -1); + + _ = lua.pcall(0, 0, 0); // CALL main() +} + +test "Readonly table" { + if (!EXCEPTIONS_ENABLED) + return error.SkipZigTest; + + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + try lua.newtable(); + lua.setreadonly(-1, true); + try lua.setglobal("List"); + + const src = + \\List[1] = "test" + ; + const bc = try luau.compile(testing.allocator, src, .{ + .debugLevel = 2, + .optimizationLevel = 2, + }); + defer testing.allocator.free(bc); + + try lua.load("module", bc, 0); + + try expectError(error.Runtime, lua.pcall(0, 0, 0).check()); // CALL main() +} + +test "Metamethods" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + try lua.Lopenlibs(); + + _ = try lua.Lnewmetatable("MyMetatable"); + + try lua.Zsetfieldfn(-1, luau.Metamethods.index, struct { + fn inner(l: *State) !i32 { + l.Lchecktype(1, .Table); + const key = l.tostring(2) orelse unreachable; + expectEqualStrings("test", key) catch unreachable; + try l.pushstring("Hello, world"); + return 1; + } + }.inner); + + try lua.Zsetfieldfn(-1, luau.Metamethods.tostring, struct { + fn inner(l: *State) !i32 { + l.Lchecktype(1, .Table); + try l.pushstring("MyMetatable"); + return 1; + } + }.inner); + + try lua.newtable(); + lua.pushvalue(-2); + _ = try lua.setmetatable(-2); + + try expectEqual(.String, lua.getfield(-1, "test")); + try expectEqualStrings("Hello, world", lua.tostring(-1) orelse @panic("bad")); + lua.pop(1); + + try expectEqual(.Function, lua.getglobal("tostring")); + lua.pushvalue(-2); + _ = lua.pcall(1, 1, 0); + try expectEqualStrings("MyMetatable", lua.tostring(-1) orelse @panic("bad")); + lua.pop(1); +} + +test "Zig Error Fn Lua Handled" { + if (!EXCEPTIONS_ENABLED) + return error.SkipZigTest; + + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + const zigEFn = struct { + fn inner(_: *State) !i32 { + return error.Fail; + } + }.inner; + + try lua.Zpushfunction(zigEFn, "zigEFn"); + try expectEqual(error.Runtime, lua.pcall(0, 0, 0).check()); + try expectEqualStrings("Fail", lua.tostring(-1).?); +} + +test "getfieldObject" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + try lua.Lopenlibs(); + + try lua.newtable(); + try lua.Zsetfield(-1, "test", true); + try lua.Zsetglobal("some", "Value"); + + // switch (try lua.getfieldObj(-1, "test")) { + // .boolean => |b| try expectEqual(true, b), + // else => @panic("Failed"), + // } + + // switch (try lua.getglobalObj("some")) { + // .string => |s| try expectEqualStrings("Value", s), + // else => @panic("Failed"), + // } + + // _ = lua.newbuffer(2); + // lua.pushnil(); + // switch (try lua.typeOfObj(-2)) { + // .buffer => |buf| try expectEqualStrings(&[_]u8{ 0, 0 }, buf), + // else => @panic("Failed"), + // } + // lua.pop(1); + + // switch (try lua.typeOfObj(-1)) { + // .nil => {}, + // else => @panic("Failed"), + // } + // try expectEqual(.nil, lua.typeOf(-1)); // should not be consumed + // try expectEqual(.nil, lua.typeOf(-2)); // should not be consumed + // try expectEqual(.buffer, lua.typeOf(-3)); + // lua.pop(2); + + // lua.pushnumber(1.2); + // switch (try lua.typeOfObj(-1)) { + // .number => |n| { + // // can leak if not handled, stack grows + // try expectEqual(1.2, n); + // }, + // else => @panic("Failed"), + // } + // try expectEqual(.number, lua.typeOf(-1)); // should not be consumed + // try expectEqual(.number, lua.typeOf(-2)); // should not be consumed + // try expectEqual(.buffer, lua.typeOf(-3)); + + // switch (try lua.typeOfObjConsumed(-1)) { + // .number => |n| { + // // pops automatically with value + // try expectEqual(1.2, n); + // }, + // else => @panic("Failed"), + // } + // // should be consumed + // try expectEqual(.number, lua.typeOf(-1)); // should not be consumed + // try expectEqual(.number, lua.typeOf(-2)); // should not be consumed + // try expectEqual(.buffer, lua.typeOf(-3)); + // lua.pop(2); + + // const res = try lua.typeOfObj(-1); + // if (res == .buffer) { + // try expectEqualStrings(&[_]u8{ 0, 0 }, res.buffer); + // } else @panic("Failed"); +} + +test "FFlags" { + try expectError(error.UnknownFlag, luau.FFlags.SetByName(bool, "someunknownflag", true)); + try expectError(error.UnknownFlag, luau.FFlags.SetByName(i32, "someunknownflag", 1)); + try expectError(error.UnknownFlag, luau.FFlags.SetByName(c_int, "someunknownflag", 1)); + + try expectEqual(null, luau.FFlags.GetByName(bool, "someunknownflag")); + try expectEqual(null, luau.FFlags.GetByName(i32, "someunknownflag")); + try expectEqual(null, luau.FFlags.GetByName(c_int, "someunknownflag")); + + var bool_flags = luau.FFlags.Get(bool).iterator(); + while (bool_flags.next()) |flag| { + const name: []const u8 = std.mem.span(flag.name); + try expect(name.len > 0); + const current = flag.value; + flag.value = !current; + try expectEqual(!current, luau.FFlags.GetByName(bool, name).?.value); + flag.value = current; + try expectEqual(current, luau.FFlags.GetByName(bool, name).?.value); + } + + var int_flags = luau.FFlags.Get(i32).iterator(); + while (int_flags.next()) |flag| { + const name: []const u8 = std.mem.span(flag.name); + try expect(name.len > 0); + const current = flag.value; + flag.value = current - 1; + try expectEqual(current - 1, luau.FFlags.GetByName(i32, name).?.value); + flag.value = current; + try expectEqual(current, luau.FFlags.GetByName(i32, name).?.value); + } +} + +test "State getInfo" { + var lua = try luau.init(&std.testing.allocator); + defer lua.deinit(); + + lua.openbase(); + + const src = + \\function MyFunction() + \\ func() + \\end + \\ + \\MyFunction() + ; + const bc = try luau.compile(testing.allocator, src, .{ + .debugLevel = 2, + .optimizationLevel = 2, + }); + defer testing.allocator.free(bc); + + try lua.Zsetglobalfn("func", struct { + fn inner(L: *State) !i32 { + var ar: luau.VM.lua.Debug = .{ .ssbuf = undefined }; + try expect(L.getinfo(1, "snl", &ar)); + try expect(ar.what == .lua); + try std.testing.expectEqualSentinel(u8, 0, "MyFunction", ar.name orelse @panic("Failed")); + try std.testing.expectEqualStrings("[string \"module\"]", ar.short_src orelse @panic("Failed")); + try expect(ar.linedefined.? == 1); + return 1; + } + }.inner); + + try lua.load("module", bc, 0); + + _ = lua.pcall(0, 1, 0); // CALL main() +} + +test "yielding error" { + { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + lua.openbase(); + lua.opencoroutine(); + + const src = + \\local ok, res = pcall(foo) + \\assert(not ok) + \\assert(res == "error") + ; + const bc = try luau.compile(testing.allocator, src, .{ + .debugLevel = 2, + .optimizationLevel = 2, + }); + defer testing.allocator.free(bc); + + try lua.Zsetglobalfn("foo", struct { + fn inner(L: *State) !i32 { + return L.yield(0); + } + }.inner); + + try lua.load("module", bc, 0); + + try expectEqual(.Yield, lua.resumethread(lua, 0)); + + try lua.pushstring("error"); + try expectEqual(.Ok, lua.resumeerror(lua)); + } + + { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + lua.openbase(); + lua.opencoroutine(); + + const src = + \\local ok, res = pcall(foo) + \\assert(not ok) + \\assert(res == "fmt error 10") + ; + const bc = try luau.compile(testing.allocator, src, .{ + .debugLevel = 2, + .optimizationLevel = 2, + }); + defer testing.allocator.free(bc); + + try lua.Zsetglobalfn("foo", struct { + fn inner(L: *State) !i32 { + return L.yield(0); + } + }.inner); + + try lua.load("module", bc, 0); + + try expectEqual(.Yield, lua.resumethread(lua, 0)); + try expectEqual(.Ok, lua.Zresumeferror(lua, "fmt error {d}", .{10})); + } +} + +test "Ast/Parser - HotComments" { + const src = + \\--!HotComments + \\--!optimize 2 + ; + + const luau_allocator = luau.Ast.Allocator.init(); + defer luau_allocator.deinit(); + + const names = luau.Ast.Lexer.AstNameTable.init(luau_allocator); + defer names.deinit(); + + var result = luau.Ast.Parser.parse(src, names, luau_allocator, .{}); + defer result.deinit(); + + try testing.expectEqual(2, result.hotcomments.size()); + + { + try expectEqualStrings("HotComments", result.hotcomments.at(0).content.slice()); + try expectEqualStrings("optimize 2", result.hotcomments.at(1).content.slice()); + } +} + +test "Thread Data" { + const Sample = struct { + a: i32, + b: i32, + }; + + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + const zigFn = struct { + fn inner(L: *State) !i32 { + const data = L.getthreaddata(*Sample); // should exists + try expectEqual(10, data.a); + try expectEqual(20, data.b); + return 0; + } + }.inner; + + var data = Sample{ .a = 10, .b = 20 }; + lua.setthreaddata(*Sample, &data); + + try lua.Zpushfunction(zigFn, "zigFn"); + try expectEqual(.Ok, lua.pcall(0, 0, 0)); +} + +test "Alloc (P)" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + inline for (0..20) |_| { + try lua.createtable(0, 100); + } + + lua.pop(20); +} + +test "Alloc Ref (P)" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + const noopFn = struct { + fn inner(_: *State) callconv(.c) i32 { + return 0; + } + }.inner; + + inline for (0..15) |_| { + try lua.createtable(0, 100); + try lua.pushcclosure(noopFn, "test", 0); + try lua.rawsetfield(-2, "test"); + } + + lua.pop(15); + + inline for (0..15) |_| { + try lua.createtable(0, 100); + inline for (0..5) |_| + try lua.createtable(0, 100); + try lua.pushcclosure(noopFn, "test", 5); + try lua.rawsetfield(-2, "test"); + } + + lua.pop(15); + + { + const T = try lua.newthread(); + + const src = + \\local a = foo(); + \\local dead = (function() + \\ foo() + \\end); + \\foo() + \\dead = nil; + \\for _ = 0, 10 do + \\ (function() + \\ foo(a) + \\ end)() + \\end + \\a = 1 + ; + const bc = try luau.compile(testing.allocator, src, .{ + .debugLevel = 0, + .optimizationLevel = 1, + }); + defer testing.allocator.free(bc); + + try T.Zsetglobalfn("foo", struct { + fn inner(L: *State) !i32 { + inline for (0..15) |_| { + try L.createtable(0, 40); + } + return 1; + } + }.inner); + + try T.load("module", bc, 0); + + T.call(0, 0); + } + lua.pop(1); + { + const T = try lua.newthread(); + + const src = + \\local a = foo(); + \\local dead = (function() + \\ foo() + \\end); + \\foo() + \\dead = nil; + \\for _ = 0, 10 do + \\ (function() + \\ foo(a) + \\ end)() + \\end + \\a = 1 + ; + const bc = try luau.compile(testing.allocator, src, .{ + .debugLevel = 0, + .optimizationLevel = 1, + }); + defer testing.allocator.free(bc); + + try T.Zsetglobalfn("foo", struct { + fn inner(L: *State) !i32 { + inline for (0..15) |_| { + try L.createtable(0, 40); + } + return 1; + } + }.inner); + + try T.load("module", bc, 0); + + T.call(0, 0); + } + lua.pop(1); + + inline for (0..15) |_| { + try lua.createtable(0, 100); + try lua.pushcclosure(noopFn, "test", 0); + try lua.rawsetfield(-2, "test"); + } + + lua.pop(15); +} + +test "String (S)" { + var lua = try luau.init(&testing.allocator); + defer lua.deinit(); + + lua.openbase(); + + { + const T = try lua.newthread(); + + errdefer std.debug.print("error: {s}\n", .{T.tostring(-1).?}); + + const src = + \\assert(foo() == `Hello, world`) + ; + const bc = try luau.compile(testing.allocator, src, .{ + .debugLevel = 0, + .optimizationLevel = 1, + }); + defer testing.allocator.free(bc); + + try T.Zsetglobalfn("foo", struct { + fn inner(L: *State) !i32 { + try L.pushlstring("Hello, world"); + return 1; + } + }.inner); + + try T.load("module", bc, 0); + + _ = try T.pcall(0, 0, 0).check(); + } + if (EXCEPTIONS_ENABLED) { + const T = try lua.newthread(); + + errdefer std.debug.print("error: {s}\n", .{T.tostring(-1).?}); + + const src = + \\local ok, res = pcall(foo) + \\assert(res == `foo bar baz, a long string, luau`) + \\assert(not ok) + ; + const bc = try luau.compile(testing.allocator, src, .{ + .debugLevel = 1, + .optimizationLevel = 2, + }); + defer testing.allocator.free(bc); + + try T.Zsetglobalfn("foo", struct { + fn inner(L: *State) !i32 { + return L.Zerror("foo bar baz, a long string, luau"); + } + }.inner); + + try T.load("module", bc, 0); + + _ = try T.pcall(0, 0, 0).check(); + } +} diff --git a/src/builtin/builtin.zig b/src/builtin/builtin.zig new file mode 100644 index 0000000..818409c --- /dev/null +++ b/src/builtin/builtin.zig @@ -0,0 +1,54 @@ +const std = @import("std"); + +const parser = @import("../shell/parser.zig"); + +const cd = @import("cmds/cd.zig"); +const echo = @import("cmds/echo.zig"); +const exit = @import("cmds/exit.zig"); +const pwd = @import("cmds/pwd.zig"); +const run = @import("cmds/run.zig"); + +pub const Shell = struct { + running: bool = true, + interrupted: bool = false, + environ: *const std.process.Environ.Map, +}; + +pub fn execute( + io: std.Io, + allocator: std.mem.Allocator, + shell: *Shell, + command: parser.Command, +) !bool { + if (command.argv.len == 0) + return true; + + const name = command.argv[0]; + + if (std.mem.eql(u8, name, "cd")) { + try cd.execute(io, shell.environ, command.argv); + return true; + } + + if (std.mem.eql(u8, name, "pwd")) { + try pwd.execute(io, allocator); + return true; + } + + if (std.mem.eql(u8, name, "run")) { + try run.execute(io, allocator, command.argv); + return true; + } + + if (std.mem.eql(u8, name, "echo")) { + try echo.execute(io, command.argv); + return true; + } + + if (std.mem.eql(u8, name, "exit")) { + exit.execute(&shell.running); + return true; + } + + return false; +} diff --git a/src/builtin/cmds/cd.zig b/src/builtin/cmds/cd.zig new file mode 100644 index 0000000..ce7c903 --- /dev/null +++ b/src/builtin/cmds/cd.zig @@ -0,0 +1,20 @@ +const std = @import("std"); + +pub fn execute( + io: std.Io, + environ: *const std.process.Environ.Map, + argv: []const []const u8, +) !void { + const path = if (argv.len >= 2) + argv[1] + else + environ.get("USERPROFILE") orelse environ.get("HOME") orelse { + std.debug.print("xsh: cd: cannot find home directory\n", .{}); + return; + }; + + var dir = try std.Io.Dir.cwd().openDir(io, path, .{}); + defer dir.close(io); + + try std.process.setCurrentDir(io, dir); +} diff --git a/src/builtin/cmds/echo.zig b/src/builtin/cmds/echo.zig new file mode 100644 index 0000000..9e85055 --- /dev/null +++ b/src/builtin/cmds/echo.zig @@ -0,0 +1,16 @@ +const std = @import("std"); + +pub fn execute( + io: std.Io, + argv: []const []const u8, +) !void { + for (argv[1..], 0..) |argument, index| { + if (index != 0) { + try std.Io.File.stdout().writeStreamingAll(io, " "); + } + + try std.Io.File.stdout().writeStreamingAll(io, argument); + } + + try std.Io.File.stdout().writeStreamingAll(io, "\n"); +} diff --git a/src/builtin/cmds/exit.zig b/src/builtin/cmds/exit.zig new file mode 100644 index 0000000..5498b99 --- /dev/null +++ b/src/builtin/cmds/exit.zig @@ -0,0 +1,3 @@ +pub fn execute(running: *bool) void { + running.* = false; +} diff --git a/src/builtin/cmds/pwd.zig b/src/builtin/cmds/pwd.zig new file mode 100644 index 0000000..f22c689 --- /dev/null +++ b/src/builtin/cmds/pwd.zig @@ -0,0 +1,12 @@ +const std = @import("std"); + +pub fn execute( + io: std.Io, + allocator: std.mem.Allocator, +) !void { + const path = try std.process.currentPathAlloc(io, allocator); + defer allocator.free(path); + + try std.Io.File.stdout().writeStreamingAll(io, path); + try std.Io.File.stdout().writeStreamingAll(io, "\n"); +} diff --git a/src/builtin/cmds/run.zig b/src/builtin/cmds/run.zig new file mode 100644 index 0000000..6e49059 --- /dev/null +++ b/src/builtin/cmds/run.zig @@ -0,0 +1,33 @@ +const std = @import("std"); + +const script = @import("../../script/script.zig"); + +pub fn execute( + io: std.Io, + allocator: std.mem.Allocator, + argv: []const []const u8, +) !void { + if (argv.len < 2) { + std.debug.print("xsh: run: missing script path\n", .{}); + return; + } + + var file = try std.Io.Dir.cwd().openFile(io, argv[1], .{}); + defer file.close(io); + + var file_reader = file.reader(io, &.{}); + + const source = try file_reader.interface.allocRemaining( + allocator, + .limited(16 * 1024 * 1024), + ); + + const name_z = try allocator.dupeZ(u8, argv[1]); + + try script.run( + allocator, + io, + name_z, + source, + ); +} diff --git a/src/builtin/cmds/which.zig b/src/builtin/cmds/which.zig new file mode 100644 index 0000000..e69de29 diff --git a/src/config.zig b/src/config.zig new file mode 100644 index 0000000..36c19d3 --- /dev/null +++ b/src/config.zig @@ -0,0 +1,12 @@ +pub const prompt = "xsh> "; + +pub const max_input_size = 4096; +pub const max_script_size = 16 * 1024 * 1024; +pub const max_exec_args = 64; + +pub const features = .{ + .luau = true, + .aliases = false, + .history = false, + .autocomplete = false +}; diff --git a/src/include/bridge.h b/src/include/bridge.h new file mode 100644 index 0000000..6190821 --- /dev/null +++ b/src/include/bridge.h @@ -0,0 +1,11 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +void xsh_set_luau_flags(void); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..3081f86 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,93 @@ +const std = @import("std"); + +const lexer = @import("shell/lexer.zig"); +const parser = @import("shell/parser.zig"); +const execute = @import("shell/execute.zig"); + +const builtin = @import("builtin/builtin.zig"); + +const script = @import("script/script.zig"); + +const config = @import("config.zig"); + +pub fn main(init: std.process.Init) !void { + const allocator = init.gpa; + const io = init.io; + + var shell = builtin.Shell{ + .environ = init.environ_map, + }; + + var stderr_buffer: [1024]u8 = undefined; + var stderr_file_writer = std.Io.File.stderr().writer(io, &stderr_buffer); + const stderr = &stderr_file_writer.interface; + + while (shell.running) { + try std.Io.File.stdout().writeStreamingAll(io, config.prompt); + + var input: [4096]u8 = undefined; + + const line = readLine(io, &input) catch |err| { + try stderr.print("xsh: error reading input: {}\n", .{err}); + try stderr_file_writer.flush(); + continue; + } orelse break; + + if (shell.interrupted) { + try std.Io.File.stdout().writeStreamingAll(io, "^C\n"); + continue; + } + + const source = std.mem.trim(u8, line, " \t\r\n"); + + if (source.len == 0) + continue; + + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + + const arena_allocator = arena.allocator(); + + runPipeline(arena_allocator, io, &shell, source) catch |err| { + try stderr.print("xsh error: {}\n", .{err}); + try stderr_file_writer.flush(); + }; + } +} + +fn runPipeline(allocator: std.mem.Allocator, io: std.Io, shell: *builtin.Shell, source: []const u8) !void { + const tokens = try lexer.lex(allocator, source); + const command = try parser.parse(allocator, tokens.items); + + try execute.run( + io, + allocator, + shell, + command, + ); +} + +fn readLine( + io: std.Io, + buffer: []u8, +) !?[]u8 { + var reader_buffer: [1024]u8 = undefined; + + var file_reader = std.Io.File.stdin().reader( + io, + &reader_buffer, + ); + + const reader = &file_reader.interface; + + const result = try reader.takeDelimiterInclusive('\n'); + + if (result.len == 0) + return null; + + const len = @min(result.len, buffer.len); + + @memcpy(buffer[0..len], result[0..len]); + + return buffer[0..len]; +} diff --git a/src/script/luau.zig b/src/script/luau.zig new file mode 100644 index 0000000..16a4c31 --- /dev/null +++ b/src/script/luau.zig @@ -0,0 +1,63 @@ +const std = @import("std"); + +const luau = @import("luau"); + +const c = @cImport({ + @cInclude("../include/bridge.h"); +}); + +const State = luau.State; + +var shell_io: std.Io = undefined; + +pub fn init(L: *State, io: std.Io) !void { + shell_io = io; + + try L.newtable(); + + try L.Zpushfunction(exec, "exec"); + try L.setfield(-2, "exec"); + + try L.setglobal("xsh"); +} + +fn exec(L: *State) i32 { + const argc = L.gettop(); + + if (argc == 0) { + L.pushinteger(1); + return 1; + } + + var argv: [64][]const u8 = undefined; + + if (argc > argv.len) { + L.pushinteger(1); + return 1; + } + + for (0..@intCast(argc)) |index| { + const arg = L.tostring(@intCast(index + 1)) orelse { + L.pushinteger(1); + return 1; + }; + + argv[index] = arg; + } + var child = std.process.spawn(shell_io, .{ + .argv = argv[0..@intCast(argc)], + }) catch { + L.pushinteger(1); + return 1; + }; + + const result = child.wait(shell_io) catch { + L.pushinteger(1); + return 1; + }; + + _ = result; + + L.pushinteger(0); + return 1; +} diff --git a/src/script/script.zig b/src/script/script.zig new file mode 100644 index 0000000..d539366 --- /dev/null +++ b/src/script/script.zig @@ -0,0 +1,69 @@ +const std = @import("std"); +const zluau = @import("luau"); + +const luau = @import("luau.zig"); + +const c = @cImport({ + @cInclude("../include/bridge.h"); +}); + +pub fn run( + allocator: std.mem.Allocator, + io: std.Io, + name: [:0]const u8, + source: []const u8, +) !void { + c.xsh_set_luau_flags(); + + var L = try zluau.init(&allocator); + defer L.deinit(); + + try L.Lopenlibs(); + + try luau.init(L, io); + + const bytecode = zluau.compile( + allocator, + source, + .{}, + ) catch |err| { + std.debug.print( + "xsh: failed to compile '{s}': {}\n", + .{ name, err }, + ); + return err; + }; + + defer allocator.free(bytecode); + + // try L.load(name, bytecode, 0); + L.load(name, bytecode, 0) catch |err| { + const message = L.tostring(-1) orelse "unknown Luau load error"; + + std.debug.print( + "xsh: failed to load '{s}': {s} ({})\n", + .{ name, message, err }, + ); + + L.pop(1); + + return err; + }; + + const call = L.pcall(0, 0, 0); + + if (call.check()) |_| { + return; + } else |err| { + const message = L.tostring(-1) orelse "unknown Luau runtime error"; + + std.debug.print( + "xsh: {s}\n", + .{message}, + ); + + L.pop(1); + + return err; + } +} diff --git a/src/shell/execute.zig b/src/shell/execute.zig new file mode 100644 index 0000000..e2b2368 --- /dev/null +++ b/src/shell/execute.zig @@ -0,0 +1,24 @@ +const std = @import("std"); + +const builtin = @import("../builtin/builtin.zig"); + +const parser = @import("parser.zig"); + +pub fn run( + io: std.Io, + allocator: std.mem.Allocator, + shell: *builtin.Shell, + command: parser.Command, +) !void { + if (try builtin.execute(io, allocator, shell, command)) + return; + + var child = try std.process.spawn( + io, + .{ + .argv = command.argv, + }, + ); + + _ = try child.wait(io); +} diff --git a/src/shell/lexer.zig b/src/shell/lexer.zig new file mode 100644 index 0000000..ddd4e91 --- /dev/null +++ b/src/shell/lexer.zig @@ -0,0 +1,29 @@ +const std = @import("std"); + +pub const Token = struct { + text: []const u8, +}; + +pub fn lex( + allocator: std.mem.Allocator, + source: []const u8, +) !std.ArrayList(Token) { + var tokens: std.ArrayList(Token) = .empty; + + var iterator = std.mem.tokenizeAny( + u8, + source, + " \t\r\n", + ); + + while (iterator.next()) |word| { + try tokens.append( + allocator, + .{ + .text = word, + }, + ); + } + + return tokens; +} diff --git a/src/shell/parser.zig b/src/shell/parser.zig new file mode 100644 index 0000000..f43423a --- /dev/null +++ b/src/shell/parser.zig @@ -0,0 +1,29 @@ +const std = @import("std"); + +const lexer = @import("lexer.zig"); + +pub const Command = struct { + argv: []const []const u8, +}; + +pub fn parse( + allocator: std.mem.Allocator, + tokens: []const lexer.Token, +) !Command { + if (tokens.len == 0) { + return error.EmptyCommand; + } + + const argv = try allocator.alloc( + []const u8, + tokens.len, + ); + + for (tokens, 0..) |token, index| { + argv[index] = token.text; + } + + return .{ + .argv = argv, + }; +} diff --git a/src/shell/shell.zig b/src/shell/shell.zig new file mode 100644 index 0000000..e69de29 diff --git a/test.luau b/test.luau new file mode 100644 index 0000000..e78c665 --- /dev/null +++ b/test.luau @@ -0,0 +1,20 @@ +const X = 1000 +const Y = 10 + +const Res = X + Y + +const Str = `is {Res} == 1010?` + +class Foo + public Bar: string + + function new(Bar: string) + return Test {Foo = Bar} + end + +end + +print(Str) +print(if Res == 1010 then "Yes it is!" else "No it isn't.") + +print(Foo, Foo.new("Classes work!").Bar)