Skip to content

Commit b4ac96c

Browse files
feat!: add Wasm support
1 parent 67b9e6e commit b4ac96c

File tree

7 files changed

+270
-12
lines changed

7 files changed

+270
-12
lines changed

build.zig

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,47 @@
11
const std = @import("std");
2+
const ts = @import("tree_sitter");
3+
4+
const wasm_url = "https://github.com/tree-sitter/tree-sitter-c/releases/download/v0.24.1/tree-sitter-c.wasm";
25

36
pub fn build(b: *std.Build) !void {
47
const target = b.standardTargetOptions(.{});
58
const optimize = b.standardOptimizeOption(.{});
69

7-
const core = b.dependency("tree_sitter", .{
10+
const options = b.addOptions();
11+
const enable_wasm = b.option(bool, "enable-wasm", "Enable Wasm support") orelse false;
12+
options.addOption(bool, "enable_wasm", enable_wasm);
13+
14+
const core = b.dependencyFromBuildZig(ts, .{
815
.target = target,
916
.optimize = optimize,
17+
.amalgamated = true,
18+
.@"build-shared" = false,
19+
.@"enable-wasm" = enable_wasm,
1020
});
1121
const core_lib = core.artifact("tree-sitter");
22+
const wasmtime = if (enable_wasm) core.builder.lazyDependency(ts.wasmtimeDep(target.result), .{}) else null;
1223

1324
const module = b.addModule("tree_sitter", .{
1425
.root_source_file = b.path("src/root.zig"),
1526
.target = target,
1627
.optimize = optimize,
1728
});
1829
module.linkLibrary(core_lib);
30+
module.addOptions("build", options);
31+
if (wasmtime) |dep| {
32+
module.addLibraryPath(dep.path("lib"));
33+
module.linkSystemLibrary("wasmtime", .{
34+
.preferred_link_mode = .static,
35+
});
36+
}
1937

2038
const docs = b.addObject(.{
2139
.name = "tree_sitter",
2240
.root_source_file = b.path("src/root.zig"),
2341
.target = target,
2442
.optimize = .Debug,
2543
});
44+
docs.root_module.addOptions("build", options);
2645

2746
const install_docs = b.addInstallDirectory(.{
2847
.source_dir = docs.getEmittedDocs(),
@@ -39,9 +58,31 @@ pub fn build(b: *std.Build) !void {
3958
.optimize = optimize,
4059
});
4160
tests.linkLibrary(core_lib);
61+
tests.root_module.addOptions("build", options);
62+
if (wasmtime) |dep| {
63+
tests.root_module.addLibraryPath(dep.path("lib"));
64+
tests.root_module.linkSystemLibrary("wasmtime", .{
65+
.preferred_link_mode = .static,
66+
});
67+
tests.root_module.linkSystemLibrary("unwind", .{});
68+
if (target.result.os.tag == .windows) {
69+
if (target.result.abi != .msvc) {
70+
tests.root_module.linkSystemLibrary("unwind", .{});
71+
tests.root_module.linkSystemLibrary("advapi32", .{});
72+
tests.root_module.linkSystemLibrary("bcrypt", .{});
73+
tests.root_module.linkSystemLibrary("ntdll", .{});
74+
tests.root_module.linkSystemLibrary("ole32", .{});
75+
tests.root_module.linkSystemLibrary("shell32", .{});
76+
tests.root_module.linkSystemLibrary("userenv", .{});
77+
tests.root_module.linkSystemLibrary("ws2_32", .{});
78+
} else {
79+
const fail = b.addFail("FIXME: cannot build with enable-wasm for MSVC");
80+
tests.step.dependOn(&fail.step);
81+
}
82+
}
83+
}
4284

4385
const run_tests = b.addRunArtifact(tests);
44-
4586
const test_step = b.step("test", "Run unit tests");
4687
test_step.dependOn(&run_tests.step);
4788

@@ -55,6 +96,19 @@ pub fn build(b: *std.Build) !void {
5596
.optimize = optimize,
5697
}) orelse continue;
5798
tests.linkLibrary(dep.artifact("tree-sitter-c"));
99+
100+
if (enable_wasm) {
101+
// FIXME: prevent the file from being downloaded multiple times
102+
std.log.info("Downloading {s}\n", .{ wasm_url });
103+
const run_curl = b.addSystemCommand(&.{"curl", "-LSsf", wasm_url, "-o"});
104+
const wasm_file = run_curl.addOutputFileArg("tree-sitter-c.wasm");
105+
run_curl.expectStdErrEqual("");
106+
tests.step.dependOn(&run_curl.step);
107+
tests.root_module.addAnonymousImport("tree-sitter-c.wasm", .{
108+
.root_source_file = wasm_file,
109+
});
110+
}
111+
58112
break;
59113
}
60114
}

build.zig.zon

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22
.name = .tree_sitter,
33
.version = "0.25.1",
44
.fingerprint = 0x841224b4264a3d65,
5-
.minimum_zig_version = "0.14.0",
5+
.minimum_zig_version = "0.14.1",
66
.dependencies = .{
77
.tree_sitter = .{
8-
.url = "https://github.com/tree-sitter/tree-sitter/archive/refs/heads/release-0.25.tar.gz",
9-
.hash = "tree_sitter-0.25.8-Tw2sRxW7CwCiJosGHB0dIWv0tROYNsI6hSEzNmsfOrhR",
8+
// tree-sitter/tree-sitter#4743
9+
.url = "https://github.com/tree-sitter/tree-sitter/archive/ef93906.tar.gz",
10+
.hash = "tree_sitter-0.25.8-Tw2sR8q-CwBpDiIhhNPHvHyqWjnWrbaDAf9PRcvEyGkh",
1011
},
1112
.tree_sitter_c = .{
12-
.url = "https://github.com/tree-sitter/tree-sitter-c/archive/976d785.tar.gz",
13-
.hash = "tree_sitter_c-0.24.0-VJuGjG5iQADj4pKFZy8LQF8Pcisq3KN9rbKo8amF6uoJ",
13+
// tree-sitter/tree-sitter-c#275
14+
.url = "https://github.com/tree-sitter/tree-sitter-c/archive/3450141.tar.gz",
15+
.hash = "tree_sitter_c-0.24.0-VJuGjP9iQAAhFIo6ihR4_n7OB3dn5BSU6_smy9xLwoL8",
1416
.lazy = true,
1517
},
1618
},

src/language.zig

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,14 @@ pub const Language = opaque {
7171
return ts_language_abi_version(self);
7272
}
7373

74+
/// Check if the language came from a Wasm module.
75+
///
76+
/// If so, then in order to use this language with a `Parser`,
77+
/// that parser must have a `WasmStore` assigned.
78+
pub inline fn isWasm(self: *const Language) bool {
79+
return ts_language_is_wasm(self);
80+
}
81+
7482
/// Get the number of distinct node types in this language.
7583
pub inline fn symbolCount(self: *const Language) u32 {
7684
return ts_language_symbol_count(self);
@@ -156,6 +164,7 @@ extern fn ts_language_delete(self: *const Language) void;
156164
extern fn ts_language_field_count(self: *const Language) u32;
157165
extern fn ts_language_field_id_for_name(self: *const Language, name: [*]const u8, name_length: u32) u16;
158166
extern fn ts_language_field_name_for_id(self: *const Language, id: u16) ?[*:0]const u8;
167+
extern fn ts_language_is_wasm(language: *const Language) bool;
159168
extern fn ts_language_metadata(self: *const Language) ?*const LanguageMetadata;
160169
extern fn ts_language_name(self: *const Language) ?[*:0]const u8;
161170
extern fn ts_language_next_state(self: *const Language, state: u16, symbol: u16) u16;

src/parser.zig

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const build = @import("build");
12
const std = @import("std");
23

34
const Input = @import("types.zig").Input;
@@ -10,6 +11,7 @@ const Node = @import("node.zig").Node;
1011
const Point = @import("types.zig").Point;
1112
const Range = @import("types.zig").Range;
1213
const Tree = @import("tree.zig").Tree;
14+
const WasmStore = @import("wasm.zig").WasmStore;
1315

1416
/// A stateful object that is used to produce
1517
/// a syntax tree based on some source code.
@@ -31,11 +33,10 @@ pub const Parser = opaque {
3133

3234
/// Set the language that the parser should use for parsing.
3335
///
34-
/// Returns an error if the language has an incompatible version.
35-
pub fn setLanguage(self: *Parser, language: ?*const Language) error{IncompatibleVersion}!void {
36-
if (!ts_parser_set_language(self, language)) {
37-
return error.IncompatibleVersion;
38-
}
36+
/// Returns an error if the language has an incompatible version,
37+
/// or if it was loaded from Wasm and the parser lacks a Wasm store.
38+
pub fn setLanguage(self: *Parser, language: ?*const Language) error{IncompatibleLanguage}!void {
39+
if (!ts_parser_set_language(self, language)) return error.IncompatibleLanguage;
3940
}
4041

4142
/// Get the parser's current logger.
@@ -185,6 +186,20 @@ pub const Parser = opaque {
185186
ts_parser_reset(self);
186187
}
187188

189+
/// Assign the given Wasm store to the parser.
190+
///
191+
/// A parser must have a Wasm store in order to use Wasm languages.
192+
pub fn setWasmStore(self: *Parser, store: *WasmStore) void {
193+
if (comptime !build.enable_wasm) @compileError("Wasm is not supported");
194+
ts_parser_set_wasm_store(self, store);
195+
}
196+
197+
/// Remove the parser's current Wasm store, if any, and return it.
198+
pub fn takeWasmStore(self: *Parser) ?*WasmStore {
199+
if (comptime !build.enable_wasm) @compileError("Wasm is not supported");
200+
return ts_parser_take_wasm_store(self);
201+
}
202+
188203
/// Set the file to which the parser should write debugging graphs
189204
/// during parsing. The graphs are formatted in the DOT language.
190205
///
@@ -244,3 +259,5 @@ extern fn ts_parser_cancellation_flag(self: *const Parser) ?*const usize;
244259
extern fn ts_parser_set_logger(self: *Parser, logger: Logger) void;
245260
extern fn ts_parser_logger(self: *const Parser) Logger;
246261
extern fn ts_parser_print_dot_graphs(self: *Parser, fd: c_int) void;
262+
extern fn ts_parser_set_wasm_store(parser: *Parser, store: *WasmStore) void;
263+
extern fn ts_parser_take_wasm_store(parser: *Parser) ?*WasmStore;

src/root.zig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,8 @@ pub const QueryCursor = @import("query_cursor.zig").QueryCursor;
2727
pub const Tree = @import("tree.zig").Tree;
2828
pub const TreeCursor = @import("tree_cursor.zig").TreeCursor;
2929

30+
const wasm = @import("wasm.zig");
31+
pub const WasmEngine = wasm.WasmEngine;
32+
pub const WasmStore = wasm.WasmStore;
33+
3034
pub const setAllocator = @import("alloc.zig").setAllocator;

src/test.zig

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const build = @import("build");
12
const std = @import("std");
23
const testing = std.testing;
34
const ts = @import("root.zig");
@@ -19,6 +20,7 @@ test "Language" {
1920
try testing.expect(language.isVisible(1));
2021
try testing.expect(!language.isSupertype(1));
2122
try testing.expect(language.nextState(1, 161) > 1);
23+
try testing.expect(!language.isWasm());
2224

2325
const copy = language.dupe();
2426
try testing.expectEqual(language, copy);
@@ -363,3 +365,40 @@ test "QueryCursor" {
363365
try testing.expectEqual(1, match.captures[0].index);
364366
try testing.expectEqualStrings("(", match.captures[0].node.type());
365367
}
368+
369+
test "Wasm" {
370+
if (comptime !build.enable_wasm) return error.SkipZigTest;
371+
372+
const engine = try ts.WasmEngine.init(null);
373+
defer engine.deinit();
374+
375+
var error_message: []u8 = undefined;
376+
const store = try ts.WasmStore.create(testing.allocator, engine, &error_message);
377+
errdefer testing.allocator.free(error_message);
378+
defer store.destroy();
379+
380+
const wasm = @embedFile("tree-sitter-c.wasm");
381+
const language = try store.loadLanguage(testing.allocator, "c", wasm, &error_message);
382+
errdefer testing.allocator.free(error_message);
383+
defer language.destroy();
384+
385+
try testing.expect(language.isWasm());
386+
try testing.expectEqual(1, store.languageCount());
387+
388+
const parser = ts.Parser.create();
389+
defer parser.destroy();
390+
391+
try testing.expectError(error.IncompatibleLanguage, parser.setLanguage(language));
392+
parser.setWasmStore(store);
393+
defer _ = parser.takeWasmStore();
394+
try parser.setLanguage(language);
395+
396+
const tree = try parser.parseBuffer("int main() {}", null, .UTF_8);
397+
defer tree.destroy();
398+
399+
try testing.expectEqualStrings("translation_unit", tree.rootNode().type());
400+
401+
try testing.expectError(error.ParseError, store.loadLanguage(testing.allocator, "c", "", &error_message));
402+
try testing.expectEqualStrings("failed to parse dylink section of wasm module", error_message);
403+
testing.allocator.free(error_message);
404+
}

0 commit comments

Comments
 (0)