diff --git a/debug/app.ona b/debug/app.ona index c66b16b..c445d84 100644 --- a/debug/app.ona +++ b/debug/app.ona @@ -1,6 +1,13 @@ # Test comment. -@log_info("game is loading") + +test = { + .message = "don't you lecture me with your 30 dollar scripting language" +} + +# test.message = "game is loading" + +@log_info(test.message) return { .title = "Afterglow", diff --git a/source/ona/kym.zig b/source/ona/kym.zig index 3e054db..79f4af8 100644 --- a/source/ona/kym.zig +++ b/source/ona/kym.zig @@ -143,19 +143,13 @@ pub const RuntimeEnv = struct { }); defer { - const frame = self.frames.pop(); - - coral.debug.assert(frame != null); + const frame = self.frames.pop() orelse unreachable; { - var pops_remaining = (self.local_refs.values.len - frame.?.locals_top) + frame.?.arg_count; + var pops_remaining = (self.local_refs.values.len - frame.locals_top) + frame.arg_count; while (pops_remaining != 0) : (pops_remaining -= 1) { - const local = self.local_refs.pop(); - - coral.debug.assert(local != null); - - self.discard(local.?); + self.discard(self.local_refs.pop() orelse unreachable); } } } @@ -278,34 +272,18 @@ pub const RuntimeEnv = struct { } pub fn push_boolean(self: *RuntimeEnv, boolean: bool) RuntimeError!void { - if (self.frames.is_empty()) { - return self.raise(error.IllegalState, "attempt to push boolean outside a call frame"); - } - return self.local_refs.push_one(try self.new_boolean(boolean)); } pub fn push_fixed(self: *RuntimeEnv, fixed: Fixed) RuntimeError!void { - if (self.frames.is_empty()) { - return self.raise(error.IllegalState, "attempt to push fixed outside a call frame"); - } - return self.local_refs.push_one(try self.new_fixed(fixed)); } pub fn push_float(self: *RuntimeEnv, float: Float) RuntimeError!void { - if (self.frames.is_empty()) { - return self.raise(error.IllegalState, "attempt to push float outside a call frame"); - } - return self.local_refs.push_one(try self.new_float(float)); } pub fn push_ref(self: *RuntimeEnv, ref: ?*RuntimeRef) RuntimeError!void { - if (self.frames.is_empty()) { - return self.raise(error.IllegalState, "attempt to push ref outside a call frame"); - } - return self.local_refs.push_one(if (ref) |live_ref| self.acquire(live_ref) else null); } @@ -415,6 +393,18 @@ pub const RuntimeEnv = struct { return error_value; } + pub fn set_dynamic( + self: *RuntimeEnv, + indexable: *const DynamicObject, + index_ref: *const RuntimeRef, + value_ref: ?*const RuntimeRef, + ) RuntimeError!void { + return indexable.typeinfo.set(.{ + .env = self, + .userdata = indexable.userdata, + }, index_ref, value_ref); + } + pub fn syscallable(self: *RuntimeEnv, name: []const coral.io.Byte) RuntimeError!Caller { return self.syscallers.lookup(name) orelse self.raise(error.BadOperation, "attempt to get undefined syscall"); } @@ -453,11 +443,15 @@ pub const RuntimeRef = opaque {}; pub const Typeinfo = struct { name: []const coral.io.Byte, - call: ?*const fn (method: Method) RuntimeError!?*RuntimeRef = null, + call: *const fn (method: Method) RuntimeError!?*RuntimeRef = default_call, clean: *const fn (method: Method) void = default_clean, get: *const fn (method: Method, index: *const RuntimeRef) RuntimeError!?*RuntimeRef = default_get, set: *const fn (method: Method, index: *const RuntimeRef, value: ?*const RuntimeRef) RuntimeError!void = default_set, + fn default_call(method: Method) RuntimeError!?*RuntimeRef { + return method.env.raise(error.TypeMismatch, "object is not callable"); + } + fn default_clean(_: Method) void { // Nothing to clean by default. } diff --git a/source/ona/kym/Ast.zig b/source/ona/kym/Ast.zig index bcc7c4c..74f0688 100755 --- a/source/ona/kym/Ast.zig +++ b/source/ona/kym/Ast.zig @@ -18,7 +18,18 @@ pub const Expression = union (enum) { symbol_literal: []const coral.io.Byte, table_literal: TableLiteral, grouped_expression: *Expression, - get_local: []const coral.io.Byte, + resolve_local: []const coral.io.Byte, + + get_field: struct { + object_expression: *Expression, + identifier: []const coral.io.Byte, + }, + + set_field: struct { + object_expression: *Expression, + identifier: []const coral.io.Byte, + value_expression: *Expression, + }, call_system: struct { identifier: []const coral.io.Byte, @@ -80,10 +91,10 @@ pub const ParseError = error { const Self = @This(); pub const Statement = union (enum) { - return_expression: Expression, return_nothing, + return_expression: Expression, - set_local: struct { + assign_local: struct { identifier: []const coral.io.Byte, expression: Expression, }, @@ -113,10 +124,10 @@ fn binary_operation_parser( inline for (operators) |operator| { const token = comptime operator.token(); - if (self.tokenizer.is_token(coral.io.tag_of(token))) { + if (self.tokenizer.token == coral.io.tag_of(token)) { self.tokenizer.step(); - if (self.tokenizer.token == null) { + if (self.tokenizer.token == .end) { return self.report("expected other half of expression after `" ++ comptime token.text() ++ "`"); } @@ -178,10 +189,12 @@ pub fn parse(self: *Self, data: []const coral.io.Byte) ParseError!void { const allocator = self.arena.as_allocator(); var has_returned = false; - while (true) { - self.tokenizer.skip(.newline); + self.tokenizer.skip_newlines(); + + while (true) { + switch (self.tokenizer.token) { + .end => return, - switch (self.tokenizer.token orelse return) { .keyword_return => { if (has_returned) { return self.report("multiple returns in function scope but expected only one"); @@ -190,12 +203,12 @@ pub fn parse(self: *Self, data: []const coral.io.Byte) ParseError!void { try self.statements.push_one(get_statement: { self.tokenizer.step(); - if (!self.tokenizer.is_token_null_or(.newline)) { + if (self.tokenizer.token != .end and self.tokenizer.token != .newline) { break: get_statement .{.return_expression = try self.parse_expression()}; } - if (!self.tokenizer.is_token_null_or(.newline)) { - return self.report("unexpected token after return"); + if (self.tokenizer.token != .end and self.tokenizer.token != .newline) { + return self.report("expected end or newline after return statement"); } break: get_statement .return_nothing; @@ -207,28 +220,22 @@ pub fn parse(self: *Self, data: []const coral.io.Byte) ParseError!void { .identifier => |identifier| { self.tokenizer.step(); - const no_effect_message = "statement has no effect"; - - switch (self.tokenizer.token orelse return self.report(no_effect_message)) { - .newline => return self.report(no_effect_message), + switch (self.tokenizer.token) { + .end, .newline => return self.report("statement has no effect"), .symbol_equals => { self.tokenizer.step(); - if (self.tokenizer.token == null) { + if (self.tokenizer.token == .end) { return self.report("expected expression after `=`"); } try self.statements.push_one(.{ - .set_local = .{ + .assign_local = .{ .expression = try self.parse_expression(), .identifier = identifier, }, }); - - if (!self.tokenizer.is_token_null_or(.newline)) { - return self.report("unexpected token after assignment"); - } }, else => return self.report("expected `=` after local"), @@ -238,10 +245,8 @@ pub fn parse(self: *Self, data: []const coral.io.Byte) ParseError!void { .special_identifier => |identifier| { self.tokenizer.step(); - const missing_arguments_message = "system call is missing arguments"; - - switch (self.tokenizer.token orelse return self.report(missing_arguments_message)) { - .newline => return self.report(missing_arguments_message), + switch (self.tokenizer.token) { + .end, .newline => return self.report("system call is missing arguments"), .symbol_paren_left => { self.tokenizer.step(); @@ -249,20 +254,20 @@ pub fn parse(self: *Self, data: []const coral.io.Byte) ParseError!void { var expressions_list = Expression.List.make(allocator); while (true) { - if (self.tokenizer.is_token(.symbol_paren_right)) { + if (self.tokenizer.token == .symbol_paren_right) { break; } try expressions_list.push_one(try self.parse_expression()); - switch (self.tokenizer.token orelse return self.report("unexpected end after after `(`")) { + switch (self.tokenizer.token) { .symbol_comma => continue, .symbol_paren_right => break, else => return self.report("expected `)` or argument after `(`"), } } - self.tokenizer.step(); + self.tokenizer.skip_newlines(); try self.statements.push_one(.{ .call_system = .{ @@ -300,200 +305,234 @@ const parse_expression = binary_operation_parser(parse_equality, &.{ fn parse_factor(self: *Self) ParseError!Expression { const allocator = self.arena.as_allocator(); - switch (self.tokenizer.token orelse return self.report("expected operand after operator")) { - .symbol_paren_left => { - self.tokenizer.skip(.newline); + var expression = @as(Expression, get: { + switch (self.tokenizer.token) { + .symbol_paren_left => { + self.tokenizer.skip_newlines(); - if (self.tokenizer.token == null) { - return self.report("expected an expression after `(`"); - } - - const expression = try self.parse_expression(); - - if (!self.tokenizer.is_token(.symbol_paren_right)) { - return self.report("expected a closing `)` after expression"); - } - - self.tokenizer.step(); - - return Expression{.grouped_expression = try coral.io.allocate_one(allocator, expression)}; - }, - - .keyword_nil => { - self.tokenizer.step(); - - return .nil_literal; - }, - - .keyword_true => { - self.tokenizer.step(); - - return .true_literal; - }, - - .keyword_false => { - self.tokenizer.step(); - - return .false_literal; - }, - - .number => |value| { - self.tokenizer.step(); - - return Expression{.number_literal = value}; - }, - - .string => |value| { - self.tokenizer.step(); - - return Expression{.string_literal = value}; - }, - - .special_identifier => |identifier| { - self.tokenizer.skip(.newline); - - var expression_list = Expression.List.make(allocator); - - while (true) { - switch (self.tokenizer.token orelse return self.report("expected expression or `)` after `(`")) { - .symbol_paren_right => { - self.tokenizer.step(); - - return Expression{ - .call_system = .{ - .identifier = identifier, - .argument_expressions = expression_list, - }, - }; - }, - - else => { - try expression_list.push_one(try self.parse_expression()); - - switch (self.tokenizer.token orelse return self.report("expected `,` or `)` after argument")) { - .symbol_comma => continue, - - .symbol_paren_right => { - self.tokenizer.step(); - - return Expression{ - .call_system = .{ - .identifier = identifier, - .argument_expressions = expression_list, - }, - }; - }, - - else => return self.report("expected `,` or `)` after argument"), - } - }, + if (self.tokenizer.token == .end) { + return self.report("expected an expression after `(`"); } - } - }, - .identifier => |identifier| { - self.tokenizer.step(); + const expression = try self.parse_expression(); - return Expression{.get_local = identifier}; - }, - - .symbol_brace_left => { - var table_literal = Expression.TableLiteral.make(allocator); - - self.tokenizer.skip(.newline); - - while (true) { - switch (self.tokenizer.token orelse return self.report("unexpected end of table literal")) { - .symbol_brace_right => { - self.tokenizer.step(); - - return Expression{.table_literal = table_literal}; - }, - - .symbol_bracket_left => { - self.tokenizer.skip(.newline); - - if (!self.tokenizer.is_token(.symbol_equals)) { - return self.report("expected expression after identifier"); - } - }, - - .symbol_period => { - self.tokenizer.step(); - - if (!self.tokenizer.is_token(.identifier)) { - return self.report("expected identifier after `.`"); - } - - const identifier = self.tokenizer.token.?.identifier; - - self.tokenizer.skip(.newline); - - if (!self.tokenizer.is_token(.symbol_equals)) { - return self.report("expected `=` after key"); - } - - self.tokenizer.skip(.newline); - - if (self.tokenizer.token == null) { - return self.report("unexpected end after `=`"); - } - - try table_literal.push_one(.{ - .value_expression = try self.parse_expression(), - .key_expression = .{.symbol_literal = identifier}, - }); - - switch (self.tokenizer.token orelse return self.report("unexpected end of table")) { - .symbol_comma => self.tokenizer.skip(.newline), - - .symbol_brace_right => { - self.tokenizer.step(); - - return Expression{.table_literal = table_literal}; - }, - - else => return self.report("expected `,` or `}` after expression"), - } - }, - - else => return self.report("expected `}` or fields in table literal"), + if (self.tokenizer.token != .symbol_paren_right) { + return self.report("expected a closing `)` after expression"); } - } - }, - .symbol_minus => { - self.tokenizer.skip(.newline); + self.tokenizer.skip_newlines(); - if (self.tokenizer.token == null) { - return self.report("expected expression after numeric negation (`-`)"); - } + break: get Expression{.grouped_expression = try coral.io.allocate_one(allocator, expression)}; + }, - return Expression{ - .unary_operation = .{ - .expression = try coral.io.allocate_one(allocator, try self.parse_factor()), - .operator = .numeric_negation, + .keyword_nil => { + self.tokenizer.skip_newlines(); + + break: get .nil_literal; + }, + + .keyword_true => { + self.tokenizer.skip_newlines(); + + break: get .true_literal; + }, + + .keyword_false => { + self.tokenizer.skip_newlines(); + + break: get .false_literal; + }, + + .number => |value| { + self.tokenizer.skip_newlines(); + + break: get .{.number_literal = value}; + }, + + .string => |value| { + self.tokenizer.skip_newlines(); + + break: get .{.string_literal = value}; + }, + + .special_identifier => |identifier| { + self.tokenizer.skip_newlines(); + + var expression_list = Expression.List.make(allocator); + + while (true) { + switch (self.tokenizer.token) { + .end => return self.report("expected expression or `)` after `(`"), + + .symbol_paren_right => { + self.tokenizer.skip_newlines(); + + break: get .{ + .call_system = .{ + .identifier = identifier, + .argument_expressions = expression_list, + }, + }; + }, + + else => { + try expression_list.push_one(try self.parse_expression()); + + switch (self.tokenizer.token) { + .end => return self.report("expected `,` or `)` after argument"), + .symbol_comma => continue, + + .symbol_paren_right => { + self.tokenizer.skip_newlines(); + + break: get .{ + .call_system = .{ + .identifier = identifier, + .argument_expressions = expression_list, + }, + }; + }, + + else => return self.report("expected `,` or `)` after argument"), + } + }, + } + } + }, + + .identifier => |identifier| { + self.tokenizer.skip_newlines(); + + break: get .{.resolve_local = identifier}; + }, + + .symbol_brace_left => { + var table_literal = Expression.TableLiteral.make(allocator); + + self.tokenizer.skip_newlines(); + + while (true) { + switch (self.tokenizer.token) { + .symbol_brace_right => { + self.tokenizer.skip_newlines(); + + break: get .{.table_literal = table_literal}; + }, + + .symbol_bracket_left => { + self.tokenizer.skip_newlines(); + + if (self.tokenizer.token != .symbol_equals) { + return self.report("expected expression after identifier"); + } + }, + + .symbol_period => { + self.tokenizer.step(); + + const identifier = switch (self.tokenizer.token) { + .identifier => |identifier| identifier, + else => return self.report("expected identifier after `.`"), + }; + + self.tokenizer.skip_newlines(); + + if (self.tokenizer.token != .symbol_equals) { + return self.report("expected `=` after key"); + } + + self.tokenizer.skip_newlines(); + + if (self.tokenizer.token == .end) { + return self.report("unexpected end after `=`"); + } + + try table_literal.push_one(.{ + .value_expression = try self.parse_expression(), + .key_expression = .{.symbol_literal = identifier}, + }); + + switch (self.tokenizer.token) { + .symbol_comma => self.tokenizer.skip_newlines(), + + .symbol_brace_right => { + self.tokenizer.skip_newlines(); + + break: get .{.table_literal = table_literal}; + }, + + else => return self.report("expected `,` or `}` after expression"), + } + }, + + else => return self.report("expected `}` or fields in table literal"), + } + } + }, + + .symbol_minus => { + self.tokenizer.skip_newlines(); + + if (self.tokenizer.token == .end) { + return self.report("expected expression after numeric negation (`-`)"); + } + + break: get .{ + .unary_operation = .{ + .expression = try coral.io.allocate_one(allocator, try self.parse_factor()), + .operator = .numeric_negation, + }, + }; + }, + + .symbol_bang => { + self.tokenizer.skip_newlines(); + + if (self.tokenizer.token == .end) { + return self.report("expected expression after boolean negation (`!`)"); + } + + break: get .{ + .unary_operation = .{ + .expression = try coral.io.allocate_one(allocator, try self.parse_factor()), + .operator = .boolean_negation, + }, + }; + }, + + else => return self.report("unexpected token in expression"), + } + }); + + while (self.tokenizer.token == .symbol_period) { + self.tokenizer.skip_newlines(); + + const identifier = switch (self.tokenizer.token) { + .identifier => |identifier| identifier, + else => return self.report("expected identifier after `.`"), + }; + + self.tokenizer.skip_newlines(); + + expression = switch (self.tokenizer.token) { + .symbol_equals => .{ + .set_field = .{ + .value_expression = try coral.io.allocate_one(allocator, try self.parse_expression()), + .object_expression = try coral.io.allocate_one(allocator, expression), + .identifier = identifier, }, - }; - }, + }, - .symbol_bang => { - self.tokenizer.skip(.newline); - - if (self.tokenizer.token == null) { - return self.report("expected expression after boolean negation (`!`)"); - } - - return Expression{ - .unary_operation = .{ - .expression = try coral.io.allocate_one(allocator, try self.parse_factor()), - .operator = .boolean_negation, + else => .{ + .get_field = .{ + .object_expression = try coral.io.allocate_one(allocator, expression), + .identifier = identifier, }, - }; - }, - - else => return self.report("unexpected token in expression"), + }, + }; } + + return expression; } const parse_term = binary_operation_parser(parse_factor, &.{ diff --git a/source/ona/kym/Chunk.zig b/source/ona/kym/Chunk.zig index 095ab96..0c115db 100644 --- a/source/ona/kym/Chunk.zig +++ b/source/ona/kym/Chunk.zig @@ -99,12 +99,35 @@ const AstCompiler = struct { try self.compile_expression(grouped_expression.*); }, - .get_local => |local| { + .resolve_local => |local| { try self.chunk.append_opcode(.{ - .push_local = self.resolve_local(local) orelse return self.chunk.env.raise(error.OutOfMemory, "undefined local"), + .push_local = self.resolve_local(local) orelse { + return self.chunk.env.raise(error.OutOfMemory, "undefined local"); + }, }); }, + .get_field => |get_field| { + try self.compile_expression(get_field.object_expression.*); + + try self.chunk.append_opcode(.{ + .push_const = try self.chunk.declare_constant_symbol(get_field.identifier), + }); + + try self.chunk.append_opcode(.get_dynamic); + }, + + .set_field => |set_field| { + try self.compile_expression(set_field.object_expression.*); + + try self.chunk.append_opcode(.{ + .push_const = try self.chunk.declare_constant_symbol(set_field.identifier), + }); + + try self.compile_expression(set_field.value_expression.*); + try self.chunk.append_opcode(.set_dynamic); + }, + .call_system => |call| { if (call.argument_expressions.values.len > coral.math.max_int(@typeInfo(u8).Int)) { return self.chunk.env.raise(error.OutOfMemory, "functions may receive a maximum of 255 locals"); @@ -126,7 +149,7 @@ const AstCompiler = struct { .return_expression => |return_expression| try self.compile_expression(return_expression), .return_nothing => try self.chunk.append_opcode(.push_nil), - .set_local => |local| { + .assign_local => |local| { try self.compile_expression(local.expression); if (self.resolve_local(local.identifier)) |index| { @@ -193,6 +216,8 @@ pub const Opcode = union (enum) { push_local: u8, push_table: u32, set_local: u8, + get_dynamic, + set_dynamic, call: u8, syscall: u8, @@ -355,6 +380,46 @@ fn execute(self: *Self, env: *kym.RuntimeEnv) kym.RuntimeError!?*kym.RuntimeRef try env.set_local(local, ref); }, + .get_dynamic => { + const index_ref = try env.pop_local() orelse { + return env.raise(error.TypeMismatch, "nil is not a valid index"); + }; + + defer env.discard(index_ref); + + const indexable_ref = try env.pop_local() orelse { + return env.raise(error.TypeMismatch, "nil is not a valid indexable"); + }; + + defer env.discard(indexable_ref); + + const value_ref = try env.get_dynamic(try kym.unbox_dynamic(env, indexable_ref), index_ref); + + defer env.discard(value_ref); + + try env.push_ref(value_ref); + }, + + .set_dynamic => { + const index_ref = try env.pop_local() orelse { + return env.raise(error.TypeMismatch, "nil is not a valid index"); + }; + + defer env.discard(index_ref); + + const indexable_ref = try env.pop_local() orelse { + return env.raise(error.TypeMismatch, "nil is not a valid indexable"); + }; + + defer env.discard(indexable_ref); + + const value_ref = try env.pop_local(); + + defer env.discard(value_ref); + + try env.set_dynamic(try kym.unbox_dynamic(env, indexable_ref), index_ref, value_ref); + }, + .call => |arg_count| { const result_ref = call: { const callable_ref = try env.pop_local() orelse { diff --git a/source/ona/kym/tokens.zig b/source/ona/kym/tokens.zig index 76fac23..7aa0e61 100755 --- a/source/ona/kym/tokens.zig +++ b/source/ona/kym/tokens.zig @@ -1,6 +1,7 @@ const coral = @import("coral"); pub const Token = union(enum) { + end, unknown: coral.io.Byte, newline, @@ -41,6 +42,7 @@ pub const Token = union(enum) { pub fn text(self: Token) []const coral.io.Byte { return switch (self) { + .end => "end", .unknown => |unknown| @as([*]const coral.io.Byte, @ptrCast(&unknown))[0 .. 1], .newline => "newline", @@ -85,26 +87,12 @@ pub const Token = union(enum) { pub const Tokenizer = struct { source: []const coral.io.Byte, lines_stepped: usize = 1, - token: ?Token = null, + token: Token = .end, - const TokenTag = coral.io.Tag(Token); - - pub fn is_token(self: *Tokenizer, token_tag: TokenTag) bool { - return if (self.token) |token| token == token_tag else false; - } - - pub fn is_token_null_or(self: *Tokenizer, token_tag: TokenTag) bool { - return if (self.token) |token| token == token_tag else true; - } - - pub fn skip(self: *Tokenizer, skip_token_tag: TokenTag) void { + pub fn skip_newlines(self: *Tokenizer) void { self.step(); - while (self.token) |token| { - if (token != skip_token_tag) { - return; - } - + while (self.token == .newline) { self.step(); } } @@ -429,7 +417,7 @@ pub const Tokenizer = struct { } } - self.token = null; + self.token = .end; return; }