Merge pull request #724 from zigtools/comptime-bebe-steps

Implement comptime interpreter
This commit is contained in:
Auguste Rame 2022-11-16 18:13:35 -05:00 committed by GitHub
commit 355d56376f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1711 additions and 10 deletions

View File

@ -7,7 +7,7 @@ const zls_version = std.builtin.Version{ .major = 0, .minor = 11, .patch = 0 };
pub fn build(b: *std.build.Builder) !void {
const current_zig = builtin.zig_version;
const min_zig = std.SemanticVersion.parse("0.10.0-dev.4458+b120c819d") catch return; // builtins changed to @min / @max
if (current_zig.order(min_zig).compare(.lt)) @panic(b.fmt("Your Zig version v{} does not meet the minimum build requirement of v{}", .{current_zig, min_zig}));
if (current_zig.order(min_zig).compare(.lt)) @panic(b.fmt("Your Zig version v{} does not meet the minimum build requirement of v{}", .{ current_zig, min_zig }));
const target = b.standardTargetOptions(.{});
@ -19,7 +19,7 @@ pub fn build(b: *std.build.Builder) !void {
const enable_tracy = b.option(bool, "enable_tracy", "Whether tracy should be enabled.") orelse false;
const coverage = b.option(bool, "generate_coverage", "Generate coverage data with kcov") orelse false;
const coverage_output_dir = b.option([]const u8, "coverage_output_dir", "Output directory for coverage data") orelse b.pathJoin(&.{b.install_prefix, "kcov"});
const coverage_output_dir = b.option([]const u8, "coverage_output_dir", "Output directory for coverage data") orelse b.pathJoin(&.{ b.install_prefix, "kcov" });
exe_options.addOption(
shared.ZigVersion,
@ -122,10 +122,10 @@ pub fn build(b: *std.build.Builder) !void {
var tests = b.addTest("tests/tests.zig");
if(coverage) {
const src_dir = b.pathJoin(&.{b.build_root, "src"});
if (coverage) {
const src_dir = b.pathJoin(&.{ b.build_root, "src" });
const include_pattern = b.fmt("--include-pattern={s}", .{src_dir});
tests.setExecCmd(&[_]?[]const u8{
"kcov",
include_pattern,

1469
src/ComptimeInterpreter.zig Normal file

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@ const BuildConfig = @import("special/build_runner.zig").BuildConfig;
const tracy = @import("tracy.zig");
const Config = @import("Config.zig");
const translate_c = @import("translate_c.zig");
const ComptimeInterpreter = @import("ComptimeInterpreter.zig");
const DocumentStore = @This();
@ -55,6 +56,8 @@ pub const Handle = struct {
uri: Uri,
text: [:0]const u8,
tree: Ast,
/// Not null if a ComptimeInterpreter is actually used
interpreter: ?*ComptimeInterpreter = null,
document_scope: analysis.DocumentScope,
/// Contains one entry for every import in the document
import_uris: std.ArrayListUnmanaged(Uri) = .{},
@ -189,6 +192,12 @@ pub fn refreshDocument(self: *DocumentStore, uri: Uri, new_text: [:0]const u8) !
const handle = self.handles.get(uri) orelse unreachable;
// TODO: Handle interpreter cross reference
if (handle.interpreter) |int| {
int.deinit();
handle.interpreter = null;
}
self.allocator.free(handle.text);
handle.text = new_text;
@ -927,3 +936,17 @@ pub fn errorCompletionItems(self: DocumentStore, arena: std.mem.Allocator, handl
pub fn enumCompletionItems(self: DocumentStore, arena: std.mem.Allocator, handle: Handle) ![]types.CompletionItem {
return try self.tagStoreCompletionItems(arena, handle, "enum_completions");
}
pub fn ensureInterpreterExists(self: *DocumentStore, uri: Uri) !void {
var handle = self.handles.get(uri).?;
if (handle.interpreter == null) {
var int = try self.allocator.create(ComptimeInterpreter);
int.* = ComptimeInterpreter{
.allocator = self.allocator,
.document_store = self,
.uri = uri,
};
handle.interpreter = int;
_ = try int.interpret(0, null, .{});
}
}

View File

@ -19,6 +19,7 @@ const Ast = std.zig.Ast;
const tracy = @import("tracy.zig");
const uri_utils = @import("uri.zig");
const diff = @import("diff.zig");
const ComptimeInterpreter = @import("ComptimeInterpreter.zig");
const data = @import("data/data.zig");
const snipped_data = @import("data/snippets.zig");
@ -322,6 +323,23 @@ fn publishDiagnostics(server: *Server, writer: anytype, handle: DocumentStore.Ha
}
}
if (handle.interpreter) |int| {
try diagnostics.ensureUnusedCapacity(allocator, int.errors.count());
var err_it = int.errors.iterator();
while (err_it.next()) |err| {
try diagnostics.append(allocator, .{
.range = offsets.nodeToRange(tree, err.key_ptr.*, server.offset_encoding),
.severity = .Error,
.code = err.value_ptr.code,
.source = "zls",
.message = err.value_ptr.message,
});
}
}
// try diagnostics.appendSlice(allocator, handle.interpreter.?.diagnostics.items);
try send(writer, server.arena.allocator(), types.Notification{
.method = "textDocument/publishDiagnostics",
.params = .{
@ -485,6 +503,33 @@ fn typeToCompletion(
null,
),
.primitive, .array_index => {},
.@"comptime" => |co| {
const ti = co.type.getTypeInfo();
switch (ti) {
.@"struct" => |st| {
var fit = st.fields.iterator();
while (fit.next()) |entry| {
try list.append(allocator, .{
.label = entry.key_ptr.*,
.kind = .Field,
.insertText = entry.key_ptr.*,
.insertTextFormat = .PlainText,
});
}
var it = st.scope.declarations.iterator();
while (it.next()) |entry| {
try list.append(allocator, .{
.label = entry.key_ptr.*,
.kind = if (entry.value_ptr.isConstant()) .Constant else .Variable,
.insertText = entry.key_ptr.*,
.insertTextFormat = .PlainText,
});
}
},
else => {},
}
},
}
}
@ -803,7 +848,10 @@ fn hoverSymbol(server: *Server, decl_handle: analysis.DeclWithHandle) error{OutO
const resolved_type = try decl_handle.resolveType(&server.document_store, &server.arena, &bound_type_params);
const resolved_type_str = if (resolved_type) |rt|
if (rt.type.is_type_val) "type" else switch (rt.type.data) { // TODO: Investigate random weird numbers like 897 that cause index of bounds
if (rt.type.is_type_val) switch (rt.type.data) {
.@"comptime" => |*co| try std.fmt.allocPrint(server.arena.allocator(), "{ }", .{co.interpreter.formatTypeInfo(co.type.getTypeInfo())}),
else => "type",
} else switch (rt.type.data) { // TODO: Investigate random weird numbers like 897 that cause index of bounds
.pointer,
.slice,
.error_union,
@ -2023,6 +2071,8 @@ fn hoverHandler(server: *Server, writer: anytype, id: types.RequestId, req: requ
};
const hover = maybe_hover orelse return try respondGeneric(writer, id, null_result_response);
// TODO: Figure out a better solution for comptime interpreter diags
try server.publishDiagnostics(writer, handle.*);
try send(writer, server.arena.allocator(), types.Response{
.id = id,

View File

@ -5,6 +5,7 @@ const types = @import("types.zig");
const offsets = @import("offsets.zig");
const log = std.log.scoped(.analysis);
const ast = @import("ast.zig");
const ComptimeInterpreter = @import("ComptimeInterpreter.zig");
var using_trail: std.ArrayList([*]const u8) = undefined;
var resolve_trail: std.ArrayList(NodeWithHandle) = undefined;
@ -306,7 +307,7 @@ pub fn getDeclNameToken(tree: Ast, node: Ast.Node.Index) ?Ast.TokenIndex {
};
}
fn getDeclName(tree: Ast, node: Ast.Node.Index) ?[]const u8 {
pub fn getDeclName(tree: Ast, node: Ast.Node.Index) ?[]const u8 {
const name = tree.tokenSlice(getDeclNameToken(tree, node) orelse return null);
return switch (tree.nodes.items(.tag)[node]) {
.test_decl => name[1 .. name.len - 1],
@ -491,7 +492,7 @@ fn resolveUnwrapErrorType(store: *DocumentStore, arena: *std.heap.ArenaAllocator
.type = .{ .data = .{ .other = n }, .is_type_val = rhs.type.is_type_val },
.handle = rhs.handle,
},
.primitive, .slice, .pointer, .array_index => return null,
.primitive, .slice, .pointer, .array_index, .@"comptime" => return null,
};
if (rhs.handle.tree.nodes.items(.tag)[rhs_node] == .error_union) {
@ -742,7 +743,60 @@ pub fn resolveTypeOfNodeInternal(store: *DocumentStore, arena: *std.heap.ArenaAl
const has_body = decl.handle.tree.nodes.items(.tag)[decl_node] == .fn_decl;
const body = decl.handle.tree.nodes.items(.data)[decl_node].rhs;
return try resolveReturnType(store, arena, fn_decl, decl.handle, bound_type_params, if (has_body) body else null);
if (try resolveReturnType(store, arena, fn_decl, decl.handle, bound_type_params, if (has_body) body else null)) |ret| {
return ret;
} else {
// TODO: Better case-by-case; we just use the ComptimeInterpreter when all else fails,
// probably better to use it more liberally
// TODO: Handle non-isolate args; e.g. `const T = u8; TypeFunc(T);`
// var interpreter = ComptimeInterpreter{ .tree = tree, .allocator = arena.allocator() };
// var top_decl = try (try interpreter.interpret(0, null, .{})).getValue();
// var top_scope = interpreter.typeToTypeInfo(top_decl.@"type".info_idx).@"struct".scope;
// var fn_decl_scope = top_scope.getParentScopeFromNode(node);
log.info("Invoking interpreter!", .{});
store.ensureInterpreterExists(handle.uri) catch |err| {
log.err("Interpreter error: {s}", .{@errorName(err)});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
return null;
};
var interpreter = handle.interpreter.?;
// TODO: Start from current/nearest-current scope
const result = interpreter.interpret(node, interpreter.root_type.?.getTypeInfo().getScopeOfType().?, .{}) catch |err| {
log.err("Interpreter error: {s}", .{@errorName(err)});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
return null;
};
const val = result.getValue() catch |err| {
log.err("Interpreter error: {s}", .{@errorName(err)});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
return null;
};
const ti = val.@"type".getTypeInfo();
if (ti != .@"type") {
log.err("Not a type: { }", .{interpreter.formatTypeInfo(ti)});
return null;
}
return TypeWithHandle{
.type = .{
.data = .{ .@"comptime" = .{ .interpreter = interpreter, .type = val.value_data.@"type" } },
.is_type_val = true,
},
.handle = node_handle.handle,
};
}
}
return null;
},
@ -965,6 +1019,10 @@ pub const Type = struct {
other: Ast.Node.Index,
primitive: Ast.Node.Index,
array_index,
@"comptime": struct {
interpreter: *ComptimeInterpreter,
type: ComptimeInterpreter.Type,
},
},
/// If true, the type `type`, the attached data is the value of the type value.
is_type_val: bool,

View File

@ -1,4 +1,6 @@
// used by tests as a package
// Used by tests as a package, can be used by tools such as
// zigbot9001 to take advantage of zls' tools
pub const analysis = @import("analysis.zig");
pub const header = @import("header.zig");
pub const offsets = @import("offsets.zig");
@ -8,3 +10,5 @@ pub const Server = @import("Server.zig");
pub const translate_c = @import("translate_c.zig");
pub const types = @import("types.zig");
pub const URI = @import("uri.zig");
pub const DocumentStore = @import("DocumentStore.zig");
pub const ComptimeInterpreter = @import("ComptimeInterpreter.zig");

View File

@ -0,0 +1,96 @@
const std = @import("std");
const zls = @import("zls");
const Ast = std.zig.Ast;
const ComptimeInterpreter = zls.ComptimeInterpreter;
const allocator: std.mem.Allocator = std.testing.allocator;
test "ComptimeInterpreter - basic test" {
var config = zls.Config{};
var doc_store = zls.DocumentStore{
.allocator = allocator,
.config = &config,
};
defer doc_store.deinit();
_ = try doc_store.openDocument("file:///file.zig",
\\pub fn ReturnMyType(comptime my_arg: bool) type {
\\ var abc = z: {break :z if (!my_arg) 123 else 0;};
\\ if (abc == 123) return u69;
\\ return u8;
\\}
);
var interpreter = ComptimeInterpreter{
.allocator = allocator,
.document_store = &doc_store,
.uri = "file:///file.zig",
};
defer interpreter.deinit();
_ = try interpreter.interpret(0, null, .{});
var bool_type = try interpreter.createType(std.math.maxInt(std.zig.Ast.Node.Index), .{ .@"bool" = .{} });
var arg_false = ComptimeInterpreter.Value{
.interpreter = &interpreter,
.node_idx = std.math.maxInt(std.zig.Ast.Node.Index),
.@"type" = bool_type,
.value_data = try interpreter.createValueData(.{ .@"bool" = false }),
};
var arg_true = ComptimeInterpreter.Value{
.interpreter = &interpreter,
.node_idx = std.math.maxInt(std.zig.Ast.Node.Index),
.@"type" = bool_type,
.value_data = try interpreter.createValueData(.{ .@"bool" = true }),
};
const rmt = interpreter.root_type.?.getTypeInfo().@"struct".scope.declarations.get("ReturnMyType").?;
const call_with_false = try interpreter.call(null, rmt.node_idx, &.{
arg_false,
}, .{});
defer call_with_false.scope.deinit();
const call_with_true = try interpreter.call(null, rmt.node_idx, &.{
arg_true,
}, .{});
defer call_with_true.scope.deinit();
try std.testing.expectFmt("u69", "{any}", .{interpreter.formatTypeInfo(call_with_false.result.value.value_data.@"type".getTypeInfo())});
try std.testing.expectFmt("u8", "{any}", .{interpreter.formatTypeInfo(call_with_true.result.value.value_data.@"type".getTypeInfo())});
}
test "ComptimeInterpreter - struct" {
var config = zls.Config{};
var doc_store = zls.DocumentStore{
.allocator = allocator,
.config = &config,
};
defer doc_store.deinit();
_ = try doc_store.openDocument("file:///file.zig",
\\pub fn ReturnMyType() type {
\\ return struct {
\\ slay: bool,
\\ var abc = 123;
\\ };
\\}
);
var interpreter = ComptimeInterpreter{
.allocator = allocator,
.document_store = &doc_store,
.uri = "file:///file.zig",
};
defer interpreter.deinit();
_ = try interpreter.interpret(0, null, .{});
const rmt = interpreter.root_type.?.getTypeInfo().@"struct".scope.declarations.get("ReturnMyType").?;
const z = try interpreter.call(null, rmt.node_idx, &.{}, .{});
defer z.scope.deinit();
try std.testing.expectFmt("struct {slay: bool, var abc: comptime_int = 123, }", "{any}", .{interpreter.formatTypeInfo(z.result.value.value_data.@"type".getTypeInfo())});
}

View File

@ -17,4 +17,5 @@ comptime {
// Language features
_ = @import("language_features/cimport.zig");
_ = @import("language_features/comptime_interpreter.zig");
}