lib/python/src/compile/compiler.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 //! The compiler walks a program's syntax tree once, in source order, and appends stack-machine
  2 //! instructions to a chunk as it goes.
  3 //!
  4 //! The compiler has to turn nested control flow into jumps, keep the value stack balanced on every
  5 //! path, and reject programs whose `break`, `continue`, `return` or `def` lacks a valid place.
  6 //!
  7 //! A jump forward goes to code yet to be generated, so its target is unknown when the jump is
  8 //! emitted. A `for` loop keeps its iterator on the value stack while the body runs, so leaving the
  9 //! loop early by `break` or `return` has to drop it. Python's `and` and `or` return the operand
 10 //! that decided them, and when the left operand decides, the right one is skipped. A chained
 11 //! comparison computes each middle operand once and stops at the first false result.
 12 //!
 13 //! The compiled code follows the evaluation rules of the [Python 3.14 language
 14 //! reference](https://docs.python.org/3.14/reference/) for these constructs, and the tests check
 15 //! them: `and` and `or` return an operand, a chained comparison short-circuits, and a loop's `else`
 16 //! block runs when the loop ends without `break`.
 17 //!
 18 //! The compiler emits a forward jump with a placeholder target, keeps the jump's position, and
 19 //! fills in the target once the code after the jump exists. Each loop records how many values a
 20 //! `break` has to pop and the positions of the jumps that `break` emitted, so that, when the loop
 21 //! ends, it can point those jumps past its `else` block. A `return` pops the iterators of every
 22 //! loop around it before it returns. At top level, an expression statement records its value with
 23 //! `save`, so the program's value is the last one recorded. Inside a function, the compiler pops an
 24 //! expression statement's value. A `def` inside a function body fails with `NestedFunction`, so
 25 //! every function lives in the top-level chunk. The chunk borrows names, parameter lists and string
 26 //! constants from the syntax tree and the source text, so both have to outlive it.
 27 const std = @import("std");
 28 const syntax = @import("../syntax/root.zig");
 29 const code = @import("../code/root.zig");
 30 
 31 const CompileError = error{
 32     BreakOutsideLoop,
 33     ContinueOutsideLoop,
 34     NestedFunction,
 35     TopLevelReturn,
 36 };
 37 
 38 /// The errors `compile` returns: four for programs it rejects, and `error.OutOfMemory`. A caller
 39 /// switches on this error set to report why a parsed program failed to compile. `BreakOutsideLoop`
 40 /// and `ContinueOutsideLoop` mean a `break` or `continue` outside every loop body and every loop's
 41 /// `else` block. `NestedFunction` means a `def` inside a function body. `TopLevelReturn` means a
 42 /// `return` outside every function. An error carries no position in the source.
 43 pub const Error = CompileError || std.mem.Allocator.Error;
 44 
 45 /// Compiles a parsed program into a new top-level chunk that ends with `ret`. A caller that wants
 46 /// the bytecode of a program calls this function directly, and the package's `execute` calls it
 47 /// after `parse`. The function reads the program without changing it. The call allocates the
 48 /// chunk's lists and function bodies from the given allocator. The caller owns the result and frees
 49 /// it with `Chunk.deinit` and the same allocator. The chunk borrows names, parameter lists and
 50 /// string constants from the program's syntax tree and from the source text, so both have to
 51 /// outlive it. The call returns `BreakOutsideLoop`, `ContinueOutsideLoop`, `NestedFunction` or
 52 /// `TopLevelReturn` for a program it rejects, and `error.OutOfMemory` when an allocation fails. On
 53 /// any error the function frees everything it allocated.
 54 pub fn compile(allocator: std.mem.Allocator, program: *const syntax.Program) Error!code.Chunk {
 55     var compiler = Compiler{
 56         .allocator = allocator,
 57         .chunk = .{},
 58         .loop = null,
 59         .top_level = true,
 60     };
 61     errdefer compiler.chunk.deinit(allocator);
 62     try compiler.statements(program.statements);
 63     try compiler.chunk.emit(allocator, .{ .op = .ret });
 64     return compiler.chunk;
 65 }
 66 
 67 const Compiler = struct {
 68     allocator: std.mem.Allocator,
 69     chunk: code.Chunk,
 70     loop: ?*Loop,
 71     top_level: bool,
 72 
 73     fn statements(self: *Compiler, statement_nodes: []const syntax.Statement) Error!void {
 74         for (statement_nodes) |statement_node| try self.statement(statement_node);
 75     }
 76 
 77     fn statement(self: *Compiler, statement_node: syntax.Statement) Error!void {
 78         switch (statement_node) {
 79             .expression => |expression_node| {
 80                 try self.expression(expression_node);
 81                 try self.chunk.emit(self.allocator, .{ .op = if (self.top_level) .save else .pop });
 82             },
 83             .assign => |assign| {
 84                 try self.expression(assign.value);
 85                 const name = try self.chunk.addName(self.allocator, assign.name);
 86                 try self.chunk.emit(self.allocator, .{ .op = .store, .operand = name });
 87             },
 88             .subscript_assign => |assign| {
 89                 try self.expression(assign.target);
 90                 try self.expression(assign.index);
 91                 try self.expression(assign.value);
 92                 try self.chunk.emit(self.allocator, .{ .op = .store_subscript });
 93             },
 94             .delete => |delete| switch (delete) {
 95                 .name => |name_value| {
 96                     const name = try self.chunk.addName(self.allocator, name_value);
 97                     try self.chunk.emit(self.allocator, .{ .op = .delete, .operand = name });
 98                 },
 99                 .subscript => |subscript| {
100                     try self.expression(subscript.target);
101                     try self.expression(subscript.index);
102                     try self.chunk.emit(self.allocator, .{ .op = .delete_subscript });
103                 },
104             },
105             .break_stmt => try self.breakStatement(),
106             .continue_stmt => try self.continueStatement(),
107             .function => |function_node| try self.function(function_node),
108             .for_stmt => |for_stmt| try self.forStatement(for_stmt),
109             .if_stmt => |if_stmt| try self.ifStatement(if_stmt),
110             .pass => {},
111             .return_stmt => |return_stmt| {
112                 if (self.top_level) return Error.TopLevelReturn;
113                 try self.emitLoopCleanup();
114                 if (return_stmt.value) |value| {
115                     try self.expression(value);
116                 } else {
117                     const none = try self.chunk.addConstant(self.allocator, .none);
118                     try self.chunk.emit(self.allocator, .{ .op = .constant, .operand = none });
119                 }
120                 try self.chunk.emit(self.allocator, .{ .op = .return_value });
121             },
122             .while_stmt => |while_stmt| try self.whileStatement(while_stmt),
123         }
124     }
125 
126     fn function(self: *Compiler, function_node: syntax.ast.Function) Error!void {
127         if (!self.top_level) return Error.NestedFunction;
128         var child = Compiler{
129             .allocator = self.allocator,
130             .chunk = .{},
131             .loop = null,
132             .top_level = false,
133         };
134         var child_owned = true;
135         errdefer if (child_owned) child.chunk.deinit(self.allocator);
136         try child.statements(function_node.body);
137         const none = try child.chunk.addConstant(self.allocator, .none);
138         try child.chunk.emit(self.allocator, .{ .op = .constant, .operand = none });
139         try child.chunk.emit(self.allocator, .{ .op = .return_value });
140         var function_value = code.Function{
141             .name = function_node.name,
142             .params = function_node.params,
143             .chunk = child.chunk,
144         };
145         child_owned = false;
146         var owned = true;
147         errdefer if (owned) function_value.deinit(self.allocator);
148         const function_index = try self.chunk.addFunction(self.allocator, function_value);
149         owned = false;
150         const constant = try self.chunk.addConstant(self.allocator, .{ .function = function_index });
151         try self.chunk.emit(self.allocator, .{ .op = .constant, .operand = constant });
152         const name = try self.chunk.addName(self.allocator, function_node.name);
153         try self.chunk.emit(self.allocator, .{ .op = .store, .operand = name });
154     }
155 
156     fn forStatement(self: *Compiler, for_stmt: syntax.ast.ForStatement) Error!void {
157         try self.expression(for_stmt.iterable);
158         try self.chunk.emit(self.allocator, .{ .op = .iter });
159         const loop_start = self.chunk.instructions.items.len;
160         const exit_jump = try self.emitJump(.for_next);
161         const name = try self.chunk.addName(self.allocator, for_stmt.name);
162         try self.chunk.emit(self.allocator, .{ .op = .store, .operand = name });
163         var loop = Loop{
164             .parent = self.loop,
165             .continue_target = loop_start,
166             .break_pops = 1,
167         };
168         defer loop.break_jumps.deinit(self.allocator);
169         self.loop = &loop;
170         defer self.loop = loop.parent;
171         try self.statements(for_stmt.body);
172         try self.emitJumpTo(loop_start);
173         self.patchJump(exit_jump);
174         try self.statements(for_stmt.otherwise);
175         for (loop.break_jumps.items) |jump| self.patchJump(jump);
176     }
177 
178     fn ifStatement(self: *Compiler, if_stmt: syntax.ast.IfStatement) Error!void {
179         try self.expression(if_stmt.condition);
180         const false_jump = try self.emitJump(.jump_if_false);
181         try self.chunk.emit(self.allocator, .{ .op = .pop });
182         try self.statements(if_stmt.body);
183         if (if_stmt.otherwise.len > 0) {
184             const end_jump = try self.emitJump(.jump);
185             self.patchJump(false_jump);
186             try self.chunk.emit(self.allocator, .{ .op = .pop });
187             try self.statements(if_stmt.otherwise);
188             self.patchJump(end_jump);
189         } else {
190             const end_jump = try self.emitJump(.jump);
191             self.patchJump(false_jump);
192             try self.chunk.emit(self.allocator, .{ .op = .pop });
193             self.patchJump(end_jump);
194         }
195     }
196 
197     fn whileStatement(self: *Compiler, while_stmt: syntax.ast.WhileStatement) Error!void {
198         const loop_start = self.chunk.instructions.items.len;
199         try self.expression(while_stmt.condition);
200         const exit_jump = try self.emitJump(.jump_if_false);
201         try self.chunk.emit(self.allocator, .{ .op = .pop });
202         var loop = Loop{
203             .parent = self.loop,
204             .continue_target = loop_start,
205         };
206         defer loop.break_jumps.deinit(self.allocator);
207         self.loop = &loop;
208         defer self.loop = loop.parent;
209         try self.statements(while_stmt.body);
210         try self.emitJumpTo(loop_start);
211         self.patchJump(exit_jump);
212         try self.chunk.emit(self.allocator, .{ .op = .pop });
213         try self.statements(while_stmt.otherwise);
214         for (loop.break_jumps.items) |jump| self.patchJump(jump);
215     }
216 
217     fn breakStatement(self: *Compiler) Error!void {
218         const loop = self.loop orelse return Error.BreakOutsideLoop;
219         try self.emitPops(loop.break_pops);
220         const jump = try self.emitJump(.jump);
221         try loop.break_jumps.append(self.allocator, jump);
222     }
223 
224     fn continueStatement(self: *Compiler) Error!void {
225         const loop = self.loop orelse return Error.ContinueOutsideLoop;
226         try self.emitJumpTo(loop.continue_target);
227     }
228 
229     fn emitLoopCleanup(self: *Compiler) std.mem.Allocator.Error!void {
230         var count: usize = 0;
231         var current = self.loop;
232         while (current) |loop| {
233             count += loop.break_pops;
234             current = loop.parent;
235         }
236         try self.emitPops(count);
237     }
238 
239     fn emitPops(self: *Compiler, count: usize) std.mem.Allocator.Error!void {
240         for (0..count) |_| try self.chunk.emit(self.allocator, .{ .op = .pop });
241     }
242 
243     fn emitJump(self: *Compiler, op: code.Op) std.mem.Allocator.Error!usize {
244         try self.chunk.emit(self.allocator, .{ .op = op });
245         return self.chunk.instructions.items.len - 1;
246     }
247 
248     fn emitJumpTo(self: *Compiler, target: usize) std.mem.Allocator.Error!void {
249         try self.chunk.emit(self.allocator, .{ .op = .jump, .operand = target });
250     }
251 
252     fn patchJump(self: *Compiler, instruction_index: usize) void {
253         self.chunk.instructions.items[instruction_index].operand = self.chunk.instructions.items.len;
254     }
255 
256     fn expression(self: *Compiler, expression_node: *const syntax.Expression) Error!void {
257         switch (expression_node.*) {
258             .none => {
259                 const constant = try self.chunk.addConstant(self.allocator, .none);
260                 try self.chunk.emit(self.allocator, .{ .op = .constant, .operand = constant });
261             },
262             .boolean => |value| {
263                 const constant = try self.chunk.addConstant(self.allocator, .{ .boolean = value });
264                 try self.chunk.emit(self.allocator, .{ .op = .constant, .operand = constant });
265             },
266             .integer => |value| {
267                 const constant = try self.chunk.addConstant(self.allocator, .{ .integer = value });
268                 try self.chunk.emit(self.allocator, .{ .op = .constant, .operand = constant });
269             },
270             .string => |value| {
271                 const constant = try self.chunk.addConstant(self.allocator, .{ .string = value });
272                 try self.chunk.emit(self.allocator, .{ .op = .constant, .operand = constant });
273             },
274             .name => |name| {
275                 const index = try self.chunk.addName(self.allocator, name);
276                 try self.chunk.emit(self.allocator, .{ .op = .load, .operand = index });
277             },
278             .unary => |unary| {
279                 try self.expression(unary.operand);
280                 switch (unary.op) {
281                     .negate => try self.chunk.emit(self.allocator, .{ .op = .neg }),
282                     .not => try self.chunk.emit(self.allocator, .{ .op = .not }),
283                 }
284             },
285             .binary => |binary| {
286                 try self.expression(binary.left);
287                 try self.expression(binary.right);
288                 try self.chunk.emit(self.allocator, .{ .op = switch (binary.op) {
289                     .add => .add,
290                     .sub => .sub,
291                     .mul => .mul,
292                 } });
293             },
294             .comparison => |comparison_node| try self.comparison(comparison_node),
295             .logical => |logical_node| try self.logical(logical_node),
296             .call => |call| {
297                 try self.expression(call.target);
298                 for (call.arguments) |argument| try self.expression(argument);
299                 try self.chunk.emit(self.allocator, .{ .op = .call, .operand = call.arguments.len });
300             },
301             .attribute => |attribute| {
302                 try self.expression(attribute.target);
303                 const index = try self.chunk.addName(self.allocator, attribute.name);
304                 try self.chunk.emit(self.allocator, .{ .op = .attribute, .operand = index });
305             },
306             .list => |list| {
307                 for (list.items) |item| try self.expression(item);
308                 try self.chunk.emit(self.allocator, .{ .op = .build_list, .operand = list.items.len });
309             },
310             .tuple => |tuple| {
311                 for (tuple.items) |item| try self.expression(item);
312                 try self.chunk.emit(self.allocator, .{ .op = .build_tuple, .operand = tuple.items.len });
313             },
314             .dict => |dict| {
315                 for (dict.items) |item| {
316                     try self.expression(item.key);
317                     try self.expression(item.value);
318                 }
319                 try self.chunk.emit(self.allocator, .{ .op = .build_dict, .operand = dict.items.len });
320             },
321             .subscript => |subscript| {
322                 try self.expression(subscript.target);
323                 switch (subscript.selector) {
324                     .index => |index| {
325                         try self.expression(index);
326                         try self.chunk.emit(self.allocator, .{ .op = .subscript });
327                     },
328                     .slice => |slice| {
329                         try self.optionalExpression(slice.start);
330                         try self.optionalExpression(slice.stop);
331                         try self.optionalExpression(slice.step);
332                         try self.chunk.emit(self.allocator, .{ .op = .slice });
333                     },
334                 }
335             },
336         }
337     }
338 
339     fn optionalExpression(self: *Compiler, expression_node: ?*const syntax.Expression) Error!void {
340         if (expression_node) |node| {
341             try self.expression(node);
342         } else {
343             const constant = try self.chunk.addConstant(self.allocator, .none);
344             try self.chunk.emit(self.allocator, .{ .op = .constant, .operand = constant });
345         }
346     }
347 
348     fn comparison(self: *Compiler, comparison_node: syntax.ast.Comparison) Error!void {
349         try self.expression(comparison_node.left);
350         var false_jumps = std.ArrayListUnmanaged(usize).empty;
351         defer false_jumps.deinit(self.allocator);
352         for (comparison_node.terms, 0..) |term, index| {
353             const last = index + 1 == comparison_node.terms.len;
354             try self.expression(term.right);
355             if (!last) {
356                 try self.chunk.emit(self.allocator, .{ .op = .dup });
357                 try self.chunk.emit(self.allocator, .{ .op = .rotate_three });
358             }
359             try self.chunk.emit(self.allocator, .{ .op = comparisonOp(term.op) });
360             if (!last) {
361                 const false_jump = try self.emitJump(.jump_if_false);
362                 try false_jumps.append(self.allocator, false_jump);
363                 try self.chunk.emit(self.allocator, .{ .op = .pop });
364             }
365         }
366         if (false_jumps.items.len > 0) {
367             const end_jump = try self.emitJump(.jump);
368             for (false_jumps.items) |jump| self.patchJump(jump);
369             try self.chunk.emit(self.allocator, .{ .op = .swap });
370             try self.chunk.emit(self.allocator, .{ .op = .pop });
371             self.patchJump(end_jump);
372         }
373     }
374 
375     fn comparisonOp(op: syntax.ast.ComparisonOp) code.Op {
376         return switch (op) {
377             .equal => .equal,
378             .not_equal => .not_equal,
379             .less => .less,
380             .less_equal => .less_equal,
381             .greater => .greater,
382             .greater_equal => .greater_equal,
383             .contains => .contains,
384             .not_contains => .not_contains,
385             .identical => .identical,
386             .not_identical => .not_identical,
387         };
388     }
389 
390     fn logical(self: *Compiler, logical_node: syntax.ast.Logical) Error!void {
391         switch (logical_node.op) {
392             .and_op => {
393                 try self.expression(logical_node.left);
394                 const false_jump = try self.emitJump(.jump_if_false);
395                 try self.chunk.emit(self.allocator, .{ .op = .pop });
396                 try self.expression(logical_node.right);
397                 self.patchJump(false_jump);
398             },
399             .or_op => {
400                 try self.expression(logical_node.left);
401                 const false_jump = try self.emitJump(.jump_if_false);
402                 const end_jump = try self.emitJump(.jump);
403                 self.patchJump(false_jump);
404                 try self.chunk.emit(self.allocator, .{ .op = .pop });
405                 try self.expression(logical_node.right);
406                 self.patchJump(end_jump);
407             },
408         }
409     }
410 };
411 
412 const Loop = struct {
413     parent: ?*Loop,
414     continue_target: usize,
415     break_pops: usize = 0,
416     break_jumps: std.ArrayListUnmanaged(usize) = .empty,
417 };
418 
419 test "compile emits bytecode" {
420     const bytes = "x = 1\nx + 2";
421     var stream = try @import("../source/root.zig").tokenize(std.testing.allocator, bytes);
422     defer stream.deinit(std.testing.allocator);
423     var program = try @import("../syntax/root.zig").parse(std.testing.allocator, bytes, stream.tokens);
424     defer program.deinit();
425     var chunk_value = try compile(std.testing.allocator, &program);
426     defer chunk_value.deinit(std.testing.allocator);
427 
428     try std.testing.expect(chunk_value.instructions.items.len > 0);
429     try std.testing.expectEqualStrings("x", chunk_value.names.items[0]);
430 }
431 
432 test "compile rejects top level return" {
433     const bytes = "return 1";
434     var stream = try @import("../source/root.zig").tokenize(std.testing.allocator, bytes);
435     defer stream.deinit(std.testing.allocator);
436     var program = try @import("../syntax/root.zig").parse(std.testing.allocator, bytes, stream.tokens);
437     defer program.deinit();
438 
439     try std.testing.expectError(Error.TopLevelReturn, compile(std.testing.allocator, &program));
440 }
441 
442 test "compile rejects loop control outside loops" {
443     {
444         const bytes = "break";
445         var stream = try @import("../source/root.zig").tokenize(std.testing.allocator, bytes);
446         defer stream.deinit(std.testing.allocator);
447         var program = try @import("../syntax/root.zig").parse(std.testing.allocator, bytes, stream.tokens);
448         defer program.deinit();
449 
450         try std.testing.expectError(Error.BreakOutsideLoop, compile(std.testing.allocator, &program));
451     }
452     {
453         const bytes = "continue";
454         var stream = try @import("../source/root.zig").tokenize(std.testing.allocator, bytes);
455         defer stream.deinit(std.testing.allocator);
456         var program = try @import("../syntax/root.zig").parse(std.testing.allocator, bytes, stream.tokens);
457         defer program.deinit();
458 
459         try std.testing.expectError(Error.ContinueOutsideLoop, compile(std.testing.allocator, &program));
460     }
461 }
462 
463 test "compile emits jumps for control flow" {
464     const bytes =
465         \\x = 0
466         \\while x < 3:
467         \\    if x == 1:
468         \\        pass
469         \\    else:
470         \\        x = x + 1
471         \\    x = x + 1
472         \\x
473     ;
474     var stream = try @import("../source/root.zig").tokenize(std.testing.allocator, bytes);
475     defer stream.deinit(std.testing.allocator);
476     var program = try @import("../syntax/root.zig").parse(std.testing.allocator, bytes, stream.tokens);
477     defer program.deinit();
478     var chunk_value = try compile(std.testing.allocator, &program);
479     defer chunk_value.deinit(std.testing.allocator);
480 
481     var has_jump = false;
482     var has_jump_if_false = false;
483     for (chunk_value.instructions.items) |instruction| {
484         if (instruction.op == .jump) has_jump = true;
485         if (instruction.op == .jump_if_false) has_jump_if_false = true;
486     }
487     try std.testing.expect(has_jump);
488     try std.testing.expect(has_jump_if_false);
489 }
490 
491 test "compile emits jumps for logical operators" {
492     const bytes = "x = True or missing\nx and 7";
493     var stream = try @import("../source/root.zig").tokenize(std.testing.allocator, bytes);
494     defer stream.deinit(std.testing.allocator);
495     var program = try @import("../syntax/root.zig").parse(std.testing.allocator, bytes, stream.tokens);
496     defer program.deinit();
497     var chunk_value = try compile(std.testing.allocator, &program);
498     defer chunk_value.deinit(std.testing.allocator);
499 
500     var jumps: usize = 0;
501     var false_jumps: usize = 0;
502     for (chunk_value.instructions.items) |instruction| {
503         if (instruction.op == .jump) jumps += 1;
504         if (instruction.op == .jump_if_false) false_jumps += 1;
505     }
506     try std.testing.expect(jumps >= 1);
507     try std.testing.expect(false_jumps >= 2);
508 }
509 
510 test "compile emits stack operations for chained comparisons" {
511     const bytes = "1 < x <= y";
512     var stream = try @import("../source/root.zig").tokenize(std.testing.allocator, bytes);
513     defer stream.deinit(std.testing.allocator);
514     var program = try @import("../syntax/root.zig").parse(std.testing.allocator, bytes, stream.tokens);
515     defer program.deinit();
516     var chunk_value = try compile(std.testing.allocator, &program);
517     defer chunk_value.deinit(std.testing.allocator);
518 
519     var has_dup = false;
520     var has_rotate = false;
521     var has_swap = false;
522     for (chunk_value.instructions.items) |instruction| {
523         if (instruction.op == .dup) has_dup = true;
524         if (instruction.op == .rotate_three) has_rotate = true;
525         if (instruction.op == .swap) has_swap = true;
526     }
527     try std.testing.expect(has_dup);
528     try std.testing.expect(has_rotate);
529     try std.testing.expect(has_swap);
530 }
531 
532 test "compile emits membership and identity comparisons" {
533     const bytes = "x in xs is not ys";
534     var stream = try @import("../source/root.zig").tokenize(std.testing.allocator, bytes);
535     defer stream.deinit(std.testing.allocator);
536     var program = try @import("../syntax/root.zig").parse(std.testing.allocator, bytes, stream.tokens);
537     defer program.deinit();
538     var chunk_value = try compile(std.testing.allocator, &program);
539     defer chunk_value.deinit(std.testing.allocator);
540 
541     var has_contains = false;
542     var has_not_identical = false;
543     for (chunk_value.instructions.items) |instruction| {
544         if (instruction.op == .contains) has_contains = true;
545         if (instruction.op == .not_identical) has_not_identical = true;
546     }
547     try std.testing.expect(has_contains);
548     try std.testing.expect(has_not_identical);
549 }
550 
551 test "compile emits list operations" {
552     const bytes = "[1, 2][0]";
553     var stream = try @import("../source/root.zig").tokenize(std.testing.allocator, bytes);
554     defer stream.deinit(std.testing.allocator);
555     var program = try @import("../syntax/root.zig").parse(std.testing.allocator, bytes, stream.tokens);
556     defer program.deinit();
557     var chunk_value = try compile(std.testing.allocator, &program);
558     defer chunk_value.deinit(std.testing.allocator);
559 
560     var has_build_list = false;
561     var has_subscript = false;
562     for (chunk_value.instructions.items) |instruction| {
563         if (instruction.op == .build_list) has_build_list = true;
564         if (instruction.op == .subscript) has_subscript = true;
565     }
566     try std.testing.expect(has_build_list);
567     try std.testing.expect(has_subscript);
568 }
569 
570 test "compile emits attribute calls" {
571     const bytes = "xs.append(1)";
572     var stream = try @import("../source/root.zig").tokenize(std.testing.allocator, bytes);
573     defer stream.deinit(std.testing.allocator);
574     var program = try @import("../syntax/root.zig").parse(std.testing.allocator, bytes, stream.tokens);
575     defer program.deinit();
576     var chunk_value = try compile(std.testing.allocator, &program);
577     defer chunk_value.deinit(std.testing.allocator);
578 
579     var has_attribute = false;
580     var has_call = false;
581     for (chunk_value.instructions.items) |instruction| {
582         if (instruction.op == .attribute) has_attribute = true;
583         if (instruction.op == .call) has_call = true;
584     }
585     try std.testing.expect(has_attribute);
586     try std.testing.expect(has_call);
587 }
588 
589 test "compile emits slice operations" {
590     const bytes = "[1, 2, 3][1:]";
591     var stream = try @import("../source/root.zig").tokenize(std.testing.allocator, bytes);
592     defer stream.deinit(std.testing.allocator);
593     var program = try @import("../syntax/root.zig").parse(std.testing.allocator, bytes, stream.tokens);
594     defer program.deinit();
595     var chunk_value = try compile(std.testing.allocator, &program);
596     defer chunk_value.deinit(std.testing.allocator);
597 
598     var has_build_list = false;
599     var has_slice = false;
600     for (chunk_value.instructions.items) |instruction| {
601         if (instruction.op == .build_list) has_build_list = true;
602         if (instruction.op == .slice) has_slice = true;
603     }
604     try std.testing.expect(has_build_list);
605     try std.testing.expect(has_slice);
606 }
607 
608 test "compile emits tuple operations" {
609     const bytes = "(1, 2)";
610     var stream = try @import("../source/root.zig").tokenize(std.testing.allocator, bytes);
611     defer stream.deinit(std.testing.allocator);
612     var program = try @import("../syntax/root.zig").parse(std.testing.allocator, bytes, stream.tokens);
613     defer program.deinit();
614     var chunk_value = try compile(std.testing.allocator, &program);
615     defer chunk_value.deinit(std.testing.allocator);
616 
617     var has_build_tuple = false;
618     for (chunk_value.instructions.items) |instruction| {
619         if (instruction.op == .build_tuple) has_build_tuple = true;
620     }
621     try std.testing.expect(has_build_tuple);
622 }
623 
624 test "compile emits dictionary operations" {
625     const bytes = "{\"a\": 1}";
626     var stream = try @import("../source/root.zig").tokenize(std.testing.allocator, bytes);
627     defer stream.deinit(std.testing.allocator);
628     var program = try @import("../syntax/root.zig").parse(std.testing.allocator, bytes, stream.tokens);
629     defer program.deinit();
630     var chunk_value = try compile(std.testing.allocator, &program);
631     defer chunk_value.deinit(std.testing.allocator);
632 
633     var has_build_dict = false;
634     for (chunk_value.instructions.items) |instruction| {
635         if (instruction.op == .build_dict) has_build_dict = true;
636     }
637     try std.testing.expect(has_build_dict);
638 }
639 
640 test "compile emits list for loop operations" {
641     const bytes =
642         \\for x in [1]:
643         \\    pass
644     ;
645     var stream = try @import("../source/root.zig").tokenize(std.testing.allocator, bytes);
646     defer stream.deinit(std.testing.allocator);
647     var program = try @import("../syntax/root.zig").parse(std.testing.allocator, bytes, stream.tokens);
648     defer program.deinit();
649     var chunk_value = try compile(std.testing.allocator, &program);
650     defer chunk_value.deinit(std.testing.allocator);
651 
652     var has_iter = false;
653     var has_for_next = false;
654     for (chunk_value.instructions.items) |instruction| {
655         if (instruction.op == .iter) has_iter = true;
656         if (instruction.op == .for_next) has_for_next = true;
657     }
658     try std.testing.expect(has_iter);
659     try std.testing.expect(has_for_next);
660 }
661 
662 test "compile emits subscript assignment" {
663     const bytes =
664         \\xs = [1]
665         \\xs[0] = 2
666     ;
667     var stream = try @import("../source/root.zig").tokenize(std.testing.allocator, bytes);
668     defer stream.deinit(std.testing.allocator);
669     var program = try @import("../syntax/root.zig").parse(std.testing.allocator, bytes, stream.tokens);
670     defer program.deinit();
671     var chunk_value = try compile(std.testing.allocator, &program);
672     defer chunk_value.deinit(std.testing.allocator);
673 
674     var has_store_subscript = false;
675     for (chunk_value.instructions.items) |instruction| {
676         if (instruction.op == .store_subscript) has_store_subscript = true;
677     }
678     try std.testing.expect(has_store_subscript);
679 }
680 
681 test "compile emits deletion operations" {
682     const bytes =
683         \\del x
684         \\del xs[0]
685     ;
686     var stream = try @import("../source/root.zig").tokenize(std.testing.allocator, bytes);
687     defer stream.deinit(std.testing.allocator);
688     var program = try @import("../syntax/root.zig").parse(std.testing.allocator, bytes, stream.tokens);
689     defer program.deinit();
690     var chunk_value = try compile(std.testing.allocator, &program);
691     defer chunk_value.deinit(std.testing.allocator);
692 
693     var has_delete = false;
694     var has_delete_subscript = false;
695     for (chunk_value.instructions.items) |instruction| {
696         if (instruction.op == .delete) has_delete = true;
697         if (instruction.op == .delete_subscript) has_delete_subscript = true;
698     }
699     try std.testing.expect(has_delete);
700     try std.testing.expect(has_delete_subscript);
701 }