From f82b361005b23344e8ddedae14aec85352c83c37 Mon Sep 17 00:00:00 2001 From: Brett Broadhurst Date: Fri, 3 Apr 2026 11:24:47 -0600 Subject: [PATCH] feat: assignment operations --- src/Ast.zig | 2 + src/Ast/Render.zig | 4 ++ src/AstGen.zig | 70 ++++++++----------- src/Parse.zig | 27 ++++--- src/Story/runtime_tests.zig | 4 ++ src/Story/testdata/assignment-op/input.txt | 0 src/Story/testdata/assignment-op/story.ink | 5 ++ .../testdata/assignment-op/transcript.txt | 1 + src/tokenizer.zig | 14 +++- 9 files changed, 79 insertions(+), 48 deletions(-) create mode 100644 src/Story/testdata/assignment-op/input.txt create mode 100644 src/Story/testdata/assignment-op/story.ink create mode 100644 src/Story/testdata/assignment-op/transcript.txt diff --git a/src/Ast.zig b/src/Ast.zig index f1815d4..ac73e35 100644 --- a/src/Ast.zig +++ b/src/Ast.zig @@ -51,6 +51,8 @@ pub const Node = struct { divert_expr, selector_expr, assign_stmt, + assign_add_stmt, + assign_sub_stmt, block_stmt, content_stmt, divert_stmt, diff --git a/src/Ast/Render.zig b/src/Ast/Render.zig index 5bba3f1..7a969f6 100644 --- a/src/Ast/Render.zig +++ b/src/Ast/Render.zig @@ -83,6 +83,8 @@ fn nodeTagToString(tag: Ast.Node.Tag) []const u8 { .call_expr => "CallExpr", .choice_expr => "ChoiceContentExpr", .assign_stmt => "AssignStmt", + .assign_add_stmt => "AssignAddStmt", + .assign_sub_stmt => "AssignSubStmt", .block_stmt => "BlockStmt", .content_stmt => "ContentStmt", .divert_stmt => "DivertStmt", @@ -396,6 +398,8 @@ fn renderAstWalk( .logical_lesser_or_equal_expr, .logical_lesser_expr, .assign_stmt, + .assign_add_stmt, + .assign_sub_stmt, .divert_stmt, .return_stmt, .expr_stmt, diff --git a/src/AstGen.zig b/src/AstGen.zig index 32fd579..1e21f2a 100644 --- a/src/AstGen.zig +++ b/src/AstGen.zig @@ -783,44 +783,8 @@ fn expr(gi: *GenIr, scope: *Scope, optional_node: ?*const Ast.Node) InnerError!I .logical_lesser_expr => return binaryOp(gi, scope, node, .cmp_lt), .logical_lesser_or_equal_expr => return binaryOp(gi, scope, node, .cmp_lte), .call_expr => return callExpr(gi, scope, node, .call), - .choice_expr => unreachable, - .divert_expr => unreachable, .selector_expr => return fieldAccess(gi, scope, node), - .assign_stmt => unreachable, - .block_stmt => unreachable, - .content_stmt => unreachable, - .divert_stmt => unreachable, - .return_stmt => unreachable, - .expr_stmt => unreachable, - .choice_stmt => unreachable, - .choice_star_stmt => unreachable, - .choice_plus_stmt => unreachable, - .gather_point_stmt => unreachable, - .gathered_stmt => unreachable, - .function_prototype => unreachable, - .stitch_prototype => unreachable, - .knot_prototype => unreachable, - .function_decl => unreachable, - .stitch_decl => unreachable, - .knot_decl => unreachable, - .const_decl => unreachable, - .var_decl => unreachable, - .list_decl => unreachable, - .temp_decl => unreachable, - .parameter_decl => unreachable, - .ref_parameter_decl => unreachable, - .argument_list => unreachable, - .parameter_list => unreachable, - .switch_stmt => unreachable, // Handled in switchStmt - .switch_case => unreachable, // Handled in switchStmt - .if_stmt => unreachable, // Handled in ifStmt - .multi_if_stmt => unreachable, // Handled in multiIfStmt - .if_branch => unreachable, // Handled in ifStmt and multiIfStmt - .else_branch => unreachable, // Handled in switchStmt, multiIfStmt, and ifStmt - .content => unreachable, - .inline_logic_expr => unreachable, - .inline_if_stmt => unreachable, - .invalid => unreachable, + inline else => |_| unreachable, } } @@ -1164,7 +1128,7 @@ fn contentStmt(gi: *GenIr, scope: *Scope, node: *const Ast.Node) !void { } } -fn assignStmt(gi: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!void { +fn assign(gi: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!void { const astgen = gi.astgen; const identifier_node = node.data.bin.lhs.?; const expr_node = node.data.bin.rhs.?; @@ -1178,6 +1142,32 @@ fn assignStmt(gi: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!void return fail(astgen, identifier_node, "unknown identifier", .{}); } +fn assignOp( + gi: *GenIr, + scope: *Scope, + node: *const Ast.Node, + op_inst_tag: Ir.Inst.Tag, +) InnerError!void { + const astgen = gi.astgen; + const identifier_node = node.data.bin.lhs.?; + const expr_node = node.data.bin.rhs.?; + const name_str = try astgen.strFromNode(identifier_node); + + if (scope.lookup(name_str.index)) |decl| { + const lhs = try gi.addUnaryNode(.load, decl.inst_index.toRef()); + const rhs = try expr(gi, scope, expr_node); + + const result = switch (op_inst_tag) { + .add => try gi.addBinaryNode(.add, lhs, rhs), + .sub => try gi.addBinaryNode(.sub, lhs, rhs), + else => unreachable, + }; + _ = try gi.addBinaryNode(.store, decl.inst_index.toRef(), result); + return; + } + return fail(astgen, identifier_node, "unknown identifier", .{}); +} + fn choiceStmt(gi: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!void { const astgen = gi.astgen; const gpa = astgen.gpa; @@ -1540,7 +1530,9 @@ fn blockInner(gi: *GenIr, parent_scope: *Scope, stmt_list: []*Ast.Node) !void { .var_decl => try varDecl(gi, &child_scope, node), .const_decl => try varDecl(gi, &child_scope, node), .temp_decl => try tempDecl(gi, &child_scope, node), - .assign_stmt => try assignStmt(gi, &child_scope, node), + .assign_stmt => try assign(gi, &child_scope, node), + .assign_add_stmt => try assignOp(gi, &child_scope, node, .add), + .assign_sub_stmt => try assignOp(gi, &child_scope, node, .sub), .content_stmt => try contentStmt(gi, &child_scope, node), .choice_stmt => try choiceStmt(gi, &child_scope, node), .expr_stmt => try exprStmt(gi, &child_scope, node), diff --git a/src/Parse.zig b/src/Parse.zig index 0e46110..7a921a2 100644 --- a/src/Parse.zig +++ b/src/Parse.zig @@ -111,6 +111,15 @@ fn getTokenInfixType(tag: Token.Tag) Ast.Node.Tag { }; } +fn getTokenAssignType(tag: Token.Tag) ?Ast.Node.Tag { + return switch (tag) { + .equal => .assign_stmt, + .plus_equal => .assign_add_stmt, + .minus_equal => .assign_sub_stmt, + else => null, + }; +} + fn getBindingPower(tag: Token.Tag) Precedence { return switch (tag) { .ampersand_ampersand, .keyword_and => .logical_and, @@ -703,16 +712,18 @@ fn parseAssignStmt(p: *Parse) Error!*Ast.Node { const main_token = p.token; const lhs = try parseIdentifierExpr(p); - if (!p.checkToken(.equal)) return parseExprStmt(p, lhs); - _ = p.nextToken(); + if (getTokenAssignType(p.token.tag)) |op| { + _ = p.nextToken(); - const rhs = try p.expectExpr(); - _ = try p.expectNewline(); + const rhs = try p.expectExpr(); + _ = try p.expectNewline(); - return .createBinary(p.arena, .assign_stmt, .{ - .start = main_token.loc.start, - .end = p.token.loc.start, - }, lhs, rhs); + return .createBinary(p.arena, op, .{ + .start = main_token.loc.start, + .end = p.token.loc.start, + }, lhs, rhs); + } + return parseExprStmt(p, lhs); } fn parseTempDecl(p: *Parse) Error!*Ast.Node { diff --git a/src/Story/runtime_tests.zig b/src/Story/runtime_tests.zig index ee82555..c9ac936 100644 --- a/src/Story/runtime_tests.zig +++ b/src/Story/runtime_tests.zig @@ -6,6 +6,10 @@ test "fixture - variable arithmetic" { try testRuntimeFixture("variable-arithmetic"); } +test "fixture - assignment op" { + try testRuntimeFixture("assignment-op"); +} + test "fixture - constant folding" { try testRuntimeFixture("constant-folding"); } diff --git a/src/Story/testdata/assignment-op/input.txt b/src/Story/testdata/assignment-op/input.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/Story/testdata/assignment-op/story.ink b/src/Story/testdata/assignment-op/story.ink new file mode 100644 index 0000000..db14cf0 --- /dev/null +++ b/src/Story/testdata/assignment-op/story.ink @@ -0,0 +1,5 @@ +~ temp foo = 0 +~ foo = 1 +~ foo += 4 +~ foo -= 2 +{foo} diff --git a/src/Story/testdata/assignment-op/transcript.txt b/src/Story/testdata/assignment-op/transcript.txt new file mode 100644 index 0000000..00750ed --- /dev/null +++ b/src/Story/testdata/assignment-op/transcript.txt @@ -0,0 +1 @@ +3 diff --git a/src/tokenizer.zig b/src/tokenizer.zig index 588f8b5..f131bde 100644 --- a/src/tokenizer.zig +++ b/src/tokenizer.zig @@ -119,6 +119,7 @@ pub const Tokenizer = struct { slash, equal, bang, + plus, pipe, less_than, greater_than, @@ -197,7 +198,7 @@ pub const Tokenizer = struct { }, '+' => { self.index += 1; - result.tag = .plus; + continue :state .plus; }, '-' => { self.index += 1; @@ -284,6 +285,13 @@ pub const Tokenizer = struct { } }, }, + .plus => switch (self.buffer[self.index]) { + '=' => { + self.index += 1; + result.tag = .plus_equal; + }, + else => result.tag = .plus, + }, .pipe => switch (self.buffer[self.index]) { '|' => { self.index += 1; @@ -296,6 +304,10 @@ pub const Tokenizer = struct { self.index += 1; result.tag = .right_arrow; }, + '=' => { + self.index += 1; + result.tag = .minus_equal; + }, else => result.tag = .minus, }, .slash => switch (self.buffer[self.index]) {