1use crate::ast::{BinOp, Pattern, UnaryOp};
38use crate::chunk::{Chunk, Program, StructInfo};
39use crate::effects::EffectSet;
40use crate::errors::QalaError;
41use crate::opcode::{Opcode, STDLIB_FN_BASE};
42use crate::span::{LineIndex, Span};
43use crate::typed_ast::{
44 TypedAst, TypedBlock, TypedElseBranch, TypedExpr, TypedInterpPart, TypedItem, TypedMatchArm,
45 TypedMatchArmBody, TypedStmt,
46};
47use crate::types::{QalaType, Symbol};
48use crate::value::ConstValue;
49use std::collections::{BTreeMap, HashMap};
50
51const COMPTIME_BUDGET: u32 = 100_000;
54
55const COMPTIME_MAX_FRAMES: usize = 256;
59
60type StdlibRow = (&'static str, Option<&'static str>, fn() -> EffectSet);
62
63const STDLIB_TABLE: &[StdlibRow] = &[
73 ("print", None, EffectSet::io),
74 ("println", None, EffectSet::io),
75 ("sqrt", None, EffectSet::pure),
76 ("abs", None, EffectSet::pure),
77 ("assert", None, EffectSet::panic),
78 ("len", None, EffectSet::pure),
79 ("push", None, EffectSet::alloc),
80 ("pop", None, EffectSet::alloc),
81 ("type_of", None, EffectSet::pure),
82 ("open", None, EffectSet::io),
83 ("close", None, EffectSet::io),
84 ("map", None, EffectSet::pure),
85 ("filter", None, EffectSet::pure),
86 ("reduce", None, EffectSet::pure),
87 ("read_all", Some("FileHandle"), EffectSet::io),
88];
89
90const BUILTIN_VARIANTS: &[(&str, &str, &str)] = &[
96 ("Result", "Ok", "Ok"),
97 ("Result", "Err", "Err"),
98 ("Option", "Some", "Some"),
99 ("Option", "None", "None"),
100];
101
102struct Codegen<'a> {
105 program: Program,
108 scopes: Vec<LocalsScope>,
111 current_fn_id: u16,
113 src: &'a str,
115 line_index: LineIndex,
117 fn_table: HashMap<FnKey, u16>,
121 stdlib_table: HashMap<FnKey, (u16, EffectSet)>,
124 enum_variant_table: BTreeMap<(String, String), u16>,
129 enum_variant_payload_count: HashMap<(String, String), u8>,
131 struct_field_index: HashMap<(String, String), u16>,
134 struct_id_table: BTreeMap<String, u16>,
139 fn_effects: HashMap<u16, EffectSet>,
142 errors: Vec<QalaError>,
145 local_names_acc: Vec<(u16, String)>,
151}
152
153struct LocalsScope {
155 locals: Vec<(String, u16)>,
158 deferred: Vec<TypedExpr>,
161 loop_meta: Option<LoopMeta>,
164}
165
166struct LoopMeta {
168 break_patches: Vec<usize>,
170 continue_target: usize,
174 continue_patches: Vec<usize>,
179}
180
181#[derive(Hash, Eq, PartialEq, Clone)]
184struct FnKey {
185 type_name: Option<String>,
187 name: String,
189}
190
191#[derive(Copy, Clone, PartialEq, Eq)]
193enum ExitKind {
194 Fallthrough,
197 Return,
199 Break,
201 Continue,
203 QuestionProp,
205}
206
207pub fn compile_program(ast: &TypedAst, src: &str) -> Result<Program, Vec<QalaError>> {
215 let mut cg = Codegen::new(src);
216 cg.build_tables(ast);
217 for item in ast {
218 if let Err(e) = cg.compile_item(item) {
219 cg.errors.push(e);
220 }
221 }
222 if !cg.errors.is_empty() {
223 cg.errors.sort_by_key(|e| (e.span().start, e.span().len));
224 return Err(cg.errors);
225 }
226 Ok(cg.program)
227}
228
229impl<'a> Codegen<'a> {
230 fn new(src: &'a str) -> Self {
233 let mut program = Program::new();
234 for (enum_name, variant_name, _) in BUILTIN_VARIANTS {
237 program
238 .enum_variant_names
239 .push((enum_name.to_string(), variant_name.to_string()));
240 }
241 let mut enum_variant_table: BTreeMap<(String, String), u16> = BTreeMap::new();
242 let mut enum_variant_payload_count: HashMap<(String, String), u8> = HashMap::new();
243 for (i, (enum_name, variant_name, _)) in BUILTIN_VARIANTS.iter().enumerate() {
244 enum_variant_table.insert((enum_name.to_string(), variant_name.to_string()), i as u16);
245 let payload = if *variant_name == "None" { 0 } else { 1 };
247 enum_variant_payload_count
248 .insert((enum_name.to_string(), variant_name.to_string()), payload);
249 }
250 let mut stdlib_table: HashMap<FnKey, (u16, EffectSet)> = HashMap::new();
251 let mut fn_effects: HashMap<u16, EffectSet> = HashMap::new();
252 for (i, (name, type_name, effect_fn)) in STDLIB_TABLE.iter().enumerate() {
253 let fn_id = STDLIB_FN_BASE + i as u16;
254 let effect = effect_fn();
255 let key = FnKey {
256 type_name: type_name.map(|s| s.to_string()),
257 name: name.to_string(),
258 };
259 stdlib_table.insert(key, (fn_id, effect));
260 fn_effects.insert(fn_id, effect);
261 }
262 Codegen {
263 program,
264 scopes: Vec::new(),
265 current_fn_id: 0,
266 src,
267 line_index: LineIndex::new(src),
268 fn_table: HashMap::new(),
269 stdlib_table,
270 enum_variant_table,
271 enum_variant_payload_count,
272 struct_field_index: HashMap::new(),
273 struct_id_table: BTreeMap::new(),
274 fn_effects,
275 errors: Vec::new(),
276 local_names_acc: Vec::new(),
277 }
278 }
279
280 fn build_tables(&mut self, ast: &TypedAst) {
284 for item in ast {
285 match item {
286 TypedItem::Fn(decl) => {
287 let fn_id = self.program.chunks.len() as u16;
288 self.program.chunks.push(Chunk::new());
289 self.program.fn_names.push(decl.name.clone());
290 self.fn_table.insert(
291 FnKey {
292 type_name: decl.type_name.clone(),
293 name: decl.name.clone(),
294 },
295 fn_id,
296 );
297 self.fn_effects.insert(fn_id, decl.effect);
298 }
299 TypedItem::Struct(decl) => {
300 for (i, field) in decl.fields.iter().enumerate() {
301 self.struct_field_index
302 .insert((decl.name.clone(), field.name.clone()), i as u16);
303 }
304 }
305 TypedItem::Enum(decl) => {
306 for variant in &decl.variants {
307 let variant_id = self.program.enum_variant_names.len() as u16;
308 self.program
309 .enum_variant_names
310 .push((decl.name.clone(), variant.name.clone()));
311 self.enum_variant_table
312 .insert((decl.name.clone(), variant.name.clone()), variant_id);
313 self.enum_variant_payload_count.insert(
314 (decl.name.clone(), variant.name.clone()),
315 variant.fields.len() as u8,
316 );
317 }
318 }
319 TypedItem::Interface(_) => {}
322 }
323 }
324 if let Some(&main_id) = self.fn_table.get(&FnKey {
326 type_name: None,
327 name: "main".to_string(),
328 }) {
329 self.program.main_index = main_id as usize;
330 }
331 }
332
333 fn compile_item(&mut self, item: &TypedItem) -> Result<(), QalaError> {
337 match item {
338 TypedItem::Fn(decl) => {
339 let fn_id = self
340 .fn_table
341 .get(&FnKey {
342 type_name: decl.type_name.clone(),
343 name: decl.name.clone(),
344 })
345 .copied()
346 .ok_or_else(|| QalaError::Type {
347 span: decl.span,
348 message: format!(
349 "codegen: function `{}` was not pre-registered",
350 decl.name
351 ),
352 })?;
353 self.current_fn_id = fn_id;
354 self.local_names_acc.clear();
356 self.push_scope();
358 for (i, param) in decl.params.iter().enumerate() {
359 self.register_local(param.name.clone(), i as u16);
360 }
361 let terminated = self.compile_block(&decl.body)?;
362 if !terminated {
363 let line = self.line_at(decl.body.span);
366 self.emit_defers_for_exit(ExitKind::Fallthrough, line)?;
367 self.chunk_mut().write_op(Opcode::Return, line);
368 }
369 self.pop_scope_no_defers();
372 self.finalize_local_names();
374 Ok(())
375 }
376 TypedItem::Struct(_) | TypedItem::Enum(_) | TypedItem::Interface(_) => Ok(()),
378 }
379 }
380
381 fn chunk_mut(&mut self) -> &mut Chunk {
385 &mut self.program.chunks[self.current_fn_id as usize]
386 }
387
388 fn line_at(&self, span: Span) -> u32 {
390 self.line_index.location(self.src, span.start as usize).0 as u32
391 }
392
393 fn register_struct(&mut self, name: &str, field_count: u16) -> u16 {
402 if let Some(&id) = self.struct_id_table.get(name) {
403 return id;
404 }
405 let id = self.program.structs.len() as u16;
406 self.program.structs.push(StructInfo {
407 name: name.to_string(),
408 field_count,
409 });
410 self.struct_id_table.insert(name.to_string(), id);
411 id
412 }
413
414 fn push_scope(&mut self) {
418 self.scopes.push(LocalsScope {
419 locals: Vec::new(),
420 deferred: Vec::new(),
421 loop_meta: None,
422 });
423 }
424
425 fn push_loop_scope(&mut self, continue_target: usize) {
427 self.scopes.push(LocalsScope {
428 locals: Vec::new(),
429 deferred: Vec::new(),
430 loop_meta: Some(LoopMeta {
431 break_patches: Vec::new(),
432 continue_target,
433 continue_patches: Vec::new(),
434 }),
435 });
436 }
437
438 fn pop_scope_no_defers(&mut self) {
442 self.scopes.pop();
443 }
444
445 fn pop_scope_and_emit_defers(&mut self, line: u32) -> Result<(), QalaError> {
465 let deferred: Vec<TypedExpr> = match self.scopes.last() {
469 Some(s) => s.deferred.iter().rev().cloned().collect(),
470 None => return Ok(()),
471 };
472 for expr in &deferred {
475 self.compile_expr(expr)?;
476 self.chunk_mut().write_op(Opcode::Pop, line);
477 }
478 self.scopes.pop();
480 Ok(())
481 }
482
483 fn emit_defers_for_exit(&mut self, exit_kind: ExitKind, line: u32) -> Result<(), QalaError> {
488 let depth = match exit_kind {
489 ExitKind::Fallthrough => 1.min(self.scopes.len()),
490 ExitKind::Return | ExitKind::QuestionProp => self.scopes.len(),
491 ExitKind::Break | ExitKind::Continue => {
492 let mut count = 0usize;
494 let mut found = false;
495 for scope in self.scopes.iter().rev() {
496 count += 1;
497 if scope.loop_meta.is_some() {
498 found = true;
499 break;
500 }
501 }
502 if !found {
503 return Err(QalaError::Type {
504 span: Span::new(0, 0),
505 message: "codegen: break/continue outside a loop".to_string(),
506 });
507 }
508 count
509 }
510 };
511 let mut to_emit: Vec<TypedExpr> = Vec::new();
515 let n = self.scopes.len();
516 for scope in self.scopes[n - depth..].iter().rev() {
517 for deferred in scope.deferred.iter().rev() {
518 to_emit.push(deferred.clone());
519 }
520 }
521 for deferred in &to_emit {
524 self.compile_expr(deferred)?;
525 self.chunk_mut().write_op(Opcode::Pop, line);
526 }
527 Ok(())
528 }
529
530 fn compile_block(&mut self, block: &TypedBlock) -> Result<bool, QalaError> {
543 self.push_scope();
544 let mut terminated = false;
545 for stmt in &block.stmts {
546 if self.compile_stmt(stmt)? {
547 terminated = true;
549 break;
550 }
551 }
552 if !terminated {
553 if let Some(value) = &block.value {
554 self.compile_expr(value)?;
555 }
556 let line = self.line_at(block.span);
557 self.pop_scope_and_emit_defers(line)?;
560 } else {
561 self.pop_scope_no_defers();
563 }
564 Ok(terminated)
565 }
566
567 fn compile_stmt(&mut self, stmt: &TypedStmt) -> Result<bool, QalaError> {
570 match stmt {
571 TypedStmt::Let {
572 name, init, span, ..
573 } => {
574 let line = self.line_at(*span);
575 self.compile_expr(init)?;
576 let slot = self.next_slot();
577 self.register_local(name.clone(), slot);
578 self.chunk_mut().write_op(Opcode::SetLocal, line);
579 self.chunk_mut().write_u16(slot, line);
580 Ok(false)
581 }
582 TypedStmt::If {
583 cond,
584 then_block,
585 else_branch,
586 span,
587 } => self.compile_if(cond, then_block, else_branch.as_ref(), *span),
588 TypedStmt::While { cond, body, span } => self.compile_while(cond, body, *span),
589 TypedStmt::For {
590 var,
591 iter,
592 body,
593 span,
594 ..
595 } => self.compile_for(var, iter, body, *span),
596 TypedStmt::Return { value, span } => {
597 let line = self.line_at(*span);
598 if let Some(v) = value {
599 self.compile_expr(v)?;
600 }
601 self.emit_defers_for_exit(ExitKind::Return, line)?;
602 self.chunk_mut().write_op(Opcode::Return, line);
603 Ok(true)
604 }
605 TypedStmt::Break { span } => {
606 let line = self.line_at(*span);
607 self.emit_defers_for_exit(ExitKind::Break, line)?;
608 let pos = self.chunk_mut().emit_jump(Opcode::Jump, line);
609 self.record_break_patch(pos)?;
611 Ok(true)
612 }
613 TypedStmt::Continue { span } => {
614 let line = self.line_at(*span);
615 self.emit_defers_for_exit(ExitKind::Continue, line)?;
616 let target = self.innermost_continue_target()?;
617 if target == usize::MAX {
618 let pos = self.chunk_mut().emit_jump(Opcode::Jump, line);
622 self.record_continue_patch(pos)?;
623 } else {
624 self.chunk_mut().emit_loop(target, line)?;
625 }
626 Ok(true)
627 }
628 TypedStmt::Defer { expr, .. } => {
629 if let Some(scope) = self.scopes.last_mut() {
632 scope.deferred.push(expr.clone());
633 }
634 Ok(false)
635 }
636 TypedStmt::Expr { expr, span } => {
637 let line = self.line_at(*span);
638 self.compile_expr(expr)?;
639 if !matches!(expr.ty(), QalaType::Void) {
641 self.chunk_mut().write_op(Opcode::Pop, line);
642 }
643 Ok(false)
644 }
645 }
646 }
647
648 fn register_local(&mut self, name: String, slot: u16) {
659 if let Some(scope) = self.scopes.last_mut() {
660 scope.locals.push((name.clone(), slot));
661 }
662 self.local_names_acc.push((slot, name));
663 }
664
665 fn finalize_local_names(&mut self) {
675 let max_slot = self
676 .local_names_acc
677 .iter()
678 .map(|(slot, _)| *slot as usize)
679 .max();
680 let mut names: Vec<String> = match max_slot {
681 Some(m) => vec![String::new(); m + 1],
682 None => Vec::new(),
683 };
684 for (slot, name) in self.local_names_acc.drain(..) {
685 names[slot as usize] = name;
686 }
687 self.chunk_mut().local_names = names;
688 }
689
690 fn next_slot(&self) -> u16 {
693 let mut max: i32 = -1;
694 for scope in &self.scopes {
695 for (_, slot) in &scope.locals {
696 max = max.max(*slot as i32);
697 }
698 }
699 (max + 1) as u16
700 }
701
702 fn record_break_patch(&mut self, pos: usize) -> Result<(), QalaError> {
704 for scope in self.scopes.iter_mut().rev() {
705 if let Some(meta) = &mut scope.loop_meta {
706 meta.break_patches.push(pos);
707 return Ok(());
708 }
709 }
710 Err(QalaError::Type {
711 span: Span::new(0, 0),
712 message: "codegen: break outside a loop".to_string(),
713 })
714 }
715
716 fn record_continue_patch(&mut self, pos: usize) -> Result<(), QalaError> {
720 for scope in self.scopes.iter_mut().rev() {
721 if let Some(meta) = &mut scope.loop_meta {
722 meta.continue_patches.push(pos);
723 return Ok(());
724 }
725 }
726 Err(QalaError::Type {
727 span: Span::new(0, 0),
728 message: "codegen: continue outside a loop".to_string(),
729 })
730 }
731
732 fn innermost_continue_target(&self) -> Result<usize, QalaError> {
734 for scope in self.scopes.iter().rev() {
735 if let Some(meta) = &scope.loop_meta {
736 return Ok(meta.continue_target);
737 }
738 }
739 Err(QalaError::Type {
740 span: Span::new(0, 0),
741 message: "codegen: continue outside a loop".to_string(),
742 })
743 }
744
745 fn emit_const(&mut self, value: ConstValue, line: u32) {
750 let idx = self.chunk_mut().add_constant(value);
751 self.chunk_mut().write_op(Opcode::Const, line);
752 self.chunk_mut().write_u16(idx, line);
753 }
754
755 fn emit_call(&mut self, fn_id: u16, argc: u8, line: u32) {
760 self.chunk_mut().write_op(Opcode::Call, line);
761 self.chunk_mut().write_u16(fn_id, line);
762 let chunk = self.chunk_mut();
763 chunk.code.push(argc);
764 chunk.source_lines.push(line);
765 }
766
767 fn resolve_callee_fn_id(&self, name: &str) -> Option<u16> {
772 let key = FnKey {
773 type_name: None,
774 name: name.to_string(),
775 };
776 if let Some(&fn_id) = self.fn_table.get(&key) {
777 return Some(fn_id);
778 }
779 self.stdlib_table.get(&key).map(|(fn_id, _)| *fn_id)
780 }
781
782 fn resolve_local(&self, name: &str) -> Option<u16> {
785 for scope in self.scopes.iter().rev() {
786 for (local_name, slot) in scope.locals.iter().rev() {
787 if local_name == name {
788 return Some(*slot);
789 }
790 }
791 }
792 None
793 }
794
795 fn emit_binop(&mut self, op: &BinOp, operand_ty: &QalaType, line: u32) {
800 let is_float = matches!(operand_ty, QalaType::F64);
801 let opcode = match (op, is_float) {
802 (BinOp::Add, false) => Opcode::Add,
803 (BinOp::Add, true) => Opcode::FAdd,
804 (BinOp::Sub, false) => Opcode::Sub,
805 (BinOp::Sub, true) => Opcode::FSub,
806 (BinOp::Mul, false) => Opcode::Mul,
807 (BinOp::Mul, true) => Opcode::FMul,
808 (BinOp::Div, false) => Opcode::Div,
809 (BinOp::Div, true) => Opcode::FDiv,
810 (BinOp::Rem, _) => Opcode::Mod,
811 (BinOp::Eq, false) => Opcode::Eq,
812 (BinOp::Eq, true) => Opcode::FEq,
813 (BinOp::Ne, false) => Opcode::Ne,
814 (BinOp::Ne, true) => Opcode::FNe,
815 (BinOp::Lt, false) => Opcode::Lt,
816 (BinOp::Lt, true) => Opcode::FLt,
817 (BinOp::Le, false) => Opcode::Le,
818 (BinOp::Le, true) => Opcode::FLe,
819 (BinOp::Gt, false) => Opcode::Gt,
820 (BinOp::Gt, true) => Opcode::FGt,
821 (BinOp::Ge, false) => Opcode::Ge,
822 (BinOp::Ge, true) => Opcode::FGe,
823 (BinOp::And, _) | (BinOp::Or, _) => Opcode::Not,
826 };
827 self.chunk_mut().write_op(opcode, line);
828 }
829
830 fn emit_unop(&mut self, op: &UnaryOp, operand_ty: &QalaType, line: u32) {
832 let opcode = match op {
833 UnaryOp::Not => Opcode::Not,
834 UnaryOp::Neg => {
835 if matches!(operand_ty, QalaType::F64) {
836 Opcode::FNeg
837 } else {
838 Opcode::Neg
839 }
840 }
841 };
842 self.chunk_mut().write_op(opcode, line);
843 }
844
845 fn try_fold_binary(
857 &mut self,
858 op: &BinOp,
859 operand_ty: &QalaType,
860 line: u32,
861 span: Span,
862 lhs_start: usize,
863 rhs_start: usize,
864 ) -> Result<bool, QalaError> {
865 let chunk = self.chunk_mut();
866 let end = chunk.code.len();
867 if rhs_start - lhs_start != 3 || end - rhs_start != 3 {
869 return Ok(false);
870 }
871 if chunk.code[lhs_start] != Opcode::Const as u8
872 || chunk.code[rhs_start] != Opcode::Const as u8
873 {
874 return Ok(false);
875 }
876 let a_idx = chunk.read_u16(lhs_start + 1);
877 let b_idx = chunk.read_u16(rhs_start + 1);
878 if a_idx as usize >= chunk.constants.len() || b_idx as usize >= chunk.constants.len() {
879 return Ok(false);
880 }
881 let a = chunk.constants[a_idx as usize].clone();
882 let b = chunk.constants[b_idx as usize].clone();
883 let result = match fold_binary_value(&a, &b, op, operand_ty, span)? {
884 Some(v) => v,
885 None => return Ok(false),
886 };
887 let chunk = self.chunk_mut();
889 chunk.code.truncate(lhs_start);
890 chunk.source_lines.truncate(lhs_start);
891 self.emit_const(result, line);
892 Ok(true)
893 }
894
895 fn try_fold_unary(
901 &mut self,
902 op: &UnaryOp,
903 line: u32,
904 span: Span,
905 operand_start: usize,
906 ) -> Result<bool, QalaError> {
907 let chunk = self.chunk_mut();
908 let end = chunk.code.len();
909 if end - operand_start != 3 || chunk.code[operand_start] != Opcode::Const as u8 {
911 return Ok(false);
912 }
913 let idx = chunk.read_u16(operand_start + 1);
914 if idx as usize >= chunk.constants.len() {
915 return Ok(false);
916 }
917 let off = operand_start;
918 let v = chunk.constants[idx as usize].clone();
919 let result = match (op, &v) {
920 (UnaryOp::Not, ConstValue::Bool(b)) => ConstValue::Bool(!b),
921 (UnaryOp::Neg, ConstValue::I64(n)) => {
922 let r = n.checked_neg().ok_or(QalaError::IntegerOverflow {
927 span,
928 op: BinOp::Sub,
929 lhs: 0,
930 rhs: *n,
931 })?;
932 ConstValue::I64(r)
933 }
934 (UnaryOp::Neg, ConstValue::F64(x)) => ConstValue::F64(-x),
935 _ => return Ok(false),
937 };
938 let chunk = self.chunk_mut();
939 chunk.code.truncate(off);
940 chunk.source_lines.truncate(off);
941 self.emit_const(result, line);
942 Ok(true)
943 }
944
945 fn compile_expr(&mut self, e: &TypedExpr) -> Result<(), QalaError> {
954 match e {
955 TypedExpr::Int { value, span, .. } => {
956 let line = self.line_at(*span);
957 self.emit_const(ConstValue::I64(*value), line);
958 Ok(())
959 }
960 TypedExpr::Float { value, span, .. } => {
961 let line = self.line_at(*span);
962 self.emit_const(ConstValue::F64(*value), line);
963 Ok(())
964 }
965 TypedExpr::Byte { value, span, .. } => {
966 let line = self.line_at(*span);
967 self.emit_const(ConstValue::Byte(*value), line);
968 Ok(())
969 }
970 TypedExpr::Str { value, span, .. } => {
971 let line = self.line_at(*span);
972 self.emit_const(ConstValue::Str(value.clone()), line);
973 Ok(())
974 }
975 TypedExpr::Bool { value, span, .. } => {
976 let line = self.line_at(*span);
977 self.emit_const(ConstValue::Bool(*value), line);
978 Ok(())
979 }
980 TypedExpr::Ident { name, span, .. } => {
981 let line = self.line_at(*span);
982 if let Some(slot) = self.resolve_local(name) {
983 self.chunk_mut().write_op(Opcode::GetLocal, line);
984 self.chunk_mut().write_u16(slot, line);
985 } else if let Some(fn_id) = self.resolve_callee_fn_id(name) {
986 self.emit_const(ConstValue::Function(fn_id), line);
989 } else {
990 return Err(QalaError::Type {
993 span: *span,
994 message: format!("codegen: unresolved name `{name}`"),
995 });
996 }
997 Ok(())
998 }
999 TypedExpr::Paren { inner, .. } => self.compile_expr(inner),
1000 TypedExpr::Unary {
1001 op, operand, span, ..
1002 } => {
1003 let operand_start = self.chunk_mut().code.len();
1004 self.compile_expr(operand)?;
1005 let line = self.line_at(*span);
1006 if !self.try_fold_unary(op, line, *span, operand_start)? {
1007 self.emit_unop(op, operand.ty(), line);
1008 }
1009 Ok(())
1010 }
1011 TypedExpr::Binary {
1012 op, lhs, rhs, span, ..
1013 } => {
1014 let line = self.line_at(*span);
1015 if matches!(op, BinOp::And | BinOp::Or) {
1016 let lhs_start = self.chunk_mut().code.len();
1020 self.compile_expr(lhs)?;
1021 if self.try_fold_short_circuit(op, rhs, line, lhs_start)? {
1023 return Ok(());
1024 }
1025 self.chunk_mut().write_op(Opcode::Dup, line);
1026 let skip = if matches!(op, BinOp::And) {
1027 self.chunk_mut().emit_jump(Opcode::JumpIfFalse, line)
1028 } else {
1029 self.chunk_mut().emit_jump(Opcode::JumpIfTrue, line)
1030 };
1031 self.chunk_mut().write_op(Opcode::Pop, line);
1032 self.compile_expr(rhs)?;
1033 self.chunk_mut().patch_jump(skip)?;
1034 Ok(())
1035 } else {
1036 let lhs_start = self.chunk_mut().code.len();
1037 self.compile_expr(lhs)?;
1038 let rhs_start = self.chunk_mut().code.len();
1039 self.compile_expr(rhs)?;
1040 if !self.try_fold_binary(op, lhs.ty(), line, *span, lhs_start, rhs_start)? {
1041 self.emit_binop(op, lhs.ty(), line);
1042 }
1043 Ok(())
1044 }
1045 }
1046 TypedExpr::Call {
1047 callee, args, span, ..
1048 } => {
1049 let line = self.line_at(*span);
1050 self.compile_call(callee, args, line, *span)
1051 }
1052 TypedExpr::Pipeline {
1053 lhs, call, span, ..
1054 } => {
1055 let line = self.line_at(*span);
1056 self.compile_pipeline(lhs, call, line, *span)
1057 }
1058 TypedExpr::Interpolation { parts, span, .. } => {
1059 let line = self.line_at(*span);
1060 self.compile_interpolation(parts, line)
1061 }
1062 TypedExpr::Try { expr, span, .. } => {
1063 let line = self.line_at(*span);
1064 self.compile_try(expr, line, *span)
1065 }
1066 TypedExpr::OrElse {
1067 expr,
1068 fallback,
1069 span,
1070 ..
1071 } => {
1072 let line = self.line_at(*span);
1073 self.compile_or_else(expr, fallback, line, *span)
1074 }
1075 TypedExpr::MethodCall {
1076 receiver,
1077 name,
1078 args,
1079 span,
1080 ..
1081 } => {
1082 let line = self.line_at(*span);
1083 self.compile_method_call(receiver, name, args, line, *span)
1084 }
1085 TypedExpr::FieldAccess {
1086 obj, name, span, ..
1087 } => {
1088 let line = self.line_at(*span);
1089 self.compile_expr(obj)?;
1090 let field_idx = self.resolve_field_index(obj.ty(), name, *span)?;
1092 self.chunk_mut().write_op(Opcode::Field, line);
1093 self.chunk_mut().write_u16(field_idx, line);
1094 Ok(())
1095 }
1096 TypedExpr::Index {
1097 obj, index, span, ..
1098 } => {
1099 let line = self.line_at(*span);
1100 self.compile_expr(obj)?;
1101 self.compile_expr(index)?;
1102 self.chunk_mut().write_op(Opcode::Index, line);
1103 Ok(())
1104 }
1105 TypedExpr::Tuple { elems, span, .. } => {
1106 let line = self.line_at(*span);
1107 for elem in elems {
1108 self.compile_expr(elem)?;
1109 }
1110 self.chunk_mut().write_op(Opcode::MakeTuple, line);
1111 self.chunk_mut().write_u16(elems.len() as u16, line);
1112 Ok(())
1113 }
1114 TypedExpr::ArrayLit { elems, span, .. } => {
1115 let line = self.line_at(*span);
1116 for elem in elems {
1117 self.compile_expr(elem)?;
1118 }
1119 self.chunk_mut().write_op(Opcode::MakeArray, line);
1120 self.chunk_mut().write_u16(elems.len() as u16, line);
1121 Ok(())
1122 }
1123 TypedExpr::ArrayRepeat {
1124 value, count, span, ..
1125 } => {
1126 let line = self.line_at(*span);
1127 self.compile_array_repeat(value, count, line, *span)
1128 }
1129 TypedExpr::StructLit {
1130 name, fields, span, ..
1131 } => {
1132 let line = self.line_at(*span);
1133 for field in fields {
1136 self.compile_expr(&field.value)?;
1137 }
1138 let struct_id = self.register_struct(name, fields.len() as u16);
1142 self.chunk_mut().write_op(Opcode::MakeStruct, line);
1143 self.chunk_mut().write_u16(struct_id, line);
1144 Ok(())
1145 }
1146 TypedExpr::Range {
1147 start,
1148 end,
1149 inclusive,
1150 span,
1151 ..
1152 } => {
1153 let line = self.line_at(*span);
1154 self.compile_range(start.as_deref(), end.as_deref(), *inclusive, line, *span)
1155 }
1156 TypedExpr::Block { block, .. } => {
1157 self.compile_block(block)?;
1160 Ok(())
1161 }
1162 TypedExpr::Match {
1163 scrutinee,
1164 arms,
1165 span,
1166 ..
1167 } => {
1168 let line = self.line_at(*span);
1169 self.compile_match(scrutinee, arms, line, *span)
1170 }
1171 TypedExpr::Comptime { body, span, .. } => {
1172 let line = self.line_at(*span);
1173 self.compile_comptime(body, line, *span)
1174 }
1175 }
1176 }
1177
1178 fn try_fold_short_circuit(
1186 &mut self,
1187 op: &BinOp,
1188 rhs: &TypedExpr,
1189 line: u32,
1190 lhs_start: usize,
1191 ) -> Result<bool, QalaError> {
1192 let rhs_start = self.chunk_mut().code.len();
1194 if rhs_start - lhs_start != 3 || self.chunk_mut().code[lhs_start] != Opcode::Const as u8 {
1195 return Ok(false);
1196 }
1197 self.compile_expr(rhs)?;
1198 if self.try_fold_binary(op, &QalaType::Bool, line, rhs.span(), lhs_start, rhs_start)? {
1199 return Ok(true);
1200 }
1201 let chunk = self.chunk_mut();
1204 chunk.code.truncate(rhs_start);
1205 chunk.source_lines.truncate(rhs_start);
1206 Ok(false)
1207 }
1208
1209 fn compile_call(
1214 &mut self,
1215 callee: &TypedExpr,
1216 args: &[TypedExpr],
1217 line: u32,
1218 span: Span,
1219 ) -> Result<(), QalaError> {
1220 let name = match callee {
1221 TypedExpr::Ident { name, .. } => name.clone(),
1222 _ => {
1226 return Err(QalaError::Type {
1227 span,
1228 message: "codegen: only named callees are supported in v1".to_string(),
1229 });
1230 }
1231 };
1232 if let Some(&variant_id) = self.enum_variant_lookup(&name) {
1235 for arg in args {
1236 self.compile_expr(arg)?;
1237 }
1238 let payload = args.len() as u8;
1239 self.chunk_mut().write_op(Opcode::MakeEnumVariant, line);
1240 self.chunk_mut().write_u16(variant_id, line);
1241 let chunk = self.chunk_mut();
1242 chunk.code.push(payload);
1243 chunk.source_lines.push(line);
1244 return Ok(());
1245 }
1246 if let Some(fn_id) = self.resolve_callee_fn_id(&name) {
1248 for arg in args {
1249 self.compile_expr(arg)?;
1250 }
1251 self.emit_call(fn_id, args.len() as u8, line);
1252 return Ok(());
1253 }
1254 Err(QalaError::Type {
1255 span,
1256 message: format!("codegen: unresolved callee `{name}`"),
1257 })
1258 }
1259
1260 fn enum_variant_lookup(&self, variant_name: &str) -> Option<&u16> {
1266 self.enum_variant_table
1267 .iter()
1268 .find(|((_, v), _)| v == variant_name)
1269 .map(|(_, id)| id)
1270 }
1271
1272 fn compile_comptime(
1283 &mut self,
1284 body: &TypedExpr,
1285 line: u32,
1286 span: Span,
1287 ) -> Result<(), QalaError> {
1288 let throwaway_id = self.program.chunks.len() as u16;
1293 self.program.chunks.push(Chunk::new());
1294 let saved_fn_id = self.current_fn_id;
1295 let saved_scopes = std::mem::take(&mut self.scopes);
1296 self.current_fn_id = throwaway_id;
1297 self.push_scope();
1298 let compile_result = self.compile_expr(body);
1299 self.chunk_mut().write_op(Opcode::Return, line);
1302 let throwaway = self.program.chunks.pop().ok_or_else(|| QalaError::Parse {
1304 span,
1305 message: "internal: comptime chunk was removed during compilation".to_string(),
1306 })?;
1307 self.current_fn_id = saved_fn_id;
1308 self.scopes = saved_scopes;
1309 compile_result?;
1310 let mut interp = ComptimeInterpreter::new(&self.program, &self.fn_effects, span);
1312 let value = interp.run(throwaway)?;
1313 if !is_constable_primitive(&value) {
1315 return Err(QalaError::ComptimeResultNotConstable {
1316 span,
1317 type_name: value_type_name(&value),
1318 });
1319 }
1320 self.emit_const(value, line);
1322 Ok(())
1323 }
1324
1325 fn compile_pipeline(
1333 &mut self,
1334 lhs: &TypedExpr,
1335 call: &TypedExpr,
1336 line: u32,
1337 span: Span,
1338 ) -> Result<(), QalaError> {
1339 match call {
1340 TypedExpr::Ident { name, .. } => {
1342 self.compile_expr(lhs)?;
1343 self.dispatch_named_call(name, 1, line, span)
1344 }
1345 TypedExpr::Call { callee, args, .. } => {
1347 let name = match callee.as_ref() {
1348 TypedExpr::Ident { name, .. } => name.clone(),
1349 _ => {
1350 return Err(QalaError::Type {
1351 span,
1352 message: "codegen: pipeline callee must be a named function"
1353 .to_string(),
1354 });
1355 }
1356 };
1357 self.compile_expr(lhs)?;
1358 for arg in args {
1359 self.compile_expr(arg)?;
1360 }
1361 self.dispatch_named_call(&name, (args.len() + 1) as u8, line, span)
1362 }
1363 _ => Err(QalaError::Type {
1364 span,
1365 message: "codegen: pipeline right-hand side must be a call".to_string(),
1366 }),
1367 }
1368 }
1369
1370 fn dispatch_named_call(
1374 &mut self,
1375 name: &str,
1376 argc: u8,
1377 line: u32,
1378 span: Span,
1379 ) -> Result<(), QalaError> {
1380 if let Some(&variant_id) = self.enum_variant_lookup(name) {
1381 self.chunk_mut().write_op(Opcode::MakeEnumVariant, line);
1382 self.chunk_mut().write_u16(variant_id, line);
1383 let chunk = self.chunk_mut();
1384 chunk.code.push(argc);
1385 chunk.source_lines.push(line);
1386 return Ok(());
1387 }
1388 if let Some(fn_id) = self.resolve_callee_fn_id(name) {
1389 self.emit_call(fn_id, argc, line);
1390 return Ok(());
1391 }
1392 Err(QalaError::Type {
1393 span,
1394 message: format!("codegen: unresolved callee `{name}`"),
1395 })
1396 }
1397
1398 fn compile_interpolation(
1403 &mut self,
1404 parts: &[TypedInterpPart],
1405 line: u32,
1406 ) -> Result<(), QalaError> {
1407 if parts
1409 .iter()
1410 .all(|p| matches!(p, TypedInterpPart::Literal(_)))
1411 {
1412 let mut s = String::new();
1413 for part in parts {
1414 if let TypedInterpPart::Literal(text) = part {
1415 s.push_str(text);
1416 }
1417 }
1418 self.emit_const(ConstValue::Str(s), line);
1419 return Ok(());
1420 }
1421 let mut emitted = 0u16;
1425 for part in parts {
1426 match part {
1427 TypedInterpPart::Literal(text) => {
1428 if text.is_empty() {
1429 continue;
1430 }
1431 self.emit_const(ConstValue::Str(text.clone()), line);
1432 emitted += 1;
1433 }
1434 TypedInterpPart::Expr(e) => {
1435 self.compile_expr(e)?;
1436 if !matches!(e.ty(), QalaType::Str) {
1437 self.chunk_mut().write_op(Opcode::ToStr, line);
1438 }
1439 emitted += 1;
1440 }
1441 }
1442 }
1443 if emitted == 1 {
1445 return Ok(());
1446 }
1447 if emitted == 0 {
1449 self.emit_const(ConstValue::Str(String::new()), line);
1450 return Ok(());
1451 }
1452 self.chunk_mut().write_op(Opcode::ConcatN, line);
1453 self.chunk_mut().write_u16(emitted, line);
1454 Ok(())
1455 }
1456
1457 fn compile_try(&mut self, inner: &TypedExpr, line: u32, span: Span) -> Result<(), QalaError> {
1462 self.compile_expr(inner)?;
1463 let ok_id = match inner.ty() {
1465 QalaType::Result(_, _) => 0u16,
1466 QalaType::Option(_) => 2u16,
1467 _ => {
1468 return Err(QalaError::Type {
1469 span,
1470 message: "codegen: `?` operand is not a Result or Option".to_string(),
1471 });
1472 }
1473 };
1474 self.chunk_mut().write_op(Opcode::MatchVariant, line);
1482 self.chunk_mut().write_u16(ok_id, line);
1483 let miss_patch = self.chunk_mut().code.len();
1484 self.chunk_mut().write_i16(i16::MAX, line);
1485 let skip_err = self.chunk_mut().emit_jump(Opcode::Jump, line);
1488 self.chunk_mut().patch_jump(miss_patch)?;
1492 self.emit_defers_for_exit(ExitKind::QuestionProp, line)?;
1493 self.chunk_mut().write_op(Opcode::Return, line);
1494 self.chunk_mut().patch_jump(skip_err)?;
1496 Ok(())
1497 }
1498
1499 fn compile_or_else(
1503 &mut self,
1504 expr: &TypedExpr,
1505 fallback: &TypedExpr,
1506 line: u32,
1507 span: Span,
1508 ) -> Result<(), QalaError> {
1509 self.compile_expr(expr)?;
1510 let ok_id = match expr.ty() {
1511 QalaType::Result(_, _) => 0u16,
1512 QalaType::Option(_) => 2u16,
1513 _ => {
1514 return Err(QalaError::Type {
1515 span,
1516 message: "codegen: `or` operand is not a Result or Option".to_string(),
1517 });
1518 }
1519 };
1520 self.chunk_mut().write_op(Opcode::MatchVariant, line);
1525 self.chunk_mut().write_u16(ok_id, line);
1526 let miss_patch = self.chunk_mut().code.len();
1527 self.chunk_mut().write_i16(i16::MAX, line);
1528 let skip_fallback = self.chunk_mut().emit_jump(Opcode::Jump, line);
1531 self.chunk_mut().patch_jump(miss_patch)?;
1534 self.chunk_mut().write_op(Opcode::Pop, line);
1535 self.compile_expr(fallback)?;
1536 self.chunk_mut().patch_jump(skip_fallback)?;
1537 Ok(())
1538 }
1539
1540 fn compile_method_call(
1544 &mut self,
1545 receiver: &TypedExpr,
1546 name: &str,
1547 args: &[TypedExpr],
1548 line: u32,
1549 span: Span,
1550 ) -> Result<(), QalaError> {
1551 let type_name = match receiver.ty() {
1553 QalaType::Named(Symbol(s)) => s.clone(),
1554 QalaType::FileHandle => "FileHandle".to_string(),
1555 other => {
1556 return Err(QalaError::Type {
1557 span,
1558 message: format!(
1559 "codegen: method call on non-named type `{}`",
1560 other.display()
1561 ),
1562 });
1563 }
1564 };
1565 let key = FnKey {
1566 type_name: Some(type_name.clone()),
1567 name: name.to_string(),
1568 };
1569 let fn_id = self
1570 .fn_table
1571 .get(&key)
1572 .copied()
1573 .or_else(|| self.stdlib_table.get(&key).map(|(id, _)| *id))
1574 .ok_or_else(|| QalaError::Type {
1575 span,
1576 message: format!("codegen: unresolved method `{type_name}.{name}`"),
1577 })?;
1578 self.compile_expr(receiver)?;
1580 for arg in args {
1581 self.compile_expr(arg)?;
1582 }
1583 self.emit_call(fn_id, (args.len() + 1) as u8, line);
1584 Ok(())
1585 }
1586
1587 fn resolve_field_index(
1590 &self,
1591 obj_ty: &QalaType,
1592 field_name: &str,
1593 span: Span,
1594 ) -> Result<u16, QalaError> {
1595 let struct_name = match obj_ty {
1596 QalaType::Named(Symbol(s)) => s.clone(),
1597 other => {
1598 return Err(QalaError::Type {
1599 span,
1600 message: format!(
1601 "codegen: field access on non-struct type `{}`",
1602 other.display()
1603 ),
1604 });
1605 }
1606 };
1607 self.struct_field_index
1608 .get(&(struct_name.clone(), field_name.to_string()))
1609 .copied()
1610 .ok_or_else(|| QalaError::Type {
1611 span,
1612 message: format!("codegen: no field `{field_name}` on `{struct_name}`"),
1613 })
1614 }
1615
1616 fn compile_array_repeat(
1621 &mut self,
1622 value: &TypedExpr,
1623 count: &TypedExpr,
1624 line: u32,
1625 span: Span,
1626 ) -> Result<(), QalaError> {
1627 let n = match count {
1628 TypedExpr::Int { value: n, .. } if *n >= 0 => *n,
1629 _ => {
1630 return Err(QalaError::Parse {
1631 span,
1632 message: "array repeat count must be a non-negative integer literal (v1)"
1633 .to_string(),
1634 });
1635 }
1636 };
1637 if n > 1024 {
1638 return Err(QalaError::Parse {
1639 span,
1640 message: "array repeat count too large (v1 limit is 1024)".to_string(),
1641 });
1642 }
1643 for _ in 0..n {
1644 self.compile_expr(value)?;
1645 }
1646 self.chunk_mut().write_op(Opcode::MakeArray, line);
1647 self.chunk_mut().write_u16(n as u16, line);
1648 Ok(())
1649 }
1650
1651 fn compile_range(
1655 &mut self,
1656 start: Option<&TypedExpr>,
1657 end: Option<&TypedExpr>,
1658 inclusive: bool,
1659 line: u32,
1660 span: Span,
1661 ) -> Result<(), QalaError> {
1662 let bound = |e: Option<&TypedExpr>| match e {
1663 Some(TypedExpr::Int { value, .. }) => Some(*value),
1664 _ => None,
1665 };
1666 let (Some(lo), Some(hi)) = (bound(start), bound(end)) else {
1667 return Err(QalaError::Parse {
1668 span,
1669 message: "range bounds must be integer literals in v1 (a runtime range \
1670 iterator is a v2 feature)"
1671 .to_string(),
1672 });
1673 };
1674 let last = if inclusive { hi } else { hi - 1 };
1675 if last < lo {
1676 self.chunk_mut().write_op(Opcode::MakeArray, line);
1678 self.chunk_mut().write_u16(0, line);
1679 return Ok(());
1680 }
1681 let count = last - lo + 1;
1682 if count > 1024 {
1683 return Err(QalaError::Parse {
1684 span,
1685 message: "range too large to materialize (v1 limit is 1024 elements)".to_string(),
1686 });
1687 }
1688 for v in lo..=last {
1689 self.emit_const(ConstValue::I64(v), line);
1690 }
1691 self.chunk_mut().write_op(Opcode::MakeArray, line);
1692 self.chunk_mut().write_u16(count as u16, line);
1693 Ok(())
1694 }
1695
1696 fn compile_if(
1702 &mut self,
1703 cond: &TypedExpr,
1704 then_block: &TypedBlock,
1705 else_branch: Option<&TypedElseBranch>,
1706 span: Span,
1707 ) -> Result<bool, QalaError> {
1708 let line = self.line_at(span);
1709 match cond {
1711 TypedExpr::Bool { value: true, .. } => {
1712 self.compile_block(then_block)?;
1713 return Ok(false);
1714 }
1715 TypedExpr::Bool { value: false, .. } => {
1716 match else_branch {
1717 Some(TypedElseBranch::Block(b)) => {
1718 self.compile_block(b)?;
1719 }
1720 Some(TypedElseBranch::If(s)) => {
1721 self.compile_stmt(s)?;
1722 }
1723 None => {}
1724 }
1725 return Ok(false);
1726 }
1727 _ => {}
1728 }
1729 self.compile_expr(cond)?;
1731 let jmp_else = self.chunk_mut().emit_jump(Opcode::JumpIfFalse, line);
1732 let then_terminated = self.compile_block(then_block)?;
1733 let jmp_end = if !then_terminated && else_branch.is_some() {
1734 Some(self.chunk_mut().emit_jump(Opcode::Jump, line))
1735 } else {
1736 None
1737 };
1738 self.chunk_mut().patch_jump(jmp_else)?;
1739 match else_branch {
1740 Some(TypedElseBranch::Block(b)) => {
1741 self.compile_block(b)?;
1742 }
1743 Some(TypedElseBranch::If(s)) => {
1744 self.compile_stmt(s)?;
1745 }
1746 None => {}
1747 }
1748 if let Some(je) = jmp_end {
1749 self.chunk_mut().patch_jump(je)?;
1750 }
1751 Ok(false)
1754 }
1755
1756 fn compile_while(
1760 &mut self,
1761 cond: &TypedExpr,
1762 body: &TypedBlock,
1763 span: Span,
1764 ) -> Result<bool, QalaError> {
1765 let line = self.line_at(span);
1766 let loop_start = self.chunk_mut().code.len();
1767 self.compile_expr(cond)?;
1768 let jmp_end = self.chunk_mut().emit_jump(Opcode::JumpIfFalse, line);
1769 self.push_loop_scope(loop_start);
1771 self.compile_block(body)?;
1772 self.chunk_mut().emit_loop(loop_start, line)?;
1773 self.chunk_mut().patch_jump(jmp_end)?;
1774 self.patch_loop_breaks()?;
1776 self.pop_scope_no_defers();
1777 Ok(false)
1778 }
1779
1780 fn compile_for(
1786 &mut self,
1787 var: &str,
1788 iter: &TypedExpr,
1789 body: &TypedBlock,
1790 span: Span,
1791 ) -> Result<bool, QalaError> {
1792 let line = self.line_at(span);
1793 self.compile_expr(iter)?;
1795 let arr_slot = self.next_slot();
1796 self.bind_hidden_slot(arr_slot);
1797 self.chunk_mut().write_op(Opcode::SetLocal, line);
1798 self.chunk_mut().write_u16(arr_slot, line);
1799 self.emit_const(ConstValue::I64(0), line);
1801 let idx_slot = self.next_slot();
1802 self.bind_hidden_slot(idx_slot);
1803 self.chunk_mut().write_op(Opcode::SetLocal, line);
1804 self.chunk_mut().write_u16(idx_slot, line);
1805 let loop_start = self.chunk_mut().code.len();
1807 self.chunk_mut().write_op(Opcode::GetLocal, line);
1808 self.chunk_mut().write_u16(idx_slot, line);
1809 self.chunk_mut().write_op(Opcode::GetLocal, line);
1810 self.chunk_mut().write_u16(arr_slot, line);
1811 self.chunk_mut().write_op(Opcode::Len, line);
1812 self.chunk_mut().write_op(Opcode::Lt, line);
1813 let jmp_end = self.chunk_mut().emit_jump(Opcode::JumpIfFalse, line);
1814 self.push_loop_scope(usize::MAX);
1819 let var_slot = self.next_slot();
1822 self.register_local(var.to_string(), var_slot);
1823 self.chunk_mut().write_op(Opcode::GetLocal, line);
1825 self.chunk_mut().write_u16(arr_slot, line);
1826 self.chunk_mut().write_op(Opcode::GetLocal, line);
1827 self.chunk_mut().write_u16(idx_slot, line);
1828 self.chunk_mut().write_op(Opcode::Index, line);
1829 self.chunk_mut().write_op(Opcode::SetLocal, line);
1830 self.chunk_mut().write_u16(var_slot, line);
1831 self.compile_block(body)?;
1833 self.patch_loop_continues()?;
1835 self.chunk_mut().write_op(Opcode::GetLocal, line);
1836 self.chunk_mut().write_u16(idx_slot, line);
1837 self.emit_const(ConstValue::I64(1), line);
1838 self.chunk_mut().write_op(Opcode::Add, line);
1839 self.chunk_mut().write_op(Opcode::SetLocal, line);
1840 self.chunk_mut().write_u16(idx_slot, line);
1841 self.chunk_mut().emit_loop(loop_start, line)?;
1842 self.chunk_mut().patch_jump(jmp_end)?;
1843 self.patch_loop_breaks()?;
1844 self.pop_scope_no_defers();
1845 Ok(false)
1846 }
1847
1848 fn bind_hidden_slot(&mut self, slot: u16) {
1853 self.register_local(String::new(), slot);
1854 }
1855
1856 fn patch_loop_breaks(&mut self) -> Result<(), QalaError> {
1859 let patches: Vec<usize> = self
1860 .scopes
1861 .iter()
1862 .rev()
1863 .find_map(|s| s.loop_meta.as_ref().map(|m| m.break_patches.clone()))
1864 .unwrap_or_default();
1865 for pos in patches {
1866 self.chunk_mut().patch_jump(pos)?;
1867 }
1868 Ok(())
1869 }
1870
1871 fn patch_loop_continues(&mut self) -> Result<(), QalaError> {
1875 let patches: Vec<usize> = self
1876 .scopes
1877 .iter()
1878 .rev()
1879 .find_map(|s| s.loop_meta.as_ref().map(|m| m.continue_patches.clone()))
1880 .unwrap_or_default();
1881 for pos in patches {
1882 self.chunk_mut().patch_jump(pos)?;
1883 }
1884 Ok(())
1885 }
1886
1887 fn compile_match(
1897 &mut self,
1898 scrutinee: &TypedExpr,
1899 arms: &[TypedMatchArm],
1900 line: u32,
1901 span: Span,
1902 ) -> Result<(), QalaError> {
1903 self.compile_expr(scrutinee)?;
1904 let enum_name = match scrutinee.ty() {
1906 QalaType::Named(Symbol(s)) => Some(s.clone()),
1907 _ => None,
1908 };
1909 let mut end_jumps: Vec<usize> = Vec::new();
1911 for (i, arm) in arms.iter().enumerate() {
1912 let is_last = i + 1 == arms.len();
1913 let mut fail_jumps: Vec<usize> = Vec::new();
1916 let mut bindings: Vec<(String, u16)> = Vec::new();
1919 let mut binding_keeps_scrutinee = false;
1925 match &arm.pattern {
1926 Pattern::Wildcard { .. } => {
1927 self.chunk_mut().write_op(Opcode::Pop, line);
1929 }
1930 Pattern::Binding { name, .. } => {
1931 if let Some(variant_id) = self.maybe_variant_id(enum_name.as_deref(), name) {
1937 if !is_last {
1938 self.chunk_mut().write_op(Opcode::Dup, line);
1939 }
1940 self.chunk_mut().write_op(Opcode::MatchVariant, line);
1941 self.chunk_mut().write_u16(variant_id, line);
1942 let patch = self.chunk_mut().code.len();
1943 self.chunk_mut().write_i16(i16::MAX, line);
1944 fail_jumps.push(patch);
1945 } else {
1946 if !is_last && arm.guard.is_some() {
1954 self.chunk_mut().write_op(Opcode::Dup, line);
1955 binding_keeps_scrutinee = true;
1956 }
1957 let slot = self.next_slot();
1958 self.bind_hidden_slot(slot);
1959 self.chunk_mut().write_op(Opcode::SetLocal, line);
1960 self.chunk_mut().write_u16(slot, line);
1961 bindings.push((name.clone(), slot));
1962 }
1963 }
1964 Pattern::Variant { name, sub, .. } => {
1965 if !is_last {
1968 self.chunk_mut().write_op(Opcode::Dup, line);
1969 }
1970 let variant_id = self.variant_id_for(enum_name.as_deref(), name, span)?;
1971 self.chunk_mut().write_op(Opcode::MatchVariant, line);
1972 self.chunk_mut().write_u16(variant_id, line);
1973 let patch = self.chunk_mut().code.len();
1974 self.chunk_mut().write_i16(i16::MAX, line);
1975 fail_jumps.push(patch);
1976 let mut slots: Vec<(String, u16)> = Vec::new();
1981 for sub_pat in sub {
1982 let bind_name = match sub_pat {
1985 Pattern::Binding { name, .. } => Some(name.clone()),
1986 Pattern::Wildcard { .. } => None,
1987 _ => None,
1988 };
1989 let slot = self.next_slot();
1990 self.bind_hidden_slot(slot);
1991 slots.push((bind_name.unwrap_or_default(), slot));
1992 }
1993 for (_, slot) in slots.iter().rev() {
1996 self.chunk_mut().write_op(Opcode::SetLocal, line);
1997 self.chunk_mut().write_u16(*slot, line);
1998 }
1999 for (bind_name, slot) in slots {
2001 if !bind_name.is_empty() {
2002 bindings.push((bind_name, slot));
2003 }
2004 }
2005 }
2006 Pattern::Int { value, .. } => {
2007 fail_jumps.push(self.emit_literal_pattern_test(
2008 ConstValue::I64(*value),
2009 is_last,
2010 line,
2011 ));
2012 }
2013 Pattern::Bool { value, .. } => {
2014 fail_jumps.push(self.emit_literal_pattern_test(
2015 ConstValue::Bool(*value),
2016 is_last,
2017 line,
2018 ));
2019 }
2020 Pattern::Byte { value, .. } => {
2021 fail_jumps.push(self.emit_literal_pattern_test(
2022 ConstValue::Byte(*value),
2023 is_last,
2024 line,
2025 ));
2026 }
2027 Pattern::Str { value, .. } => {
2028 fail_jumps.push(self.emit_literal_pattern_test(
2029 ConstValue::Str(value.clone()),
2030 is_last,
2031 line,
2032 ));
2033 }
2034 Pattern::Float { value, .. } => {
2035 fail_jumps.push(self.emit_literal_pattern_test(
2036 ConstValue::F64(*value),
2037 is_last,
2038 line,
2039 ));
2040 }
2041 }
2042 self.push_scope();
2045 for (bind_name, bind_slot) in bindings {
2046 self.register_local(bind_name, bind_slot);
2047 }
2048 if let Some(guard) = &arm.guard {
2050 self.compile_expr(guard)?;
2051 fail_jumps.push(self.chunk_mut().emit_jump(Opcode::JumpIfFalse, line));
2052 }
2053 if binding_keeps_scrutinee {
2059 self.chunk_mut().write_op(Opcode::Pop, line);
2060 }
2061 self.compile_match_arm_body(&arm.body)?;
2062 self.pop_scope_and_emit_defers(line)?;
2063 end_jumps.push(self.chunk_mut().emit_jump(Opcode::Jump, line));
2065 for pf in fail_jumps {
2067 self.chunk_mut().patch_jump(pf)?;
2068 }
2069 }
2070 for je in end_jumps {
2072 self.chunk_mut().patch_jump(je)?;
2073 }
2074 Ok(())
2075 }
2076
2077 fn emit_literal_pattern_test(&mut self, lit: ConstValue, is_last: bool, line: u32) -> usize {
2081 if !is_last {
2082 self.chunk_mut().write_op(Opcode::Dup, line);
2083 }
2084 self.emit_const(lit, line);
2085 self.chunk_mut().write_op(Opcode::Eq, line);
2086 self.chunk_mut().emit_jump(Opcode::JumpIfFalse, line)
2087 }
2088
2089 fn maybe_variant_id(&self, enum_name: Option<&str>, variant_name: &str) -> Option<u16> {
2094 if let Some(en) = enum_name {
2095 if let Some(&id) = self
2096 .enum_variant_table
2097 .get(&(en.to_string(), variant_name.to_string()))
2098 {
2099 return Some(id);
2100 }
2101 return None;
2103 }
2104 self.enum_variant_lookup(variant_name).copied()
2106 }
2107
2108 fn variant_id_for(
2112 &self,
2113 enum_name: Option<&str>,
2114 variant_name: &str,
2115 span: Span,
2116 ) -> Result<u16, QalaError> {
2117 if let Some(en) = enum_name
2118 && let Some(&id) = self
2119 .enum_variant_table
2120 .get(&(en.to_string(), variant_name.to_string()))
2121 {
2122 return Ok(id);
2123 }
2124 self.enum_variant_lookup(variant_name)
2125 .copied()
2126 .ok_or_else(|| QalaError::Type {
2127 span,
2128 message: format!("codegen: unknown variant `{variant_name}` in match"),
2129 })
2130 }
2131
2132 fn compile_match_arm_body(&mut self, body: &TypedMatchArmBody) -> Result<(), QalaError> {
2134 match body {
2135 TypedMatchArmBody::Expr(e) => self.compile_expr(e),
2136 TypedMatchArmBody::Block(b) => {
2137 self.compile_block(b)?;
2138 Ok(())
2139 }
2140 }
2141 }
2142}
2143
2144fn fold_binary_value(
2155 a: &ConstValue,
2156 b: &ConstValue,
2157 op: &BinOp,
2158 operand_ty: &QalaType,
2159 span: Span,
2160) -> Result<Option<ConstValue>, QalaError> {
2161 let overflow =
2162 |op: BinOp, lhs: i64, rhs: i64| QalaError::IntegerOverflow { span, op, lhs, rhs };
2163 let folded = match (a, b, op) {
2164 (ConstValue::I64(x), ConstValue::I64(y), BinOp::Add)
2166 if matches!(operand_ty, QalaType::I64) =>
2167 {
2168 ConstValue::I64(
2169 x.checked_add(*y)
2170 .ok_or_else(|| overflow(BinOp::Add, *x, *y))?,
2171 )
2172 }
2173 (ConstValue::I64(x), ConstValue::I64(y), BinOp::Sub)
2174 if matches!(operand_ty, QalaType::I64) =>
2175 {
2176 ConstValue::I64(
2177 x.checked_sub(*y)
2178 .ok_or_else(|| overflow(BinOp::Sub, *x, *y))?,
2179 )
2180 }
2181 (ConstValue::I64(x), ConstValue::I64(y), BinOp::Mul)
2182 if matches!(operand_ty, QalaType::I64) =>
2183 {
2184 ConstValue::I64(
2185 x.checked_mul(*y)
2186 .ok_or_else(|| overflow(BinOp::Mul, *x, *y))?,
2187 )
2188 }
2189 (ConstValue::I64(x), ConstValue::I64(y), BinOp::Div)
2190 if matches!(operand_ty, QalaType::I64) =>
2191 {
2192 if *y == 0 {
2193 return Ok(None);
2194 }
2195 ConstValue::I64(
2196 x.checked_div(*y)
2197 .ok_or_else(|| overflow(BinOp::Div, *x, *y))?,
2198 )
2199 }
2200 (ConstValue::I64(x), ConstValue::I64(y), BinOp::Rem)
2201 if matches!(operand_ty, QalaType::I64) =>
2202 {
2203 if *y == 0 {
2204 return Ok(None);
2205 }
2206 ConstValue::I64(
2207 x.checked_rem(*y)
2208 .ok_or_else(|| overflow(BinOp::Rem, *x, *y))?,
2209 )
2210 }
2211 (ConstValue::F64(x), ConstValue::F64(y), BinOp::Add) => ConstValue::F64(x + y),
2213 (ConstValue::F64(x), ConstValue::F64(y), BinOp::Sub) => ConstValue::F64(x - y),
2214 (ConstValue::F64(x), ConstValue::F64(y), BinOp::Mul) => ConstValue::F64(x * y),
2215 (ConstValue::F64(x), ConstValue::F64(y), BinOp::Div) => ConstValue::F64(x / y),
2216 (ConstValue::Str(x), ConstValue::Str(y), BinOp::Add) => {
2218 let mut s = String::with_capacity(x.len() + y.len());
2219 s.push_str(x);
2220 s.push_str(y);
2221 ConstValue::Str(s)
2222 }
2223 (ConstValue::I64(x), ConstValue::I64(y), BinOp::Eq) => ConstValue::Bool(x == y),
2225 (ConstValue::I64(x), ConstValue::I64(y), BinOp::Ne) => ConstValue::Bool(x != y),
2226 (ConstValue::I64(x), ConstValue::I64(y), BinOp::Lt) => ConstValue::Bool(x < y),
2227 (ConstValue::I64(x), ConstValue::I64(y), BinOp::Le) => ConstValue::Bool(x <= y),
2228 (ConstValue::I64(x), ConstValue::I64(y), BinOp::Gt) => ConstValue::Bool(x > y),
2229 (ConstValue::I64(x), ConstValue::I64(y), BinOp::Ge) => ConstValue::Bool(x >= y),
2230 (ConstValue::F64(x), ConstValue::F64(y), BinOp::Eq) => ConstValue::Bool(x == y),
2232 (ConstValue::F64(x), ConstValue::F64(y), BinOp::Ne) => ConstValue::Bool(x != y),
2233 (ConstValue::F64(x), ConstValue::F64(y), BinOp::Lt) => ConstValue::Bool(x < y),
2234 (ConstValue::F64(x), ConstValue::F64(y), BinOp::Le) => ConstValue::Bool(x <= y),
2235 (ConstValue::F64(x), ConstValue::F64(y), BinOp::Gt) => ConstValue::Bool(x > y),
2236 (ConstValue::F64(x), ConstValue::F64(y), BinOp::Ge) => ConstValue::Bool(x >= y),
2237 (ConstValue::Byte(x), ConstValue::Byte(y), BinOp::Eq) => ConstValue::Bool(x == y),
2239 (ConstValue::Byte(x), ConstValue::Byte(y), BinOp::Ne) => ConstValue::Bool(x != y),
2240 (ConstValue::Bool(x), ConstValue::Bool(y), BinOp::And) => ConstValue::Bool(*x && *y),
2242 (ConstValue::Bool(x), ConstValue::Bool(y), BinOp::Or) => ConstValue::Bool(*x || *y),
2243 (ConstValue::Bool(x), ConstValue::Bool(y), BinOp::Eq) => ConstValue::Bool(x == y),
2244 (ConstValue::Bool(x), ConstValue::Bool(y), BinOp::Ne) => ConstValue::Bool(x != y),
2245 (ConstValue::Str(x), ConstValue::Str(y), BinOp::Eq) => ConstValue::Bool(x == y),
2247 (ConstValue::Str(x), ConstValue::Str(y), BinOp::Ne) => ConstValue::Bool(x != y),
2248 _ => return Ok(None),
2250 };
2251 Ok(Some(folded))
2252}
2253
2254fn is_constable_primitive(v: &ConstValue) -> bool {
2258 matches!(
2259 v,
2260 ConstValue::I64(_)
2261 | ConstValue::F64(_)
2262 | ConstValue::Bool(_)
2263 | ConstValue::Byte(_)
2264 | ConstValue::Str(_)
2265 | ConstValue::Void
2266 )
2267}
2268
2269fn value_type_name(v: &ConstValue) -> String {
2273 match v {
2274 ConstValue::I64(_) => "i64",
2275 ConstValue::F64(_) => "f64",
2276 ConstValue::Bool(_) => "bool",
2277 ConstValue::Byte(_) => "byte",
2278 ConstValue::Str(_) => "str",
2279 ConstValue::Void => "void",
2280 ConstValue::Function(_) => "fn",
2281 }
2282 .to_string()
2283}
2284
2285struct Frame {
2290 chunk_id: u16,
2293 locals: Vec<ConstValue>,
2296 ip: usize,
2298}
2299
2300struct ComptimeInterpreter<'a> {
2306 program: &'a Program,
2308 fn_effects: &'a HashMap<u16, EffectSet>,
2310 stack: Vec<ConstValue>,
2312 frames: Vec<Frame>,
2314 budget: u32,
2316 block_span: Span,
2318}
2319
2320impl<'a> ComptimeInterpreter<'a> {
2321 fn new(
2323 program: &'a Program,
2324 fn_effects: &'a HashMap<u16, EffectSet>,
2325 block_span: Span,
2326 ) -> Self {
2327 ComptimeInterpreter {
2328 program,
2329 fn_effects,
2330 stack: Vec::new(),
2331 frames: Vec::new(),
2332 budget: COMPTIME_BUDGET,
2333 block_span,
2334 }
2335 }
2336
2337 fn underflow(&self) -> QalaError {
2341 QalaError::Type {
2342 span: self.block_span,
2343 message: "comptime interpreter: stack underflow".to_string(),
2344 }
2345 }
2346
2347 fn pop(&mut self) -> Result<ConstValue, QalaError> {
2349 self.stack.pop().ok_or_else(|| self.underflow())
2350 }
2351
2352 fn run(&mut self, throwaway: Chunk) -> Result<ConstValue, QalaError> {
2355 self.frames.push(Frame {
2357 chunk_id: u16::MAX,
2358 locals: Vec::new(),
2359 ip: 0,
2360 });
2361 while !self.frames.is_empty() {
2362 let frame_idx = self.frames.len() - 1;
2364 let chunk_id = self.frames[frame_idx].chunk_id;
2365 let chunk: &Chunk = if chunk_id == u16::MAX {
2366 &throwaway
2367 } else {
2368 self.program
2369 .chunks
2370 .get(chunk_id as usize)
2371 .ok_or_else(|| QalaError::Type {
2372 span: self.block_span,
2373 message: "comptime interpreter: call to a missing chunk".to_string(),
2374 })?
2375 };
2376 let ip = self.frames[frame_idx].ip;
2377 if ip >= chunk.code.len() {
2379 self.frames.pop();
2380 if self.frames.is_empty() {
2381 return Ok(ConstValue::Void);
2382 }
2383 self.stack.push(ConstValue::Void);
2384 continue;
2385 }
2386 if self.budget == 0 {
2387 return Err(QalaError::ComptimeBudgetExceeded {
2388 span: self.block_span,
2389 });
2390 }
2391 self.budget -= 1;
2392 let op_byte = chunk.code[ip];
2393 let op = Opcode::from_u8(op_byte).ok_or_else(|| QalaError::Type {
2394 span: self.block_span,
2395 message: format!("comptime interpreter: undefined opcode {op_byte:#x}"),
2396 })?;
2397 let next_ip = ip + 1 + op.operand_bytes() as usize;
2400 self.frames[frame_idx].ip = next_ip;
2401 match op {
2402 Opcode::Const => {
2403 let idx = chunk.read_u16(ip + 1) as usize;
2404 let v = chunk
2405 .constants
2406 .get(idx)
2407 .cloned()
2408 .ok_or_else(|| QalaError::Type {
2409 span: self.block_span,
2410 message: "comptime interpreter: bad constant index".to_string(),
2411 })?;
2412 self.stack.push(v);
2413 }
2414 Opcode::Pop => {
2415 self.pop()?;
2416 }
2417 Opcode::Dup => {
2418 let top = self.stack.last().cloned().ok_or_else(|| self.underflow())?;
2419 self.stack.push(top);
2420 }
2421 Opcode::GetLocal => {
2422 let slot = chunk.read_u16(ip + 1) as usize;
2423 let v = self.frames[frame_idx]
2424 .locals
2425 .get(slot)
2426 .cloned()
2427 .ok_or_else(|| QalaError::Type {
2428 span: self.block_span,
2429 message: "comptime interpreter: unbound local".to_string(),
2430 })?;
2431 self.stack.push(v);
2432 }
2433 Opcode::SetLocal => {
2434 let slot = chunk.read_u16(ip + 1) as usize;
2435 let v = self.pop()?;
2436 let locals = &mut self.frames[frame_idx].locals;
2437 if slot >= locals.len() {
2438 locals.resize(slot + 1, ConstValue::Void);
2439 }
2440 locals[slot] = v;
2441 }
2442 Opcode::Add
2443 | Opcode::Sub
2444 | Opcode::Mul
2445 | Opcode::Div
2446 | Opcode::Mod
2447 | Opcode::FAdd
2448 | Opcode::FSub
2449 | Opcode::FMul
2450 | Opcode::FDiv => {
2451 let b = self.pop()?;
2452 let a = self.pop()?;
2453 let r = self.eval_arith(op, a, b)?;
2454 self.stack.push(r);
2455 }
2456 Opcode::Neg => {
2457 let a = self.pop()?;
2458 match a {
2459 ConstValue::I64(n) => {
2460 let r = n.checked_neg().ok_or(QalaError::IntegerOverflow {
2461 span: self.block_span,
2462 op: BinOp::Sub,
2463 lhs: 0,
2464 rhs: n,
2465 })?;
2466 self.stack.push(ConstValue::I64(r));
2467 }
2468 _ => return Err(self.type_error("NEG expects i64")),
2469 }
2470 }
2471 Opcode::FNeg => {
2472 let a = self.pop()?;
2473 match a {
2474 ConstValue::F64(x) => self.stack.push(ConstValue::F64(-x)),
2475 _ => return Err(self.type_error("F_NEG expects f64")),
2476 }
2477 }
2478 Opcode::Eq
2479 | Opcode::Ne
2480 | Opcode::Lt
2481 | Opcode::Le
2482 | Opcode::Gt
2483 | Opcode::Ge
2484 | Opcode::FEq
2485 | Opcode::FNe
2486 | Opcode::FLt
2487 | Opcode::FLe
2488 | Opcode::FGt
2489 | Opcode::FGe => {
2490 let b = self.pop()?;
2491 let a = self.pop()?;
2492 let r = self.eval_compare(op, a, b)?;
2493 self.stack.push(ConstValue::Bool(r));
2494 }
2495 Opcode::Not => {
2496 let a = self.pop()?;
2497 match a {
2498 ConstValue::Bool(b) => self.stack.push(ConstValue::Bool(!b)),
2499 _ => return Err(self.type_error("NOT expects bool")),
2500 }
2501 }
2502 Opcode::Jump => {
2503 let offset = chunk.read_i16(ip + 1);
2504 self.frames[frame_idx].ip = jump_target(next_ip, offset)?;
2505 }
2506 Opcode::JumpIfFalse => {
2507 let offset = chunk.read_i16(ip + 1);
2508 let cond = self.pop()?;
2509 if matches!(cond, ConstValue::Bool(false)) {
2510 self.frames[frame_idx].ip = jump_target(next_ip, offset)?;
2511 }
2512 }
2513 Opcode::JumpIfTrue => {
2514 let offset = chunk.read_i16(ip + 1);
2515 let cond = self.pop()?;
2516 if matches!(cond, ConstValue::Bool(true)) {
2517 self.frames[frame_idx].ip = jump_target(next_ip, offset)?;
2518 }
2519 }
2520 Opcode::Call => {
2521 let fn_id = chunk.read_u16(ip + 1);
2522 let argc = chunk.code[ip + 3];
2523 let effect = self
2526 .fn_effects
2527 .get(&fn_id)
2528 .copied()
2529 .unwrap_or_else(EffectSet::full);
2530 if !effect.is_pure() {
2531 let fn_name = self
2532 .program
2533 .fn_names
2534 .get(fn_id as usize)
2535 .cloned()
2536 .unwrap_or_else(|| format!("#{fn_id}"));
2537 return Err(QalaError::ComptimeEffectViolation {
2538 span: self.block_span,
2539 fn_name,
2540 effect: effect.display(),
2541 });
2542 }
2543 if fn_id >= STDLIB_FN_BASE {
2546 return Err(QalaError::Type {
2547 span: self.block_span,
2548 message: "comptime interpreter: stdlib calls inside comptime \
2549 are a v2 feature"
2550 .to_string(),
2551 });
2552 }
2553 if self.frames.len() >= COMPTIME_MAX_FRAMES {
2554 return Err(QalaError::ComptimeBudgetExceeded {
2556 span: self.block_span,
2557 });
2558 }
2559 let mut locals: Vec<ConstValue> = Vec::with_capacity(argc as usize);
2561 for _ in 0..argc {
2562 locals.push(self.pop()?);
2563 }
2564 locals.reverse();
2565 self.frames.push(Frame {
2566 chunk_id: fn_id,
2567 locals,
2568 ip: 0,
2569 });
2570 }
2571 Opcode::Return => {
2572 let result = self.pop()?;
2573 self.frames.pop();
2574 if self.frames.is_empty() {
2575 return Ok(result);
2576 }
2577 self.stack.push(result);
2578 }
2579 Opcode::ToStr => {
2580 let v = self.pop()?;
2581 self.stack.push(ConstValue::Str(v.to_string()));
2582 }
2583 Opcode::ConcatN => {
2584 let count = chunk.read_u16(ip + 1) as usize;
2585 if self.stack.len() < count {
2586 return Err(self.underflow());
2587 }
2588 let parts = self.stack.split_off(self.stack.len() - count);
2589 let mut s = String::new();
2590 for part in parts {
2591 match part {
2592 ConstValue::Str(p) => s.push_str(&p),
2593 other => s.push_str(&other.to_string()),
2594 }
2595 }
2596 self.stack.push(ConstValue::Str(s));
2597 }
2598 Opcode::MakeArray
2601 | Opcode::MakeTuple
2602 | Opcode::MakeStruct
2603 | Opcode::MakeEnumVariant
2604 | Opcode::Index
2605 | Opcode::Field
2606 | Opcode::Len
2607 | Opcode::MatchVariant => {
2608 return Err(QalaError::Type {
2609 span: self.block_span,
2610 message: format!(
2611 "comptime interpreter: {} is not supported inside comptime (v1)",
2612 op.name()
2613 ),
2614 });
2615 }
2616 Opcode::GetGlobal | Opcode::SetGlobal => {
2617 return Err(QalaError::Type {
2618 span: self.block_span,
2619 message: "comptime interpreter: globals are not supported in comptime"
2620 .to_string(),
2621 });
2622 }
2623 Opcode::Halt => {
2624 return Err(QalaError::Type {
2625 span: self.block_span,
2626 message: "comptime interpreter: unexpected HALT".to_string(),
2627 });
2628 }
2629 }
2630 }
2631 Err(QalaError::Type {
2632 span: self.block_span,
2633 message: "comptime interpreter: ran out of frames without a result".to_string(),
2634 })
2635 }
2636
2637 fn type_error(&self, message: &str) -> QalaError {
2639 QalaError::Type {
2640 span: self.block_span,
2641 message: format!("comptime interpreter: {message}"),
2642 }
2643 }
2644
2645 fn eval_arith(
2649 &self,
2650 op: Opcode,
2651 a: ConstValue,
2652 b: ConstValue,
2653 ) -> Result<ConstValue, QalaError> {
2654 let int_overflow = |bin: BinOp, x: i64, y: i64| QalaError::IntegerOverflow {
2655 span: self.block_span,
2656 op: bin,
2657 lhs: x,
2658 rhs: y,
2659 };
2660 match (op, a, b) {
2661 (Opcode::Add, ConstValue::I64(x), ConstValue::I64(y)) => Ok(ConstValue::I64(
2662 x.checked_add(y)
2663 .ok_or_else(|| int_overflow(BinOp::Add, x, y))?,
2664 )),
2665 (Opcode::Sub, ConstValue::I64(x), ConstValue::I64(y)) => Ok(ConstValue::I64(
2666 x.checked_sub(y)
2667 .ok_or_else(|| int_overflow(BinOp::Sub, x, y))?,
2668 )),
2669 (Opcode::Mul, ConstValue::I64(x), ConstValue::I64(y)) => Ok(ConstValue::I64(
2670 x.checked_mul(y)
2671 .ok_or_else(|| int_overflow(BinOp::Mul, x, y))?,
2672 )),
2673 (Opcode::Div, ConstValue::I64(x), ConstValue::I64(y)) => {
2674 if y == 0 {
2675 return Err(self.type_error("division by zero"));
2676 }
2677 Ok(ConstValue::I64(
2678 x.checked_div(y)
2679 .ok_or_else(|| int_overflow(BinOp::Div, x, y))?,
2680 ))
2681 }
2682 (Opcode::Mod, ConstValue::I64(x), ConstValue::I64(y)) => {
2683 if y == 0 {
2684 return Err(self.type_error("remainder by zero"));
2685 }
2686 Ok(ConstValue::I64(
2687 x.checked_rem(y)
2688 .ok_or_else(|| int_overflow(BinOp::Rem, x, y))?,
2689 ))
2690 }
2691 (Opcode::FAdd, ConstValue::F64(x), ConstValue::F64(y)) => Ok(ConstValue::F64(x + y)),
2692 (Opcode::FSub, ConstValue::F64(x), ConstValue::F64(y)) => Ok(ConstValue::F64(x - y)),
2693 (Opcode::FMul, ConstValue::F64(x), ConstValue::F64(y)) => Ok(ConstValue::F64(x * y)),
2694 (Opcode::FDiv, ConstValue::F64(x), ConstValue::F64(y)) => Ok(ConstValue::F64(x / y)),
2695 _ => Err(self.type_error("arithmetic operand type mismatch")),
2696 }
2697 }
2698
2699 fn eval_compare(&self, op: Opcode, a: ConstValue, b: ConstValue) -> Result<bool, QalaError> {
2701 match (op, a, b) {
2702 (Opcode::Eq, ConstValue::I64(x), ConstValue::I64(y)) => Ok(x == y),
2703 (Opcode::Ne, ConstValue::I64(x), ConstValue::I64(y)) => Ok(x != y),
2704 (Opcode::Lt, ConstValue::I64(x), ConstValue::I64(y)) => Ok(x < y),
2705 (Opcode::Le, ConstValue::I64(x), ConstValue::I64(y)) => Ok(x <= y),
2706 (Opcode::Gt, ConstValue::I64(x), ConstValue::I64(y)) => Ok(x > y),
2707 (Opcode::Ge, ConstValue::I64(x), ConstValue::I64(y)) => Ok(x >= y),
2708 (Opcode::Eq, ConstValue::Bool(x), ConstValue::Bool(y)) => Ok(x == y),
2709 (Opcode::Ne, ConstValue::Bool(x), ConstValue::Bool(y)) => Ok(x != y),
2710 (Opcode::Eq, ConstValue::Str(x), ConstValue::Str(y)) => Ok(x == y),
2711 (Opcode::Ne, ConstValue::Str(x), ConstValue::Str(y)) => Ok(x != y),
2712 (Opcode::FEq, ConstValue::F64(x), ConstValue::F64(y)) => Ok(x == y),
2713 (Opcode::FNe, ConstValue::F64(x), ConstValue::F64(y)) => Ok(x != y),
2714 (Opcode::FLt, ConstValue::F64(x), ConstValue::F64(y)) => Ok(x < y),
2715 (Opcode::FLe, ConstValue::F64(x), ConstValue::F64(y)) => Ok(x <= y),
2716 (Opcode::FGt, ConstValue::F64(x), ConstValue::F64(y)) => Ok(x > y),
2717 (Opcode::FGe, ConstValue::F64(x), ConstValue::F64(y)) => Ok(x >= y),
2718 _ => Err(self.type_error("comparison operand type mismatch")),
2719 }
2720 }
2721}
2722
2723fn jump_target(after_operand: usize, offset: i16) -> Result<usize, QalaError> {
2727 let target = after_operand as isize + offset as isize;
2728 if target < 0 {
2729 return Err(QalaError::Type {
2730 span: Span::new(0, 0),
2731 message: "comptime interpreter: jump target out of range".to_string(),
2732 });
2733 }
2734 Ok(target as usize)
2735}
2736
2737#[cfg(test)]
2738mod tests {
2739 use super::*;
2740 use crate::lexer::Lexer;
2741 use crate::parser::Parser;
2742 use crate::typechecker::check_program;
2743
2744 fn compile(src: &str) -> Result<Program, Vec<QalaError>> {
2747 let tokens = Lexer::tokenize(src).expect("lex failed");
2748 let ast = Parser::parse(&tokens).expect("parse failed");
2749 let (typed, terrors, _) = check_program(&ast, src);
2750 assert!(terrors.is_empty(), "typecheck errors: {terrors:?}");
2751 compile_program(&typed, src)
2752 }
2753
2754 fn compile_ok(src: &str) -> Program {
2756 compile(src).unwrap_or_else(|e| panic!("codegen errors: {e:?}"))
2757 }
2758
2759 fn typecheck(src: &str) -> TypedAst {
2762 let tokens = Lexer::tokenize(src).expect("lex failed");
2763 let ast = Parser::parse(&tokens).expect("parse failed");
2764 let (typed, terrors, _) = check_program(&ast, src);
2765 assert!(terrors.is_empty(), "typecheck errors: {terrors:?}");
2766 typed
2767 }
2768
2769 fn compile_expr_of(src: &str, fn_name: &str) -> (Vec<u8>, Vec<ConstValue>) {
2777 let typed = typecheck(src);
2778 let mut cg = Codegen::new(src);
2779 cg.build_tables(&typed);
2780 let decl = typed
2782 .iter()
2783 .find_map(|item| match item {
2784 TypedItem::Fn(d) if d.name == fn_name => Some(d),
2785 _ => None,
2786 })
2787 .unwrap_or_else(|| panic!("function `{fn_name}` not found"));
2788 let fn_id = cg.fn_table[&FnKey {
2789 type_name: None,
2790 name: fn_name.to_string(),
2791 }];
2792 cg.current_fn_id = fn_id;
2793 cg.push_scope();
2794 for (i, param) in decl.params.iter().enumerate() {
2795 if let Some(scope) = cg.scopes.last_mut() {
2796 scope.locals.push((param.name.clone(), i as u16));
2797 }
2798 }
2799 let value = decl
2800 .body
2801 .value
2802 .as_ref()
2803 .unwrap_or_else(|| panic!("function `{fn_name}` body has no trailing value"));
2804 cg.compile_expr(value).expect("compile_expr failed");
2805 let chunk = &cg.program.chunks[fn_id as usize];
2806 (chunk.code.clone(), chunk.constants.clone())
2807 }
2808
2809 fn compile_expr_err(src: &str, fn_name: &str) -> QalaError {
2812 let typed = typecheck(src);
2813 let mut cg = Codegen::new(src);
2814 cg.build_tables(&typed);
2815 let decl = typed
2816 .iter()
2817 .find_map(|item| match item {
2818 TypedItem::Fn(d) if d.name == fn_name => Some(d),
2819 _ => None,
2820 })
2821 .unwrap_or_else(|| panic!("function `{fn_name}` not found"));
2822 let fn_id = cg.fn_table[&FnKey {
2823 type_name: None,
2824 name: fn_name.to_string(),
2825 }];
2826 cg.current_fn_id = fn_id;
2827 cg.push_scope();
2828 for (i, param) in decl.params.iter().enumerate() {
2829 if let Some(scope) = cg.scopes.last_mut() {
2830 scope.locals.push((param.name.clone(), i as u16));
2831 }
2832 }
2833 let value = decl
2834 .body
2835 .value
2836 .as_ref()
2837 .expect("body has no trailing value");
2838 cg.compile_expr(value)
2839 .expect_err("expected a codegen error")
2840 }
2841
2842 fn opcodes(code: &[u8]) -> Vec<Opcode> {
2845 let mut ops = Vec::new();
2846 let mut off = 0;
2847 while off < code.len() {
2848 match Opcode::from_u8(code[off]) {
2849 Some(op) => {
2850 ops.push(op);
2851 off += 1 + op.operand_bytes() as usize;
2852 }
2853 None => break,
2854 }
2855 }
2856 ops
2857 }
2858
2859 #[test]
2861 fn skeleton_empty_program_compiles_to_empty_program() {
2862 let p = compile_program(&Vec::new(), "").expect("empty program should compile");
2863 assert!(p.chunks.is_empty(), "chunks should be empty");
2864 assert_eq!(p.main_index, 0);
2865 assert!(p.globals.is_empty());
2866 assert!(p.fn_names.is_empty());
2867 assert_eq!(p.enum_variant_names.len(), 4);
2869 }
2870
2871 #[test]
2873 fn skeleton_single_main_fn_produces_one_chunk() {
2874 let p = compile_ok("fn main() is io { }");
2875 assert_eq!(p.chunks.len(), 1, "one user fn -> one chunk");
2876 assert_eq!(p.main_index, 0);
2877 assert_eq!(p.fn_names, vec!["main".to_string()]);
2878 assert!(
2880 !p.chunks[0].code.is_empty(),
2881 "main chunk should have a RETURN"
2882 );
2883 assert_eq!(p.chunks[0].code[0], Opcode::Return as u8);
2884 }
2885
2886 #[test]
2888 fn skeleton_build_tables_records_struct_field_indices() {
2889 let src = "struct Point { x: f64, y: f64 }\nfn main() is io { }";
2890 let tokens = Lexer::tokenize(src).expect("lex");
2891 let ast = Parser::parse(&tokens).expect("parse");
2892 let (typed, terrors, _) = check_program(&ast, src);
2893 assert!(terrors.is_empty(), "{terrors:?}");
2894 let mut cg = Codegen::new(src);
2895 cg.build_tables(&typed);
2896 assert_eq!(
2897 cg.struct_field_index
2898 .get(&("Point".to_string(), "x".to_string())),
2899 Some(&0)
2900 );
2901 assert_eq!(
2902 cg.struct_field_index
2903 .get(&("Point".to_string(), "y".to_string())),
2904 Some(&1)
2905 );
2906 }
2907
2908 #[test]
2910 fn skeleton_build_tables_records_enum_variant_ids_and_payloads() {
2911 let src = "enum Shape { Circle(f64), Rect(f64, f64), Triangle }\nfn main() is io { }";
2912 let tokens = Lexer::tokenize(src).expect("lex");
2913 let ast = Parser::parse(&tokens).expect("parse");
2914 let (typed, terrors, _) = check_program(&ast, src);
2915 assert!(terrors.is_empty(), "{terrors:?}");
2916 let mut cg = Codegen::new(src);
2917 cg.build_tables(&typed);
2918 for v in ["Circle", "Rect", "Triangle"] {
2921 assert!(
2922 cg.enum_variant_table
2923 .contains_key(&("Shape".to_string(), v.to_string())),
2924 "missing variant {v}"
2925 );
2926 }
2927 assert_eq!(
2928 cg.enum_variant_payload_count
2929 .get(&("Shape".to_string(), "Circle".to_string())),
2930 Some(&1)
2931 );
2932 assert_eq!(
2933 cg.enum_variant_payload_count
2934 .get(&("Shape".to_string(), "Rect".to_string())),
2935 Some(&2)
2936 );
2937 assert_eq!(
2938 cg.enum_variant_payload_count
2939 .get(&("Shape".to_string(), "Triangle".to_string())),
2940 Some(&0)
2941 );
2942 let circle_id = cg.enum_variant_table[&("Shape".to_string(), "Circle".to_string())];
2945 assert_eq!(
2946 cg.program.enum_variant_names[circle_id as usize],
2947 ("Shape".to_string(), "Circle".to_string())
2948 );
2949 }
2950
2951 #[test]
2955 fn enum_variant_lookup_is_deterministic_with_shared_variant_name() {
2956 let src = "enum A { Done }\nenum B { Done }\nfn main() is io { }";
2957 let tokens = Lexer::tokenize(src).expect("lex");
2958 let ast = Parser::parse(&tokens).expect("parse");
2959 let (typed, terrors, _) = check_program(&ast, src);
2960 assert!(terrors.is_empty(), "{terrors:?}");
2961 let mut cg1 = Codegen::new(src);
2962 cg1.build_tables(&typed);
2963 let mut cg2 = Codegen::new(src);
2964 cg2.build_tables(&typed);
2965 let id1 = cg1.enum_variant_lookup("Done").copied();
2968 let id2 = cg2.enum_variant_lookup("Done").copied();
2969 assert!(id1.is_some(), "lookup should find a variant named Done");
2970 assert_eq!(
2971 id1, id2,
2972 "variant id must be deterministic across instances"
2973 );
2974 }
2975
2976 #[test]
2979 fn skeleton_stdlib_fn_ids_are_reserved_and_separate_from_user_ids() {
2980 let src = "fn helper() is pure { }\nfn main() is io { }";
2981 let tokens = Lexer::tokenize(src).expect("lex");
2982 let ast = Parser::parse(&tokens).expect("parse");
2983 let (typed, terrors, _) = check_program(&ast, src);
2984 assert!(terrors.is_empty(), "{terrors:?}");
2985 let mut cg = Codegen::new(src);
2986 cg.build_tables(&typed);
2987 let helper_id = cg.fn_table[&FnKey {
2989 type_name: None,
2990 name: "helper".to_string(),
2991 }];
2992 assert_eq!(helper_id, 0);
2993 let (println_id, println_effect) = cg.stdlib_table[&FnKey {
2995 type_name: None,
2996 name: "println".to_string(),
2997 }];
2998 assert_eq!(println_id, STDLIB_FN_BASE + 1);
2999 assert_eq!(println_effect, EffectSet::io());
3000 assert_eq!(cg.fn_effects.get(&println_id), Some(&EffectSet::io()));
3002 let (sqrt_id, _) = cg.stdlib_table[&FnKey {
3004 type_name: None,
3005 name: "sqrt".to_string(),
3006 }];
3007 assert_eq!(cg.fn_effects.get(&sqrt_id), Some(&EffectSet::pure()));
3008 }
3009
3010 #[test]
3012 fn skeleton_push_then_pop_empty_scope_is_a_noop() {
3013 let mut cg = Codegen::new("");
3014 cg.program.chunks.push(Chunk::new());
3015 cg.program.fn_names.push("f".to_string());
3016 cg.current_fn_id = 0;
3017 cg.push_scope();
3018 assert_eq!(cg.scopes.len(), 1);
3019 cg.pop_scope_and_emit_defers(1).expect("pop empty scope");
3020 assert!(cg.scopes.is_empty());
3021 assert!(cg.program.chunks[0].code.is_empty());
3023 }
3024
3025 #[test]
3027 fn skeleton_pop_scope_emits_a_single_registered_defer() {
3028 let src = "fn f() is io { }";
3032 let tokens = Lexer::tokenize(src).expect("lex");
3033 let ast = Parser::parse(&tokens).expect("parse");
3034 let (typed, _, _) = check_program(&ast, src);
3035 let mut cg = Codegen::new(src);
3036 cg.build_tables(&typed);
3037 cg.current_fn_id = 0;
3038 cg.push_scope();
3039 let call = TypedExpr::Call {
3044 callee: Box::new(TypedExpr::Ident {
3045 name: "println".to_string(),
3046 ty: QalaType::Function {
3047 params: vec![QalaType::Str],
3048 returns: Box::new(QalaType::Void),
3049 },
3050 span: Span::new(0, 7),
3051 }),
3052 args: vec![TypedExpr::Str {
3053 value: "a".to_string(),
3054 ty: QalaType::Str,
3055 span: Span::new(8, 3),
3056 }],
3057 ty: QalaType::Void,
3058 span: Span::new(0, 12),
3059 };
3060 if let Some(scope) = cg.scopes.last_mut() {
3061 scope.deferred.push(call);
3062 }
3063 cg.pop_scope_and_emit_defers(1).expect("emit defers");
3064 assert!(cg.scopes.is_empty(), "scope should be popped");
3065 let ops = opcodes(&cg.program.chunks[0].code);
3068 assert_eq!(ops, vec![Opcode::Const, Opcode::Call, Opcode::Pop]);
3069 }
3070
3071 #[test]
3073 fn skeleton_fn_params_occupy_slots_zero_and_one() {
3074 let p = compile_ok("fn add(a: i64, b: i64) -> i64 is pure { return a + b }");
3078 assert_eq!(p.chunks.len(), 1);
3079 assert_eq!(p.fn_names, vec!["add".to_string()]);
3080 }
3081
3082 #[test]
3084 fn skeleton_type_level_items_emit_no_chunks() {
3085 let p = compile_ok(
3086 "struct Point { x: i64, y: i64 }\n\
3087 enum Dir { North, South }\n\
3088 interface Show { fn show(self) -> str }\n\
3089 fn main() is io { }",
3090 );
3091 assert_eq!(p.chunks.len(), 1);
3093 assert_eq!(p.fn_names, vec!["main".to_string()]);
3094 }
3095
3096 #[test]
3098 fn skeleton_two_functions_get_sequential_fn_ids() {
3099 let p = compile_ok("fn first() is pure { }\nfn second() is pure { }");
3100 assert_eq!(p.chunks.len(), 2);
3101 assert_eq!(p.fn_names, vec!["first".to_string(), "second".to_string()]);
3102 }
3103
3104 #[test]
3106 fn skeleton_empty_fn_body_emits_a_return() {
3107 let p = compile_ok("fn main() is io { }");
3108 assert_eq!(p.chunks[0].code, vec![Opcode::Return as u8]);
3109 assert_eq!(p.chunks[0].source_lines.len(), 1);
3110 }
3111
3112 #[test]
3114 fn skeleton_line_at_maps_span_to_source_line() {
3115 let cg = Codegen::new("abc");
3116 assert_eq!(cg.line_at(Span::new(0, 3)), 1);
3117 let cg2 = Codegen::new("abc\ndef");
3118 assert_eq!(cg2.line_at(Span::new(4, 3)), 2);
3120 }
3121
3122 #[test]
3127 fn compile_expr_int_literal_emits_one_const() {
3128 let (code, consts) = compile_expr_of("fn f() -> i64 is pure { 42 }", "f");
3129 assert_eq!(opcodes(&code), vec![Opcode::Const]);
3130 assert_eq!(consts, vec![ConstValue::I64(42)]);
3131 }
3132
3133 #[test]
3135 fn compile_expr_float_literal_emits_one_const() {
3136 let (code, consts) = compile_expr_of("fn f() -> f64 is pure { 3.5 }", "f");
3137 assert_eq!(opcodes(&code), vec![Opcode::Const]);
3138 assert_eq!(consts, vec![ConstValue::F64(3.5)]);
3139 }
3140
3141 #[test]
3143 fn compile_expr_bool_literal_emits_one_const() {
3144 let (_, consts) = compile_expr_of("fn f() -> bool is pure { true }", "f");
3145 assert_eq!(consts, vec![ConstValue::Bool(true)]);
3146 }
3147
3148 #[test]
3150 fn compile_expr_byte_literal_emits_one_const() {
3151 let (_, consts) = compile_expr_of("fn f() -> byte is pure { b'A' }", "f");
3152 assert_eq!(consts, vec![ConstValue::Byte(0x41)]);
3153 }
3154
3155 #[test]
3157 fn compile_expr_str_literal_emits_one_const() {
3158 let (_, consts) = compile_expr_of("fn f() -> str is pure { \"hi\" }", "f");
3159 assert_eq!(consts, vec![ConstValue::Str("hi".to_string())]);
3160 }
3161
3162 #[test]
3164 fn compile_expr_ident_param_emits_get_local() {
3165 let (code, _) = compile_expr_of("fn f(x: i64) -> i64 is pure { x }", "f");
3166 assert_eq!(opcodes(&code), vec![Opcode::GetLocal]);
3167 assert_eq!(code[0], Opcode::GetLocal as u8);
3169 assert_eq!(u16::from_le_bytes([code[1], code[2]]), 0);
3170 }
3171
3172 #[test]
3175 fn compile_expr_binary_arith_folds_to_one_const() {
3176 let (code, consts) = compile_expr_of("fn f() -> i64 is pure { 1 + 2 }", "f");
3177 assert_eq!(opcodes(&code), vec![Opcode::Const], "1+2 should fold");
3178 assert_eq!(consts.last(), Some(&ConstValue::I64(3)));
3181 }
3182
3183 #[test]
3185 fn compile_expr_binary_chain_folds_bottom_up() {
3186 let (code, consts) = compile_expr_of("fn f() -> i64 is pure { 1 + 2 + 3 }", "f");
3187 assert_eq!(opcodes(&code), vec![Opcode::Const]);
3188 assert_eq!(consts.last(), Some(&ConstValue::I64(6)));
3189 }
3190
3191 #[test]
3193 fn compile_expr_binary_with_local_does_not_fold() {
3194 let (code, _) = compile_expr_of("fn f(x: i64) -> i64 is pure { x + 1 }", "f");
3195 assert_eq!(
3196 opcodes(&code),
3197 vec![Opcode::GetLocal, Opcode::Const, Opcode::Add]
3198 );
3199 }
3200
3201 #[test]
3203 fn compile_expr_fold_add_overflow_is_an_error() {
3204 let err = compile_expr_err("fn f() -> i64 is pure { 9223372036854775807 + 1 }", "f");
3205 match err {
3206 QalaError::IntegerOverflow { op, lhs, rhs, .. } => {
3207 assert_eq!(op, BinOp::Add);
3208 assert_eq!(lhs, i64::MAX);
3209 assert_eq!(rhs, 1);
3210 }
3211 other => panic!("expected IntegerOverflow, got {other:?}"),
3212 }
3213 }
3214
3215 #[test]
3217 fn compile_expr_fold_mul_overflow_is_an_error() {
3218 let err = compile_expr_err("fn f() -> i64 is pure { 9223372036854775807 * 2 }", "f");
3219 match err {
3220 QalaError::IntegerOverflow { op, lhs, rhs, .. } => {
3221 assert_eq!(op, BinOp::Mul);
3222 assert_eq!(lhs, i64::MAX);
3223 assert_eq!(rhs, 2);
3224 }
3225 other => panic!("expected IntegerOverflow, got {other:?}"),
3226 }
3227 }
3228
3229 #[test]
3231 fn compile_expr_fold_f64_overflow_yields_infinity() {
3232 let (code, consts) = compile_expr_of("fn f() -> f64 is pure { 1e300 * 1e300 }", "f");
3233 assert_eq!(opcodes(&code), vec![Opcode::Const]);
3234 match consts.last() {
3235 Some(ConstValue::F64(x)) => assert!(x.is_infinite() && *x > 0.0),
3236 other => panic!("expected F64(inf), got {other:?}"),
3237 }
3238 }
3239
3240 #[test]
3242 fn compile_expr_fold_f64_div_zero_yields_nan() {
3243 let (code, consts) = compile_expr_of("fn f() -> f64 is pure { 0.0 / 0.0 }", "f");
3244 assert_eq!(opcodes(&code), vec![Opcode::Const]);
3245 match consts.last() {
3246 Some(ConstValue::F64(x)) => assert!(x.is_nan()),
3247 other => panic!("expected F64(NaN), got {other:?}"),
3248 }
3249 }
3250
3251 #[test]
3253 fn compile_expr_fold_string_concat() {
3254 let (code, consts) = compile_expr_of("fn f() -> str is pure { \"a\" + \"b\" }", "f");
3255 assert_eq!(opcodes(&code), vec![Opcode::Const]);
3256 assert_eq!(consts.last(), Some(&ConstValue::Str("ab".to_string())));
3257 }
3258
3259 #[test]
3261 fn compile_expr_fold_comparison() {
3262 let (code, consts) = compile_expr_of("fn f() -> bool is pure { 1 < 2 }", "f");
3263 assert_eq!(opcodes(&code), vec![Opcode::Const]);
3264 assert_eq!(consts.last(), Some(&ConstValue::Bool(true)));
3265 }
3266
3267 #[test]
3269 fn compile_expr_comparison_with_local_does_not_fold() {
3270 let (code, _) = compile_expr_of("fn f(x: i64) -> bool is pure { x < 2 }", "f");
3271 assert_eq!(
3272 opcodes(&code),
3273 vec![Opcode::GetLocal, Opcode::Const, Opcode::Lt]
3274 );
3275 }
3276
3277 #[test]
3279 fn compile_expr_fold_boolean_and() {
3280 let (code, consts) = compile_expr_of("fn f() -> bool is pure { true && false }", "f");
3281 assert_eq!(opcodes(&code), vec![Opcode::Const]);
3282 assert_eq!(consts.last(), Some(&ConstValue::Bool(false)));
3283 }
3284
3285 #[test]
3292 fn compile_expr_fold_unary() {
3293 let (code, consts) = compile_expr_of("fn f() -> bool is pure { !true }", "f");
3294 assert_eq!(opcodes(&code), vec![Opcode::Const]);
3295 assert_eq!(consts.last(), Some(&ConstValue::Bool(false)));
3296 let (_, consts) = compile_expr_of("fn f() -> bool is pure { !false }", "f");
3297 assert_eq!(consts.last(), Some(&ConstValue::Bool(true)));
3298 let (code, consts) = compile_expr_of("fn f() -> i64 is pure { -(42) }", "f");
3299 assert_eq!(opcodes(&code), vec![Opcode::Const]);
3300 assert_eq!(consts.last(), Some(&ConstValue::I64(-42)));
3301 }
3302
3303 #[test]
3305 fn compile_expr_fold_f64_unary() {
3306 let (code, consts) = compile_expr_of("fn f() -> f64 is pure { -(3.5) }", "f");
3307 assert_eq!(opcodes(&code), vec![Opcode::Const]);
3308 assert_eq!(consts.last(), Some(&ConstValue::F64(-3.5)));
3309 }
3310
3311 #[test]
3313 fn compile_expr_picks_int_vs_float_opcode() {
3314 let (code, _) = compile_expr_of("fn f(a: i64, b: i64) -> i64 is pure { a + b }", "f");
3315 assert!(opcodes(&code).contains(&Opcode::Add));
3316 let (code, _) = compile_expr_of("fn f(a: f64, b: f64) -> f64 is pure { a + b }", "f");
3317 assert!(opcodes(&code).contains(&Opcode::FAdd));
3318 }
3319
3320 #[test]
3322 fn compile_expr_user_call_emits_args_then_call() {
3323 let src = "fn add(a: i64, b: i64) -> i64 is pure { return a + b }\n\
3324 fn main() -> i64 is pure { add(1, 2) }";
3325 let (code, _) = compile_expr_of(src, "main");
3326 assert_eq!(
3327 opcodes(&code),
3328 vec![Opcode::Const, Opcode::Const, Opcode::Call]
3329 );
3330 let call_off = 6; assert_eq!(code[call_off], Opcode::Call as u8);
3333 assert_eq!(
3334 u16::from_le_bytes([code[call_off + 1], code[call_off + 2]]),
3335 0
3336 );
3337 assert_eq!(code[call_off + 3], 2);
3338 }
3339
3340 #[test]
3342 fn compile_expr_stdlib_call_emits_arg_then_call() {
3343 let (code, _) = compile_expr_of("fn main() is io { println(\"hi\") }", "main");
3344 assert_eq!(opcodes(&code), vec![Opcode::Const, Opcode::Call]);
3345 assert_eq!(code[3], Opcode::Call as u8);
3347 assert_eq!(u16::from_le_bytes([code[4], code[5]]), STDLIB_FN_BASE + 1);
3348 assert_eq!(code[6], 1);
3349 }
3350
3351 #[test]
3353 fn compile_expr_records_source_lines_in_lockstep() {
3354 let src = "fn f() -> i64 is pure {\n x_helper() + 1\n}\n\
3356 fn x_helper() -> i64 is pure { 5 }";
3357 let typed = typecheck(src);
3358 let mut cg = Codegen::new(src);
3359 cg.build_tables(&typed);
3360 let fn_id = cg.fn_table[&FnKey {
3361 type_name: None,
3362 name: "f".to_string(),
3363 }];
3364 cg.current_fn_id = fn_id;
3365 cg.push_scope();
3366 let decl = typed
3367 .iter()
3368 .find_map(|i| match i {
3369 TypedItem::Fn(d) if d.name == "f" => Some(d),
3370 _ => None,
3371 })
3372 .unwrap();
3373 cg.compile_expr(decl.body.value.as_ref().unwrap())
3374 .expect("compile_expr");
3375 let chunk = &cg.program.chunks[fn_id as usize];
3376 assert_eq!(chunk.code.len(), chunk.source_lines.len());
3378 assert!(chunk.source_lines.iter().all(|&l| l == 2));
3380 }
3381
3382 #[test]
3386 fn compile_expr_pipeline_desugars_to_call() {
3387 let src = "fn double(x: i64) -> i64 is pure { return x * 2 }\n\
3388 fn f() -> i64 is pure { 5 |> double }";
3389 let (code, _) = compile_expr_of(src, "f");
3390 assert_eq!(opcodes(&code), vec![Opcode::Const, Opcode::Call]);
3391 assert_eq!(code[3], Opcode::Call as u8);
3393 assert_eq!(u16::from_le_bytes([code[4], code[5]]), 0);
3394 assert_eq!(code[6], 1);
3395 }
3396
3397 #[test]
3399 fn compile_expr_pipeline_chain() {
3400 let src = "fn double(x: i64) -> i64 is pure { return x * 2 }\n\
3401 fn add_one(x: i64) -> i64 is pure { return x + 1 }\n\
3402 fn f() -> i64 is pure { 5 |> double |> add_one }";
3403 let (code, _) = compile_expr_of(src, "f");
3404 assert_eq!(
3405 opcodes(&code),
3406 vec![Opcode::Const, Opcode::Call, Opcode::Call]
3407 );
3408 }
3409
3410 #[test]
3417 fn compile_expr_pipeline_with_extra_args() {
3418 let src = "fn is_even(x: i64) -> bool is pure { return x % 2 == 0 }\n\
3419 fn main() is io {\n\
3420 let numbers = [1, 2, 3]\n\
3421 let evens = numbers |> filter(is_even)\n\
3422 }";
3423 let p = compile_ok(src);
3424 let main_id = p.fn_names.iter().position(|n| n == "main").unwrap();
3425 let ops = opcodes(&p.chunks[main_id].code);
3426 assert!(ops.contains(&Opcode::Call), "pipeline emits a CALL");
3429 let mut off = 0;
3431 let mut found_argc2 = false;
3432 let code = &p.chunks[main_id].code;
3433 while off < code.len() {
3434 let op = Opcode::from_u8(code[off]).unwrap();
3435 if op == Opcode::Call && code[off + 3] == 2 {
3436 found_argc2 = true;
3437 }
3438 off += 1 + op.operand_bytes() as usize;
3439 }
3440 assert!(found_argc2, "the filter CALL should have argc 2");
3441 }
3442
3443 #[test]
3445 fn compile_expr_interpolation_no_conversion() {
3446 let src = "fn f(name: str) -> str is pure { \"hi {name}\" }";
3447 let (code, _) = compile_expr_of(src, "f");
3448 assert_eq!(
3450 opcodes(&code),
3451 vec![Opcode::Const, Opcode::GetLocal, Opcode::ConcatN]
3452 );
3453 }
3454
3455 #[test]
3457 fn compile_expr_interpolation_with_conversion() {
3458 let src = "fn f(x: i64) -> str is pure { \"got: {x}\" }";
3459 let (code, _) = compile_expr_of(src, "f");
3460 assert_eq!(
3461 opcodes(&code),
3462 vec![
3463 Opcode::Const,
3464 Opcode::GetLocal,
3465 Opcode::ToStr,
3466 Opcode::ConcatN
3467 ]
3468 );
3469 }
3470
3471 #[test]
3473 fn compile_expr_all_literal_string_is_one_const() {
3474 let (code, consts) = compile_expr_of("fn f() -> str is pure { \"hello, world!\" }", "f");
3475 assert_eq!(opcodes(&code), vec![Opcode::Const]);
3476 assert_eq!(
3477 consts.last(),
3478 Some(&ConstValue::Str("hello, world!".to_string()))
3479 );
3480 }
3481
3482 #[test]
3486 fn compile_expr_try_emits_match_variant_and_return() {
3487 let src = "fn get() -> Result<i64, str> is pure { return Ok(1) }\n\
3488 fn f() -> Result<i64, str> is pure { let x = get()?\n return Ok(x) }";
3489 let p = compile_ok(src);
3490 let f_id = p.fn_names.iter().position(|n| n == "f").unwrap();
3491 let ops = opcodes(&p.chunks[f_id].code);
3492 assert!(
3493 ops.contains(&Opcode::MatchVariant),
3494 "try should MATCH_VARIANT"
3495 );
3496 assert!(
3497 ops.contains(&Opcode::Return),
3498 "try should RETURN on the err path"
3499 );
3500 }
3501
3502 #[test]
3505 fn compile_expr_or_else_emits_match_variant_and_fallback() {
3506 let src = "fn get() -> Result<i64, str> is pure { return Ok(1) }\n\
3507 fn f() -> i64 is pure { get() or 0 }";
3508 let (code, _) = compile_expr_of(src, "f");
3509 let ops = opcodes(&code);
3510 assert!(ops.contains(&Opcode::MatchVariant));
3511 assert!(ops.contains(&Opcode::Const));
3513 }
3514
3515 #[test]
3517 fn compile_expr_method_call_emits_receiver_then_call() {
3518 let src = "fn f(h: FileHandle) -> Result<str, str> is io { h.read_all() }";
3519 let (code, _) = compile_expr_of(src, "f");
3520 assert_eq!(opcodes(&code), vec![Opcode::GetLocal, Opcode::Call]);
3522 let call_off = 3;
3524 assert_eq!(code[call_off + 3], 1);
3525 }
3526
3527 #[test]
3529 fn compile_expr_field_access_emits_field_opcode() {
3530 let src = "struct Point { x: i64, y: i64 }\n\
3531 fn f(p: Point) -> i64 is pure { p.y }";
3532 let (code, _) = compile_expr_of(src, "f");
3533 assert_eq!(opcodes(&code), vec![Opcode::GetLocal, Opcode::Field]);
3534 assert_eq!(u16::from_le_bytes([code[4], code[5]]), 1);
3536 }
3537
3538 #[test]
3540 fn compile_expr_index_emits_index_opcode() {
3541 let src = "fn f(arr: [i64]) -> i64 is pure { arr[0] }";
3542 let (code, _) = compile_expr_of(src, "f");
3543 assert_eq!(
3544 opcodes(&code),
3545 vec![Opcode::GetLocal, Opcode::Const, Opcode::Index]
3546 );
3547 }
3548
3549 #[test]
3551 fn compile_expr_tuple_emits_make_tuple() {
3552 let (code, _) = compile_expr_of("fn f() -> (i64, i64, i64) is pure { (1, 2, 3) }", "f");
3553 let ops = opcodes(&code);
3554 assert_eq!(ops.last(), Some(&Opcode::MakeTuple));
3555 let mt = code.len() - 3;
3557 assert_eq!(u16::from_le_bytes([code[mt + 1], code[mt + 2]]), 3);
3558 }
3559
3560 #[test]
3563 fn compile_expr_array_lit_emits_make_array() {
3564 let (code, _) = compile_expr_of("fn f() -> [i64; 3] is pure { [1, 2, 3] }", "f");
3565 let ops = opcodes(&code);
3566 assert_eq!(ops.last(), Some(&Opcode::MakeArray));
3567 let ma = code.len() - 3;
3568 assert_eq!(u16::from_le_bytes([code[ma + 1], code[ma + 2]]), 3);
3569 }
3570
3571 #[test]
3573 fn compile_expr_array_repeat_materializes() {
3574 let (code, _) = compile_expr_of("fn f() -> [i64] is pure { [0; 5] }", "f");
3575 let ops = opcodes(&code);
3576 assert_eq!(ops.iter().filter(|o| **o == Opcode::Const).count(), 5);
3578 assert_eq!(ops.last(), Some(&Opcode::MakeArray));
3579 }
3580
3581 #[test]
3585 fn compile_expr_struct_lit_emits_make_struct() {
3586 let src = "struct Point { x: f64, y: f64 }\n\
3587 fn f() -> Point is pure { Point { x: 1.0, y: 2.0 } }";
3588 let (code, _) = compile_expr_of(src, "f");
3589 let ops = opcodes(&code);
3590 assert_eq!(ops.last(), Some(&Opcode::MakeStruct));
3591 let ms = code.len() - 3;
3592 assert_eq!(
3593 u16::from_le_bytes([code[ms + 1], code[ms + 2]]),
3594 0,
3595 "MAKE_STRUCT operand is the struct id of the first registered struct"
3596 );
3597 }
3598
3599 #[test]
3603 fn struct_literal_registers_the_struct_in_the_program_table() {
3604 let src = "struct Point { x: f64, y: f64 }\n\
3605 fn main() -> Point is pure { Point { x: 1.0, y: 2.0 } }";
3606 let typed = typecheck(src);
3607 let program = compile_program(&typed, src).expect("compile");
3608 assert_eq!(program.structs.len(), 1, "exactly one struct registered");
3609 assert_eq!(program.structs[0].name, "Point");
3610 assert_eq!(program.structs[0].field_count, 2);
3611 }
3612
3613 #[test]
3616 fn repeated_struct_literals_reuse_one_id_distinct_structs_get_distinct_ids() {
3617 let src = "struct A { v: i64 }\n\
3618 struct B { v: i64 }\n\
3619 fn two() -> A is pure { let _x = B { v: 1 }\nA { v: 2 } }\n\
3620 fn again() -> A is pure { A { v: 3 } }\n\
3621 fn main() is pure { }";
3622 let typed = typecheck(src);
3623 let program = compile_program(&typed, src).expect("compile");
3624 assert_eq!(program.structs.len(), 2, "A reused, B distinct");
3626 let names: Vec<&str> = program.structs.iter().map(|s| s.name.as_str()).collect();
3628 assert_eq!(names, vec!["B", "A"]);
3629 }
3630
3631 #[test]
3633 fn compile_expr_enum_variant_constructor() {
3634 let src = "enum Shape { Circle(f64), Rect(f64, f64) }\n\
3635 fn f() -> Shape is pure { Circle(5.0) }";
3636 let (code, _) = compile_expr_of(src, "f");
3637 let ops = opcodes(&code);
3638 assert_eq!(ops, vec![Opcode::Const, Opcode::MakeEnumVariant]);
3639 let mev = 3;
3641 assert_eq!(code[mev + 3], 1);
3642 }
3643
3644 #[test]
3646 fn compile_expr_match_with_variants() {
3647 let src = "enum Shape { Circle(f64), Rect(f64, f64), Triangle }\n\
3648 fn f(s: Shape) -> f64 is pure {\n\
3649 match s { Circle(r) => 1.0, Rect(w, h) => 2.0, Triangle => 0.0 }\n\
3650 }";
3651 let (code, _) = compile_expr_of(src, "f");
3652 let ops = opcodes(&code);
3653 assert_eq!(
3655 ops.iter().filter(|o| **o == Opcode::MatchVariant).count(),
3656 3
3657 );
3658 assert!(ops.contains(&Opcode::GetLocal));
3660 }
3661
3662 #[test]
3664 fn compile_expr_match_with_wildcard() {
3665 let src = "fn f(v: i64) -> str is pure {\n\
3666 match v { 0 => \"zero\", _ => \"other\" }\n\
3667 }";
3668 let (code, _) = compile_expr_of(src, "f");
3669 let ops = opcodes(&code);
3670 assert!(ops.contains(&Opcode::Eq), "literal arm should test via EQ");
3672 assert!(
3673 ops.contains(&Opcode::Pop),
3674 "wildcard arm should POP scrutinee"
3675 );
3676 }
3677
3678 #[test]
3680 fn compile_expr_match_with_guards() {
3681 let src = "fn classify(value: i64) -> str is pure {\n\
3682 match value {\n\
3683 v if v > 0 => \"positive\",\n\
3684 v if v < 0 => \"negative\",\n\
3685 _ => \"zero\",\n\
3686 }\n\
3687 }";
3688 let (code, _) = compile_expr_of(src, "classify");
3689 let ops = opcodes(&code);
3690 assert!(
3692 ops.contains(&Opcode::JumpIfFalse),
3693 "guards emit JUMP_IF_FALSE"
3694 );
3695 assert!(!code.is_empty());
3696 }
3697
3698 #[test]
3700 fn compile_expr_range_materializes_to_array() {
3701 let src = "fn f() -> void is pure { for i in 0..3 { } }";
3702 let p = compile_ok(src);
3704 let ops = opcodes(&p.chunks[0].code);
3705 assert!(ops.contains(&Opcode::MakeArray));
3707 }
3708
3709 #[test]
3711 fn compile_expr_block_expression() {
3712 let src = "fn f() -> i64 is pure { let y = { let x = 1\n x + 1 }\n return y }";
3713 let p = compile_ok(src);
3714 assert!(!p.chunks[0].code.is_empty());
3716 let ops = opcodes(&p.chunks[0].code);
3717 assert!(ops.contains(&Opcode::SetLocal));
3718 assert!(ops.contains(&Opcode::Return));
3719 }
3720
3721 fn fn_ops(src: &str, fn_name: &str) -> Vec<Opcode> {
3725 let p = compile_ok(src);
3726 let id = p
3727 .fn_names
3728 .iter()
3729 .position(|n| n == fn_name)
3730 .unwrap_or_else(|| panic!("fn `{fn_name}` not compiled"));
3731 opcodes(&p.chunks[id].code)
3732 }
3733
3734 fn fn_code(src: &str, fn_name: &str) -> Vec<u8> {
3736 let p = compile_ok(src);
3737 let id = p.fn_names.iter().position(|n| n == fn_name).unwrap();
3738 p.chunks[id].code.clone()
3739 }
3740
3741 #[test]
3743 fn stmt_let_emits_const_and_set_local() {
3744 let ops = fn_ops("fn main() is io { let x = 1\n println(\"{x}\") }", "main");
3745 assert_eq!(ops[0], Opcode::Const);
3747 assert_eq!(ops[1], Opcode::SetLocal);
3748 }
3749
3750 #[test]
3752 fn stmt_if_with_else_emits_jump_pattern() {
3753 let src = "fn main() is io {\n\
3754 let c = 1 < 2\n\
3755 if c { println(\"a\") } else { println(\"b\") }\n\
3756 }";
3757 let ops = fn_ops(src, "main");
3758 assert!(ops.contains(&Opcode::JumpIfFalse));
3759 assert!(ops.contains(&Opcode::Jump));
3760 }
3761
3762 #[test]
3764 fn stmt_if_without_else_emits_only_jump_if_false() {
3765 let src = "fn main() is io {\n\
3766 let c = 1 < 2\n\
3767 if c { println(\"a\") }\n\
3768 }";
3769 let ops = fn_ops(src, "main");
3770 assert!(ops.contains(&Opcode::JumpIfFalse));
3771 }
3772
3773 #[test]
3778 fn stmt_while_emits_loop_pattern() {
3779 let src = "fn main(flag: bool) is io {\n\
3780 while flag { break }\n\
3781 }";
3782 let ops = fn_ops(src, "main");
3783 assert!(ops.contains(&Opcode::JumpIfFalse));
3784 let jumps = ops
3787 .iter()
3788 .filter(|o| matches!(o, Opcode::Jump | Opcode::JumpIfFalse))
3789 .count();
3790 assert!(jumps >= 2, "while should emit an exit jump and a back-jump");
3791 }
3792
3793 #[test]
3796 fn stmt_for_emits_indexed_walk() {
3797 let ops = fn_ops(
3798 "fn main() is io { for i in 0..3 { println(\"{i}\") } }",
3799 "main",
3800 );
3801 assert!(ops.contains(&Opcode::MakeArray), "the range materializes");
3802 assert!(ops.contains(&Opcode::Len), "the bounds check uses LEN");
3803 assert!(ops.contains(&Opcode::Index), "the element load uses INDEX");
3804 }
3805
3806 #[test]
3809 fn stmt_return_with_value_emits_return() {
3810 let ops = fn_ops("fn f(x: i64) -> i64 is pure { return x + 1 }", "f");
3811 assert_eq!(ops.last(), Some(&Opcode::Return));
3812 }
3813
3814 #[test]
3816 fn stmt_bare_return_emits_return() {
3817 let ops = fn_ops("fn main() is io { return }", "main");
3818 assert!(ops.contains(&Opcode::Return));
3819 }
3820
3821 #[test]
3823 fn stmt_defer_emits_nothing_at_the_defer_site() {
3824 let src = "fn cleanup() is io { }\n\
3829 fn main() is io { defer cleanup() }";
3830 let ops = fn_ops(src, "main");
3831 assert!(ops.contains(&Opcode::Call), "the defer body runs at exit");
3834 assert_eq!(ops.last(), Some(&Opcode::Return));
3835 }
3836
3837 #[test]
3839 fn stmt_defer_lifo_at_fall_through() {
3840 let src = "fn a() is io { }\n\
3841 fn b() is io { }\n\
3842 fn c() is io { }\n\
3843 fn main() is io { defer a()\n defer b()\n defer c() }";
3844 let p = compile_ok(src);
3845 let main_id = p.fn_names.iter().position(|n| n == "main").unwrap();
3846 let a_id = p.fn_names.iter().position(|n| n == "a").unwrap() as u16;
3847 let b_id = p.fn_names.iter().position(|n| n == "b").unwrap() as u16;
3848 let c_id = p.fn_names.iter().position(|n| n == "c").unwrap() as u16;
3849 let code = &p.chunks[main_id].code;
3851 let mut call_ids = Vec::new();
3852 let mut off = 0;
3853 while off < code.len() {
3854 let op = Opcode::from_u8(code[off]).unwrap();
3855 if op == Opcode::Call {
3856 call_ids.push(u16::from_le_bytes([code[off + 1], code[off + 2]]));
3857 }
3858 off += 1 + op.operand_bytes() as usize;
3859 }
3860 assert_eq!(call_ids, vec![c_id, b_id, a_id]);
3862 }
3863
3864 #[test]
3867 fn stmt_defer_fires_once_at_explicit_return() {
3868 let src = "fn cleanup() is io { }\n\
3869 fn main() is io { defer cleanup()\n return }";
3870 let p = compile_ok(src);
3871 let main_id = p.fn_names.iter().position(|n| n == "main").unwrap();
3872 let cleanup_id = p.fn_names.iter().position(|n| n == "cleanup").unwrap() as u16;
3873 let code = &p.chunks[main_id].code;
3874 let mut cleanup_calls = 0;
3875 let mut off = 0;
3876 while off < code.len() {
3877 let op = Opcode::from_u8(code[off]).unwrap();
3878 if op == Opcode::Call
3879 && u16::from_le_bytes([code[off + 1], code[off + 2]]) == cleanup_id
3880 {
3881 cleanup_calls += 1;
3882 }
3883 off += 1 + op.operand_bytes() as usize;
3884 }
3885 assert_eq!(cleanup_calls, 1, "defer must fire exactly once");
3888 }
3889
3890 #[test]
3893 fn stmt_defer_in_loop_fires_per_iteration() {
3894 let src = "fn cleanup() is io { }\n\
3895 fn main() is io { for i in 0..3 { defer cleanup() } }";
3896 let p = compile_ok(src);
3897 let main_id = p.fn_names.iter().position(|n| n == "main").unwrap();
3898 let cleanup_id = p.fn_names.iter().position(|n| n == "cleanup").unwrap() as u16;
3899 let code = &p.chunks[main_id].code;
3900 let mut cleanup_calls = 0;
3904 let mut off = 0;
3905 while off < code.len() {
3906 let op = Opcode::from_u8(code[off]).unwrap();
3907 if op == Opcode::Call
3908 && u16::from_le_bytes([code[off + 1], code[off + 2]]) == cleanup_id
3909 {
3910 cleanup_calls += 1;
3911 }
3912 off += 1 + op.operand_bytes() as usize;
3913 }
3914 assert!(
3915 cleanup_calls >= 1,
3916 "the per-iteration defer body is emitted"
3917 );
3918 }
3919
3920 #[test]
3924 fn stmt_dce_skips_code_after_return() {
3925 let src = "fn f() is io { return\n let unused = 2 }";
3926 let code = fn_code(src, "f");
3927 assert_eq!(opcodes(&code), vec![Opcode::Return]);
3929 }
3930
3931 #[test]
3933 fn stmt_dce_constant_if_true_emits_only_then() {
3934 let src = "fn do_a() is io { }\n\
3935 fn do_b() is io { }\n\
3936 fn main() is io { if true { do_a() } else { do_b() } }";
3937 let p = compile_ok(src);
3938 let main_id = p.fn_names.iter().position(|n| n == "main").unwrap();
3939 let do_a = p.fn_names.iter().position(|n| n == "do_a").unwrap() as u16;
3940 let do_b = p.fn_names.iter().position(|n| n == "do_b").unwrap() as u16;
3941 let code = &p.chunks[main_id].code;
3942 let ops = opcodes(code);
3943 assert!(
3945 !ops.contains(&Opcode::JumpIfFalse),
3946 "constant if needs no jump"
3947 );
3948 let mut calls = Vec::new();
3950 let mut off = 0;
3951 while off < code.len() {
3952 let op = Opcode::from_u8(code[off]).unwrap();
3953 if op == Opcode::Call {
3954 calls.push(u16::from_le_bytes([code[off + 1], code[off + 2]]));
3955 }
3956 off += 1 + op.operand_bytes() as usize;
3957 }
3958 assert!(calls.contains(&do_a), "the then-branch runs");
3959 assert!(!calls.contains(&do_b), "the dead else-branch is dropped");
3960 }
3961
3962 #[test]
3964 fn stmt_dce_constant_if_false_emits_only_else() {
3965 let src = "fn do_a() is io { }\n\
3966 fn do_b() is io { }\n\
3967 fn main() is io { if false { do_a() } else { do_b() } }";
3968 let p = compile_ok(src);
3969 let main_id = p.fn_names.iter().position(|n| n == "main").unwrap();
3970 let do_a = p.fn_names.iter().position(|n| n == "do_a").unwrap() as u16;
3971 let do_b = p.fn_names.iter().position(|n| n == "do_b").unwrap() as u16;
3972 let code = &p.chunks[main_id].code;
3973 let mut calls = Vec::new();
3974 let mut off = 0;
3975 while off < code.len() {
3976 let op = Opcode::from_u8(code[off]).unwrap();
3977 if op == Opcode::Call {
3978 calls.push(u16::from_le_bytes([code[off + 1], code[off + 2]]));
3979 }
3980 off += 1 + op.operand_bytes() as usize;
3981 }
3982 assert!(!calls.contains(&do_a), "the dead then-branch is dropped");
3983 assert!(calls.contains(&do_b), "the else-branch runs");
3984 }
3985
3986 #[test]
3988 fn stmt_no_dce_for_runtime_if_condition() {
3989 let src = "fn do_a() is io { }\n\
3990 fn do_b() is io { }\n\
3991 fn main(x: i64) is io { if x > 0 { do_a() } else { do_b() } }";
3992 let ops = fn_ops(src, "main");
3993 assert!(
3994 ops.contains(&Opcode::JumpIfFalse),
3995 "runtime if keeps its jump"
3996 );
3997 }
3998
3999 #[test]
4001 fn six_bundled_examples_compile_to_non_empty_bytecode() {
4002 for name in [
4003 "hello",
4004 "fibonacci",
4005 "effects",
4006 "pattern-matching",
4007 "pipeline",
4008 "defer-demo",
4009 ] {
4010 let path = format!(
4011 "{}/../../playground/public/examples/{name}.qala",
4012 env!("CARGO_MANIFEST_DIR"),
4013 );
4014 let src = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {path}: {e}"));
4015 let tokens =
4016 Lexer::tokenize(&src).unwrap_or_else(|e| panic!("{name}.qala: lex error: {e:?}"));
4017 let ast = Parser::parse(&tokens)
4018 .unwrap_or_else(|e| panic!("{name}.qala: parse error: {e:?}"));
4019 let (typed, terrors, _) = check_program(&ast, &src);
4020 assert!(
4021 terrors.is_empty(),
4022 "{name}.qala: typecheck errors: {terrors:?}"
4023 );
4024 let program = compile_program(&typed, &src)
4025 .unwrap_or_else(|e| panic!("{name}.qala: compile errors: {e:?}"));
4026 assert!(
4027 !program.chunks.is_empty(),
4028 "{name}.qala: program has no chunks"
4029 );
4030 for (i, chunk) in program.chunks.iter().enumerate() {
4032 assert!(
4033 !chunk.code.is_empty(),
4034 "{name}.qala: chunk {i} ({}) is empty",
4035 program.fn_names[i],
4036 );
4037 }
4038 let disassembly = program.disassemble();
4039 assert!(!disassembly.is_empty(), "{name}.qala: disassembly is empty");
4040 }
4041 }
4042
4043 #[test]
4046 fn compile_is_deterministic() {
4047 let path = format!(
4048 "{}/../../playground/public/examples/fibonacci.qala",
4049 env!("CARGO_MANIFEST_DIR"),
4050 );
4051 let src = std::fs::read_to_string(&path).expect("read fibonacci.qala");
4052 let tokens = Lexer::tokenize(&src).expect("lex");
4053 let ast = Parser::parse(&tokens).expect("parse");
4054 let (typed, _, _) = check_program(&ast, &src);
4055 let first = compile_program(&typed, &src).expect("compile 1");
4056 let second = compile_program(&typed, &src).expect("compile 2");
4057 assert_eq!(first.chunks.len(), second.chunks.len());
4058 for (a, b) in first.chunks.iter().zip(second.chunks.iter()) {
4059 assert_eq!(a.code, b.code, "chunk code differs across compiles");
4060 assert_eq!(
4061 a.source_lines, b.source_lines,
4062 "source map differs across compiles"
4063 );
4064 }
4065 assert_eq!(
4066 first.disassemble(),
4067 second.disassemble(),
4068 "disassembly is non-deterministic"
4069 );
4070 }
4071
4072 #[test]
4074 fn stmt_break_emits_forward_jump() {
4075 let src = "fn main() is io { for i in 0..3 { if i == 1 { break } } }";
4076 let ops = fn_ops(src, "main");
4077 assert!(ops.contains(&Opcode::Jump), "break emits a JUMP");
4080 }
4081
4082 #[test]
4084 fn stmt_continue_emits_back_jump() {
4085 let src = "fn main() is io { for i in 0..3 { if i == 1 { continue } } }";
4086 let ops = fn_ops(src, "main");
4087 assert!(ops.contains(&Opcode::Jump), "continue emits a JUMP");
4088 assert!(!ops.is_empty());
4091 }
4092
4093 #[test]
4096 fn stmt_defer_resolves_block_local_on_fall_through() {
4097 let src = "fn capture(n: i64) is io { println(\"{n}\") }\n\
4110 fn main() is io {\n\
4111 let v = 42\n\
4112 defer capture(v)\n\
4113 }";
4114 let p = compile_ok(src);
4116 let mut vm = crate::vm::Vm::new(p, src.to_string());
4118 vm.run().expect("the program runs without error");
4119 assert_eq!(
4120 vm.console,
4121 vec!["42\n"],
4122 "the defer ran and printed the block-local value"
4123 );
4124 }
4125
4126 #[test]
4128 fn stmt_defer_with_break_fires_on_both_paths() {
4129 let src = "fn cleanup() is io { }\n\
4130 fn main() is io {\n\
4131 for i in 0..3 { defer cleanup()\n if i == 1 { break } }\n\
4132 }";
4133 let p = compile_ok(src);
4134 let main_id = p.fn_names.iter().position(|n| n == "main").unwrap();
4135 let cleanup_id = p.fn_names.iter().position(|n| n == "cleanup").unwrap() as u16;
4136 let code = &p.chunks[main_id].code;
4137 let mut cleanup_calls = 0;
4138 let mut off = 0;
4139 while off < code.len() {
4140 let op = Opcode::from_u8(code[off]).unwrap();
4141 if op == Opcode::Call
4142 && u16::from_le_bytes([code[off + 1], code[off + 2]]) == cleanup_id
4143 {
4144 cleanup_calls += 1;
4145 }
4146 off += 1 + op.operand_bytes() as usize;
4147 }
4148 assert_eq!(
4151 cleanup_calls, 2,
4152 "the defer fires on the break path and the fall-through"
4153 );
4154 }
4155
4156 fn asm(ops: &[(Opcode, &[u8])]) -> Chunk {
4161 let mut c = Chunk::new();
4162 for (op, operands) in ops {
4163 c.write_op(*op, 1);
4164 for b in *operands {
4165 c.code.push(*b);
4166 c.source_lines.push(1);
4167 }
4168 }
4169 c
4170 }
4171
4172 #[test]
4176 fn comptime_folds_arithmetic_to_one_const() {
4177 let src = "fn main() is io { let x = comptime { 1 + 2 }\n println(\"{x}\") }";
4178 let p = compile_ok(src);
4179 let main_id = p.fn_names.iter().position(|n| n == "main").unwrap();
4180 assert!(
4182 p.chunks[main_id].constants.contains(&ConstValue::I64(3)),
4183 "comptime of 1 + 2 should embed CONST(I64(3))"
4184 );
4185 }
4186
4187 #[test]
4189 fn comptime_string_result() {
4190 let src = "fn f() -> str is pure { comptime { \"hello\" } }";
4191 let p = compile_ok(src);
4192 assert!(
4193 p.chunks[0]
4194 .constants
4195 .contains(&ConstValue::Str("hello".to_string()))
4196 );
4197 }
4198
4199 #[test]
4201 fn comptime_block_with_local() {
4202 let src = "fn f() -> i64 is pure { comptime { let x = 2\n x * x } }";
4203 let p = compile_ok(src);
4204 assert!(
4205 p.chunks[0].constants.contains(&ConstValue::I64(4)),
4206 "comptime block result should be I64(4)"
4207 );
4208 }
4209
4210 #[test]
4212 fn comptime_division_by_zero_errors() {
4213 let program = Program::new();
4215 let effects: HashMap<u16, EffectSet> = HashMap::new();
4216 let mut chunk = Chunk::new();
4217 let one = chunk.add_constant(ConstValue::I64(1));
4218 let zero = chunk.add_constant(ConstValue::I64(0));
4219 let mut body = asm(&[
4220 (Opcode::Const, &one.to_le_bytes()),
4221 (Opcode::Const, &zero.to_le_bytes()),
4222 (Opcode::Div, &[]),
4223 (Opcode::Return, &[]),
4224 ]);
4225 body.constants = chunk.constants;
4226 let mut interp = ComptimeInterpreter::new(&program, &effects, Span::new(0, 1));
4227 match interp.run(body) {
4228 Err(QalaError::Type { message, .. }) => {
4229 assert!(
4230 message.contains("division by zero"),
4231 "expected division-by-zero, got: {message}"
4232 );
4233 }
4234 other => panic!("expected a division-by-zero Type error, got {other:?}"),
4235 }
4236 }
4237
4238 #[test]
4241 fn comptime_budget_exhaustion_errors() {
4242 let program = Program::new();
4243 let effects: HashMap<u16, EffectSet> = HashMap::new();
4244 let body = asm(&[(Opcode::Jump, &(-3i16).to_le_bytes())]);
4248 let mut interp = ComptimeInterpreter::new(&program, &effects, Span::new(5, 1));
4249 match interp.run(body) {
4250 Err(QalaError::ComptimeBudgetExceeded { span }) => {
4251 assert_eq!(span, Span::new(5, 1));
4252 }
4253 other => panic!("expected ComptimeBudgetExceeded, got {other:?}"),
4254 }
4255 }
4256
4257 #[test]
4260 fn comptime_effect_violation_on_impure_call() {
4261 let mut program = Program::new();
4264 program.chunks.push(Chunk::new()); program.fn_names.push("do_io".to_string());
4266 let mut effects: HashMap<u16, EffectSet> = HashMap::new();
4267 effects.insert(0, EffectSet::io());
4268 let body = asm(&[
4269 (Opcode::Call, &[0, 0, 0]), (Opcode::Return, &[]),
4271 ]);
4272 let mut interp = ComptimeInterpreter::new(&program, &effects, Span::new(3, 1));
4273 match interp.run(body) {
4274 Err(QalaError::ComptimeEffectViolation {
4275 fn_name, effect, ..
4276 }) => {
4277 assert_eq!(fn_name, "do_io");
4278 assert_eq!(effect, "io");
4279 }
4280 other => panic!("expected ComptimeEffectViolation, got {other:?}"),
4281 }
4282 }
4283
4284 #[test]
4291 fn comptime_array_body_is_rejected() {
4292 let src = "fn f() -> [i64; 3] is pure { comptime { [1, 2, 3] } }";
4293 match compile(src) {
4294 Err(errors) => {
4295 assert!(
4296 errors.iter().any(|e| matches!(
4297 e,
4298 QalaError::Type { message, .. } if message.contains("MAKE_ARRAY")
4299 )),
4300 "expected a MAKE_ARRAY-unsupported error, got {errors:?}"
4301 );
4302 }
4303 Ok(_) => panic!("comptime with an array body should not compile"),
4304 }
4305 }
4306
4307 #[test]
4309 fn comptime_constable_primitive_predicate() {
4310 assert!(is_constable_primitive(&ConstValue::I64(1)));
4311 assert!(is_constable_primitive(&ConstValue::F64(1.0)));
4312 assert!(is_constable_primitive(&ConstValue::Bool(true)));
4313 assert!(is_constable_primitive(&ConstValue::Byte(0)));
4314 assert!(is_constable_primitive(&ConstValue::Str(String::new())));
4315 assert!(is_constable_primitive(&ConstValue::Void));
4316 assert!(!is_constable_primitive(&ConstValue::Function(0)));
4318 assert_eq!(value_type_name(&ConstValue::Function(0)), "fn");
4319 assert_eq!(value_type_name(&ConstValue::I64(0)), "i64");
4320 }
4321
4322 #[test]
4327 fn comptime_deep_recursion_is_bounded_by_the_frame_cap() {
4328 let mut program = Program::new();
4329 let recursive = asm(&[(Opcode::Call, &[0, 0, 0])]);
4332 program.chunks.push(recursive);
4333 program.fn_names.push("recurse".to_string());
4334 let mut effects: HashMap<u16, EffectSet> = HashMap::new();
4335 effects.insert(0, EffectSet::pure());
4336 let body = asm(&[(Opcode::Call, &[0, 0, 0]), (Opcode::Return, &[])]);
4338 let mut interp = ComptimeInterpreter::new(&program, &effects, Span::new(0, 1));
4339 match interp.run(body) {
4340 Err(QalaError::ComptimeBudgetExceeded { .. }) => {
4341 }
4344 other => panic!("expected ComptimeBudgetExceeded, got {other:?}"),
4345 }
4346 }
4347
4348 #[test]
4352 fn comptime_calls_a_pure_user_function() {
4353 let src = "fn square(x: i64) -> i64 is pure { return x * x }\n\
4354 fn f() -> i64 is pure { comptime { square(5) } }";
4355 let p = compile_ok(src);
4356 let f_id = p.fn_names.iter().position(|n| n == "f").unwrap();
4357 assert!(
4359 p.chunks[f_id].constants.contains(&ConstValue::I64(25)),
4360 "comptime square(5) should embed CONST(I64(25))"
4361 );
4362 let ops = opcodes(&p.chunks[f_id].code);
4365 assert!(ops.contains(&Opcode::Const));
4366 }
4367
4368 #[test]
4371 fn comptime_program_compiles_through_the_public_entry() {
4372 let src = "fn f() -> i64 is pure { comptime { 10 * 10 } }";
4373 let p = compile_ok(src);
4374 assert_eq!(p.chunks.len(), 1);
4375 assert!(p.chunks[0].constants.contains(&ConstValue::I64(100)));
4376 }
4377
4378 #[test]
4385 fn for_loop_continue_reaches_the_increment_not_the_bounds_check() {
4386 let src = "fn main() is io {\n\
4387 for i in 0..5 {\n\
4388 if i == 2 { continue }\n\
4389 println(\"{i}\")\n\
4390 }\n\
4391 }";
4392 let p = compile_ok(src);
4393 let main_id = p.fn_names.iter().position(|n| n == "main").unwrap();
4394 let chunk = &p.chunks[main_id];
4395 assert_eq!(
4396 chunk.code.len(),
4397 chunk.source_lines.len(),
4398 "lockstep invariant broken after for+continue"
4399 );
4400 let dis = chunk.disassemble(&p);
4401 assert!(!dis.is_empty(), "disassembly is empty");
4402 let line_count = dis.matches('\n').count();
4405 let op_count = opcodes(&chunk.code).len();
4406 assert_eq!(
4407 line_count, op_count,
4408 "disassembly line count ({line_count}) != opcode count ({op_count}): \
4409 continue-jump may have landed mid-instruction"
4410 );
4411 }
4412
4413 #[test]
4417 fn compiled_chunk_records_parameter_and_let_names_by_slot() {
4418 let src = "fn add(a: i64, b: i64) -> i64 is pure {\n\
4419 \x20\x20let sum = a + b\n\
4420 \x20\x20return sum\n\
4421 }";
4422 let p = compile_ok(src);
4423 let id = p.fn_names.iter().position(|n| n == "add").unwrap();
4424 let names = &p.chunks[id].local_names;
4425 assert_eq!(names[0], "a", "param slot 0 is `a`");
4427 assert_eq!(names[1], "b", "param slot 1 is `b`");
4428 assert_eq!(names[2], "sum", "the `let` slot is `sum`");
4429 }
4430
4431 #[test]
4435 fn compiled_chunk_names_the_for_variable_and_leaves_hidden_slots_empty() {
4436 let src = "fn main() -> void is pure {\n\
4437 \x20\x20for n in [1, 2, 3] {\n\
4438 \x20\x20\x20\x20let doubled = n + n\n\
4439 \x20\x20}\n\
4440 }";
4441 let p = compile_ok(src);
4442 let id = p.fn_names.iter().position(|n| n == "main").unwrap();
4443 let names = &p.chunks[id].local_names;
4444 assert!(
4446 names.iter().any(|n| n == "n"),
4447 "the for variable `n` is named"
4448 );
4449 assert!(
4450 names.iter().any(|n| n == "doubled"),
4451 "the body `let doubled` is named"
4452 );
4453 assert!(
4455 names.iter().any(|n| n.is_empty()),
4456 "a for loop has hidden unnamed temporary slots"
4457 );
4458 }
4459}