diff --git a/examples/hello.ry b/examples/hello.ry index 3c0c53c..886e348 100644 --- a/examples/hello.ry +++ b/examples/hello.ry @@ -3,7 +3,7 @@ const ( ) fn f() i32 { - var a = 3; + //var a = 3; return 2; } diff --git a/src/ast.zig b/src/ast.zig index cb166d1..7a5c4cd 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -128,7 +128,7 @@ pub const StructExpr = struct { }; pub const GetExpr = struct { - struc: *Expr, + target: *Expr, name: Token, }; diff --git a/src/ast_printer.zig b/src/ast_printer.zig index 82ecfde..ac42790 100644 --- a/src/ast_printer.zig +++ b/src/ast_printer.zig @@ -272,7 +272,7 @@ pub fn printExpr(expr: *const Expr) void { .Get => |get| { warn("("); - printExpr(get.struc); + printExpr(get.target); 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 { var it = ctx.symbol_table.iterator(); @@ -389,6 +402,10 @@ pub fn printContext(ctx: CompilationContext) void { prettyType(param_kv.value), ); } + + // go through scopes + std.debug.warn("scope info:\n"); + printScope(fn_sym.scope, 1); }, .Struct => |typemap| { diff --git a/src/codegen.zig b/src/codegen.zig index 012373c..d481a7f 100644 --- a/src/codegen.zig +++ b/src/codegen.zig @@ -121,10 +121,9 @@ pub const Codegen = struct { }, .Get => |get| { - // TODO rename get.struc to get.target - var struc = get.struc.*; + var target = get.target.*; - switch (struc) { + switch (target) { .Variable => |vari| { // first, we must check if the target is a type // and emit accordingly @@ -158,7 +157,7 @@ pub const Codegen = struct { }, else => { - std.debug.warn("Invalid get target: {}\n", ast.ExprType(struc)); + std.debug.warn("Invalid get target: {}\n", ast.ExprType(target)); return CompileError.EmitError; }, } diff --git a/src/comp_ctx.zig b/src/comp_ctx.zig index f223bf9..e645dd9 100644 --- a/src/comp_ctx.zig +++ b/src/comp_ctx.zig @@ -29,15 +29,41 @@ pub const SymbolUnderlyingType = union(SymbolUnderlyingTypeEnum) { Enum: []const u8, }; -pub const Scope = std.StringHashMap(SymbolUnderlyingType); -pub const Environment = struct { - parent: *Environment, - scope: Scope, +pub const Environment = std.StringHashMap(SymbolUnderlyingType); +pub const ScopeList = std.ArrayList(*Scope); +pub const Scope = struct { + 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: // - a return type -// - TODO parameters +// - parameters pub const FunctionSymbol = struct { decl: ast.FnDecl, return_type: SymbolUnderlyingType, @@ -45,7 +71,7 @@ pub const FunctionSymbol = struct { /// Parameters for a function are also a table instead of an ArrayList /// because we want to resolve identifiers to them. parameters: UnderlyingTypeMap, - env: *Environment, + scope: *Scope, /// Find a given identifier in the function. Can resolve to either a parameter pub fn findSymbol(self: *const @This(), identifier: []const u8) ?SymbolData { @@ -98,6 +124,9 @@ pub const CompilationContext = struct { allocator: *std.mem.Allocator, symbol_table: SymbolTable, + current_function: ?*FunctionSymbol = null, + current_scope: ?*Scope = null, + pub fn init(allocator: *std.mem.Allocator) CompilationContext { return CompilationContext{ .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 /// This does not help if you want a full SymbolUnderlyingType, use /// solveType() for that. @@ -152,7 +205,7 @@ pub const CompilationContext = struct { decl: ast.FnDecl, ret_type: SymbolUnderlyingType, param_types: TypeList, - symbols: SymbolTable, + scope: *Scope, ) !void { var type_map = UnderlyingTypeMap.init(self.allocator); @@ -165,7 +218,7 @@ pub const CompilationContext = struct { .decl = decl, .return_type = ret_type, .parameters = type_map, - .symbols = symbols, + .scope = scope, }, }); } diff --git a/src/parsers.zig b/src/parsers.zig index 04f9bf1..2e278cc 100644 --- a/src/parsers.zig +++ b/src/parsers.zig @@ -320,11 +320,11 @@ pub const Parser = struct { 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); expr.* = Expr{ .Get = ast.GetExpr{ - .struc = struc, + .target = target, .name = name, }, }; @@ -906,11 +906,11 @@ pub const Parser = struct { 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 => { return try self.mkSet( - get.struc, + get.target, get.name, try self.mkBinary(expr, binop.?, value), ); diff --git a/src/types.zig b/src/types.zig index 0015b2c..dfe384b 100644 --- a/src/types.zig +++ b/src/types.zig @@ -97,8 +97,8 @@ pub const TypeSolver = struct { ) anyerror!SymbolUnderlyingType { switch (expr.*) { .Binary => |binary| { - var left_type = self.resolveExprType(ctx, binary.left); - var right_type = self.resolveExprType(ctx, binary.right); + var left_type = try self.resolveExprType(ctx, binary.left); + var right_type = try self.resolveExprType(ctx, binary.right); return switch (binary.op) { // all numeric operations return numeric types @@ -114,7 +114,7 @@ pub const TypeSolver = struct { // for now, unary operators only have .Not .Unary => |unary| { - var right_type = self.resolveExprType(ctx, unary.right); + var right_type = try self.resolveExprType(ctx, unary.right); return switch (unary.op) { .Negate => right_type, .Not => right_type, @@ -159,9 +159,43 @@ pub const TypeSolver = struct { 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 => { std.debug.warn("TODO resolve expr {}\n", ast.ExprType(expr.*)); @@ -174,14 +208,15 @@ pub const TypeSolver = struct { self: *@This(), ctx: *comp.CompilationContext, stmt: ast.Stmt, - ) !void { + ) anyerror!void { switch (stmt) { // There are no side-effects to the type system when the statement // is just an expression or a println. we just resolve it // to ensure we dont have type errors. - .Expr => |expr_ptr| try self.resolveExprType(ctx, expr_ptr), - .Println => |expr_ptr| try self.resolveExprType(ctx, expr_ptr), + .Expr, .Println => |expr_ptr| { + _ = try self.resolveExprType(ctx, expr_ptr); + }, // VarDecl means we check the type of the expression and // 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. .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 // values, however, we must ensure that the expression type // matches the function type (must fetch from context, or we could // 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, } @@ -233,12 +296,24 @@ pub const TypeSolver = struct { 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| { 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 symbols and scope resolution, that's @@ -249,7 +324,7 @@ pub const TypeSolver = struct { // and everything else 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); } },