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 }