diff --git a/src/AstGen.zig b/src/AstGen.zig new file mode 100644 index 0000000..73a98ce --- /dev/null +++ b/src/AstGen.zig @@ -0,0 +1,8 @@ +const std = @import("std"); +const Ast = @import("./Ast.zig"); +const Story = @import("./Story.zig"); + +/// Perform code generation via tree-walk. +pub fn generate(_: std.mem.Allocator, _: *const Ast) !Story { + return error.OutOfMemory; +} diff --git a/src/Story.zig b/src/Story.zig new file mode 100644 index 0000000..afe9d5d --- /dev/null +++ b/src/Story.zig @@ -0,0 +1,544 @@ +//! Virtual machine state for story execution. +const std = @import("std"); +const tokenizer = @import("./tokenizer.zig"); +const Ast = @import("./Ast.zig"); +const AstGen = @import("./AstGen.zig"); +pub const Object = @import("./object.zig").Object; +const assert = std.debug.assert; +const Story = @This(); + +allocator: std.mem.Allocator, +dump_writer: ?*std.Io.Writer = null, +is_exited: bool = false, +can_advance: bool = false, +gc_objects: std.SinglyLinkedList = .{}, +globals: std.StringHashMapUnmanaged(?*Object) = .empty, +paths: std.ArrayListUnmanaged(*Object) = .empty, +stack: std.ArrayListUnmanaged(?*Object) = .empty, +call_stack: std.ArrayListUnmanaged(CallFrame) = .empty, +stack_max: usize = 128, + +pub const CallFrame = struct { + ip: usize, + sp: usize, + callee: *Object.ContentPath, +}; + +pub const Opcode = enum(u8) { + exit, + ret, + pop, + true, + false, + add, + sub, + mul, + div, + mod, + neg, + not, + cmp_eq, + cmp_lt, + cmp_gt, + cmp_lte, + cmp_gte, + jmp, + jmp_t, + jmp_f, + call, + divert, + load_const, + load, + store, + load_global, + store_global, + load_choice_id, + content, + line, + glue, + choice, + flush, + _, +}; + +fn getObjectType(object: *const Object) []const u8 { + switch (object.tag) { + .number => return "Number", + .string => return "String", + .content_path => return "ContentPath", + } +} + +fn printObject(writer: *std.Io.Writer, object: *const Object) !void { + const type_string = getObjectType(object); + switch (object.tag) { + .number => { + const typed_object: *const Object.Number = @ptrCast(object); + switch (typed_object.data) { + .boolean => |value| { + try writer.print("", .{ + type_string, + if (value) "true" else "false", + object, + }); + }, + .floating => |value| { + try writer.print("", .{ + type_string, + value, + object, + }); + }, + .integer => |value| { + try writer.print("", .{ + type_string, + value, + object, + }); + }, + } + }, + .string => { + const typed_object: *const Object.String = @ptrCast(object); + const string_bytes = typed_object.bytes[0..typed_object.length]; + try writer.print("", .{ + type_string, + string_bytes, + object, + }); + }, + .content_path => { + try writer.print("", .{ type_string, object }); + }, + } +} + +pub const Dumper = struct { + story: *const Story, + + fn dumpSimpleInst( + _: *const Dumper, + writer: *std.Io.Writer, + offset: usize, + op: Opcode, + ) !usize { + try writer.print("{s}\n", .{@tagName(op)}); + return offset + 1; + } + + fn dumpByteInst( + _: *const Dumper, + writer: *std.Io.Writer, + context: *const Object.ContentPath, + offset: usize, + op: Opcode, + ) !usize { + const arg = context.bytes[offset + 1]; + if (op == .load_const) { + try writer.print("{s} {d} (", .{ @tagName(op), arg }); + try printObject(writer, context.const_pool[arg]); + try writer.print(")\n", .{}); + } else { + try writer.print("{s} {x}\n", .{ @tagName(op), arg }); + } + return offset + 2; + } + + fn dumpGlobalInst( + _: *const Dumper, + writer: *std.Io.Writer, + context: *const Object.ContentPath, + offset: usize, + op: Opcode, + ) !usize { + const arg = context.bytes[offset + 1]; + const global_name: *Object.String = @ptrCast(context.const_pool[arg]); + const name_bytes = global_name.bytes[0..global_name.length]; + + try writer.print("{s} {x} '{s}'\n", .{ @tagName(op), arg, name_bytes }); + return offset + 2; + } + + fn dumpJumpInst( + _: *const Dumper, + writer: *std.Io.Writer, + context: *const Object.ContentPath, + offset: usize, + op: Opcode, + ) !usize { + var jump: u16 = @as(u16, context.bytes[offset + 1]) << 8; + jump |= context.bytes[offset + 2]; + + try writer.print("{s} 0x{x:0>4} (0x{x:0>4} -> 0x{x:0>4})\n", .{ + @tagName(op), + jump, + offset, + offset + 3 + jump, + }); + return offset + 3; + } + + pub fn dumpInst( + self: *const Dumper, + writer: *std.Io.Writer, + path: *const Object.ContentPath, + offset: usize, + should_prefix: bool, + ) !usize { + const name_object = path.name; + const name_bytes = name_object.bytes[0..name_object.length]; + const op: Opcode = @enumFromInt(path.bytes[offset]); + + if (should_prefix) { + try writer.print("<{s}>:0x{x:0>4} | ", .{ name_bytes, offset }); + } else { + try writer.print("0x{x:0>4} | ", .{offset}); + } + switch (op) { + .exit, + .ret, + .pop, + .true, + .false, + .add, + .sub, + .mul, + .div, + .mod, + .neg, + .not, + .cmp_eq, + .cmp_lt, + .cmp_lte, + .cmp_gt, + .cmp_gte, + .flush, + .load_choice_id, + .content, + .choice, + .line, + .glue, + => return self.dumpSimpleInst(writer, offset, op), + .load_const, + .load, + .store, + => return self.dumpByteInst(writer, path, offset, op), + .load_global, + .store_global, + .call, + .divert, + => return self.dumpGlobalInst(writer, path, offset, op), + .jmp, + .jmp_t, + .jmp_f, + => return self.dumpJumpInst(writer, path, offset, op), + else => |code| { + try writer.print("Unknown opcode 0x{x:0>4}\n", .{@intFromEnum(code)}); + return offset + 1; + }, + } + } + + pub fn dump( + self: *const Dumper, + writer: *std.Io.Writer, + path: *const Object.ContentPath, + ) !void { + const name_object = path.name; + const name_bytes = name_object.bytes[0..name_object.length]; + + try writer.print("=== {s}(args: {d}, locals: {d}) ===\n", .{ + name_bytes, + path.arity, + path.locals_count, + }); + + var index: usize = 0; + while (index < path.bytes.len) { + index = try self.dumpInst(writer, path, index, false); + } + return writer.flush(); + } +}; + +pub fn deinit(story: *Story) void { + const gpa = story.allocator; + var next = story.gc_objects.first; + while (next) |node| { + const object: *Object = @alignCast(@fieldParentPtr("node", node)); + next = node.next; + object.destroy(story); + } + + story.globals.deinit(gpa); + story.paths.deinit(gpa); + story.stack.deinit(gpa); + story.call_stack.deinit(gpa); +} + +pub fn trace(story: *Story, writer: *std.Io.Writer, frame: *CallFrame) !void { + try writer.print("\tStack => stack_pointer={d}, objects=[", .{frame.sp}); + + const stack = &story.stack; + const stack_top = story.stack.items.len; + if (stack_top > 0) { + const last_slot = stack.items[stack.items.len - 1]; + for (stack.items[frame.sp .. stack.items.len - 1]) |slot| { + if (slot) |object| { + try printObject(writer, object); + } else { + try writer.writeAll("NULL"); + } + try writer.writeAll(", "); + } + if (last_slot) |object| { + try printObject(writer, object); + } else { + try writer.writeAll("NULL"); + } + } + + try writer.writeAll("]\n"); + const dumper = Dumper{ .story = story }; + _ = try dumper.dumpInst(writer, frame.callee, frame.ip, true); + return writer.flush(); +} + +fn isCallStackEmpty(vm: *const Story) bool { + return vm.call_stack.items.len == 0; +} + +fn currentFrame(vm: *Story) *CallFrame { + return &vm.call_stack.items[vm.call_stack.items.len - 1]; +} + +fn peekStack(vm: *Story, offset: usize) ?*Object { + const stack_top = vm.stack.items.len; + assert(stack_top > offset); + assert(stack_top != 0); + + return vm.stack.items[stack_top - offset - 1]; +} + +fn pushStack(vm: *Story, object: *Object) !void { + const gpa = vm.allocator; + const stack_top = vm.stack.items.len; + const max_stack_top = vm.stack_max; + if (stack_top >= max_stack_top) return error.StackOverflow; + + return vm.stack.append(gpa, object); +} + +fn popStack(vm: *Story) ?*Object { + return vm.stack.pop() orelse unreachable; +} + +fn getConstant(_: *Story, frame: *CallFrame, offset: u8) !*Object { + const constant_pool = frame.callee.const_pool; + if (offset >= constant_pool.len) return error.InvalidArgument; + return constant_pool[offset]; +} + +fn getLocal(vm: *Story, frame: *CallFrame, offset: u8) ?*Object { + const stack_top = vm.stack.capacity; + const stack_offset = frame.sp + offset; + assert(stack_top > stack_offset); + + return vm.stack.items[stack_offset]; +} + +fn setLocal(vm: *Story, frame: *CallFrame, offset: u8, value: *Object) void { + const stack_top = vm.stack.capacity; + const stack_offset = frame.sp + offset; + assert(stack_top > stack_offset); + + vm.stack.items[stack_offset] = value; +} + +fn getGlobal(vm: *Story, key: *const Object.String) !*Object { + const key_bytes = key.bytes[0..key.length]; + const val = vm.globals.get(key_bytes) orelse return error.InvalidVariable; + return val orelse unreachable; +} + +fn setGlobal(vm: *Story, key: *const Object.String, value: *Object) !void { + const key_bytes = key.bytes[0..key.length]; + return vm.globals.putAssumeCapacity(key_bytes, value); +} + +fn execute(vm: *Story, writer: *std.Io.Writer) !void { + defer { + vm.can_advance = false; + } + if (vm.isCallStackEmpty()) return; + + const frame = vm.currentFrame(); + const code = std.mem.bytesAsSlice(Opcode, frame.callee.bytes); + + while (true) { + if (vm.trace_writer) |w| { + vm.trace(w, frame) catch {}; + } + switch (code[frame.ip]) { + .exit => { + vm.is_exited = true; + return; + }, + .pop => { + const object_top = vm.popStack(); + if (object_top == null) return error.InvalidArgument; + frame.ip += 1; + }, + .add => { + const lhs = vm.peekStack(1) orelse return error.Bugged; + const rhs = vm.peekStack(0) orelse return error.Bugged; + const value = try Object.add(vm, lhs, rhs); + + _ = vm.popStack(); + _ = vm.popStack(); + try vm.pushStack(value); + frame.ip += 1; + }, + .sub, .mul, .div, .mod => |op| { + const lhs = vm.peekStack(1) orelse return error.Bugged; + const rhs = vm.peekStack(0) orelse return error.Bugged; + const value = try Object.Number.performArithmetic(vm, op, @ptrCast(lhs), @ptrCast(rhs)); + + _ = vm.popStack(); + _ = vm.popStack(); + try vm.pushStack(@ptrCast(value)); + frame.ip += 1; + }, + .neg => { + const arg_object = vm.peekStack(0); + if (arg_object) |arg| { + _ = Object.Number.negate(@ptrCast(arg)); + } + frame.ip += 1; + }, + .content => { + const arg_object = vm.popStack(); + if (arg_object) |object| { + const string_object = try Object.String.fromObject(vm, object); + const string_bytes = string_object.bytes[0..string_object.length]; + try writer.writeAll(string_bytes); + } + + frame.ip += 1; + }, + .load_const => { + const index: u8 = @intFromEnum(code[frame.ip + 1]); + const value = try vm.getConstant(frame, index); + try vm.pushStack(value); + frame.ip += 2; + }, + .load => { + const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]); + const value = vm.getLocal(frame, arg_offset) orelse return error.Bugged; + try vm.pushStack(value); + frame.ip += 2; + }, + .store => { + const value = vm.peekStack(0); + if (value) |arg| { + const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]); + vm.setLocal(frame, arg_offset, arg); + } + frame.ip += 2; + }, + .load_global => { + const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]); + const global_name = try vm.getConstant(frame, arg_offset); + assert(global_name.tag == .string); + + const global_value = try vm.getGlobal(@ptrCast(global_name)); + try vm.pushStack(global_value); + frame.ip += 2; + }, + .store_global => { + const arg_offset: u8 = @intFromEnum(code[frame.ip + 1]); + const global_name = try vm.getConstant(frame, arg_offset); + assert(global_name.tag == .string); + + const value = vm.peekStack(0); + if (value) |arg| { + try vm.setGlobal(@ptrCast(global_name), arg); + _ = vm.popStack(); + try vm.pushStack(arg); + } + frame.ip += 2; + }, + else => return error.InvalidInstruction, + } + } +} + +pub fn advance(story: *Story) ![]const u8 { + const gpa = story.allocator; + var aw = std.Io.Writer.Allocating.init(gpa); + + try story.execute(&aw.writer); + return aw.toOwnedSlice(); +} + +fn divert(vm: *Story, path_name: []const u8) !void { + const gpa = vm.allocator; + const path_object: ?*Object.ContentPath = blk: { + for (vm.paths.items) |object| { + const current_path: *Object.ContentPath = @ptrCast(object); + const current_name = current_path.name; + // TODO(Brett): We probably should create a method for doing this. + const name_bytes = current_name.bytes[0..current_name.length]; + if (std.mem.eql(u8, name_bytes, path_name)) break :blk current_path; + } + break :blk null; + }; + if (path_object) |path| { + // TODO(Brett): Add arguments? + const stack_needed = path.arity + path.locals_count; + const stack_ptr = vm.stack.items.len; + try vm.stack.ensureUnusedCapacity(gpa, stack_needed); + try vm.call_stack.ensureUnusedCapacity(gpa, 1); + + vm.stack.appendNTimesAssumeCapacity(null, stack_needed); + vm.call_stack.appendAssumeCapacity(.{ .ip = 0, .sp = stack_ptr, .callee = path }); + } else return error.InvalidPath; +} + +pub const LoadOptions = struct { + dump_writer: ?*std.Io.Writer = null, + stderr_writer: *std.Io.Writer, + use_color: bool = true, +}; + +pub fn loadFromString( + 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(); + const ast = try Ast.parse(gpa, arena, source_bytes, "", 0); + + if (options.dump_writer) |w| { + try ast.render(gpa, w, .{ + .use_color = options.use_color, + }); + } + if (ast.errors.len > 0) { + try ast.renderErrors(gpa, options.stderr_writer, .{ + .use_color = options.use_color, + }); + return error.Invalid; + } + + var story = try AstGen.generate(gpa, &ast); + errdefer story.deinit(); + + try story.divert("@main@"); + story.dump_writer = options.dump_writer; + story.can_advance = true; + return story; +} diff --git a/src/main.zig b/src/main.zig index f4f56ed..4d5842f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4,6 +4,7 @@ const fatal = std.process.fatal; var stdin_buffer: [4096]u8 align(std.heap.page_size_min) = undefined; 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 max_src_size = std.math.maxInt(u32); @@ -75,9 +76,13 @@ 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); + var story = try ink.Story.loadFromString(gpa, source_bytes, .{ + .stderr_writer = &stderr_writer.interface, .dump_writer = &stdout_writer.interface, - .dump_use_color = use_color, + .use_color = use_color, }); defer story.deinit(); } diff --git a/src/object.zig b/src/object.zig new file mode 100644 index 0000000..8d54876 --- /dev/null +++ b/src/object.zig @@ -0,0 +1,272 @@ +const std = @import("std"); +const Story = @import("./Story.zig"); + +/// Runtime Object, whose memory is managed through the virtual machine's +/// garbage collector. +/// +/// This structure is the base object type that is embedded into subtypes. +/// Each subtype has a `.create` method to allocate a new object. +pub const Object = struct { + tag: Tag, + is_marked: bool, + node: std.SinglyLinkedList.Node, + + pub const Tag = enum { + number, + string, + content_path, + }; + + pub fn destroy(object: *Object, story: *Story) void { + switch (object.tag) { + .number => { + const obj: *Object.Number = @alignCast(@fieldParentPtr("base", object)); + obj.destroy(story); + }, + .string => { + const obj: *Object.String = @alignCast(@fieldParentPtr("base", object)); + obj.destroy(story); + }, + .content_path => { + const obj: *Object.ContentPath = @alignCast(@fieldParentPtr("base", object)); + obj.destroy(story); + }, + } + } + + pub fn add(story: *Story, lhs: *Object, rhs: *Object) !*Object { + if (lhs.tag == .string or rhs.tag == .string) { + const _lhs = try Object.String.fromObject(story, lhs); + const _rhs = try Object.String.fromObject(story, rhs); + const result = try Object.String.concat(story, _lhs, _rhs); + return @ptrCast(result); + } else if (lhs.tag == .number or rhs.tag == .number) { + const _lhs = try Object.Number.fromObject(story, lhs); + const _rhs = try Object.Number.fromObject(story, rhs); + const result = try Object.Number.performArithmetic(story, .add, _lhs, _rhs); + return @ptrCast(result); + } else { + return error.InvalidArgument; + } + } + + pub const Number = struct { + base: Object, + data: Data, + + pub const Data = union(enum) { + boolean: bool, + integer: i64, + floating: f64, + }; + + pub fn create(story: *Story, data: Data) error{OutOfMemory}!*Object.Number { + const gpa = story.allocator; + const alloc_len = @sizeOf(Object.Number); + const raw = try gpa.alignedAlloc(u8, .of(Object.Number), alloc_len); + const object: *Object.Number = @ptrCast(raw); + + object.* = .{ + .base = .{ + .tag = .number, + .is_marked = false, + .node = .{}, + }, + .data = data, + }; + + story.gc_objects.prepend(&object.base.node); + return object; + } + + pub fn destroy(obj: *Object.Number, story: *Story) void { + const gpa = story.allocator; + const alloc_len = @sizeOf(Object.Number); + const base: [*]align(@alignOf(Object.Number)) u8 = @ptrCast(obj); + gpa.free(base[0..alloc_len]); + } + + pub fn fromObject(story: *Story, object: *Object) !*Object.Number { + const data: Object.Number.Data = v: switch (object.tag) { + .number => { + const number: *Object.Number = @ptrCast(object); + switch (number.data) { + .boolean => |value| break :v .{ .integer = @intFromBool(value) }, + .integer => |value| break :v .{ .integer = value }, + .floating => |value| break :v .{ .floating = value }, + } + }, + else => break :v .{ .integer = 1 }, + }; + + const obj = Object.Number.create(story, data); + // ink_gc_own(story, obj); + return obj; + } + + fn arithmeticOp(comptime T: type, op: Story.Opcode, lhs: T, rhs: T) T { + switch (op) { + .add => return lhs + rhs, + .sub => return lhs - rhs, + .mul => return lhs * rhs, + .div => return @divTrunc(lhs, rhs), + .mod => return @mod(lhs, rhs), + else => unreachable, + } + } + + pub fn negate(object: *Object.Number) *Object.Number { + switch (object.data) { + .integer => |*value| value.* = -value.*, + .floating => |*value| value.* = -value.*, + else => unreachable, + } + return object; + } + + pub fn performArithmetic( + story: *Story, + op: Story.Opcode, + lhs: *Object.Number, + rhs: *Object.Number, + ) !*Object.Number { + if (lhs.data == .floating or rhs.data == .floating) { + const _lhs = try Object.Number.fromObject(story, @ptrCast(lhs)); + const _rhs = try Object.Number.fromObject(story, @ptrCast(rhs)); + return .create(story, .{ + .floating = arithmeticOp(f64, op, _lhs.data.floating, _rhs.data.floating), + }); + } + + const _lhs = try Object.Number.fromObject(story, @ptrCast(lhs)); + const _rhs = try Object.Number.fromObject(story, @ptrCast(rhs)); + return .create(story, .{ + .integer = arithmeticOp(i64, op, _lhs.data.integer, _rhs.data.integer), + }); + } + }; + + pub const String = struct { + base: Object, + hash: u32, // TODO: Not implemented. + length: usize, // TODO: This could probably be u32. + bytes: [*]const u8, + + pub fn create( + story: *Story, + bytes: []const u8, + ) error{OutOfMemory}!*Object.String { + const gpa = story.allocator; + const alloc_len = @sizeOf(String) + bytes.len + 1; + const raw = try gpa.alignedAlloc(u8, .of(String), alloc_len); + const object: *String = @ptrCast(raw); + + object.* = .{ + .base = .{ + .tag = .string, + .is_marked = false, + .node = .{}, + }, + .hash = 0, + .length = bytes.len, + .bytes = undefined, + }; + + // Point bytes slice to the memory *after* the struct + const buf = raw[@sizeOf(String)..][0 .. bytes.len + 1]; + object.bytes = buf.ptr; + @memcpy(buf[0..bytes.len], bytes); + buf[bytes.len] = 0; + + story.gc_objects.prepend(&object.base.node); + return object; + } + + pub fn destroy(object: *Object.String, story: *Story) void { + const gpa = story.allocator; + const alloc_len = @sizeOf(Object.String) + object.length + 1; + const base: [*]align(@alignOf(Object.String)) u8 = @ptrCast(object); + gpa.free(base[0..alloc_len]); + } + + pub fn fromObject(story: *Story, object: *Object) !*Object.String { + // NOTE: 20 bytes should be enough. + const print_buffer_len = 20; + var print_buffer: [print_buffer_len]u8 = undefined; + + switch (object.tag) { + .number => { + const number_object: *Object.Number = @ptrCast(object); + const number_bytes = try std.fmt.bufPrint(&print_buffer, "{}", .{ + number_object.data.floating, + }); + return Object.String.create(story, number_bytes); + }, + .string => return @ptrCast(object), + else => unreachable, + } + } + + pub fn concat(story: *Story, lhs: *String, rhs: *String) !*Object.String { + const gpa = story.allocator; + const length = lhs.length + rhs.length; + const bytes = try gpa.alloc(u8, length + 1); + defer gpa.free(bytes); + + @memcpy(bytes[0..lhs.length], lhs.bytes[0..lhs.length]); + @memcpy(bytes[lhs.length..], rhs.bytes[0..rhs.length]); + //ink_gc_disown(story, INK_OBJ(lhs)); + //ink_gc_disown(story, INK_OBJ(rhs)); + return .create(story, bytes); + } + }; + + pub const ContentPath = struct { + base: Object, + name: *Object.String, + arity: usize, + locals_count: usize, + const_pool: []*Object, + bytes: []const u8, + + pub fn create( + story: *Story, + name: *Object.String, + arity: usize, + locals_count: usize, + const_pool: []*Object, + bytes: []const u8, + ) error{OutOfMemory}!*ContentPath { + const gpa = story.allocator; + const alloc_len = @sizeOf(ContentPath); + const raw = try gpa.alignedAlloc(u8, .of(ContentPath), alloc_len); + const object: *ContentPath = @ptrCast(raw); + + object.* = .{ + .base = .{ + .tag = .content_path, + .is_marked = false, + .node = .{}, + }, + .name = name, + .arity = arity, + .locals_count = locals_count, + .const_pool = const_pool, + .bytes = bytes, + }; + + story.gc_objects.prepend(&object.base.node); + return object; + } + + pub fn destroy(obj: *ContentPath, story: *Story) void { + const gpa = story.allocator; + gpa.free(obj.const_pool); + gpa.free(obj.bytes); + + const alloc_len = @sizeOf(ContentPath); + const base: [*]align(@alignOf(ContentPath)) u8 = @ptrCast(obj); + gpa.free(base[0..alloc_len]); + } + }; +}; diff --git a/src/root.zig b/src/root.zig index cc553b6..5567e53 100644 --- a/src/root.zig +++ b/src/root.zig @@ -1,40 +1,9 @@ -const std = @import("std"); const tokenizer = @import("tokenizer.zig"); +pub const Story = @import("Story.zig"); pub const Ast = @import("Ast.zig"); -pub const Story = struct { - pub const LoadOptions = struct { - dump_writer: *std.Io.Writer, - dump_use_color: bool = true, - }; - - pub fn loadFromString( - 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 ast = try Ast.parse(gpa, arena, source_bytes, "", 0); - defer ast.deinit(gpa); - - try ast.render(gpa, options.dump_writer, .{ - .use_color = options.dump_use_color, - }); - if (ast.errors.len > 0) { - try ast.renderErrors(gpa, options.dump_writer, .{ - .use_color = options.dump_use_color, - }); - return error.Invalid; - } - return .{}; - } - - pub fn deinit(_: *Story) void {} -}; - test { _ = tokenizer; + _ = Ast; + _ = Story; }