diff --git a/build.zig b/build.zig index 3d0ac34..6a90819 100644 --- a/build.zig +++ b/build.zig @@ -69,10 +69,14 @@ pub fn build(b: *Build) void { test_step.dependOn(&run_tests.step); // Examples - const examples = [_]struct { []const u8, []const u8 }{ + var common_examples = [_]struct { []const u8, []const u8 }{ .{ "interpreter", "examples/interpreter.zig" }, .{ "zig-function", "examples/zig-fn.zig" }, }; + const luau_examples = [_]struct { []const u8, []const u8 }{ + .{ "luau-bytecode", "examples/luau-bytecode.zig" }, + }; + const examples = if (lang == .luau) &common_examples ++ luau_examples else &common_examples; for (examples) |example| { const exe = b.addExecutable(.{ diff --git a/examples/luau-bytecode.zig b/examples/luau-bytecode.zig new file mode 100644 index 0000000..e843fcd --- /dev/null +++ b/examples/luau-bytecode.zig @@ -0,0 +1,33 @@ +//! Run Luau bytecode + +// How to recompile `test.luau.bin` bytecode binary: +// +// luau-compile --binary test.luau > test.bc +// +// This may be required if the Luau version gets upgraded. + +const std = @import("std"); + +// The ziglua module is made available in build.zig +const ziglua = @import("ziglua"); + +pub fn main() anyerror!void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + defer _ = gpa.deinit(); + + // Initialize The Lua vm and get a reference to the main thread + var lua = try ziglua.Lua.init(allocator); + defer lua.deinit(); + + // Open all Lua standard libraries + lua.openLibs(); + + // Load bytecode + const src = @embedFile("./test.luau"); + const bc = try ziglua.compile(allocator, src, ziglua.CompileOptions{}); + defer allocator.free(bc); + + try lua.loadBytecode("...", bc); + try lua.protectedCall(0, 0, 0); +} diff --git a/examples/test.luau b/examples/test.luau new file mode 100644 index 0000000..3ade40e --- /dev/null +++ b/examples/test.luau @@ -0,0 +1,13 @@ +--!strict + +function ispositive(x : number) : string + if x > 0 then + return "yes" + else + return "no" + end +end + +local result : string +result = ispositive(1) +print("result is positive:", result) diff --git a/src/libluau.zig b/src/libluau.zig index 9f04375..319ae3e 100644 --- a/src/libluau.zig +++ b/src/libluau.zig @@ -1148,8 +1148,14 @@ pub const Lua = struct { // luau_compile uses malloc to allocate the bytecode on the heap defer zig_luau_free(bytecode); + try lua.loadBytecode("...", bytecode[0..size]); + } - if (c.luau_load(lua.state, "...", bytecode, size, 0) != 0) return error.Fail; + /// Loads bytecode binary (as compiled with f.ex. 'luau-compile --binary') + /// See https://luau-lang.org/getting-started + /// See also condsiderations for binary bytecode compatibility/safety: https://github.com/luau-lang/luau/issues/493#issuecomment-1185054665 + pub fn loadBytecode(lua: *Lua, chunkname: [:0]const u8, bytecode: []const u8) !void { + if (c.luau_load(lua.state, chunkname.ptr, bytecode.ptr, bytecode.len, 0) != 0) return error.Fail; } /// If the registry already has the key `key`, returns an error @@ -1486,3 +1492,36 @@ pub fn exportFn(comptime name: []const u8, comptime func: ZigFn) void { const declaration = wrap(func); @export(declaration, .{ .name = "luaopen_" ++ name, .linkage = .Strong }); } + +/// Zig wrapper for Luau lua_CompileOptions that uses the same defaults as Luau if +/// no compile options is specified. +pub const CompileOptions = struct { + optimization_level: i32 = 1, + debug_level: i32 = 1, + coverage_level: i32 = 0, + /// global builtin to construct vectors; disabled by default (.) + vector_lib: ?[*:0]const u8 = null, + vector_ctor: ?[*:0]const u8 = null, + /// vector type name for type tables; disabled by default + vector_type: ?[*:0]const u8 = null, + /// null-terminated array of globals that are mutable; disables the import optimization for fields accessed through these + mutable_globals: ?[*:null]const ?[*:0]const u8 = null, +}; + +/// Compile luau source into bytecode, return callee owned buffer allocated through the given allocator. +pub fn compile(allocator: Allocator, source: []const u8, options: CompileOptions) ![]const u8 { + var size: usize = 0; + + var opts = c.lua_CompileOptions{ + .optimizationLevel = options.optimization_level, + .debugLevel = options.debug_level, + .coverageLevel = options.coverage_level, + .vectorLib = options.vector_lib, + .vectorCtor = options.vector_ctor, + .mutableGlobals = options.mutable_globals, + }; + const bytecode = c.luau_compile(source.ptr, source.len, &opts, &size); + if (bytecode == null) return error.Memory; + defer zig_luau_free(bytecode); + return try allocator.dupe(u8, bytecode[0..size]); +} diff --git a/src/tests.zig b/src/tests.zig index 414976c..8d2769d 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -2152,3 +2152,35 @@ test "getstack" { \\g() ); } + +test "compile and run bytecode" { + if (ziglua.lang != .luau) return; + + var lua = try Lua.init(testing.allocator); + defer lua.deinit(); + lua.openLibs(); + + // Load bytecode + const src = "return 133"; + const bc = try ziglua.compile(testing.allocator, src, ziglua.CompileOptions{}); + defer testing.allocator.free(bc); + + try lua.loadBytecode("...", bc); + try lua.protectedCall(0, 1, 0); + const v = try lua.toInteger(-1); + try testing.expectEqual(@as(i32, 133), v); + + // Try mutable globals. Calls to mutable globals should produce longer bytecode. + const src2 = "Foo.print()\nBar.print()"; + const bc1 = try ziglua.compile(testing.allocator, src2, ziglua.CompileOptions{}); + defer testing.allocator.free(bc1); + + const options = ziglua.CompileOptions{ + .mutable_globals = &[_:null]?[*:0]const u8{ "Foo", "Bar" }, + }; + const bc2 = try ziglua.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 testing.expect(bc1.len < bc2.len); +}