Merge pull request #793 from Techatrix/intern-pool

ComptimeInterpreter: Intern Pool
This commit is contained in:
Auguste Rame 2023-02-15 14:46:58 -05:00 committed by GitHub
commit c3f58538e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 5200 additions and 1042 deletions

View File

@ -178,17 +178,19 @@ pub fn build(b: *std.build.Builder) !void {
const test_step = b.step("test", "Run all the tests");
test_step.dependOn(b.getInstallStep());
const test_filter = b.option(
[]const u8,
"test-filter",
"Skip tests that do not match filter",
);
var tests = b.addTest(.{
.root_source_file = .{ .path = "tests/tests.zig" },
.target = target,
.optimize = .Debug,
});
tests.setFilter(b.option(
[]const u8,
"test-filter",
"Skip tests that do not match filter",
));
tests.setFilter(test_filter);
if (coverage) {
const src_dir = b.pathJoin(&.{ build_root_path, "src" });
@ -218,6 +220,14 @@ pub fn build(b: *std.build.Builder) !void {
tests.addModule("diffz", diffz_module);
test_step.dependOn(&tests.step);
var src_tests = b.addTest(.{
.root_source_file = .{ .path = "src/zls.zig" },
.target = target,
.optimize = .Debug,
});
src_tests.setFilter(test_filter);
test_step.dependOn(&src_tests.step);
}
const CheckSubmodulesStep = struct {

File diff suppressed because it is too large Load Diff

View File

@ -68,6 +68,10 @@ pub const Handle = struct {
associated_build_file: ?Uri = null,
pub fn deinit(self: *Handle, allocator: std.mem.Allocator) void {
if (self.interpreter) |interpreter| {
interpreter.deinit();
allocator.destroy(interpreter);
}
self.document_scope.deinit(allocator);
self.tree.deinit(allocator);
allocator.free(self.text);
@ -1014,16 +1018,26 @@ pub fn enumCompletionItems(self: DocumentStore, arena: std.mem.Allocator, handle
return try self.tagStoreCompletionItems(arena, handle, "enum_completions");
}
pub fn ensureInterpreterExists(self: *DocumentStore, uri: Uri) !void {
pub fn ensureInterpreterExists(self: *DocumentStore, uri: Uri) !*ComptimeInterpreter {
var handle = self.handles.get(uri).?;
if (handle.interpreter == null) {
var int = try self.allocator.create(ComptimeInterpreter);
int.* = ComptimeInterpreter{
if (handle.interpreter != null) return handle.interpreter.?;
{
var interpreter = try self.allocator.create(ComptimeInterpreter);
errdefer self.allocator.destroy(interpreter);
var ip = try ComptimeInterpreter.InternPool.init(self.allocator);
errdefer ip.deinit(self.allocator);
interpreter.* = ComptimeInterpreter{
.allocator = self.allocator,
.ip = ip,
.document_store = self,
.uri = uri,
};
handle.interpreter = int;
_ = try int.interpret(0, null, .{});
handle.interpreter = interpreter;
}
_ = try handle.interpreter.?.interpret(0, .none, .{});
return handle.interpreter.?;
}

View File

@ -21,6 +21,7 @@ const tracy = @import("tracy.zig");
const uri_utils = @import("uri.zig");
const diff = @import("diff.zig");
const ComptimeInterpreter = @import("ComptimeInterpreter.zig");
const analyser = @import("analyser/analyser.zig");
const data = @import("data/data.zig");
const snipped_data = @import("data/snippets.zig");
@ -362,7 +363,7 @@ fn generateDiagnostics(server: *Server, handle: DocumentStore.Handle) error{OutO
var err_it = int.errors.iterator();
while (err_it.next()) |err| {
try diagnostics.append(allocator, .{
diagnostics.appendAssumeCapacity(.{
.range = offsets.nodeToRange(tree, err.key_ptr.*, server.offset_encoding),
.severity = .Error,
.code = .{ .string = err.value_ptr.code },
@ -597,30 +598,10 @@ fn typeToCompletion(
),
.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 => {},
if (type_handle.type.is_type_val) {
try analyser.completions.dotCompletions(allocator, list, &co.interpreter.ip, co.value.ty, co.value.val, co.value.node_idx);
} else {
try analyser.completions.dotCompletions(allocator, list, &co.interpreter.ip, co.value.val, .none, co.value.node_idx);
}
},
}
@ -939,7 +920,7 @@ fn hoverSymbol(server: *Server, decl_handle: analysis.DeclWithHandle) error{OutO
const resolved_type_str = if (resolved_type) |rt|
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())}),
.@"comptime" => |co| try std.fmt.allocPrint(server.arena.allocator(), "{}", .{co.value.ty.fmtType(co.interpreter.ip)}),
else => "type",
} else switch (rt.type.data) { // TODO: Investigate random weird numbers like 897 that cause index of bounds
.pointer,

3601
src/analyser/InternPool.zig Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
pub const completions = @import("completions.zig");
pub const InternPool = @import("InternPool.zig");
pub const encoding = @import("encoding.zig");
comptime {
const std = @import("std");
std.testing.refAllDecls(@This());
}

View File

@ -0,0 +1,209 @@
const std = @import("std");
const InternPool = @import("InternPool.zig");
const types = @import("../lsp.zig");
const Ast = std.zig.Ast;
pub fn dotCompletions(
arena: std.mem.Allocator,
completions: *std.ArrayListUnmanaged(types.CompletionItem),
ip: *InternPool,
ty: InternPool.Index,
val: InternPool.Index,
node: ?Ast.Node.Index,
) error{OutOfMemory}!void {
_ = node;
const key = ip.indexToKey(ty);
const inner_key = switch (key) {
.pointer_type => |info| if (info.size == .One) ip.indexToKey(info.elem_type) else key,
else => key,
};
switch (inner_key) {
.simple_type => |simple| switch (simple) {
.type => {
const ty_key = ip.indexToKey(val);
const namespace = ty_key.getNamespace(ip.*);
if (namespace != .none) {
// TODO lookup in namespace
}
switch (ty_key) {
.error_set_type => |error_set_info| {
for (error_set_info.names) |name| {
const error_name = ip.indexToKey(name).bytes;
try completions.append(arena, .{
.label = error_name,
.kind = .Constant,
.detail = try std.fmt.allocPrint(arena, "error.{s}", .{std.zig.fmtId(error_name)}),
});
}
},
.union_type => {}, // TODO
.enum_type => |enum_index| {
const enum_info = ip.getEnum(enum_index);
var field_it = enum_info.fields.iterator();
while (field_it.next()) |entry| {
try completions.append(arena, .{
.label = entry.key_ptr.*,
.kind = .Constant,
// include field.val?
});
}
},
else => {},
}
},
else => {},
},
.pointer_type => |pointer_info| {
if (pointer_info.size == .Slice) {
var many_ptr_info = InternPool.Key{ .pointer_type = pointer_info };
many_ptr_info.pointer_type.size = .Many;
try completions.append(arena, .{
.label = "ptr",
.kind = .Field,
.detail = try std.fmt.allocPrint(arena, "ptr: {}", .{many_ptr_info.fmtType(ip.*)}),
});
try completions.append(arena, .{
.label = "len",
.kind = .Field,
.detail = "len: usize",
});
} else if (ip.indexToKey(pointer_info.elem_type) == .array_type) {
try completions.append(arena, .{
.label = "len",
.kind = .Field,
.detail = "len: usize",
});
}
},
.array_type => |array_info| {
try completions.append(arena, types.CompletionItem{
.label = "len",
.kind = .Field,
.detail = try std.fmt.allocPrint(arena, "const len: usize ({d})", .{array_info.len}), // TODO how should this be displayed
});
},
.struct_type => |struct_index| {
const struct_info = ip.getStruct(struct_index);
try completions.ensureUnusedCapacity(arena, struct_info.fields.count());
var field_it = struct_info.fields.iterator();
while (field_it.next()) |entry| {
const label = entry.key_ptr.*;
const field = entry.value_ptr.*;
completions.appendAssumeCapacity(types.CompletionItem{
.label = label,
.kind = .Field,
.detail = try std.fmt.allocPrint(arena, "{s}: {}", .{
label,
fmtFieldDetail(field, ip),
}),
});
}
},
.optional_type => |optional_info| {
try completions.append(arena, .{
.label = "?",
.kind = .Operator,
.detail = try std.fmt.allocPrint(arena, "{}", .{optional_info.payload_type.fmtType(ip.*)}),
});
},
.enum_type => |enum_index| {
const enum_info = ip.getEnum(enum_index);
for (enum_info.fields.keys()) |field_name, i| {
const field_val = enum_info.values.keys()[i];
try completions.append(arena, .{
.label = field_name,
.kind = .Field,
.detail = try std.fmt.allocPrint(arena, "{}", .{field_val.fmtValue(enum_info.tag_type, ip.*)}),
});
}
},
.union_type => |union_index| {
const union_info = ip.getUnion(union_index);
var field_it = union_info.fields.iterator();
while (field_it.next()) |entry| {
const label = entry.key_ptr.*;
const field = entry.value_ptr.*;
try completions.append(arena, .{
.label = label,
.kind = .Field,
.detail = if (field.alignment != 0)
try std.fmt.allocPrint(arena, "{s}: align({d}) {}", .{ label, field.alignment, field.ty.fmtType(ip.*) })
else
try std.fmt.allocPrint(arena, "{s}: {}", .{ label, field.ty.fmtType(ip.*) }),
});
}
},
.tuple_type => |tuple_info| {
for (tuple_info.types) |tuple_ty, i| {
try completions.append(arena, .{
.label = try std.fmt.allocPrint(arena, "{d}", .{i}),
.kind = .Field,
.detail = try std.fmt.allocPrint(arena, "{d}: {}", .{ i, tuple_ty.fmtType(ip.*) }),
});
}
},
.int_type,
.error_set_type,
.error_union_type,
.function_type,
.vector_type,
.anyframe_type,
=> {},
.simple_value,
.int_u64_value,
.int_i64_value,
.int_big_value,
.float_16_value,
.float_32_value,
.float_64_value,
.float_80_value,
.float_128_value,
=> unreachable,
.bytes,
.aggregate,
.union_value,
=> unreachable,
}
}
fn FormatContext(comptime T: type) type {
return struct {
ip: *InternPool,
item: T,
};
}
fn formatFieldDetail(
ctx: FormatContext(InternPool.Struct.Field),
comptime fmt: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) @TypeOf(writer).Error!void {
_ = options;
if (fmt.len != 0) std.fmt.invalidFmtError(fmt, InternPool.Struct.Field);
const field = ctx.item;
if (field.is_comptime) {
try writer.writeAll("comptime ");
}
if (field.alignment != 0) {
try writer.print("align({d}) ", .{field.alignment});
}
try writer.print("{}", .{field.ty.fmtType(ctx.ip.*)});
if (field.default_value != .none) {
try writer.print(" = {},", .{field.default_value.fmtValue(field.ty, ctx.ip.*)});
}
}
pub fn fmtFieldDetail(field: InternPool.Struct.Field, ip: *InternPool) std.fmt.Formatter(formatFieldDetail) {
return .{ .data = .{
.ip = ip,
.item = field,
} };
}

210
src/analyser/encoding.zig Normal file
View File

@ -0,0 +1,210 @@
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const Index = usize;
pub fn encode(extra: *std.ArrayList(u8), comptime T: type, data: anytype) Allocator.Error!void {
switch (@typeInfo(T)) {
.Type,
.NoReturn,
.ComptimeFloat,
.ComptimeInt,
.Undefined,
.Null,
.ErrorUnion,
.ErrorSet,
.Fn,
.Opaque,
.Frame,
.AnyFrame,
.EnumLiteral,
=> @compileError("Unable to encode type " ++ @typeName(T)),
.Void => {},
.Bool => try encode(extra, u1, @boolToInt(data)),
.Int => try extra.appendSlice(std.mem.asBytes(&data)),
.Float => |info| switch (info.bits) {
16 => try encode(extra, u16, @bitCast(u16, data)),
32 => try encode(extra, u32, @bitCast(u32, data)),
64 => try encode(extra, u64, @bitCast(u64, data)),
80 => try encode(extra, u80, @bitCast(u80, data)),
128 => try encode(extra, u128, @bitCast(u128, data)),
else => @compileError("Unable to encode type " ++ @typeName(T)),
},
.Pointer => |info| {
switch (info.size) {
.One => {
if (comptime canEncodeAsBytes(info.child)) {
try extra.appendNTimes(undefined, std.mem.alignPointerOffset(extra.items.ptr + extra.items.len, info.alignment).?);
try encode(extra, info.child, data.*);
} else {
@compileError("Encoding " ++ @typeName(T) ++ " would require allocation");
}
},
.Slice => {
if (comptime canEncodeAsBytes(info.child)) {
try encode(extra, u32, @intCast(u32, data.len));
try extra.appendNTimes(undefined, std.mem.alignPointerOffset(extra.items.ptr + extra.items.len, info.alignment).?);
try extra.appendSlice(std.mem.sliceAsBytes(data));
} else {
@compileError("Encoding " ++ @typeName(T) ++ " would require allocation");
}
},
.Many,
.C,
=> @compileError("Unable to encode type " ++ @typeName(T)),
}
},
.Array => |info| {
for (data) |item| {
try encode(extra, info.child, item);
}
},
.Struct => |info| {
switch (info.layout) {
.Packed,
.Extern,
=> return try extra.appendSlice(std.mem.asBytes(&data)),
.Auto => {
inline for (info.fields) |field| {
try encode(extra, field.type, @field(data, field.name));
}
},
}
},
.Optional => {
try encode(extra, bool, data == null);
if (data) |item| {
try encode(extra, item);
}
},
.Enum => |info| try encode(extra, info.tag_type, @enumToInt(data)),
.Union => @compileError("TODO"),
.Vector => |info| {
const array: [info.len]info.child = data;
try encode(extra, array);
},
}
}
pub fn decode(extra: *[]const u8, comptime T: type) T {
return switch (@typeInfo(T)) {
.Type,
.NoReturn,
.ComptimeFloat,
.ComptimeInt,
.Undefined,
.Null,
.ErrorUnion,
.ErrorSet,
.Fn,
.Opaque,
.Frame,
.AnyFrame,
.EnumLiteral,
=> @compileError("Unable to decode type " ++ @typeName(T)),
.Void => {},
.Bool => decode(extra, u1) == 1,
.Int => std.mem.bytesToValue(T, readArray(extra, @sizeOf(T))),
.Float => |info| switch (info.bits) {
16 => @bitCast(T, decode(extra, u16)),
32 => @bitCast(T, decode(extra, u32)),
64 => @bitCast(T, decode(extra, u64)),
80 => @bitCast(T, decode(extra, u80)),
128 => @bitCast(T, decode(extra, u128)),
else => @compileError("Unable to decode type " ++ @typeName(T)),
},
.Pointer => |info| {
switch (info.size) {
.One => {
if (comptime canEncodeAsBytes(info.child)) {
extra.* = alignForward(extra.*, info.alignment);
return std.mem.bytesAsValue(T, readArray(extra, @sizeOf(info.child)));
} else {
@compileError("Decoding " ++ @typeName(T) ++ " would require allocation");
}
},
.Slice => {
if (comptime canEncodeAsBytes(info.child)) {
const len = decode(extra, u32);
extra.* = alignForward(extra.*, info.alignment);
const bytes = readBytes(extra, len * @sizeOf(info.child));
return std.mem.bytesAsSlice(info.child, @alignCast(info.alignment, bytes));
} else {
@compileError("Decoding " ++ @typeName(T) ++ " would require allocation");
}
},
.Many,
.C,
=> @compileError("Unable to decode type " ++ @typeName(T)),
}
},
.Array => |info| blk: {
var array: T = undefined;
var i: usize = 0;
while (i < info.len) {
array[i] = decode(extra, info.child);
}
break :blk array;
},
.Struct => |info| {
switch (info.layout) {
.Packed,
.Extern,
=> return std.mem.bytesToValue(T, readArray(extra, @sizeOf(T))),
.Auto => {
var result: T = undefined;
inline for (info.fields) |field| {
@field(result, field.name) = decode(extra, field.type);
}
return result;
},
}
},
.Optional => |info| blk: {
const is_null = decode(extra, bool);
if (is_null) {
break :blk null;
} else {
break :blk decode(extra, info.child);
}
},
.Enum => |info| @intToEnum(T, decode(extra, info.tag_type)),
.Union => @compileError("TODO"),
.Vector => |info| decode(extra, [info.len]info.child),
};
}
pub fn canEncodeAsBytes(comptime T: type) bool {
return switch (@typeInfo(T)) {
.Void, .Bool, .Int, .Float, .Enum, .Vector => true,
.Array => |info| canEncodeAsBytes(info.child),
.Struct => |info| info.layout != .Auto,
.Union => |info| info.layout != .Auto,
else => false,
};
}
/// forward aligns `extra` until it has the given alignment
pub fn alignForward(extra: []const u8, alignment: usize) []const u8 {
const unaligned = @ptrToInt(extra.ptr);
const offset = std.mem.alignForward(unaligned, alignment) - unaligned;
const result = extra[offset..];
std.debug.assert(std.mem.isAligned(@ptrToInt(result.ptr), alignment));
return result;
}
pub fn readBytes(extra: *[]const u8, n: usize) []const u8 {
defer extra.* = extra.*[n..];
return extra.*[0..n];
}
pub fn readArray(extra: *[]const u8, comptime n: usize) *const [n]u8 {
defer extra.* = extra.*[n..];
return extra.*[0..n];
}

View File

@ -7,6 +7,7 @@ const URI = @import("uri.zig");
const log = std.log.scoped(.analysis);
const ast = @import("ast.zig");
const ComptimeInterpreter = @import("ComptimeInterpreter.zig");
const InternPool = ComptimeInterpreter.InternPool;
var using_trail: std.ArrayList([*]const u8) = undefined;
var resolve_trail: std.ArrayList(NodeWithHandle) = undefined;
@ -762,41 +763,39 @@ pub fn resolveTypeOfNodeInternal(store: *DocumentStore, node_handle: NodeWithHan
log.info("Invoking interpreter!", .{});
store.ensureInterpreterExists(handle.uri) catch |err| {
log.err("Interpreter error: {s}", .{@errorName(err)});
const interpreter = store.ensureInterpreterExists(handle.uri) catch |err| {
log.err("Failed to interpret file: {s}", .{@errorName(err)});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
return null;
};
var interpreter = handle.interpreter.?;
const root_namespace = @intToEnum(ComptimeInterpreter.Namespace.Index, 0);
// 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)});
const result = interpreter.interpret(node, root_namespace, .{}) catch |err| {
log.err("Failed to interpret node: {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)});
const value = result.getValue() catch |err| {
log.err("interpreter return no result: {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,
.data = .{ .@"comptime" = .{
.interpreter = interpreter,
.value = value,
} },
.is_type_val = value.ty == InternPool.Index.type_type,
},
.handle = node_handle.handle,
};
@ -1050,7 +1049,7 @@ pub const Type = struct {
array_index,
@"comptime": struct {
interpreter: *ComptimeInterpreter,
type: ComptimeInterpreter.Type,
value: ComptimeInterpreter.Value,
},
},
/// If true, the type `type`, the attached data is the value of the type value.

View File

@ -14,3 +14,9 @@ pub const URI = @import("uri.zig");
pub const DocumentStore = @import("DocumentStore.zig");
pub const ComptimeInterpreter = @import("ComptimeInterpreter.zig");
pub const diff = @import("diff.zig");
pub const analyser = @import("analyser/analyser.zig");
comptime {
const std = @import("std");
std.testing.refAllDecls(@This());
}

View File

@ -1,96 +1,482 @@
const std = @import("std");
const zls = @import("zls");
const builtin = @import("builtin");
const Ast = std.zig.Ast;
const ComptimeInterpreter = zls.ComptimeInterpreter;
const InternPool = zls.analyser.InternPool;
const Index = InternPool.Index;
const Key = InternPool.Key;
const ast = zls.ast;
const offsets = zls.offsets;
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();
test "ComptimeInterpreter - primitive types" {
try testExpr("true", .{ .simple_type = .bool }, .{ .simple_value = .bool_true });
try testExpr("false", .{ .simple_type = .bool }, .{ .simple_value = .bool_false });
try testExpr("5", .{ .simple_type = .comptime_int }, .{ .int_u64_value = 5 });
// TODO try testExpr("-2", .{ .simple_type = .comptime_int }, .{ .int_i64_value = -2 });
try testExpr("3.0", .{ .simple_type = .comptime_float }, null);
_ = 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())});
try testExpr("null", .{ .simple_type = .null_type }, .{ .simple_value = .null_value });
try testExpr("void", .{ .simple_type = .type }, .{ .simple_type = .void });
try testExpr("undefined", .{ .simple_type = .undefined_type }, .{ .simple_value = .undefined_value });
try testExpr("noreturn", .{ .simple_type = .type }, .{ .simple_type = .noreturn });
}
test "ComptimeInterpreter - struct" {
var config = zls.Config{};
var doc_store = zls.DocumentStore{
.allocator = allocator,
.config = &config,
};
defer doc_store.deinit();
test "ComptimeInterpreter - expressions" {
if (true) return error.SkipZigTest; // TODO
try testExpr("5 + 3", .{ .simple_type = .comptime_int }, .{ .int_u64_value = 8 });
try testExpr("5.2 + 4.2", .{ .simple_type = .comptime_float }, null);
_ = try doc_store.openDocument("file:///file.zig",
\\pub fn ReturnMyType() type {
try testExpr("3 == 3", .{ .simple_type = .bool }, .{ .simple_valueclear = .bool_true });
try testExpr("5.2 == 2.1", .{ .simple_type = .bool }, .{ .simple_value = .bool_false });
try testExpr("@as(?bool, null) orelse true", .{ .simple_type = .bool }, .{ .simple_value = .bool_true });
}
test "ComptimeInterpreter - builtins" {
if (true) return error.SkipZigTest; // TODO
try testExpr("@as(bool, true)", .{ .simple_type = .bool }, .{ .simple_value = .bool_true });
try testExpr("@as(u32, 3)", .{ .int_type = .{
.signedness = .unsigned,
.bits = 32,
} }, .{ .int_u64_value = 3 });
}
test "ComptimeInterpreter - string literal" {
var context = try Context.init(
\\const foobarbaz = "hello world!";
\\
);
defer context.deinit();
const result = try context.interpret(context.findVar("foobarbaz"));
try std.testing.expect(result.ty == .pointer_type);
try std.testing.expectEqualStrings("hello world!", result.val.?.bytes);
}
test "ComptimeInterpreter - labeled block" {
try testExpr(
\\blk: {
\\ break :blk true;
\\}
, .{ .simple_type = .bool }, .{ .simple_value = .bool_true });
try testExpr(
\\blk: {
\\ break :blk 3;
\\}
, .{ .simple_type = .comptime_int }, .{ .int_u64_value = 3 });
}
test "ComptimeInterpreter - if" {
try testExpr(
\\blk: {
\\ break :blk if (true) true else false;
\\}
, .{ .simple_type = .bool }, .{ .simple_value = .bool_true });
try testExpr(
\\blk: {
\\ break :blk if (false) true else false;
\\}
, .{ .simple_type = .bool }, .{ .simple_value = .bool_false });
try testExpr(
\\blk: {
\\ if (false) break :blk true;
\\ break :blk false;
\\}
, .{ .simple_type = .bool }, .{ .simple_value = .bool_false });
// TODO
// try testExpr(
// \\outer: {
// \\ if (:inner {
// \\ break :inner true;
// \\ }) break :outer true;
// \\ break :outer false;
// \\}
// , .{ .simple_type = .bool }, .{ .simple_value = .bool_true });
}
test "ComptimeInterpreter - variable lookup" {
try testExpr(
\\blk: {
\\ var foo = 42;
\\ break :blk foo;
\\}
, .{ .simple_type = .comptime_int }, .{ .int_u64_value = 42 });
try testExpr(
\\blk: {
\\ var foo = 1;
\\ var bar = 2;
\\ var baz = 3;
\\ break :blk bar;
\\}
, .{ .simple_type = .comptime_int }, .{ .int_u64_value = 2 });
var context = try Context.init(
\\const bar = foo;
\\const foo = 3;
);
defer context.deinit();
const result = try context.interpret(context.findVar("bar"));
try expectEqualKey(context.interpreter.ip, .{ .simple_type = .comptime_int }, result.ty);
try expectEqualKey(context.interpreter.ip, .{ .int_u64_value = 3 }, result.val);
}
test "ComptimeInterpreter - field access" {
try testExpr(
\\blk: {
\\ const foo: struct {alpha: u64, beta: bool} = undefined;
\\ break :blk foo.beta;
\\}
, .{ .simple_type = .bool }, null);
try testExpr(
\\blk: {
\\ const foo: struct {alpha: u64, beta: bool} = undefined;
\\ break :blk foo.alpha;
\\}
, .{ .int_type = .{
.signedness = .unsigned,
.bits = 64,
} }, null);
}
test "ComptimeInterpreter - optional operations" {
if (true) return error.SkipZigTest; // TODO
try testExpr(
\\blk: {
\\ const foo: ?bool = true;
\\ break :blk foo.?;
\\}
, .{ .simple_type = .bool }, .{ .simple_value = .bool_true });
try testExpr(
\\blk: {
\\ const foo: ?bool = true;
\\ break :blk foo == null;
\\}
, .{ .simple_type = .bool }, .{ .simple_value = .bool_false });
}
test "ComptimeInterpreter - pointer operations" {
if (true) return error.SkipZigTest; // TODO
try testExpr(
\\blk: {
\\ const foo: []const u8 = "";
\\ break :blk foo.len;
\\}
, .{ .simple_type = .usize }, .{ .bytes = "" });
try testExpr(
\\blk: {
\\ const foo = true;
\\ break :blk &foo;
\\}
, @panic("TODO"), .{ .simple_value = .bool_true });
try testExpr(
\\blk: {
\\ const foo = true;
\\ const bar = &foo;
\\ break :blk bar.*;
\\}
, @panic("TODO"), .{ .simple_value = .bool_true });
}
test "ComptimeInterpreter - call return primitive type" {
try testCall(
\\pub fn Foo() type {
\\ return bool;
\\}
, &.{}, .{ .simple_type = .bool });
try testCall(
\\pub fn Foo() type {
\\ return u32;
\\}
, &.{}, .{ .int_type = .{ .signedness = .unsigned, .bits = 32 } });
try testCall(
\\pub fn Foo() type {
\\ return i128;
\\}
, &.{}, .{ .int_type = .{ .signedness = .signed, .bits = 128 } });
try testCall(
\\pub fn Foo() type {
\\ const alpha = i128;
\\ return alpha;
\\}
, &.{}, .{ .int_type = .{ .signedness = .signed, .bits = 128 } });
}
test "ComptimeInterpreter - call return struct" {
var context = try Context.init(
\\pub fn Foo() type {
\\ return struct {
\\ slay: bool,
\\ var abc = 123;
\\ };
\\}
);
defer context.deinit();
const result = try context.call(context.findFn("Foo"), &.{});
var interpreter = ComptimeInterpreter{
.allocator = allocator,
.document_store = &doc_store,
.uri = "file:///file.zig",
};
defer interpreter.deinit();
try std.testing.expect(result.ty == .simple_type);
try std.testing.expect(result.ty.simple_type == .type);
const struct_info = context.interpreter.ip.getStruct(result.val.?.struct_type);
try std.testing.expectEqual(Index.none, struct_info.backing_int_ty);
try std.testing.expectEqual(std.builtin.Type.ContainerLayout.Auto, struct_info.layout);
_ = 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())});
try std.testing.expectEqual(@as(usize, 1), struct_info.fields.count());
try std.testing.expectEqualStrings("slay", struct_info.fields.keys()[0]);
try std.testing.expect(struct_info.fields.values()[0].ty == Index.bool_type);
}
test "ComptimeInterpreter - call comptime argument" {
var context = try Context.init(
\\pub fn Foo(comptime my_arg: bool) type {
\\ var abc = z: {break :z if (!my_arg) 123 else 0;};
\\ if (abc == 123) return u69;
\\ return u8;
\\}
);
defer context.deinit();
const result1 = try context.call(context.findFn("Foo"), &.{KV{
.ty = .{ .simple_type = .bool },
.val = .{ .simple_value = .bool_true },
}});
try std.testing.expect(result1.ty == .simple_type);
try std.testing.expect(result1.ty.simple_type == .type);
try std.testing.expectEqual(Key{ .int_type = .{ .signedness = .unsigned, .bits = 8 } }, result1.val.?);
var result2 = try context.call(context.findFn("Foo"), &.{KV{
.ty = .{ .simple_type = .bool },
.val = .{ .simple_value = .bool_false },
}});
try std.testing.expect(result2.ty == .simple_type);
try std.testing.expect(result2.ty.simple_type == .type);
try std.testing.expectEqual(Key{ .int_type = .{ .signedness = .unsigned, .bits = 69 } }, result2.val.?);
}
test "ComptimeInterpreter - call inner function" {
try testCall(
\\pub fn Inner() type {
\\ return bool;
\\}
\\pub fn Foo() type {
\\ return Inner();
\\}
, &.{}, .{ .simple_type = .bool });
}
//
// Helper functions
//
const KV = struct {
ty: Key,
val: ?Key,
};
const Context = struct {
config: *zls.Config,
document_store: *zls.DocumentStore,
interpreter: *ComptimeInterpreter,
pub fn init(source: []const u8) !Context {
var config = try allocator.create(zls.Config);
errdefer allocator.destroy(config);
var document_store = try allocator.create(zls.DocumentStore);
errdefer allocator.destroy(document_store);
var interpreter = try allocator.create(ComptimeInterpreter);
errdefer allocator.destroy(interpreter);
config.* = .{};
document_store.* = .{
.allocator = allocator,
.config = config,
};
errdefer document_store.deinit();
const test_uri: []const u8 = switch (builtin.os.tag) {
.windows => "file:///C:\\test.zig",
else => "file:///test.zig",
};
const handle = try document_store.openDocument(test_uri, source);
// TODO handle handle.tree.errors
interpreter.* = .{
.allocator = allocator,
.ip = try InternPool.init(allocator),
.document_store = document_store,
.uri = handle.uri,
};
errdefer interpreter.deinit();
_ = try interpretReportErrors(interpreter, 0, .none);
return .{
.config = config,
.document_store = document_store,
.interpreter = interpreter,
};
}
pub fn deinit(self: *Context) void {
self.interpreter.deinit();
self.document_store.deinit();
allocator.destroy(self.config);
allocator.destroy(self.document_store);
allocator.destroy(self.interpreter);
}
pub fn call(self: *Context, func_node: Ast.Node.Index, arguments: []const KV) !KV {
var args = try allocator.alloc(ComptimeInterpreter.Value, arguments.len);
defer allocator.free(args);
for (arguments) |argument, i| {
args[i] = .{
.interpreter = self.interpreter,
.node_idx = 0,
.ty = try self.interpreter.ip.get(self.interpreter.allocator, argument.ty),
.val = if (argument.val) |val| try self.interpreter.ip.get(self.interpreter.allocator, val) else .none,
};
}
const namespace = @intToEnum(ComptimeInterpreter.Namespace.Index, 0); // root namespace
const result = (try self.interpreter.call(namespace, func_node, args, .{})).result;
try std.testing.expect(result == .value);
try std.testing.expect(result.value.ty != .none);
return KV{
.ty = self.interpreter.ip.indexToKey(result.value.ty),
.val = if (result.value.val == .none) null else self.interpreter.ip.indexToKey(result.value.val),
};
}
pub fn interpret(self: *Context, node: Ast.Node.Index) !KV {
const namespace = @intToEnum(ComptimeInterpreter.Namespace.Index, 0); // root namespace
const result = try (try self.interpreter.interpret(node, namespace, .{})).getValue();
try std.testing.expect(result.ty != .none);
return KV{
.ty = self.interpreter.ip.indexToKey(result.ty),
.val = if (result.val == .none) null else self.interpreter.ip.indexToKey(result.val),
};
}
pub fn findFn(self: Context, name: []const u8) Ast.Node.Index {
const handle = self.interpreter.getHandle();
for (handle.tree.nodes.items(.tag)) |tag, i| {
if (tag != .fn_decl) continue;
const node = @intCast(Ast.Node.Index, i);
var buffer: [1]Ast.Node.Index = undefined;
const fn_decl = handle.tree.fullFnProto(&buffer, node).?;
const fn_name = offsets.tokenToSlice(handle.tree, fn_decl.name_token.?);
if (std.mem.eql(u8, fn_name, name)) return node;
}
std.debug.panic("failed to find function with name '{s}'", .{name});
}
pub fn findVar(self: Context, name: []const u8) Ast.Node.Index {
const handle = self.interpreter.getHandle();
var node: Ast.Node.Index = 0;
while (node < handle.tree.nodes.len) : (node += 1) {
const var_decl = handle.tree.fullVarDecl(node) orelse continue;
const name_token = var_decl.ast.mut_token + 1;
const var_name = offsets.tokenToSlice(handle.tree, name_token);
if (std.mem.eql(u8, var_name, name)) return var_decl.ast.init_node;
}
std.debug.panic("failed to find var declaration with name '{s}'", .{name});
}
};
fn testCall(
source: []const u8,
arguments: []const KV,
expected_ty: Key,
) !void {
var context = try Context.init(source);
defer context.deinit();
const result = try context.call(context.findFn("Foo"), arguments);
try expectEqualKey(context.interpreter.ip, Key{ .simple_type = .type }, result.ty);
try expectEqualKey(context.interpreter.ip, expected_ty, result.val);
}
fn testExpr(
expr: []const u8,
expected_ty: Key,
expected_val: ?Key,
) !void {
const source = try std.fmt.allocPrint(allocator,
\\const foobarbaz = {s};
, .{expr});
defer allocator.free(source);
var context = try Context.init(source);
defer context.deinit();
const result = try context.interpret(context.findVar("foobarbaz"));
try expectEqualKey(context.interpreter.ip, expected_ty, result.ty);
if (expected_val) |expected| {
try expectEqualKey(context.interpreter.ip, expected, result.val);
}
}
/// TODO refactor this code
fn expectEqualKey(ip: InternPool, expected: Key, actual: ?Key) !void {
if (actual) |actual_key| {
if (expected.eql(actual_key)) return;
if (expected.isType() and actual_key.isType()) {
std.debug.print("expected type `{}`, found type `{}`\n", .{ expected.fmtType(ip), actual_key.fmtType(ip) });
} else if (expected.isType()) {
std.debug.print("expected type `{}`, found value ({})\n", .{ expected.fmtType(ip), actual_key });
} else if (actual_key.isType()) {
std.debug.print("expected value ({}), found type `{}`\n", .{ expected, actual_key.fmtType(ip) });
} else {
std.debug.print("expected value ({}), found value ({})\n", .{ expected, actual_key }); // TODO print value
}
} else {
if (expected.isType()) {
std.debug.print("expected type `{}`, found null\n", .{expected.fmtType(ip)});
} else {
std.debug.print("expected value ({}), found null\n", .{expected});
}
}
return error.TestExpectedEqual;
}
fn interpretReportErrors(
interpreter: *ComptimeInterpreter,
node_idx: Ast.Node.Index,
namespace: InternPool.NamespaceIndex,
) !ComptimeInterpreter.InterpretResult {
const result = interpreter.interpret(node_idx, namespace, .{});
// TODO use ErrorBuilder
var err_it = interpreter.errors.iterator();
if (interpreter.errors.count() != 0) {
const handle = interpreter.getHandle();
std.debug.print("\n{s}\n", .{handle.text});
while (err_it.next()) |entry| {
const token = handle.tree.firstToken(entry.key_ptr.*);
const position = offsets.tokenToPosition(handle.tree, token, .@"utf-8");
std.debug.print("{d}:{d}: {s}\n", .{ position.line, position.character, entry.value_ptr.message });
}
}
return result;
}