diff --git a/examples/ink-c-driver.c b/examples/ink-c-driver.c index 9a6561b..76fcd41 100644 --- a/examples/ink-c-driver.c +++ b/examples/ink-c-driver.c @@ -230,7 +230,6 @@ int main(int argc, char *argv[]) int flags = 0; const char *filename = NULL; struct ink_source source; - struct ink_story *story = NULL; option_setopts(cli_options, argv); @@ -285,11 +284,6 @@ int main(int argc, char *argv[]) return rc; } - story = ink_open(); - if (!story) { - goto out; - } - const struct ink_story_options opts = { .source_bytes = source.bytes, .source_length = source.length, @@ -298,8 +292,8 @@ int main(int argc, char *argv[]) .flags = flags, }; - rc = ink_load_story_options(story, &opts); - if (rc < 0) { + struct ink_story *const story = ink_load_story_options(&opts); + if (story == NULL) { goto out; } if (!compile_only) { diff --git a/include/ink.h b/include/ink.h index f8c62ec..36293dc 100644 --- a/include/ink.h +++ b/include/ink.h @@ -71,24 +71,17 @@ enum ink_flags { }; /** - * @brief Open a story context. + * Load an Ink story with extended options. * - * @returns a new story context + * @returns an opaque pointer on success or null on failure. */ -INK_API struct ink_story *ink_open(void); +INK_API struct ink_story *ink_load_story_options(const struct ink_load_options *options); /** * Close a story context. */ INK_API void ink_close(struct ink_story *story); -/** - * Load an Ink story with extended options. - * - * @returns a non-zero value on error. - */ -INK_API int ink_load_story_options(struct ink_story *story, - const struct ink_load_options *options); /** * Determine if the story can continue. diff --git a/src/Sema.zig b/src/Sema.zig index dd06133..ee5693c 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -4,16 +4,16 @@ const Ir = @import("Ir.zig"); const Story = @import("Story.zig"); const InternPool = @import("InternPool.zig"); const compile = @import("compile.zig"); -const Module = compile.Module; +const Compilation = compile.Compilation; const assert = std.debug.assert; const Sema = @This(); gpa: std.mem.Allocator, arena: std.mem.Allocator, -module: *compile.Module, +module: *Compilation, ir: Ir, inst_map: std.AutoHashMapUnmanaged(Ir.Inst.Index, ValueInfo) = .empty, -errors: *std.ArrayListUnmanaged(Module.Error), +errors: *std.ArrayListUnmanaged(Compilation.Error), comptime_break_inst: Ir.Inst.Index = undefined, const InnerError = error{ @@ -136,17 +136,17 @@ pub fn lookupIdentifier( builder: *Builder, ident: InternPool.Index, src: SrcLoc, -) !Module.Namespace.Decl { +) !Compilation.Namespace.Decl { return sema.lookupInNamespace(builder.namespace, ident, src); } pub fn lookupInNamespace( sema: *Sema, - namespace: *Module.Namespace, + namespace: *Compilation.Namespace, ident: InternPool.Index, src: SrcLoc, -) !Module.Namespace.Decl { - var scope: ?*Module.Namespace = namespace; +) !Compilation.Namespace.Decl { + var scope: ?*Compilation.Namespace = namespace; while (scope) |s| : (scope = s.parent) { if (s.decls.get(ident)) |decl| return decl; } @@ -166,7 +166,7 @@ pub const Block = struct { pub const Builder = struct { sema: *Sema, - namespace: *Module.Namespace, + namespace: *Compilation.Namespace, code: *InternPool.CodeChunk, constants_map: std.AutoHashMapUnmanaged(InternPool.Index, u8) = .empty, labels: std.ArrayListUnmanaged(Label) = .empty, @@ -1148,7 +1148,7 @@ fn analyzeCallTarget( builder: *Builder, src: SrcLoc, callee: ValueInfo, -) !Module.Namespace.Decl { +) !Compilation.Namespace.Decl { switch (callee) { .function => |ip_index| { try builder.materialize(callee); @@ -1163,7 +1163,7 @@ fn analyzeDivertTarget( builder: *Builder, src: SrcLoc, callee: ValueInfo, -) !Module.Namespace.Decl { +) !Compilation.Namespace.Decl { switch (callee) { .knot => |ip_index| { try builder.materialize(callee); @@ -1333,29 +1333,31 @@ pub fn analyzeKnot( fn analyzeNestedDecl( sema: *Sema, - namespace: *Module.Namespace, + namespace: *Compilation.Namespace, inst: Ir.Inst.Index, ) !void { + const gpa = sema.gpa; + const arena = sema.arena; const data = sema.ir.instructions[@intFromEnum(inst)].data.payload; const extra = sema.ir.extraData(Ir.Inst.Declaration, data.extra_index).data; const decl = sema.ir.instructions[@intFromEnum(extra.value)]; const decl_name = try sema.module.intern_pool.getOrPutString( - sema.gpa, + gpa, sema.ir.nullTerminatedString(extra.name), ); - const ip_index = try sema.module.intern_pool.getOrPutValue(sema.gpa, .{ .str = decl_name }); + const ip_index = try sema.module.intern_pool.getOrPutValue(gpa, .{ .str = decl_name }); switch (decl.tag) { .decl_stitch => { - const child_namespace = try sema.module.createNamespace(namespace); - try namespace.decls.put(sema.arena, ip_index, .{ + const child_namespace = try sema.module.createNamespace(arena, namespace); + try namespace.decls.put(arena, ip_index, .{ .tag = .knot, .decl_inst = extra.value, .args_count = 0, .namespace = child_namespace, }); - try sema.module.queueWorkItem(.{ + try sema.module.queueWorkItem(arena, .{ .tag = .stitch, .decl_name = ip_index, .inst_index = extra.value, @@ -1366,7 +1368,8 @@ fn analyzeNestedDecl( } } -fn scanTopLevelDecl(sema: *Sema, namespace: *Module.Namespace, inst: Ir.Inst.Index) !void { +fn scanTopLevelDecl(sema: *Sema, namespace: *Compilation.Namespace, inst: Ir.Inst.Index) !void { + const arena = sema.arena; const data = sema.ir.instructions[@intFromEnum(inst)].data.payload; const extra = sema.ir.extraData(Ir.Inst.Declaration, data.extra_index).data; const decl_inst = sema.ir.instructions[@intFromEnum(extra.value)]; @@ -1398,8 +1401,8 @@ fn scanTopLevelDecl(sema: *Sema, namespace: *Module.Namespace, inst: Ir.Inst.Ind const _body = sema.ir.bodySlice(_extra.end, _extra.data.body_len); const _stitches = sema.ir.bodySlice(_extra.end + _body.len, _extra.data.stitches_len); - const child_namespace = try sema.module.createNamespace(namespace); - const gop = try namespace.decls.getOrPut(sema.arena, decl_name); + const child_namespace = try sema.module.createNamespace(arena, namespace); + const gop = try namespace.decls.getOrPut(arena, decl_name); if (gop.found_existing) { return sema.fail(src_loc, "duplicate identifier", .{}); } else { @@ -1412,7 +1415,7 @@ fn scanTopLevelDecl(sema: *Sema, namespace: *Module.Namespace, inst: Ir.Inst.Ind }; } - try sema.module.queueWorkItem(.{ + try sema.module.queueWorkItem(arena, .{ .tag = .knot, .decl_name = decl_name, .inst_index = extra.value, @@ -1424,8 +1427,8 @@ fn scanTopLevelDecl(sema: *Sema, namespace: *Module.Namespace, inst: Ir.Inst.Ind } }, .decl_stitch => { - const child_namespace = try sema.module.createNamespace(namespace); - const gop = try namespace.decls.getOrPut(sema.arena, decl_name); + const child_namespace = try sema.module.createNamespace(arena, namespace); + const gop = try namespace.decls.getOrPut(arena, decl_name); if (gop.found_existing) { return sema.fail(src_loc, "duplicate identifier", .{}); } else { @@ -1437,7 +1440,7 @@ fn scanTopLevelDecl(sema: *Sema, namespace: *Module.Namespace, inst: Ir.Inst.Ind .namespace = child_namespace, }; } - try sema.module.queueWorkItem(.{ + try sema.module.queueWorkItem(arena, .{ .tag = .stitch, .decl_name = decl_name, .inst_index = extra.value, @@ -1445,8 +1448,8 @@ fn scanTopLevelDecl(sema: *Sema, namespace: *Module.Namespace, inst: Ir.Inst.Ind }); }, .decl_function => { - const child_namespace = try sema.module.createNamespace(namespace); - const gop = try namespace.decls.getOrPut(sema.arena, decl_name); + const child_namespace = try sema.module.createNamespace(arena, namespace); + const gop = try namespace.decls.getOrPut(arena, decl_name); if (gop.found_existing) { return sema.fail(src_loc, "duplicate identifier", .{}); } else { @@ -1458,7 +1461,7 @@ fn scanTopLevelDecl(sema: *Sema, namespace: *Module.Namespace, inst: Ir.Inst.Ind .namespace = child_namespace, }; } - try sema.module.queueWorkItem(.{ + try sema.module.queueWorkItem(arena, .{ .tag = .function, .decl_name = decl_name, .inst_index = extra.value, @@ -1500,7 +1503,7 @@ fn resolveGlobalDecl( pub fn scanTopLevelDecls( sema: *Sema, - namespace: *Module.Namespace, + namespace: *Compilation.Namespace, decls: []const Ir.Inst.Index, ) !void { const gpa = sema.gpa; diff --git a/src/Story.zig b/src/Story.zig index aaddba0..b492933 100644 --- a/src/Story.zig +++ b/src/Story.zig @@ -1,13 +1,14 @@ //! Virtual machine state for story execution. const std = @import("std"); -const tokenizer = @import("tokenizer.zig"); +const assert = std.debug.assert; + const Ast = @import("Ast.zig"); const AstGen = @import("AstGen.zig"); -const Module = @import("compile.zig").Module; +const Compilation = @import("compile.zig").Compilation; +pub const Loader = @import("Story/Loader.zig"); pub const Object = @import("Story/Object.zig"); -const Dumper = @import("Story/Dumper.zig"); +pub const Dumper = @import("Story/Dumper.zig"); const ink = @import("root.zig"); -const assert = std.debug.assert; const Story = @This(); gpa: std.mem.Allocator, @@ -42,17 +43,6 @@ internal_counter: usize = 0, pub const default_knot_name: [:0]const u8 = "$__main__$"; -pub const VariableObserver = struct { - callback: Callback, - context: Context, - - pub const Callback = *const fn (Value, Context) anyerror!void; - - pub const Context = struct { - ptr: *anyopaque, - }; -}; - pub const Opcode = enum(u8) { /// Exit the VM normally. exit, @@ -118,6 +108,17 @@ pub const Opcode = enum(u8) { _, }; +pub const VariableObserver = struct { + callback: Callback, + context: Context, + + pub const Callback = *const fn (Value, Context) anyerror!void; + + pub const Context = struct { + ptr: *anyopaque, + }; +}; + pub const CallFrame = struct { /// Pointer to the knot that initiated the call. callee: *Object.Knot, @@ -936,70 +937,6 @@ pub fn dump(story: *Story, writer: *std.Io.Writer) !void { return Dumper.dump(story, writer); } -pub const LoadOptions = struct { - filename: []const u8, - error_writer: *std.Io.Writer, - dump_writer: ?*std.Io.Writer = null, - dump_use_color: bool = true, - dump_ast: bool = false, - dump_ir: bool = false, -}; - -pub fn fromSourceBytes( - gpa: std.mem.Allocator, - source_bytes: [:0]const u8, - options: LoadOptions, -) !Story { - var arena_allocator = std.heap.ArenaAllocator.init(gpa); - defer arena_allocator.deinit(); - - const arena = arena_allocator.allocator(); - - var comp = try Module.compile(gpa, arena, .{ - .source_bytes = source_bytes, - .filename = options.filename, - .dump_writer = options.dump_writer, - .dump_use_color = options.dump_use_color, - .dump_ast = options.dump_ast, - .dump_ir = options.dump_ir, - }); - defer comp.deinit(); - - if (comp.errors.items.len > 0) { - for (comp.errors.items) |err| { - try comp.renderError(options.error_writer, err); - } - return error.LoadFailed; - } - - // TODO: Make this configureable. - const stack_size = 128; - const eval_stack_ptr = try gpa.alloc(Value, stack_size); - errdefer gpa.free(eval_stack_ptr); - - const call_stack_ptr = try gpa.alloc(CallFrame, stack_size); - errdefer gpa.free(call_stack_ptr); - - var story: Story = .{ - .gpa = gpa, - .arena = .init(gpa), - .can_advance = false, - .dump_writer = null, - .stack = eval_stack_ptr, - .call_stack = call_stack_ptr, - }; - errdefer story.deinit(); - - try comp.setupStoryRuntime(gpa, &story); - if (story.getKnot(Story.default_knot_name)) |knot| { - try story.pushStack(.{ .object = &knot.base }); - try story.divert(knot, 0); - } - return story; -} - -var read_buffer: [4096]u8 align(std.heap.page_size_min) = undefined; - pub const LoadFileOptions = struct { error_writer: *std.Io.Writer, }; @@ -1009,6 +946,7 @@ pub fn readSourceFile( filename: []const u8, options: LoadFileOptions, ) !Story { + var read_buffer: [4096]u8 align(std.heap.page_size_min) = undefined; // FIXME: Temporary until 0.16.x var arena_allocator = std.heap.ArenaAllocator.init(gpa); defer arena_allocator.deinit(); @@ -1030,3 +968,53 @@ pub fn readSourceFile( .dump_ir = false, }); } + +pub const LoadOptions = struct { + filename: [:0]const u8, + errors: *std.ArrayListUnmanaged(Compilation.Error), + stack_size: usize = 128, +}; + +pub fn fromSourceBytes( + gpa: std.mem.Allocator, + source_bytes: [:0]const u8, + options: LoadOptions, +) !Story { + var arena_allocator = std.heap.ArenaAllocator.init(gpa); + defer arena_allocator.deinit(); + const arena = arena_allocator.allocator(); + + var tree = try Ast.parse(gpa, arena, source_bytes, options.filename, 0); + defer tree.deinit(gpa); + + var ir = try AstGen.generate(gpa, &tree); + defer ir.deinit(gpa); + + var cu = Compilation.build(gpa, tree, ir, options.errors) catch |err| switch (err) { + else => |e| return e, + }; + defer cu.deinit(); + + if (cu.hasCompileErrors()) { + return error.CompilationError; + } + return .fromCompilation(gpa, &cu, .{ + .stack_size = options.stack_size, + }); +} + +pub fn fromCompilation( + gpa: std.mem.Allocator, + cu: *Compilation, + options: Loader.Options, +) !Story { + return Loader.fromCompilation(gpa, cu, options); +} + +pub fn fromCachedCompilation( + gpa: std.mem.Allocator, + bytes: []const u8, + options: Loader.Options, +) !Story { + return Loader.fromCachedCompilation(gpa, bytes, options); +} diff --git a/src/Story/Loader.zig b/src/Story/Loader.zig new file mode 100644 index 0000000..cb357d9 --- /dev/null +++ b/src/Story/Loader.zig @@ -0,0 +1,136 @@ +const std = @import("std"); +const InternPool = @import("../InternPool.zig"); +const Compilation = @import("../compile.zig").Compilation; +const Story = @import("../Story.zig"); +const Value = Story.Value; +const Object = Story.Object; + +fn makeValueFromInterned(story: *Story, value: InternPool.Key) !Story.Value { + return switch (value) { + .bool => |boolean| .{ .bool = boolean }, + .int => |int| .{ .int = @intCast(int) }, + .float => |float| .{ .float = @bitCast(float) }, + .str => |index| blk: { + const str_object = try Object.String.create(story, .{ + .bytes = stringBytes(story, index), + }); + break :blk .{ .object = &str_object.base }; + }, + }; +} + +fn makeKnotObject( + story: *Story, + name: []const u8, + code: *InternPool.CodeChunk, +) !*Object.Knot { + return Object.Knot.create(story, .{ + .name = name, + .code = try Object.Code.create(story, .{ + .args_count = @intCast(code.args_count), + .locals_count = @intCast(code.locals_count), + .stack_size = @intCast(code.stack_size), + .constants = try code.constants.toOwnedSlice(story.gpa), + .code_bytes = try code.bytecode.toOwnedSlice(story.gpa), + }), + }); +} + +fn makeConstantsPool(mod: *Compilation, story: *Story) ![]const Story.Value { + const ip = &mod.intern_pool; + const gpa = mod.gpa; + var constants_pool: std.ArrayListUnmanaged(Value) = .empty; + defer constants_pool.deinit(mod.gpa); + try constants_pool.ensureUnusedCapacity(mod.gpa, ip.values.items.len); + + for (ip.values.items) |value| { + const obj = try makeValueFromInterned(story, value); + constants_pool.appendAssumeCapacity(obj); + } + return constants_pool.toOwnedSlice(gpa); +} + +fn stringBytes(story: *Story, index: InternPool.NullTerminatedString) [:0]const u8 { + const slice = story.string_bytes[@intFromEnum(index)..]; + return slice[0..std.mem.indexOfScalar(u8, slice, 0).? :0]; +} + +pub const Options = struct { + stack_size: usize, +}; + +pub fn fromCompilationCompat( + gpa: std.mem.Allocator, + mod: *Compilation, + story: *Story, + options: Options, +) !void { + const ip = &mod.intern_pool; + + story.stack = try gpa.alloc(Story.Value, options.stack_size); + story.call_stack = try gpa.alloc(Story.CallFrame, options.stack_size); + story.string_bytes = try ip.string_bytes.toOwnedSlice(gpa); + story.constants_pool = try makeConstantsPool(mod, story); + + for (mod.globals.items) |global| { + const ip_key = ip.internedValue(global.key); + const ip_value = ip.internedValue(global.value); + + const key_bytes = stringBytes(story, ip_key.str); + const obj = try makeValueFromInterned(story, ip_value); + try story.globals.put(gpa, key_bytes, obj); + } + for (mod.knots.items) |knot| { + const ip_key = ip.internedValue(knot.name_index); + const key_bytes = stringBytes(story, ip_key.str); + + const knot_object = try makeKnotObject(story, key_bytes, knot.code_chunk); + const value: Story.Value = .{ .object = &knot_object.base }; + try story.globals.put(gpa, key_bytes, value); + } + for (mod.stitches.items) |stitch| { + const ip_key = ip.internedValue(stitch.name_index); + const key_bytes = stringBytes(story, ip_key.str); + const stitch_object = try makeKnotObject(story, key_bytes, stitch.code_chunk); + + if (stitch.knot_index) |index| { + const parent_knot = mod.knots.items[@intFromEnum(index)]; + const s_key_value = ip.internedValue(parent_knot.name_index); + const parent_knot_name = stringBytes(story, s_key_value.str); + const parent_knot_value = story.globals.get(parent_knot_name).?; + const parent_knot_obj: *Object.Knot = @ptrCast(parent_knot_value.object); + try parent_knot_obj.members.put(gpa, key_bytes, &stitch_object.base); + } else { + const value: Story.Value = .{ .object = &stitch_object.base }; + try story.globals.put(gpa, key_bytes, value); + } + } + if (story.getKnot(Story.default_knot_name)) |knot| { + try story.pushStack(.{ .object = &knot.base }); + try story.divert(knot, 0); + } +} + +pub fn fromCompilation( + gpa: std.mem.Allocator, + mod: *Compilation, + options: Options, +) !Story { + var story: Story = .{ + .gpa = gpa, + .arena = .init(gpa), + }; + try fromCompilationCompat(gpa, mod, &story, options); + return story; +} + +pub fn fromCachedCompilation( + gpa: std.mem.Allocator, + bytes: []const u8, + options: Options, +) !Story { + _ = gpa; + _ = bytes; + _ = options; + return error.NotImplemented; +} diff --git a/src/Story/runtime_tests.zig b/src/Story/runtime_tests.zig index 252d390..3c1aeb3 100644 --- a/src/Story/runtime_tests.zig +++ b/src/Story/runtime_tests.zig @@ -2,6 +2,7 @@ const std = @import("std"); const fatal = std.process.fatal; const ink = @import("../root.zig"); const Story = ink.Story; +const Compilation = ink.Compilation; test "fixture - variable arithmetic" { try testRuntimeFixture("variable-arithmetic"); @@ -262,12 +263,15 @@ test "variable observer" { var io_w = std.Io.Writer.Allocating.init(gpa); defer io_w.deinit(); + var errors: std.ArrayListUnmanaged(Compilation.Error) = .empty; + defer errors.deinit(gpa); + var story = try ink.Story.fromSourceBytes(gpa, \\VAR foo = 1 \\~foo = 10 , .{ .filename = "", - .error_writer = &io_w.writer, + .errors = &errors, }); defer story.deinit(); @@ -297,9 +301,13 @@ const Options = struct { fn testRunner(gpa: std.mem.Allocator, source_bytes: [:0]const u8, options: Options) !void { const io_r = options.input_reader; const io_w = options.transcript_writer; + + var errors: std.ArrayListUnmanaged(Compilation.Error) = .empty; + defer errors.deinit(gpa); + var story = try Story.fromSourceBytes(gpa, source_bytes, .{ .filename = "", - .error_writer = options.error_writer, + .errors = &errors, }); defer story.deinit(); diff --git a/src/c_api.zig b/src/c_api.zig index 062ac28..c535de2 100644 --- a/src/c_api.zig +++ b/src/c_api.zig @@ -1,48 +1,14 @@ const std = @import("std"); const ink = @import("ink"); const Story = ink.Story; -const Module = ink.Module; +const Compilation = ink.Compilation; +const Ast = ink.Ast; +const AstGen = ink.AstGen; +const Ir = ink.Ir; var global_allocator: std.heap.DebugAllocator(.{}) = .init; var stdout_buffer: [4096]u8 align(std.heap.page_size_min) = undefined; -pub export fn ink_open() callconv(.c) ?*Story { - const gpa = global_allocator.allocator(); - const story = gpa.create(Story) catch |err| switch (err) { - error.OutOfMemory => return null, - }; - story.* = .{ - .gpa = gpa, - .arena = .init(gpa), - .is_exited = false, - .can_advance = false, - .stack_top = 0, - .call_stack_top = 0, - .output_marker = 0, - .choice_selected = null, - .output_buffer = .empty, - .output_scratch = .empty, - .current_choices = .empty, - .variable_observers = .empty, - .globals = .empty, - .stack = &.{}, - .call_stack = &.{}, - .code_chunks = .empty, - .gc_objects = .{}, - .constants_pool = &.{}, - .string_bytes = &.{}, - .dump_writer = null, - }; - return story; -} - -pub export fn ink_close(story: *Story) callconv(.c) void { - defer _ = global_allocator.deinit(); - const gpa = story.gpa; - story.deinit(); - gpa.destroy(story); -} - pub const InkLoadOpts = extern struct { filename: [*]const u8, filename_length: usize, @@ -51,61 +17,90 @@ pub const InkLoadOpts = extern struct { flags: i32, }; -fn loadStory(story: *Story, options: *const InkLoadOpts) !void { - const gpa = story.gpa; +const LoadStoryOptions = struct { + filename: []const u8, + source_bytes: [:0]const u8, + flags: i32, + stack_size: usize, + error_writer: *std.Io.Writer, +}; + +fn loadStory(gpa: std.mem.Allocator, options: LoadStoryOptions) !*Story { var arena_allocator = std.heap.ArenaAllocator.init(gpa); defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); - const stderr = std.fs.File.stderr(); - var stderr_writer = stderr.writer(&stdout_buffer); - var comp = try Module.compile(gpa, arena, .{ - .source_bytes = options.source_bytes[0..options.source_length :0], - .filename = options.filename[0..options.filename_length], - .dump_writer = null, - .dump_use_color = false, - .dump_ast = false, - .dump_ir = false, - }); - defer comp.deinit(); + var tree = try Ast.parse( + gpa, + arena, + options.source_bytes, + options.filename, + @intCast(options.flags), + ); + defer tree.deinit(gpa); - if (comp.errors.items.len > 0) { - for (comp.errors.items) |err| { - try comp.renderError(&stderr_writer.interface, err); + var ir = try AstGen.generate(gpa, &tree); + defer ir.deinit(gpa); + + var errors: std.ArrayListUnmanaged(Compilation.Error) = .empty; + defer errors.deinit(gpa); + + var cu = Compilation.build(gpa, tree, ir, &errors) catch |err| switch (err) { + else => |e| return e, + }; + defer cu.deinit(); + + if (cu.hasCompileErrors()) { + for (cu.errors.items) |err| { + try cu.renderError(options.error_writer, err); } - return error.LoadFailed; + return error.Failed; } - // TODO: Make this configureable. - const stack_size = 128; - const eval_stack_ptr = try gpa.alloc(Story.Value, stack_size); - errdefer gpa.free(eval_stack_ptr); - - const call_stack_ptr = try gpa.alloc(Story.CallFrame, stack_size); - errdefer gpa.free(call_stack_ptr); - - story.can_advance = false; - story.stack = eval_stack_ptr; - story.call_stack = call_stack_ptr; + const story = try gpa.create(Story); + errdefer gpa.destroy(story); + story.* = .{ + .gpa = gpa, + .arena = .init(gpa), + }; errdefer story.deinit(); - try comp.setupStoryRuntime(gpa, story); - if (story.getKnot(Story.default_knot_name)) |knot| { - try story.pushStack(.{ .object = &knot.base }); - try story.divert(knot, 0); - } + try Story.Loader.fromCompilationCompat(gpa, &cu, story, .{ + .stack_size = options.stack_size, + }); + return story; } pub export fn ink_load_story_options( - story: *Story, options: *const InkLoadOpts, -) callconv(.c) c_int { - loadStory(story, options) catch |err| { +) callconv(.c) ?*Story { + const gpa = global_allocator.allocator(); + const source_bytes = options.source_bytes[0..options.source_length :0]; + const filename = options.filename[0..options.filename_length :0]; + const stack_size = 128; + const stderr = std.fs.File.stderr(); + var stderr_writer = stderr.writer(&stdout_buffer); + + return loadStory(gpa, .{ + .filename = filename, + .source_bytes = source_bytes, + .flags = options.flags, + .error_writer = &stderr_writer.interface, + .stack_size = stack_size, + }) catch |err| { std.debug.print("{any}\n", .{@errorName(err)}); - return -1; + return null; }; - return 0; +} + +pub export fn ink_close(optional_story: ?*Story) callconv(.c) void { + defer _ = global_allocator.deinit(); + if (optional_story) |story| { + const gpa = story.gpa; + story.deinit(); + gpa.destroy(story); + } } pub export fn ink_story_can_continue(story: *Story) callconv(.c) bool { diff --git a/src/compile.zig b/src/compile.zig index 1cec0e4..30e159b 100644 --- a/src/compile.zig +++ b/src/compile.zig @@ -1,12 +1,8 @@ const std = @import("std"); const Ast = @import("Ast.zig"); -const AstGen = @import("AstGen.zig"); -const Sema = @import("Sema.zig"); const Ir = @import("Ir.zig"); -const Story = @import("Story.zig"); +const Sema = @import("Sema.zig"); const InternPool = @import("InternPool.zig"); -const Value = Story.Value; -const Object = Story.Object; const assert = std.debug.assert; pub fn IntrusiveQueue(comptime T: type) type { @@ -80,35 +76,34 @@ test IntrusiveQueue { try testing.expect(q.pop() == null); } -// TODO: Revisit this. We might not need this at all. -pub const WorkItem = struct { - tag: Tag, - next: ?*WorkItem = null, - decl_name: InternPool.Index, - inst_index: Ir.Inst.Index, - namespace: *Module.Namespace, - - pub const Tag = enum { - knot, - stitch, - function, - }; -}; - -pub const WorkQueue = IntrusiveQueue(WorkItem); - -pub const Module = struct { +pub const Compilation = struct { gpa: std.mem.Allocator, - arena: std.mem.Allocator, + arena: std.heap.ArenaAllocator, tree: Ast, ir: Ir, globals: std.ArrayListUnmanaged(InternPool.Global) = .empty, knots: std.ArrayListUnmanaged(InternPool.Knot) = .empty, stitches: std.ArrayListUnmanaged(InternPool.Stitch) = .empty, - errors: std.ArrayListUnmanaged(Error) = .empty, + errors: *std.ArrayListUnmanaged(Error), intern_pool: InternPool = .{}, work_queue: WorkQueue = .{}, + pub const WorkItem = struct { + tag: Tag, + next: ?*WorkItem = null, + decl_name: InternPool.Index, + inst_index: Ir.Inst.Index, + namespace: *Compilation.Namespace, + + pub const Tag = enum { + knot, + stitch, + function, + }; + }; + + pub const WorkQueue = IntrusiveQueue(WorkItem); + pub const Namespace = struct { parent: ?*Namespace, decls: std.AutoArrayHashMapUnmanaged(InternPool.Index, Decl), @@ -143,28 +138,29 @@ pub const Module = struct { message: []const u8, }; - fn generateFile(mod: *Module) !void { + fn analyzeAndGenerate(cu: *Compilation) !void { + const gpa = cu.gpa; + const arena = cu.arena.allocator(); const root_node: Ir.Inst.Index = .file_inst; - const gpa = mod.gpa; - const data = mod.ir.instructions[@intFromEnum(root_node)].data.payload; - const extra = mod.ir.extraData(Ir.Inst.Block, data.extra_index); - const top_level_decls = mod.ir.bodySlice(extra.end, extra.data.body_len); + const data = cu.ir.instructions[@intFromEnum(root_node)].data.payload; + const extra = cu.ir.extraData(Ir.Inst.Block, data.extra_index); + const top_level_decls = cu.ir.bodySlice(extra.end, extra.data.body_len); var knot_index: ?InternPool.Knot.Index = null; var sema: Sema = .{ - .module = mod, .gpa = gpa, - .arena = mod.arena, - .ir = mod.ir, - .errors = &mod.errors, + .arena = arena, + .module = cu, + .ir = cu.ir, + .errors = cu.errors, }; defer sema.deinit(); - const file_scope = try mod.createNamespace(null); + const file_scope = try cu.createNamespace(arena, null); try sema.scanTopLevelDecls(file_scope, top_level_decls); - while (mod.work_queue.pop()) |work_unit| { - const code_chunk = try mod.createCodeChunk(); + while (cu.work_queue.pop()) |work_unit| { + const code_chunk = try cu.createCodeChunk(arena); var builder: Sema.Builder = .{ .sema = &sema, @@ -180,8 +176,8 @@ pub const Module = struct { try sema.analyzeKnot(&builder, work_unit.inst_index); try builder.finalize(); - knot_index = @enumFromInt(mod.knots.items.len); - try mod.knots.append(gpa, .{ + knot_index = @enumFromInt(cu.knots.items.len); + try cu.knots.append(gpa, .{ .name_index = work_unit.decl_name, .code_chunk = code_chunk, }); @@ -191,7 +187,7 @@ pub const Module = struct { try sema.analyzeStitch(&builder, work_unit.inst_index); try builder.finalize(); - try mod.stitches.append(gpa, .{ + try cu.stitches.append(gpa, .{ .knot_index = knot_index, .name_index = work_unit.decl_name, .code_chunk = code_chunk, @@ -202,7 +198,7 @@ pub const Module = struct { try sema.analyzeFunction(&builder, work_unit.inst_index); try builder.finalize(); - try mod.stitches.append(gpa, .{ + try cu.stitches.append(gpa, .{ .knot_index = null, .name_index = work_unit.decl_name, .code_chunk = code_chunk, @@ -212,170 +208,78 @@ pub const Module = struct { } } - pub const Options = struct { - source_bytes: [:0]const u8, - filename: []const u8, - dump_writer: ?*std.Io.Writer = null, - dump_ast: bool = false, - dump_ir: bool = false, - dump_use_color: bool = false, - }; - - pub fn compile( + fn generate( gpa: std.mem.Allocator, - arena: std.mem.Allocator, - options: Options, - ) !Module { - const tree = try Ast.parse(gpa, arena, options.source_bytes, options.filename, 0); - if (options.dump_writer) |w| { - if (options.dump_ast) { - try w.writeAll("=== AST ===\n"); - try tree.render(gpa, w, .{ - .use_color = options.dump_use_color, - }); - try w.flush(); - } - } - - var module: Module = .{ + tree: Ast, + ir: Ir, + errors: *std.ArrayListUnmanaged(Error), + ) !Compilation { + var cu: Compilation = .{ .gpa = gpa, - .arena = arena, + .arena = .init(gpa), .tree = tree, - .ir = try AstGen.generate(gpa, &tree), + .ir = ir, + .errors = errors, }; - errdefer module.deinit(); + errdefer cu.deinit(); - if (options.dump_writer) |w| { - if (options.dump_ir) { - try w.writeAll("=== Semantic IR ===\n"); - try module.ir.dumpInfo(w); - try module.ir.render(w); - } - } - if (module.ir.hasCompileErrors()) { - const payload_index = module.ir.extra[@intFromEnum(Ir.ExtraIndex.compile_errors)]; + if (errors.items.len != 0) return cu; + + try cu.intern_pool.string_bytes.append(gpa, 0); + try cu.intern_pool.values.append(gpa, .{ .bool = true }); + try cu.intern_pool.values.append(gpa, .{ .bool = false }); + try cu.intern_pool.values.append(gpa, .{ .str = .empty }); + + cu.analyzeAndGenerate() catch |err| switch (err) { + error.OutOfMemory => return error.OutOfMemory, + error.AnalysisFail => return cu, + else => |e| @panic(@errorName(e)), + }; + return cu; + } + + pub fn build( + gpa: std.mem.Allocator, + tree: Ast, + ir: Ir, + errors: *std.ArrayListUnmanaged(Error), + ) !Compilation { + if (ir.hasCompileErrors()) { + const payload_index = ir.extra[@intFromEnum(Ir.ExtraIndex.compile_errors)]; assert(payload_index != 0); - const header = module.ir.extraData(Ir.Inst.CompileErrors, payload_index); + const header = ir.extraData(Ir.Inst.CompileErrors, payload_index); const items_len = header.data.items_len; var extra_index = header.end; // TODO: Make an iterator for this? for (0..items_len) |_| { - const item = module.ir.extraData(Ir.Inst.CompileErrors.Item, extra_index); + const item = ir.extraData(Ir.Inst.CompileErrors.Item, extra_index); extra_index = item.end; const loc = findLineColumn(tree.source, item.data.byte_offset); - try module.errors.append(gpa, .{ + try errors.append(gpa, .{ .line = loc.line, .column = loc.column, .snippet = loc.source_line, - .message = module.ir.nullTerminatedString(item.data.msg), + .message = ir.nullTerminatedString(item.data.msg), }); } - } else { - try module.intern_pool.string_bytes.append(gpa, 0); - try module.intern_pool.values.append(gpa, .{ .bool = true }); - try module.intern_pool.values.append(gpa, .{ .bool = false }); - try module.intern_pool.values.append(gpa, .{ .str = .empty }); - // TODO: Revisit this. - module.generateFile() catch |err| switch (err) { - error.OutOfMemory => return error.OutOfMemory, - error.AnalysisFail => return module, - else => |e| @panic(@errorName(e)), - }; } - return module; + return .generate(gpa, tree, ir, errors); } - fn storyStr(story: *Story, index: InternPool.NullTerminatedString) [:0]const u8 { - const slice = story.string_bytes[@intFromEnum(index)..]; - return slice[0..std.mem.indexOfScalar(u8, slice, 0).? :0]; + pub fn hasCompileErrors(cu: *const Compilation) bool { + return cu.errors.items.len > 0; } - fn makeValueFromInterned(story: *Story, value: InternPool.Key) !Story.Value { - return switch (value) { - .bool => |boolean| .{ .bool = boolean }, - .int => |int| .{ .int = @intCast(int) }, - .float => |float| .{ .float = @bitCast(float) }, - .str => |index| blk: { - const str_object = try Object.String.create(story, .{ - .bytes = storyStr(story, index), - }); - break :blk .{ .object = &str_object.base }; - }, - }; - } - - fn makeKnotObject(story: *Story, name_bytes: []const u8, code: *InternPool.CodeChunk) !*Object.Knot { - return Object.Knot.create(story, .{ - .name = name_bytes, - .code = try Object.Code.create(story, .{ - .args_count = @intCast(code.args_count), - .locals_count = @intCast(code.locals_count), - .stack_size = @intCast(code.stack_size), - .constants = try code.constants.toOwnedSlice(story.gpa), - .code_bytes = try code.bytecode.toOwnedSlice(story.gpa), - }), - }); - } - - fn makeConstantsPool(mod: *Module, story: *Story) ![]const Story.Value { - const ip = &mod.intern_pool; - const gpa = mod.gpa; - var constants_pool: std.ArrayListUnmanaged(Value) = .empty; - defer constants_pool.deinit(mod.gpa); - try constants_pool.ensureUnusedCapacity(mod.gpa, ip.values.items.len); - - for (ip.values.items) |value| { - const obj = try makeValueFromInterned(story, value); - constants_pool.appendAssumeCapacity(obj); - } - return constants_pool.toOwnedSlice(gpa); - } - - pub fn setupStoryRuntime(mod: *Module, gpa: std.mem.Allocator, story: *Story) !void { - const ip = &mod.intern_pool; - story.string_bytes = try ip.string_bytes.toOwnedSlice(mod.gpa); - story.constants_pool = try makeConstantsPool(mod, story); - - for (mod.globals.items) |global| { - const ip_key = ip.internedValue(global.key); - const ip_value = ip.internedValue(global.value); - - const key_bytes = storyStr(story, ip_key.str); - const obj = try makeValueFromInterned(story, ip_value); - try story.globals.put(gpa, key_bytes, obj); - } - for (mod.knots.items) |knot| { - const ip_key = ip.internedValue(knot.name_index); - const key_bytes = storyStr(story, ip_key.str); - - const knot_object = try makeKnotObject(story, key_bytes, knot.code_chunk); - const value: Story.Value = .{ .object = &knot_object.base }; - try story.globals.put(gpa, key_bytes, value); - } - for (mod.stitches.items) |stitch| { - const ip_key = ip.internedValue(stitch.name_index); - const key_bytes = storyStr(story, ip_key.str); - const stitch_object = try makeKnotObject(story, key_bytes, stitch.code_chunk); - - if (stitch.knot_index) |index| { - const parent_knot = mod.knots.items[@intFromEnum(index)]; - const s_key_value = ip.internedValue(parent_knot.name_index); - const parent_knot_name = storyStr(story, s_key_value.str); - const parent_knot_value = story.globals.get(parent_knot_name).?; - const parent_knot_obj: *Object.Knot = @ptrCast(parent_knot_value.object); - try parent_knot_obj.members.put(gpa, key_bytes, &stitch_object.base); - } else { - const value: Story.Value = .{ .object = &stitch_object.base }; - try story.globals.put(gpa, key_bytes, value); - } - } - } - - pub fn createNamespace(mod: *Module, parent: ?*Namespace) error{OutOfMemory}!*Namespace { - const ns = try mod.arena.create(Namespace); + pub fn createNamespace( + cu: *Compilation, + arena: std.mem.Allocator, + parent: ?*Namespace, + ) error{OutOfMemory}!*Namespace { + _ = cu; + const ns = try arena.create(Namespace); ns.* = .{ .parent = parent, .decls = .empty, @@ -383,14 +287,19 @@ pub const Module = struct { return ns; } - pub fn createCodeChunk(mod: *Module) error{OutOfMemory}!*InternPool.CodeChunk { - const chunk = try mod.arena.create(InternPool.CodeChunk); + pub fn createCodeChunk( + cu: *Compilation, + arena: std.mem.Allocator, + ) error{OutOfMemory}!*InternPool.CodeChunk { + _ = cu; + const chunk = try arena.create(InternPool.CodeChunk); chunk.* = .{}; return chunk; } pub fn queueWorkItem( - mod: *Module, + mod: *Compilation, + arena: std.mem.Allocator, options: struct { tag: WorkItem.Tag, decl_name: InternPool.Index, @@ -398,7 +307,7 @@ pub const Module = struct { namespace: *Namespace, }, ) !void { - const work_item = try mod.arena.create(WorkItem); + const work_item = try arena.create(WorkItem); work_item.* = .{ .tag = options.tag, .decl_name = options.decl_name, @@ -408,7 +317,7 @@ pub const Module = struct { mod.work_queue.push(work_item); } - pub fn renderError(mod: *const Module, w: *std.Io.Writer, compile_error: Error) !void { + pub fn renderError(mod: *const Compilation, w: *std.Io.Writer, compile_error: Error) !void { const filename = mod.tree.filename; const line = compile_error.line + 1; const column = compile_error.column + 1; @@ -427,15 +336,13 @@ pub const Module = struct { return w.flush(); } - pub fn deinit(mod: *Module) void { + pub fn deinit(mod: *Compilation) void { const gpa = mod.gpa; - mod.tree.deinit(gpa); - mod.ir.deinit(gpa); + mod.arena.deinit(); mod.intern_pool.deinit(gpa); mod.globals.deinit(gpa); mod.knots.deinit(gpa); mod.stitches.deinit(gpa); - mod.errors.deinit(gpa); mod.* = undefined; } }; @@ -472,8 +379,3 @@ pub fn findLineColumn(source: []const u8, byte_offset: usize) Loc { .source_line = source[line_start..i], }; } - -fn hack(bytes: []const u8, index: InternPool.NullTerminatedString) [:0]const u8 { - const slice = bytes[@intFromEnum(index)..]; - return slice[0..std.mem.indexOfScalar(u8, slice, 0).? :0]; -} diff --git a/src/error_tests.zig b/src/error_tests.zig index b225a4a..5dfd6cf 100644 --- a/src/error_tests.zig +++ b/src/error_tests.zig @@ -1,6 +1,7 @@ const std = @import("std"); -const compile = @import("compile.zig"); -const Module = compile.Module; +const Ast = @import("Ast.zig"); +const AstGen = @import("AstGen.zig"); +const Compilation = @import("compile.zig").Compilation; test "compiler: VAR expected expression" { try testEqual( @@ -173,17 +174,21 @@ fn testEqual(source_bytes: [:0]const u8, expected_error: []const u8) !void { const io_w = &allocating.writer; const arena = arena_allocator.allocator(); - var c = try Module.compile(gpa, arena, .{ - .source_bytes = source_bytes, - .filename = "", - .dump_writer = null, - .dump_use_color = false, - .dump_ast = false, - .dump_ir = false, - }); - defer c.deinit(); + var errors: std.ArrayListUnmanaged(Compilation.Error) = .empty; + defer errors.deinit(gpa); - try std.testing.expect(c.errors.items.len > 0); - for (c.errors.items) |err| try c.renderError(io_w, err); + var tree = try Ast.parse(gpa, arena, source_bytes, "", 0); + defer tree.deinit(gpa); + + var ir = try AstGen.generate(gpa, &tree); + defer ir.deinit(gpa); + + var cu = Compilation.build(gpa, tree, ir, &errors) catch |err| switch (err) { + else => |e| return e, + }; + defer cu.deinit(); + + try std.testing.expect(errors.items.len > 0); + for (errors.items) |err| try cu.renderError(io_w, err); return std.testing.expectEqualSlices(u8, expected_error, allocating.written()); } diff --git a/src/main.zig b/src/main.zig index 01d0d7d..ce70685 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,6 +1,10 @@ const std = @import("std"); const ink = @import("ink"); +const Ast = ink.Ast; +const AstGen = ink.AstGen; +const Ir = ink.Ir; const Story = ink.Story; +const Compilation = ink.Compilation; const fatal = std.process.fatal; var stdin_buffer: [4096]u8 align(std.heap.page_size_min) = undefined; @@ -8,6 +12,32 @@ var stdout_buffer: [4096]u8 align(std.heap.page_size_min) = undefined; var stderr_buffer: [4096]u8 align(std.heap.page_size_min) = undefined; var debug_allocator: std.heap.DebugAllocator(.{}) = .init; +const ArgsIterator = struct { + args: []const []const u8, + index: usize = 0, + + pub fn next(iter: *ArgsIterator) ?[]const u8 { + if (iter.index >= iter.args.len) return null; + defer iter.index += 1; + return iter.args[iter.index]; + } + + pub fn nextOrFatal(iter: *ArgsIterator) []const u8 { + if (iter.index >= iter.args.len) { + fatal("expected parameter after {s}", .{iter.args[iter.index - 1]}); + } + defer iter.index += 1; + return iter.args[iter.index]; + } +}; + +const FileExtension = enum { + /// Ink source file. + ink, + /// Pre-compiled source file. + inkc, +}; + pub fn main() !void { const gpa = debug_allocator.allocator(); defer _ = debug_allocator.deinit(); @@ -31,7 +61,7 @@ fn mainArgs( args_list: []const [:0]const u8, ) !void { var source_path: ?[]const u8 = null; - var arg_index: usize = 1; + var output_path: ?[]const u8 = null; var compile_only: bool = false; var dump_ast: bool = false; var dump_ir: bool = false; @@ -40,8 +70,11 @@ fn mainArgs( var use_stdin: bool = false; var use_color: bool = false; - while (arg_index < args_list.len) : (arg_index += 1) { - const arg = args_list[arg_index]; + var args_iter: ArgsIterator = .{ + .args = args_list[1..], + }; + + while (args_iter.next()) |arg| { if (std.mem.startsWith(u8, arg, "-")) { if (std.mem.eql(u8, arg, "--stdin")) { use_stdin = true; @@ -57,6 +90,9 @@ fn mainArgs( dump_trace = true; } else if (std.mem.eql(u8, arg, "--use-color")) { use_color = true; + } else if (std.mem.eql(u8, arg, "--output")) { + const next_arg = args_iter.nextOrFatal(); + output_path = next_arg; } else { fatal("invalid parameter: '{s}'", .{arg}); } @@ -82,32 +118,74 @@ fn mainArgs( }; }; - const stdout = std.fs.File.stdout(); - var stdout_writer = stdout.writer(&stdout_buffer); - const stderr = std.fs.File.stderr(); var stderr_writer = stderr.writer(&stderr_buffer); + const io_w = &stderr_writer.interface; + const stack_size = 128; - var story = Story.fromSourceBytes(gpa, source_bytes, .{ - .filename = filename, - .error_writer = &stderr_writer.interface, - .dump_writer = &stdout_writer.interface, - .dump_use_color = use_color, - .dump_ast = dump_ast, - .dump_ir = dump_ir, - }) catch |err| switch (err) { - //error.LoadFailed => std.process.exit(1), - else => |e| return e, + const file_ext: FileExtension = f: { + const bytes = std.fs.path.extension(filename); + if (std.mem.eql(u8, bytes, ".ink")) break :f .ink; + if (std.mem.eql(u8, bytes, ".inkc")) break :f .inkc; + return error.InvalidFileExtension; }; - defer story.deinit(); + switch (file_ext) { + .ink => { + var tree = try Ast.parse(gpa, arena, source_bytes, filename, 0); + defer tree.deinit(gpa); - if (dump_trace) { - story.dump_writer = &stderr_writer.interface; + var ir = try AstGen.generate(gpa, &tree); + defer ir.deinit(gpa); + + var errors: std.ArrayListUnmanaged(Compilation.Error) = .empty; + defer errors.deinit(gpa); + + if (dump_ast) { + try io_w.writeAll("=== AST ===\n"); + try tree.render(gpa, io_w, .{ + .use_color = use_color, + }); + try io_w.flush(); + } + if (dump_ir) { + try io_w.writeAll("=== IR ===\n"); + try ir.render(io_w); + try io_w.flush(); + } + + var cu = Compilation.build(gpa, tree, ir, &errors) catch |err| switch (err) { + else => |e| return e, + }; + defer cu.deinit(); + + if (cu.hasCompileErrors()) { + for (cu.errors.items) |err| { + try cu.renderError(io_w, err); + } + } else if (output_path) |_| { + return error.NotImplemented; + } else { + var story: Story = try .fromCompilation(gpa, &cu, .{ + .stack_size = stack_size, + }); + defer story.deinit(); + if (dump_story) { + try story.dump(io_w); + } + if (dump_trace) { + story.dump_writer = io_w; + } + return if (!compile_only) run(gpa, &story); + } + }, + .inkc => { + var story: Story = try .fromCachedCompilation(gpa, source_bytes, .{ + .stack_size = stack_size, + }); + defer story.deinit(); + return if (!compile_only) run(gpa, &story); + }, } - if (dump_story) { - try story.dump(&stdout_writer.interface); - } - return if (!compile_only) run(gpa, &story); } fn run(_: std.mem.Allocator, story: *Story) !void { diff --git a/src/root.zig b/src/root.zig index 3aecb3b..0cb9478 100644 --- a/src/root.zig +++ b/src/root.zig @@ -2,7 +2,9 @@ const std = @import("std"); const tokenizer = @import("tokenizer.zig"); pub const Story = @import("Story.zig"); pub const Ast = @import("Ast.zig"); -pub const Module = @import("compile.zig").Module; +pub const AstGen = @import("AstGen.zig"); +pub const Ir = @import("Ir.zig"); +pub const Compilation = @import("compile.zig").Compilation; pub const max_src_size = std.math.maxInt(u32);