Compare commits

..

5 commits

Author SHA1 Message Date
fc9f5d9ce0 add get expr analysis
- rename GetExpr.struc to GetExpr.target
2019-09-26 18:03:39 -03:00
64e39a6f1e ast_printer: print scopes inside functions
- comp_ctx: add children attr for debug info
2019-09-26 16:58:57 -03:00
97c2437d97 add basic business logic for scopes 2019-09-26 16:04:11 -03:00
e91c2dfdaf fix errs 2019-09-26 14:32:23 -03:00
ae27995eb6 add rudimentary type analysis for return, if, loop 2019-09-26 14:23:15 -03:00
7 changed files with 184 additions and 40 deletions

View file

@ -3,7 +3,7 @@ const (
) )
fn f() i32 { fn f() i32 {
var a = 3; //var a = 3;
return 2; return 2;
} }

View file

@ -128,7 +128,7 @@ pub const StructExpr = struct {
}; };
pub const GetExpr = struct { pub const GetExpr = struct {
struc: *Expr, target: *Expr,
name: Token, name: Token,
}; };

View file

@ -272,7 +272,7 @@ pub fn printExpr(expr: *const Expr) void {
.Get => |get| { .Get => |get| {
warn("("); warn("(");
printExpr(get.struc); printExpr(get.target);
warn(".{})", get.name.lexeme); warn(".{})", get.name.lexeme);
}, },
@ -369,6 +369,19 @@ fn prettyType(typ: SymbolUnderlyingType) []const u8 {
}; };
} }
pub fn printScope(scope: *Scope, ident: usize) void {
print(ident, "scope at addr {}\n", &scope);
var it = scope.env.iterator();
while (it.next()) |kv| {
print(ident, "sym: {}, typ: {}\n", kv.key, prettyType(kv.value));
}
for (scope.children.toSlice()) |child| {
printScope(scope, ident + 1);
}
}
pub fn printContext(ctx: CompilationContext) void { pub fn printContext(ctx: CompilationContext) void {
var it = ctx.symbol_table.iterator(); var it = ctx.symbol_table.iterator();
@ -389,6 +402,10 @@ pub fn printContext(ctx: CompilationContext) void {
prettyType(param_kv.value), prettyType(param_kv.value),
); );
} }
// go through scopes
std.debug.warn("scope info:\n");
printScope(fn_sym.scope, 1);
}, },
.Struct => |typemap| { .Struct => |typemap| {

View file

@ -121,10 +121,9 @@ pub const Codegen = struct {
}, },
.Get => |get| { .Get => |get| {
// TODO rename get.struc to get.target var target = get.target.*;
var struc = get.struc.*;
switch (struc) { switch (target) {
.Variable => |vari| { .Variable => |vari| {
// first, we must check if the target is a type // first, we must check if the target is a type
// and emit accordingly // and emit accordingly
@ -158,7 +157,7 @@ pub const Codegen = struct {
}, },
else => { else => {
std.debug.warn("Invalid get target: {}\n", ast.ExprType(struc)); std.debug.warn("Invalid get target: {}\n", ast.ExprType(target));
return CompileError.EmitError; return CompileError.EmitError;
}, },
} }

View file

@ -29,15 +29,41 @@ pub const SymbolUnderlyingType = union(SymbolUnderlyingTypeEnum) {
Enum: []const u8, Enum: []const u8,
}; };
pub const Scope = std.StringHashMap(SymbolUnderlyingType); pub const Environment = std.StringHashMap(SymbolUnderlyingType);
pub const Environment = struct { pub const ScopeList = std.ArrayList(*Scope);
parent: *Environment, pub const Scope = struct {
scope: Scope, parent: ?*Scope,
env: Environment,
/// Used for debug information.
children: ScopeList,
allocator: *std.mem.Allocator,
pub fn create(allocator: *std.mem.Allocator, parent: ?*Scope) !*Scope {
var scope = try allocator.create(Scope);
scope.* = Scope{
.parent = parent,
.env = Environment.init(allocator),
.children = ScopeList.init(allocator),
.allocator = allocator,
};
return scope;
}
pub fn createChild(self: *@This()) !*Scope {
return try @This().create(self.allocator, self);
}
pub fn deinit(self: *const @This()) void {
self.env.deinit();
}
}; };
// functions, for our purposes, other than symbols, have: // functions, for our purposes, other than symbols, have:
// - a return type // - a return type
// - TODO parameters // - parameters
pub const FunctionSymbol = struct { pub const FunctionSymbol = struct {
decl: ast.FnDecl, decl: ast.FnDecl,
return_type: SymbolUnderlyingType, return_type: SymbolUnderlyingType,
@ -45,7 +71,7 @@ pub const FunctionSymbol = struct {
/// Parameters for a function are also a table instead of an ArrayList /// Parameters for a function are also a table instead of an ArrayList
/// because we want to resolve identifiers to them. /// because we want to resolve identifiers to them.
parameters: UnderlyingTypeMap, parameters: UnderlyingTypeMap,
env: *Environment, scope: *Scope,
/// Find a given identifier in the function. Can resolve to either a parameter /// Find a given identifier in the function. Can resolve to either a parameter
pub fn findSymbol(self: *const @This(), identifier: []const u8) ?SymbolData { pub fn findSymbol(self: *const @This(), identifier: []const u8) ?SymbolData {
@ -98,6 +124,9 @@ pub const CompilationContext = struct {
allocator: *std.mem.Allocator, allocator: *std.mem.Allocator,
symbol_table: SymbolTable, symbol_table: SymbolTable,
current_function: ?*FunctionSymbol = null,
current_scope: ?*Scope = null,
pub fn init(allocator: *std.mem.Allocator) CompilationContext { pub fn init(allocator: *std.mem.Allocator) CompilationContext {
return CompilationContext{ return CompilationContext{
.allocator = allocator, .allocator = allocator,
@ -105,6 +134,30 @@ pub const CompilationContext = struct {
}; };
} }
/// Create a new scope out of the current one and set it as the current.
pub fn bumpScope(self: *@This()) !void {
if (self.current_scope == null) {
@panic("can't bump scope from null");
}
var child = try self.current_scope.?.createChild();
self.current_scope = child;
}
/// Set a given scope as the current scope.
pub fn setScope(self: *@This(), scope: *Scope) void {
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");
}
self.current_scope = self.current_scope.?.parent;
}
/// Solve a given type as a string into a SymbolUnderlyingTypeEnum /// Solve a given type as a string into a SymbolUnderlyingTypeEnum
/// This does not help if you want a full SymbolUnderlyingType, use /// This does not help if you want a full SymbolUnderlyingType, use
/// solveType() for that. /// solveType() for that.
@ -152,7 +205,7 @@ pub const CompilationContext = struct {
decl: ast.FnDecl, decl: ast.FnDecl,
ret_type: SymbolUnderlyingType, ret_type: SymbolUnderlyingType,
param_types: TypeList, param_types: TypeList,
symbols: SymbolTable, scope: *Scope,
) !void { ) !void {
var type_map = UnderlyingTypeMap.init(self.allocator); var type_map = UnderlyingTypeMap.init(self.allocator);
@ -165,7 +218,7 @@ pub const CompilationContext = struct {
.decl = decl, .decl = decl,
.return_type = ret_type, .return_type = ret_type,
.parameters = type_map, .parameters = type_map,
.symbols = symbols, .scope = scope,
}, },
}); });
} }

View file

@ -320,11 +320,11 @@ pub const Parser = struct {
return expr; return expr;
} }
fn mkGet(self: *@This(), struc: *Expr, name: Token) !*Expr { fn mkGet(self: *@This(), target: *Expr, name: Token) !*Expr {
var expr = try self.allocator.create(Expr); var expr = try self.allocator.create(Expr);
expr.* = Expr{ expr.* = Expr{
.Get = ast.GetExpr{ .Get = ast.GetExpr{
.struc = struc, .target = target,
.name = name, .name = name,
}, },
}; };
@ -906,11 +906,11 @@ pub const Parser = struct {
return self.doError("can not initialize struct field"); return self.doError("can not initialize struct field");
}, },
.Equal => return try self.mkSet(get.struc, get.name, value), .Equal => return try self.mkSet(get.target, get.name, value),
.PlusEqual, .MinusEqual, .StarEqual, .SlashEqual => { .PlusEqual, .MinusEqual, .StarEqual, .SlashEqual => {
return try self.mkSet( return try self.mkSet(
get.struc, get.target,
get.name, get.name,
try self.mkBinary(expr, binop.?, value), try self.mkBinary(expr, binop.?, value),
); );

View file

@ -97,8 +97,8 @@ pub const TypeSolver = struct {
) anyerror!SymbolUnderlyingType { ) anyerror!SymbolUnderlyingType {
switch (expr.*) { switch (expr.*) {
.Binary => |binary| { .Binary => |binary| {
var left_type = self.resolveExprType(ctx, binary.left); var left_type = try self.resolveExprType(ctx, binary.left);
var right_type = self.resolveExprType(ctx, binary.right); var right_type = try self.resolveExprType(ctx, binary.right);
return switch (binary.op) { return switch (binary.op) {
// all numeric operations return numeric types // all numeric operations return numeric types
@ -114,7 +114,7 @@ pub const TypeSolver = struct {
// for now, unary operators only have .Not // for now, unary operators only have .Not
.Unary => |unary| { .Unary => |unary| {
var right_type = self.resolveExprType(ctx, unary.right); var right_type = try self.resolveExprType(ctx, unary.right);
return switch (unary.op) { return switch (unary.op) {
.Negate => right_type, .Negate => right_type,
.Not => right_type, .Not => right_type,
@ -159,9 +159,43 @@ pub const TypeSolver = struct {
return func_sym.return_type; return func_sym.return_type;
}, },
// TODO variable resolution // TODO analysis for .Variable
// TODO Get (for structs and enums) .Get => |get| {
var target = get.target.*;
if (ast.ExprType(target) != .Variable) {
std.debug.warn("Expected Variable as get target, got {}\n", ast.ExprType(target));
return CompileError.TypeError;
}
const lexeme = target.Variable.lexeme;
var global_typ_opt = self.resolveGlobalType(ctx, lexeme);
// TODO:
// - name resolution for when global_typ is null + analysis of
// the name's type
// - analysis for structs
if (global_typ_opt == null) @panic("TODO name resolution");
var global_typ = global_typ_opt.?;
switch (global_typ) {
// TODO we need to fetch the given
// struct field (on get.name) type and return it
.Struct => @panic("TODO analysis of struct"),
.Enum => return global_typ,
else => {
std.debug.warn(
"Expected Struct/Enum as get target, got {}\n",
comp.SymbolUnderlyingTypeEnum(global_typ),
);
return CompileError.TypeError;
},
}
},
else => { else => {
std.debug.warn("TODO resolve expr {}\n", ast.ExprType(expr.*)); std.debug.warn("TODO resolve expr {}\n", ast.ExprType(expr.*));
@ -174,14 +208,15 @@ pub const TypeSolver = struct {
self: *@This(), self: *@This(),
ctx: *comp.CompilationContext, ctx: *comp.CompilationContext,
stmt: ast.Stmt, stmt: ast.Stmt,
) !void { ) anyerror!void {
switch (stmt) { switch (stmt) {
// There are no side-effects to the type system when the statement // There are no side-effects to the type system when the statement
// is just an expression or a println. we just resolve it // is just an expression or a println. we just resolve it
// to ensure we dont have type errors. // to ensure we dont have type errors.
.Expr => |expr_ptr| try self.resolveExprType(ctx, expr_ptr), .Expr, .Println => |expr_ptr| {
.Println => |expr_ptr| try self.resolveExprType(ctx, expr_ptr), _ = try self.resolveExprType(ctx, expr_ptr);
},
// VarDecl means we check the type of the expression and // VarDecl means we check the type of the expression and
// insert it into the context, however we need to know a pointer // insert it into the context, however we need to know a pointer
@ -189,21 +224,49 @@ pub const TypeSolver = struct {
// so it should be implicit into the context. // so it should be implicit into the context.
.VarDecl => @panic("TODO vardecl"), .VarDecl => @panic("TODO vardecl"),
// If create two scopes for each branch of the if
.If => @panic("TODO ifstmt"),
// Loop (creates 1 scope) asserts that the expression
// type is a bool
.Loop => @panic("TODO loop"),
// For (creates 1 scope) receives arrays, which we dont have yet
.For => @panic("TODO for"),
// Returns dont cause any type system things as they deal with // Returns dont cause any type system things as they deal with
// values, however, we must ensure that the expression type // values, however, we must ensure that the expression type
// matches the function type (must fetch from context, or we could // matches the function type (must fetch from context, or we could
// pull a hack with err contexts, lol) // pull a hack with err contexts, lol)
.Return => @panic("TODO return"), .Return => |ret| {
var ret_stmt_type = try self.resolveExprType(ctx, ret.value);
// TODO check if ret_stmt_type == ctx.cur_function.return_type
},
// If create two scopes for each branch of the if
.If => |ifstmt| {
_ = try self.resolveExprType(ctx, ifstmt.condition);
// TODO assert condition's type is bool
// TODO bump-dump scope
for (ifstmt.then_branch.toSlice()) |then_stmt| {
try self.stmtPass(ctx, then_stmt);
}
if (ifstmt.else_branch) |else_branch| {
// TODO bump-dump scope
for (else_branch.toSlice()) |else_stmt| {
try self.stmtPass(ctx, else_stmt);
}
}
},
// Loop (creates 1 scope) asserts that the expression
// type is a bool
.Loop => |loop| {
if (loop.condition) |cond|
// TODO assert condition's type is bool
_ = try self.resolveExprType(ctx, cond);
// TODO bump-dump scope
for (loop.then_branch.toSlice()) |then_stmt| {
try self.stmtPass(ctx, then_stmt);
}
},
// For (creates 1 scope) receives arrays, which we dont have yet
.For => @panic("TODO for"),
else => unreachable, else => unreachable,
} }
@ -233,12 +296,24 @@ pub const TypeSolver = struct {
try parameters.append(param_type.?); try parameters.append(param_type.?);
} }
// TODO scopes: bump scope // for a function, we always create a new root scope for it
// and force-set it into the current context
var scope = try comp.Scope.create(self.allocator, null);
errdefer scope.deinit();
// we must always start from a null current scope,
// functions inside functions are not allowed
std.debug.assert(ctx.current_scope == null);
ctx.setScope(scope);
for (decl.body.toSlice()) |stmt| { for (decl.body.toSlice()) |stmt| {
try self.stmtPass(ctx, stmt); try self.stmtPass(ctx, stmt);
} }
// it should be null when we dump from a function. always
ctx.dumpScope();
std.debug.assert(ctx.current_scope == null);
// TODO scopes: down scope // TODO scopes: down scope
// TODO symbols and scope resolution, that's // TODO symbols and scope resolution, that's
@ -249,7 +324,7 @@ pub const TypeSolver = struct {
// and everything else // and everything else
if (ret_type != null and parameters.len == decl.params.len) { if (ret_type != null and parameters.len == decl.params.len) {
try ctx.insertFn(decl, ret_type.?, parameters, symbols); try ctx.insertFn(decl, ret_type.?, parameters, scope);
} }
}, },