From 5268a53148a90335daffdc0a8a4f859b0ed8a376 Mon Sep 17 00:00:00 2001 From: Brett Broadhurst Date: Thu, 26 Feb 2026 18:46:16 -0700 Subject: [PATCH] feat: ast structure and utilities --- src/Ast.zig | 311 +++++++++++++++++++++++++ src/Ast/Render.zig | 567 +++++++++++++++++++++++++++++++++++++++++++++ src/Parse.zig | 67 ++++++ src/main.zig | 7 +- src/root.zig | 23 +- 5 files changed, 970 insertions(+), 5 deletions(-) create mode 100644 src/Ast.zig create mode 100644 src/Ast/Render.zig create mode 100644 src/Parse.zig diff --git a/src/Ast.zig b/src/Ast.zig new file mode 100644 index 0000000..145dd0b --- /dev/null +++ b/src/Ast.zig @@ -0,0 +1,311 @@ +const std = @import("std"); +const ink = @import("root.zig"); +const Tokenizer = @import("tokenizer.zig").Tokenizer; +const Render = @import("Ast/Render.zig"); +const Parse = @import("Parse.zig"); +const Ast = @This(); + +filename: []const u8, +source: []const u8, +root: ?*Node = null, +errors: []Error, + +pub const Node = struct { + tag: Tag, + source_start: usize, + source_end: usize, + data: Data, + + pub const Tag = enum { + file, + false_literal, + true_literal, + number_literal, + string_literal, + empty_string, + identifier, + add_expr, + subtract_expr, + multiply_expr, + divide_expr, + mod_expr, + negate_expr, + logical_equality_expr, + logical_inequality_expr, + logical_and_expr, + logical_or_expr, + logical_not_expr, + logical_greater_expr, + logical_greater_or_equal_expr, + logical_lesser_or_equal_expr, + logical_lesser_expr, + call_expr, + choice_expr, + choice_start_expr, + choice_option_expr, + choice_inner_expr, + divert_expr, + selector_expr, + assign_stmt, + block_stmt, + content_stmt, + divert_stmt, + return_stmt, + expr_stmt, + choice_stmt, + choice_star_stmt, + choice_plus_stmt, + gather_point_stmt, + gathered_stmt, + function_prototype, + stitch_prototype, + knot_prototype, + function_decl, + stitch_decl, + knot_decl, + const_decl, + var_decl, + list_decl, + temp_decl, + parameter_decl, + ref_parameter_decl, + string_expr, + argument_list, + parameter_list, + switch_stmt, + switch_case, + if_stmt, + multi_if_stmt, + if_branch, + else_branch, + content, + // TODO: Rename to SubstitutionExpr + inline_logic_expr, + invalid, + }; + + pub const Data = union { + leaf: void, + bin: struct { + lhs: ?*Node, + rhs: ?*Node, + }, + list: struct { + items: ?[]*Node, + }, + choice_expr: struct { + start_expr: ?*Node, + option_expr: ?*Node, + inner_expr: ?*Node, + }, + switch_stmt: struct { + condition_expr: ?*Node, + cases: ?[]*Node, + }, + knot_decl: struct { + prototype: *Node, + children: ?[]*Node, + }, + }; +}; + +pub const Error = struct { + tag: Tag, + loc: Location, + + pub const Location = struct { + start: usize, + end: usize, + }; + + pub const Tag = enum { + panic, + unexpected_token, + unknown_identifier, + assignment_to_const, + expected_newline, + expected_expression, + expected_double_quote, + expected_identifier, + invalid_expression, + invalid_lvalue, + too_many_arguments, + too_many_parameters, + }; +}; + +fn create( + gpa: std.mem.Allocator, + tag: Node.Tag, + source_start: usize, + source_end: usize, +) !*Node { + const node = try gpa.create(Node); + node.* = .{ + .tag = tag, + .source_start = source_start, + .source_end = source_end, + .data = undefined, + }; + return node; +} + +pub fn createLeafNode( + gpa: std.mem.Allocator, + tag: Node.Tag, + source_start: usize, + source_end: usize, +) !*Node { + const node = try Ast.create(gpa, tag, source_start, source_end); + node.*.data = .{ .leaf = undefined }; + return node; +} + +pub fn createBinaryNode( + gpa: std.mem.Allocator, + tag: Node.Tag, + source_start: usize, + source_end: usize, + lhs: ?*Node, + rhs: ?*Node, +) !*Node { + const node = try Ast.create(gpa, tag, source_start, source_end); + node.*.data = .{ + .bin = .{ + .lhs = lhs, + .rhs = rhs, + }, + }; + return node; +} + +pub fn createListNode( + gpa: std.mem.Allocator, + tag: Node.Tag, + source_start: usize, + source_end: usize, + items: ?[]*Node, +) !*Node { + const node = try Ast.create(gpa, tag, source_start, source_end); + node.*.data = .{ + .list = .{ .items = items }, + }; + return node; +} + +pub fn createChoiceExprNode( + gpa: std.mem.Allocator, + tag: Node.Tag, + source_start: usize, + source_end: usize, + start_expr: ?*Node, + option_expr: ?*Node, + inner_expr: ?*Node, +) !*Node { + const node = try Ast.create(gpa, tag, source_start, source_end); + node.*.data = .{ + .choice_expr = .{ + .start_expr = start_expr, + .option_expr = option_expr, + .inner_expr = inner_expr, + }, + }; + return node; +} + +pub fn createSwitchNode( + gpa: std.mem.Allocator, + tag: Node.Tag, + source_start: usize, + source_end: usize, + condition_expr: ?*Node, + cases_list: ?[]*Node, +) !*Node { + const node = try Ast.create(gpa, tag, source_start, source_end); + node.*.data = .{ + .switch_stmt = .{ + .condition_expr = condition_expr, + .cases = cases_list, + }, + }; + return node; +} + +pub fn createKnotDeclNode( + gpa: std.mem.Allocator, + tag: Node.Tag, + source_start: usize, + source_end: usize, + prototype: *Node, + children: ?[]*Node, +) !*Node { + const node = try Ast.create(gpa, tag, source_start, source_end); + node.*.data = .{ + .knot_decl = .{ + .prototype = prototype, + .children = children, + }, + }; + return node; +} + +pub fn parse( + gpa: std.mem.Allocator, + source: [:0]const u8, + filename: []const u8, + _: u32, +) !Ast { + var arena_allocator = std.heap.ArenaAllocator.init(gpa); + defer arena_allocator.deinit(); + + var parser: Parse = .{ + .gpa = gpa, + .arena = arena_allocator.allocator(), + .tokenizer = Tokenizer.init(source), + .token = .{ + .tag = .invalid, + .loc = .{ .start = 0, .end = 0 }, + }, + .scratch = .empty, + .grammar_stack = .empty, + .block_stack = .empty, + .choice_stack = .empty, + .errors = .empty, + .panic_mode = false, + .flags = 0, + .max_parse_depth = 128, + .max_argument_count = 128, + }; + defer parser.deinit(); + + const root = try parser.parseFile(); + return Ast{ + .filename = filename, + .source = source, + .root = root, + .errors = try parser.errors.toOwnedSlice(gpa), + }; +} + +pub fn render( + ast: Ast, + gpa: std.mem.Allocator, + writer: *std.Io.Writer, + options: Render.Options, +) !void { + return Render.renderTree(gpa, writer, &ast, options); +} + +pub fn renderErrors( + ast: Ast, + gpa: std.mem.Allocator, + writer: *std.Io.Writer, + options: Render.Options, +) !void { + return Render.renderErrors(gpa, writer, &ast, options); +} + +pub fn deinit(ast: *Ast, gpa: std.mem.Allocator) void { + gpa.free(ast.errors); + ast.* = undefined; +} diff --git a/src/Ast/Render.zig b/src/Ast/Render.zig new file mode 100644 index 0000000..57ccf53 --- /dev/null +++ b/src/Ast/Render.zig @@ -0,0 +1,567 @@ +const std = @import("std"); +const ink = @import("../root.zig"); +const Ast = ink.Ast; +const Render = @This(); + +gpa: std.mem.Allocator, +tty_config: std.Io.tty.Config, +tree: *const Ast, +prefix: Prefix, +lines: LineCache, + +const LineCache = struct { + ranges: std.ArrayListUnmanaged(SourceRange) = .empty, + + pub fn deinit(self: *LineCache, gpa: std.mem.Allocator) void { + self.ranges.deinit(gpa); + } + + pub fn build(gpa: std.mem.Allocator, bytes: []const u8) !LineCache { + var lines: LineCache = .{}; + var start: usize = 0; + + for (bytes, 0..) |c, i| { + if (c == '\n') { + try lines.ranges.append(gpa, .{ .start = start, .end = i }); + start = i + 1; + } + } + if (start != bytes.len or bytes.len == 0) { + try lines.ranges.append(gpa, .{ .start = start, .end = bytes.len }); + } + return lines; + } + + pub fn calculateLine(self: *const LineCache, offset: usize) usize { + for (self.ranges.items, 0..) |r, i| { + if (offset >= r.start and offset <= r.end) + return i; + } + return self.ranges.items.len - 1; + } +}; + +pub const SourceRange = struct { + start: usize, + end: usize, +}; + +pub const ErrorInfo = struct { + filename: []const u8, + message: []const u8, + snippet: []const u8, + line: usize, + column: usize, +}; + +fn nodeTagToString(tag: Ast.Node.Tag) []const u8 { + return switch (tag) { + .file => "File", + .false_literal => "FalseLiteral", + .true_literal => "TrueLiteral", + .number_literal => "NumberLiteral", + .string_literal => "StringLiteral", + .empty_string => "EmptyString", + .identifier => "Identifier", + .add_expr => "AddExpr", + .subtract_expr => "SubtractExpr", + .multiply_expr => "MultiplyExpr", + .divide_expr => "DivideExpr", + .mod_expr => "ModExpr", + .negate_expr => "NegateExpr", + .logical_equality_expr => "LogicalEqualityExpr", + .logical_inequality_expr => "LogicalInequalityExpr", + .logical_and_expr => "LogicalAndExpr", + .logical_or_expr => "LogicalOrExpr", + .logical_not_expr => "LogicalNotExpr", + .logical_greater_expr => "LogicalGreaterThanExpr", + .logical_greater_or_equal_expr => "LogicalGreaterThanOrEqualExpr", + .logical_lesser_or_equal_expr => "LogicalLesserThanOrEqualExpr", + .logical_lesser_expr => "LogicalLesserThanExpr", + .divert_expr => "Divert", + .selector_expr => "SelectorExpr", + .call_expr => "CallExpr", + .choice_expr => "ChoiceContentExpr", + .choice_start_expr => "ChoiceStartContentExpr", + .choice_option_expr => "ChoiceOptionContentExpr", + .choice_inner_expr => "ChoiceInnerContentExpr", + .assign_stmt => "AssignStmt", + .block_stmt => "BlockStmt", + .content_stmt => "ContentStmt", + .divert_stmt => "DivertStmt", + .return_stmt => "ReturnStmt", + .expr_stmt => "ExprStmt", + .choice_stmt => "ChoiceStmt", + .choice_star_stmt => "ChoiceStarStmt", + .choice_plus_stmt => "ChoicePlusStmt", + .gather_point_stmt => "GatherPointStmt", + .gathered_stmt => "GatheredStmt", + .function_prototype => "FunctionProto", + .stitch_prototype => "StitchProto", + .knot_prototype => "KnotProto", + .function_decl => "FunctionDecl", + .stitch_decl => "StitchDecl", + .knot_decl => "KnotDecl", + .const_decl => "ConstDecl", + .var_decl => "VarDecl", + .list_decl => "ListDecl", + .temp_decl => "TempDecl", + .parameter_decl => "ParamDecl", + .ref_parameter_decl => "RefParamDecl", + .argument_list => "ArgumentList", + .parameter_list => "ParamList", + .string_expr => "StringExpr", + .switch_stmt => "SwitchStmt", + .switch_case => "SwitchCase", + .multi_if_stmt => "MultiIfStmt", + .if_stmt => "IfStmt", + .if_branch => "IfBranch", + .else_branch => "ElseBranch", + .inline_logic_expr => "InlineLogicExpr", + .content => "Content", + .invalid => "Invalid", + }; +} + +fn makeErrorInfo(r: *Render, err: Ast.Error, message: []const u8) ErrorInfo { + const line_index = r.lines.calculateLine(err.loc.start); + const snippet_range = r.lines.ranges.items[line_index]; + const column = err.loc.start - snippet_range.start; + const snippet = r.tree.source[snippet_range.start..snippet_range.end]; + + return ErrorInfo{ + .filename = r.tree.filename, + .message = message, + .line = line_index, + .column = column, + .snippet = snippet, + }; +} + +fn renderErrorf(r: *Render, writer: *std.Io.Writer, err: Ast.Error, message: []const u8) !void { + const error_info = makeErrorInfo(r, err, message); + const filename = error_info.filename; + const line = error_info.line + 1; + const col = error_info.column + 1; + const snippet = error_info.message; + + try writer.print("{s}:{d}:{d}: error: {s}\n", .{ filename, line, col, snippet }); + try writer.print("{d:<4} | {s}\n", .{ line, error_info.snippet }); + try writer.writeAll(" | "); + + var i: usize = 0; + while (i < col) : (i += 1) { + try writer.writeByte(' '); + } + + try writer.writeByte('^'); + try writer.writeAll("\n\n"); +} + +fn renderError(r: *Render, writer: *std.Io.Writer, err: Ast.Error) !void { + switch (err.tag) { + .panic => try renderErrorf(r, writer, err, "parser panicked"), + .unknown_identifier => try renderErrorf(r, writer, err, "unknown identifier"), + .assignment_to_const => try renderErrorf(r, writer, err, "assignment to constant value"), + .unexpected_token => try renderErrorf(r, writer, err, "unexpected token"), + .expected_newline => try renderErrorf(r, writer, err, "expected newline"), + .expected_double_quote => try renderErrorf(r, writer, err, "unterminated string, expected closing quote"), + .expected_identifier => try renderErrorf(r, writer, err, "expected identifier"), + .expected_expression => try renderErrorf(r, writer, err, "expected expression"), + .invalid_expression => try renderErrorf(r, writer, err, "invalid expression"), + .invalid_lvalue => try renderErrorf(r, writer, err, "invalid lvalue for assignment"), + .too_many_arguments => try renderErrorf(r, writer, err, "too many arguments to '{s}'"), + .too_many_parameters => try renderErrorf(r, writer, err, "too many parameters defined for '{s}'"), + } +} + +const Prefix = struct { + buf: std.ArrayListUnmanaged(u8) = .empty, + + pub fn deinit(self: *Prefix, gpa: std.mem.Allocator) void { + self.buf.deinit(gpa); + } + + pub fn writeIndent(self: *const Prefix, writer: anytype) !void { + try writer.writeAll(self.buf.items); + } + + pub fn pushChildPrefix(self: *Prefix, gpa: std.mem.Allocator, is_last: bool) !usize { + const old_len = self.buf.items.len; + const seg: []const u8 = if (is_last) " " else "| "; + try self.buf.appendSlice(gpa, seg); + return old_len; + } + + pub fn restore(self: *Prefix, new_len: usize) void { + self.buf.shrinkRetainingCapacity(new_len); + } +}; + +fn writeType(r: *Render, writer: *std.Io.Writer, node: *const Ast.Node) !void { + const tag = nodeTagToString(node.tag); + try r.tty_config.setColor(writer, .magenta); + try r.tty_config.setColor(writer, .bold); + try writer.writeAll(tag); + try r.tty_config.setColor(writer, .reset); +} + +fn writeLexeme(r: *Render, writer: *std.Io.Writer, node: *const Ast.Node) !void { + const bytes = r.tree.source; + const lexeme = bytes[node.source_start..node.source_end]; + try r.tty_config.setColor(writer, .yellow); + try writer.print("`{s}`", .{lexeme}); + try r.tty_config.setColor(writer, .reset); +} + +fn writeLineSpan(r: *Render, writer: *std.Io.Writer, node: *const Ast.Node) !void { + const line_start = r.lines.calculateLine(node.source_start); + const line_end = r.lines.calculateLine(node.source_end); + + try r.tty_config.setColor(writer, .white); + try writer.writeByte('<'); + try r.tty_config.setColor(writer, .reset); + + try r.tty_config.setColor(writer, .yellow); + try writer.print("line:{d}", .{line_start + 1}); + try r.tty_config.setColor(writer, .reset); + + try r.tty_config.setColor(writer, .white); + try writer.writeByte(','); + try r.tty_config.setColor(writer, .reset); + + try writer.writeByte(' '); + + try r.tty_config.setColor(writer, .yellow); + try writer.print("line:{d}", .{line_end + 1}); + try r.tty_config.setColor(writer, .reset); + + try r.tty_config.setColor(writer, .white); + try writer.writeByte('>'); + try r.tty_config.setColor(writer, .reset); +} + +fn writeColumnSpan(r: *Render, writer: *std.Io.Writer, node: *const Ast.Node) !void { + const line_start = r.lines.calculateLine(node.source_start); + const line_range = r.lines.ranges.items[line_start]; + const column_start = (node.source_start - line_range.start); + const column_end = (node.source_end - line_range.start); + + try r.tty_config.setColor(writer, .white); + try writer.writeByte('<'); + try r.tty_config.setColor(writer, .reset); + + try r.tty_config.setColor(writer, .yellow); + try writer.print("col:{d}", .{column_start + 1}); + try r.tty_config.setColor(writer, .reset); + + try r.tty_config.setColor(writer, .white); + try writer.writeByte(','); + try r.tty_config.setColor(writer, .reset); + + try writer.writeByte(' '); + + try r.tty_config.setColor(writer, .yellow); + try writer.print("col:{d}", .{column_end + 1}); + try r.tty_config.setColor(writer, .reset); + + try r.tty_config.setColor(writer, .white); + try writer.writeByte('>'); + try r.tty_config.setColor(writer, .reset); +} + +fn writeLineColumnSpan(r: *Render, writer: *std.Io.Writer, node: *const Ast.Node) !void { + const line_start = r.lines.calculateLine(node.source_start); + const line_range = r.lines.ranges.items[line_start]; + const column_start = (node.source_start - line_range.start); + const column_end = (node.source_end - line_range.start); + + try r.tty_config.setColor(writer, .white); + try writer.writeByte('<'); + try r.tty_config.setColor(writer, .reset); + + try r.tty_config.setColor(writer, .yellow); + try writer.print("line:{d}", .{line_start + 1}); + + try r.tty_config.setColor(writer, .white); + try writer.writeByte(','); + try r.tty_config.setColor(writer, .reset); + + try writer.writeByte(' '); + + try r.tty_config.setColor(writer, .yellow); + try writer.print("col:{d}:{d}", .{ column_start + 1, column_end + 1 }); + try r.tty_config.setColor(writer, .reset); + + try r.tty_config.setColor(writer, .white); + try writer.writeByte('>'); + try r.tty_config.setColor(writer, .reset); +} + +const NodeOptions = struct { + is_last: bool, + is_root: bool, +}; + +fn renderAstNode( + r: *Render, + writer: *std.Io.Writer, + node: *const Ast.Node, + options: NodeOptions, +) !void { + if (!options.is_root) { + try r.tty_config.setColor(writer, .blue); + try r.prefix.writeIndent(writer); + if (options.is_last) { + try writer.writeAll("`--"); + } else { + try writer.writeAll("|--"); + } + try r.tty_config.setColor(writer, .reset); + } + + switch (node.tag) { + .file => { + try r.writeType(writer, node); + try writer.writeByte(' '); + try writer.writeByte('"'); + try writer.writeAll(r.tree.filename); + try writer.writeByte('"'); + }, + .block_stmt, + .choice_stmt, + .function_decl, + .gathered_stmt, + .knot_decl, + .stitch_decl, + .if_stmt, + .multi_if_stmt, + .switch_stmt, + => { + try r.writeType(writer, node); + try writer.writeByte(' '); + try r.writeLineSpan(writer, node); + }, + .assign_stmt, + .choice_plus_stmt, + .choice_star_stmt, + .const_decl, + .content_stmt, + .divert_stmt, + .expr_stmt, + .gather_point_stmt, + .list_decl, + .return_stmt, + .temp_decl, + .var_decl, + => { + try r.writeType(writer, node); + try writer.writeByte(' '); + try r.writeLineColumnSpan(writer, node); + }, + .choice_start_expr, + .choice_option_expr, + .choice_inner_expr, + .identifier, + .number_literal, + .parameter_decl, + .ref_parameter_decl, + .string_literal, + .string_expr, + => { + try r.writeType(writer, node); + try writer.writeByte(' '); + try r.writeLexeme(writer, node); + try writer.writeByte(' '); + try r.writeColumnSpan(writer, node); + }, + else => { + try r.writeType(writer, node); + try writer.writeByte(' '); + try r.writeColumnSpan(writer, node); + }, + } + try writer.writeByte('\n'); + return writer.flush(); +} + +fn renderAstWalk( + r: *Render, + writer: *std.Io.Writer, + node: *const Ast.Node, + options: NodeOptions, +) !void { + var children: std.ArrayListUnmanaged(?*const Ast.Node) = .empty; + defer children.deinit(r.gpa); + + try renderAstNode(r, writer, node, options); + switch (node.tag) { + .false_literal, + .true_literal, + .number_literal, + .string_literal, + .empty_string, + .identifier, + .parameter_decl, + .ref_parameter_decl, + .choice_start_expr, + .choice_option_expr, + .choice_inner_expr, + => {}, + .file, + .argument_list, + .parameter_list, + .block_stmt, + .choice_stmt, + .content, + => { + const list = node.data.list.items; + if (list) |items| for (items) |n| { + try children.append(r.gpa, n); + }; + }, + .choice_expr => { + const lhs = node.data.choice_expr.start_expr; + const mhs = node.data.choice_expr.option_expr; + const rhs = node.data.choice_expr.inner_expr; + + if (lhs) |n| try children.append(r.gpa, n); + if (mhs) |n| try children.append(r.gpa, n); + if (rhs) |n| try children.append(r.gpa, n); + }, + .add_expr, + .subtract_expr, + .multiply_expr, + .divide_expr, + .mod_expr, + .negate_expr, + .logical_equality_expr, + .logical_inequality_expr, + .logical_and_expr, + .logical_or_expr, + .logical_not_expr, + .logical_greater_expr, + .logical_greater_or_equal_expr, + .logical_lesser_or_equal_expr, + .logical_lesser_expr, + .assign_stmt, + .divert_stmt, + .return_stmt, + .expr_stmt, + .const_decl, + .var_decl, + .list_decl, + .temp_decl, + .content_stmt, + .choice_star_stmt, + .choice_plus_stmt, + .gather_point_stmt, + .gathered_stmt, + .function_prototype, + .stitch_prototype, + .knot_prototype, + .function_decl, + .stitch_decl, + .string_expr, + .divert_expr, + .selector_expr, + .call_expr, + .switch_case, + .if_branch, + .else_branch, + .invalid, + => { + const lhs = node.data.bin.lhs; + const rhs = node.data.bin.rhs; + + if (lhs) |n| try children.append(r.gpa, n); + if (rhs) |n| try children.append(r.gpa, n); + }, + .knot_decl => { + const lhs = node.data.knot_decl.prototype; + try children.append(r.gpa, lhs); + + const list = node.data.knot_decl.children; + if (list) |items| for (items) |n| { + try children.append(r.gpa, n); + }; + }, + .switch_stmt, .if_stmt, .multi_if_stmt => { + const expr = node.data.switch_stmt.condition_expr; + if (expr) |n| try children.append(r.gpa, n); + + const list = node.data.switch_stmt.cases; + if (list) |items| for (items) |n| { + try children.append(r.gpa, n); + }; + }, + .inline_logic_expr => { + const lhs = node.data.bin.lhs; + if (lhs) |n| try children.append(r.gpa, n); + }, + } + for (children.items, 0..) |maybe_child, i| { + const old_len = if (!options.is_root) + try r.prefix.pushChildPrefix(r.gpa, options.is_last) + else + 0; + const child_is_last = (i == children.items.len - 1); + + if (maybe_child) |child| { + try renderAstWalk(r, writer, child, .{ + .is_last = child_is_last, + .is_root = false, + }); + } + if (!options.is_root) r.prefix.restore(old_len); + } +} + +pub const Options = struct { + use_color: bool, +}; + +pub fn renderTree( + gpa: std.mem.Allocator, + writer: *std.Io.Writer, + ast: *const Ast, + options: Options, +) !void { + var r: Render = .{ + .gpa = gpa, + .tree = ast, + .tty_config = if (options.use_color) .escape_codes else .no_color, + .prefix = .{}, + .lines = try LineCache.build(gpa, ast.source), + }; + defer r.prefix.deinit(gpa); + defer r.lines.deinit(gpa); + + if (ast.root) |root| { + try r.renderAstWalk(writer, root, .{ + .is_root = true, + .is_last = true, + }); + } + try writer.flush(); +} + +pub fn renderErrors( + gpa: std.mem.Allocator, + writer: *std.Io.Writer, + ast: *const Ast, + options: Options, +) !void { + var r: Render = .{ + .gpa = gpa, + .tree = ast, + .tty_config = if (options.use_color) .escape_codes else .no_color, + .prefix = .{}, + .lines = try LineCache.build(gpa, ast.source), + }; + defer r.prefix.deinit(gpa); + defer r.lines.deinit(gpa); + + for (ast.errors) |err| try r.renderError(writer, err); + try writer.flush(); +} diff --git a/src/Parse.zig b/src/Parse.zig new file mode 100644 index 0000000..91b065c --- /dev/null +++ b/src/Parse.zig @@ -0,0 +1,67 @@ +const std = @import("std"); +const assert = std.debug.assert; +const tok = @import("tokenizer.zig"); +const Token = tok.Token; +const Tokenizer = tok.Tokenizer; +const Ast = @import("Ast.zig"); + +const Parse = @This(); + +gpa: std.mem.Allocator, +arena: std.mem.Allocator, +tokenizer: Tokenizer, +token: Token, +scratch: std.ArrayListUnmanaged(*Ast.Node), +grammar_stack: std.ArrayListUnmanaged(ScanContext), +block_stack: std.ArrayListUnmanaged(State), +choice_stack: std.ArrayListUnmanaged(State), +errors: std.ArrayListUnmanaged(Ast.Error), +panic_mode: bool, +flags: u32, +max_parse_depth: usize, +max_argument_count: usize, +knot_offset: usize = 0, + +pub const Error = error{ + ParseError, + OutOfMemory, +}; + +pub const State = struct { + level: usize, + scratch_offset: usize, + source_offset: usize, +}; + +pub const StmtContext = struct { + tag: Tag, + is_block_created: bool = false, + expression_node: ?*Ast.Node = null, + level: usize = 0, + blocks_top: usize, + choices_top: usize, + scratch_top: usize, + + pub const Tag = enum { + block, + conditional, + }; +}; + +pub const ScanContext = struct { + grammar: Tokenizer.Grammar, + source_offset: usize, +}; + +pub fn deinit(p: *Parse) void { + p.scratch.deinit(p.gpa); + p.grammar_stack.deinit(p.gpa); + p.block_stack.deinit(p.gpa); + p.choice_stack.deinit(p.gpa); + p.errors.deinit(p.gpa); + p.* = undefined; +} + +pub fn parseFile(_: *Parse) Error!*Ast.Node { + return error.OutOfMemory; +} diff --git a/src/main.zig b/src/main.zig index 0e58df0..be88392 100644 --- a/src/main.zig +++ b/src/main.zig @@ -58,7 +58,12 @@ fn mainArgs( }; }; - var story = try ink.Story.loadFromString(gpa, source_bytes, .{}); + const stdout = std.fs.File.stdout(); + var stdout_writer = stdout.writer(&stdout_buffer); + + var story = try ink.Story.loadFromString(gpa, source_bytes, .{ + .stream_writer = &stdout_writer.interface, + }); defer story.deinit(); } diff --git a/src/root.zig b/src/root.zig index 466b44e..cf574e2 100644 --- a/src/root.zig +++ b/src/root.zig @@ -1,15 +1,30 @@ const std = @import("std"); const tokenizer = @import("tokenizer.zig"); +pub const Ast = @import("Ast.zig"); pub const Story = struct { - pub const LoadOptions = struct {}; + pub const LoadOptions = struct { + stream_writer: *std.Io.Writer, + use_color: bool = true, + }; pub fn loadFromString( - _: std.mem.Allocator, + gpa: std.mem.Allocator, source_bytes: [:0]const u8, - _: LoadOptions, + options: LoadOptions, ) !Story { - std.debug.print("{s}\n", .{source_bytes}); + var ast = try Ast.parse(gpa, source_bytes, "", 0); + defer ast.deinit(gpa); + + try ast.render(gpa, options.stream_writer, .{ + .use_color = options.use_color, + }); + if (ast.errors.len > 0) { + try ast.renderErrors(gpa, options.stream_writer, .{ + .use_color = options.use_color, + }); + return error.Invalid; + } return .{}; } pub fn deinit(_: *Story) void {}