436 lines
13 KiB
Zig
436 lines
13 KiB
Zig
const std = @import("std");
|
|
const ast = @import("ast.zig");
|
|
const llvm = @import("llvm.zig");
|
|
|
|
pub const CompilationError = error{
|
|
TypeError,
|
|
UnknownName,
|
|
};
|
|
|
|
pub const SymbolTable = std.hash_map.StringHashMap(*SymbolData);
|
|
pub const TypeList = std.ArrayList(SymbolUnderlyingType);
|
|
|
|
pub const SymbolUnderlyingTypeEnum = enum {
|
|
Integer32,
|
|
Integer64,
|
|
Double,
|
|
Bool,
|
|
|
|
// opaque unsolved identifier
|
|
OpaqueType,
|
|
|
|
// solved custom type
|
|
Struct,
|
|
Enum,
|
|
};
|
|
|
|
pub const SymbolUnderlyingType = union(SymbolUnderlyingTypeEnum) {
|
|
Integer32: void,
|
|
Integer64: void,
|
|
Double: void,
|
|
Bool: void,
|
|
OpaqueType: []const u8,
|
|
|
|
Struct: []const u8,
|
|
Enum: []const u8,
|
|
};
|
|
|
|
pub const ScopeList = std.ArrayList(*Scope);
|
|
pub const Scope = struct {
|
|
parent: ?*Scope,
|
|
env: UnderlyingTypeMap,
|
|
meta_map: VariableMetadataMap,
|
|
|
|
/// List of children in the scope.
|
|
children: ScopeList,
|
|
cur_child_idx: usize = 0,
|
|
|
|
allocator: *std.mem.Allocator,
|
|
id: ?[]const u8 = null,
|
|
|
|
pub fn create(allocator: *std.mem.Allocator, parent: ?*Scope, id: ?[]const u8) !*Scope {
|
|
var scope = try allocator.create(Scope);
|
|
|
|
scope.* = Scope{
|
|
.parent = parent,
|
|
.env = UnderlyingTypeMap.init(allocator),
|
|
.children = ScopeList.init(allocator),
|
|
.allocator = allocator,
|
|
.id = id,
|
|
.meta_map = VariableMetadataMap.init(allocator),
|
|
};
|
|
|
|
return scope;
|
|
}
|
|
|
|
pub fn createChild(self: *@This(), id: ?[]const u8) !*Scope {
|
|
var child = try @This().create(self.allocator, self, id);
|
|
try self.children.append(child);
|
|
return child;
|
|
}
|
|
|
|
pub fn nextChild(self: *@This()) *Scope {
|
|
var child = self.children.at(self.cur_child_idx);
|
|
self.cur_child_idx += 1;
|
|
return child;
|
|
}
|
|
|
|
pub fn deinit(self: *const @This()) void {
|
|
self.env.deinit();
|
|
}
|
|
};
|
|
|
|
pub const Parameter = struct {
|
|
idx: usize,
|
|
name: []const u8,
|
|
typ: SymbolUnderlyingType,
|
|
|
|
llvm_alloca: llvm.LLVMValueRef = null,
|
|
};
|
|
|
|
pub const ParameterMap = std.StringHashMap(*Parameter);
|
|
|
|
// functions, for our purposes, other than symbols, have:
|
|
// - a return type
|
|
// - parameters
|
|
pub const FunctionSymbol = struct {
|
|
decl: ast.FnDecl,
|
|
return_type: SymbolUnderlyingType,
|
|
|
|
/// Parameters for a function are also a table instead of an ArrayList
|
|
/// because we want to resolve identifiers to them.
|
|
parameters: ParameterMap,
|
|
parameter_list: TypeList,
|
|
|
|
scope: *Scope,
|
|
|
|
// only contains metadata for parameters (i really need to simplify this)
|
|
meta_map: VariableMetadataMap,
|
|
};
|
|
|
|
// structs are hashmaps pointing to SymbolUnderlyingType
|
|
pub const UnderlyingTypeMap = std.hash_map.StringHashMap(SymbolUnderlyingType);
|
|
|
|
// enums have maps from fields to u32s
|
|
pub const IdentifierMap = std.StringHashMap(u32);
|
|
|
|
// TODO const
|
|
pub const SymbolType = enum {
|
|
Function,
|
|
Struct,
|
|
Enum,
|
|
Variable,
|
|
Const,
|
|
};
|
|
|
|
pub const SymbolData = union(SymbolType) {
|
|
Function: FunctionSymbol,
|
|
Struct: UnderlyingTypeMap,
|
|
Enum: IdentifierMap,
|
|
|
|
// variables (parameters and variables), for the type system
|
|
// only have types
|
|
Variable: SymbolUnderlyingType,
|
|
Const: SymbolUnderlyingType,
|
|
};
|
|
|
|
const builtin_type_identifiers = [_][]const u8{ "i32", "i64", "bool" };
|
|
|
|
const builtin_types = [_]SymbolUnderlyingTypeEnum{ .Integer32, .Integer64, .Bool };
|
|
|
|
const Using = enum {
|
|
Scope,
|
|
Function,
|
|
};
|
|
|
|
pub const VariableMetadata = struct {
|
|
typ: SymbolUnderlyingType,
|
|
llvm_alloca: llvm.LLVMValueRef = null,
|
|
|
|
using: Using,
|
|
|
|
from_scope: ?*Scope = null,
|
|
from_function: ?*FunctionSymbol = null,
|
|
|
|
pub fn withScope(
|
|
allocator: *std.mem.Allocator,
|
|
scope: *Scope,
|
|
typ: SymbolUnderlyingType,
|
|
) !*VariableMetadata {
|
|
var meta = try allocator.create(VariableMetadata);
|
|
std.debug.warn("VARMETA create from scope={}, meta={}\n", @ptrToInt(scope), @ptrToInt(meta));
|
|
meta.* = VariableMetadata{ .typ = typ, .from_scope = scope, .using = .Scope };
|
|
return meta;
|
|
}
|
|
|
|
pub fn withParam(
|
|
allocator: *std.mem.Allocator,
|
|
func: *FunctionSymbol,
|
|
typ: SymbolUnderlyingType,
|
|
) !*VariableMetadata {
|
|
var meta = try allocator.create(VariableMetadata);
|
|
std.debug.warn("VARMETA create from fndecl={}, meta={}\n", @ptrToInt(func), @ptrToInt(meta));
|
|
meta.* = VariableMetadata{ .typ = typ, .from_function = func, .using = .Function };
|
|
return meta;
|
|
}
|
|
};
|
|
|
|
// TODO rm const?
|
|
pub const VariableMetadataMap = std.StringHashMap(*VariableMetadata);
|
|
|
|
/// Represents the context for a full compiler run.
|
|
/// This is used to manage the symbol table for the compilation unit, etc.
|
|
pub const CompilationContext = struct {
|
|
allocator: *std.mem.Allocator,
|
|
symbol_table: SymbolTable,
|
|
|
|
cur_function: ?*FunctionSymbol = null,
|
|
current_scope: ?*Scope = null,
|
|
|
|
pub fn init(allocator: *std.mem.Allocator) CompilationContext {
|
|
return CompilationContext{
|
|
.allocator = allocator,
|
|
.symbol_table = SymbolTable.init(allocator),
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *@This()) void {
|
|
self.symbol_table.deinit();
|
|
}
|
|
|
|
/// Create a new scope out of the current one and set it as the current.
|
|
pub fn bumpScope(self: *@This(), scope_id: ?[]const u8) !void {
|
|
if (self.current_scope == null) {
|
|
@panic("can't bump scope from null");
|
|
}
|
|
|
|
std.debug.warn("==scope bump== '{}'\n", scope_id);
|
|
|
|
var child = try self.current_scope.?.createChild(scope_id);
|
|
self.current_scope = child;
|
|
}
|
|
|
|
/// Set a given scope as the current scope.
|
|
pub fn setScope(self: *@This(), scope: *Scope) void {
|
|
std.debug.warn("==set== set scope to {}\n", scope.id);
|
|
self.current_scope = scope;
|
|
}
|
|
|
|
/// "dump" the current scope, making the new current scope be its parent.
|
|
pub fn dumpScope(self: *@This()) void {
|
|
if (self.current_scope == null) {
|
|
@panic("can't dump scope from null");
|
|
}
|
|
|
|
const parent_id: ?[]const u8 = if (self.current_scope.?.parent == null) null else self.current_scope.?.parent.?.id;
|
|
|
|
std.debug.warn(
|
|
"==scope dump== {} to {}\n",
|
|
self.current_scope.?.id,
|
|
parent_id,
|
|
);
|
|
|
|
self.current_scope = self.current_scope.?.parent;
|
|
}
|
|
|
|
pub fn setCurrentFunction(self: *@This(), func_ctx: ?FunctionAnalysisContext) void {
|
|
self.cur_function = func_ctx;
|
|
}
|
|
|
|
/// Solve a given type as a string into a SymbolUnderlyingTypeEnum
|
|
/// This does not help if you want a full SymbolUnderlyingType, use
|
|
/// solveType() for that.
|
|
pub fn solveTypeEnum(
|
|
self: *@This(),
|
|
typ_ident: []const u8,
|
|
) SymbolUnderlyingTypeEnum {
|
|
inline for (builtin_type_identifiers) |typ, idx| {
|
|
if (std.mem.eql(u8, typ, typ_ident)) return builtin_types[idx];
|
|
}
|
|
|
|
return .OpaqueType;
|
|
}
|
|
|
|
/// Solve a given type string into a full fleged SymbolUnderlyingType.
|
|
/// This always works, since resolution into struct/enum custom types
|
|
/// become the fallback.
|
|
pub fn solveType(
|
|
self: *@This(),
|
|
typ_ident: []const u8,
|
|
) SymbolUnderlyingType {
|
|
const typ_enum_val = self.solveTypeEnum(typ_ident);
|
|
|
|
return switch (typ_enum_val) {
|
|
.Integer32 => SymbolUnderlyingType{ .Integer32 = {} },
|
|
.Integer64 => SymbolUnderlyingType{ .Integer64 = {} },
|
|
.Bool => SymbolUnderlyingType{ .Bool = {} },
|
|
.OpaqueType => SymbolUnderlyingType{ .OpaqueType = typ_ident },
|
|
else => unreachable,
|
|
};
|
|
}
|
|
|
|
pub fn insertStruct(self: *@This(), struc: ast.Struct, field_types: TypeList) !void {
|
|
var type_map = UnderlyingTypeMap.init(self.allocator);
|
|
|
|
for (struc.fields.toSlice()) |field, idx| {
|
|
_ = try type_map.put(field.name.lexeme, field_types.at(idx));
|
|
}
|
|
|
|
var symbol = try self.allocator.create(SymbolData);
|
|
symbol.* = SymbolData{ .Struct = type_map };
|
|
_ = try self.symbol_table.put(struc.name.lexeme, symbol);
|
|
}
|
|
|
|
pub fn insertFn(
|
|
self: *@This(),
|
|
decl: ast.FnDecl,
|
|
ret_type: SymbolUnderlyingType,
|
|
param_types: TypeList,
|
|
scope: *Scope,
|
|
) !void {
|
|
var param_map = ParameterMap.init(self.allocator);
|
|
|
|
for (decl.params.toSlice()) |param, idx| {
|
|
var param_sym = try self.allocator.create(Parameter);
|
|
|
|
param_sym.* = Parameter{
|
|
.name = param.name.lexeme,
|
|
.idx = idx,
|
|
.typ = param_types.at(idx),
|
|
};
|
|
_ = try param_map.put(param.name.lexeme, param_sym);
|
|
}
|
|
|
|
const lex = decl.func_name.lexeme;
|
|
|
|
var symbol = try self.allocator.create(SymbolData);
|
|
symbol.* = SymbolData{
|
|
.Function = FunctionSymbol{
|
|
.decl = decl,
|
|
.return_type = ret_type,
|
|
.parameters = param_map,
|
|
.parameter_list = param_types,
|
|
.scope = scope,
|
|
|
|
.meta_map = VariableMetadataMap.init(self.allocator),
|
|
},
|
|
};
|
|
|
|
_ = try self.symbol_table.put(lex, symbol);
|
|
|
|
var kv = self.symbol_table.get(lex);
|
|
self.cur_function = &kv.?.value.Function;
|
|
}
|
|
|
|
pub fn insertEnum(self: *@This(), enu: ast.Enum) !void {
|
|
var ident_map = IdentifierMap.init(self.allocator);
|
|
errdefer ident_map.deinit();
|
|
|
|
// TODO change this when enums get support for custom values
|
|
for (enu.fields.toSlice()) |token, idx| {
|
|
_ = try ident_map.put(token.lexeme, @intCast(u32, idx));
|
|
}
|
|
|
|
var symbol = try self.allocator.create(SymbolData);
|
|
symbol.* = SymbolData{ .Enum = ident_map };
|
|
_ = try self.symbol_table.put(enu.name.lexeme, symbol);
|
|
}
|
|
|
|
pub fn insertConst(self: *@This(), constdecl: ast.SingleConst, typ: SymbolUnderlyingType) !void {
|
|
var symbol = try self.allocator.create(SymbolData);
|
|
symbol.* = SymbolData{ .Const = typ };
|
|
_ = try self.symbol_table.put(constdecl.name.lexeme, symbol);
|
|
}
|
|
|
|
pub fn fetchGlobalSymbol(
|
|
self: *@This(),
|
|
identifier: []const u8,
|
|
typ: SymbolType,
|
|
) !*SymbolData {
|
|
var sym_kv = self.symbol_table.get(identifier);
|
|
if (sym_kv == null) {
|
|
std.debug.warn("Unknown {} '{}'\n", typ, identifier);
|
|
return CompilationError.TypeError;
|
|
}
|
|
|
|
var value = sym_kv.?.value;
|
|
|
|
var sym_typ = SymbolType(value.*);
|
|
if (sym_typ != typ) {
|
|
std.debug.warn("Expected {}, got {}\n", sym_typ, typ);
|
|
return CompilationError.TypeError;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
fn resolveVarTypeInScope(
|
|
self: *@This(),
|
|
scope_opt: ?*Scope,
|
|
name: []const u8,
|
|
create_meta: bool,
|
|
) error{OutOfMemory}!?*VariableMetadata {
|
|
if (scope_opt == null) return null;
|
|
var scope = scope_opt.?;
|
|
|
|
var kv_opt = scope.env.get(name);
|
|
if (kv_opt) |kv| {
|
|
if (create_meta) {
|
|
return try VariableMetadata.withScope(
|
|
self.allocator,
|
|
scope,
|
|
kv.value,
|
|
);
|
|
} else {
|
|
return null;
|
|
}
|
|
} else {
|
|
return self.resolveVarTypeInScope(scope.parent, name, create_meta);
|
|
}
|
|
}
|
|
|
|
/// Resolve a given name's type, assuming it is a variable.
|
|
pub fn resolveVarType(
|
|
self: *@This(),
|
|
name: []const u8,
|
|
create_meta: bool,
|
|
) !?*VariableMetadata {
|
|
var var_type: ?*VariableMetadata = null;
|
|
|
|
if (self.current_scope) |scope| {
|
|
var_type = try self.resolveVarTypeInScope(scope, name, create_meta);
|
|
if (var_type) |typ| return typ;
|
|
}
|
|
|
|
if (self.cur_function) |cur_function| {
|
|
var kv_opt = cur_function.parameters.get(name);
|
|
if (kv_opt) |kv| {
|
|
if (create_meta) {
|
|
return try VariableMetadata.withParam(
|
|
self.allocator,
|
|
cur_function,
|
|
kv.value.typ,
|
|
);
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
std.debug.warn("Unknown name {}\n", name);
|
|
return CompilationError.UnknownName;
|
|
}
|
|
|
|
pub fn insertMetadata(self: *@This(), name: []const u8, metadata: *VariableMetadata) !void {
|
|
if (self.current_scope) |scope| {
|
|
_ = try scope.meta_map.put(name, metadata);
|
|
return;
|
|
}
|
|
|
|
if (self.cur_function) |func| {
|
|
_ = try func.meta_map.put(name, metadata);
|
|
return;
|
|
}
|
|
}
|
|
};
|