1use std::collections::HashMap;
2use std::fmt;
3use std::sync::Arc;
4
5use harn_parser::TypeExpr;
6use serde::{Deserialize, Serialize};
7
8use crate::{BuiltinId, Op};
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub enum Constant {
12 Int(i64),
13 Float(f64),
14 String(String),
15 Bool(bool),
16 Nil,
17 Duration(i64),
18}
19
20impl fmt::Display for Constant {
21 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
22 match self {
23 Self::Int(value) => write!(formatter, "{value}"),
24 Self::Float(value) => write!(formatter, "{value}"),
25 Self::String(value) => write!(formatter, "\"{value}\""),
26 Self::Bool(value) => write!(formatter, "{value}"),
27 Self::Nil => formatter.write_str("nil"),
28 Self::Duration(value) => write!(formatter, "{value}ms"),
29 }
30 }
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct LocalSlotInfo {
35 pub name: String,
36 pub mutable: bool,
37 pub scope_depth: usize,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct BindingTypeSlot {
54 pub name: String,
55 pub type_expr: TypeExpr,
56 pub nominal_type_names: Vec<String>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ParamSlot {
61 pub name: String,
62 pub type_expr: Option<TypeExpr>,
63 pub has_default: bool,
64}
65
66impl ParamSlot {
67 pub fn from_typed_param(param: &harn_parser::TypedParam) -> Self {
68 Self::from_typed_param_with_type(param, param.type_expr.clone())
69 }
70
71 pub(crate) fn from_typed_param_with_type(
72 param: &harn_parser::TypedParam,
73 type_expr: Option<TypeExpr>,
74 ) -> Self {
75 Self {
76 name: param.name.clone(),
77 type_expr,
78 has_default: param.default_value.is_some(),
79 }
80 }
81
82 pub fn vec_from_typed(params: &[harn_parser::TypedParam]) -> Vec<Self> {
83 params.iter().map(Self::from_typed_param).collect()
84 }
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct CompiledFunction {
89 pub name: String,
90 pub type_params: Vec<String>,
91 pub nominal_type_names: Vec<String>,
92 pub params: Vec<ParamSlot>,
93 pub default_start: Option<usize>,
94 pub chunk: Arc<Chunk>,
95 pub is_generator: bool,
96 pub is_stream: bool,
97 pub has_rest_param: bool,
98 pub has_runtime_type_checks: bool,
99}
100
101impl CompiledFunction {
102 pub(crate) fn has_runtime_type_checks_for_params(params: &[ParamSlot]) -> bool {
103 params.iter().any(|param| {
104 param
105 .type_expr
106 .as_ref()
107 .is_some_and(crate::type_contract::requires_runtime_type_check)
108 })
109 }
110
111 pub fn param_names(&self) -> impl Iterator<Item = &str> {
112 self.params.iter().map(|param| param.name.as_str())
113 }
114
115 pub fn required_param_count(&self) -> usize {
116 self.default_start.unwrap_or(self.params.len())
117 }
118
119 pub fn declares_type_param(&self, name: &str) -> bool {
120 self.type_params.iter().any(|param| param == name)
121 }
122
123 pub fn has_nominal_type(&self, name: &str) -> bool {
124 self.nominal_type_names.iter().any(|ty| ty == name)
125 }
126}
127
128#[derive(Debug, Serialize, Deserialize)]
129pub struct Chunk {
130 pub code: Vec<u8>,
131 pub constants: Vec<Constant>,
132 #[serde(skip)]
133 constant_index: Option<HashMap<ConstantKey, u16>>,
134 pub lines: Vec<u32>,
135 pub columns: Vec<u32>,
136 pub source_file: Option<String>,
137 #[doc(hidden)]
138 pub current_col: u32,
139 pub functions: Vec<Arc<CompiledFunction>>,
140 #[doc(hidden)]
141 pub local_slots: Vec<LocalSlotInfo>,
142 #[doc(hidden)]
143 pub binding_types: Vec<BindingTypeSlot>,
144 #[doc(hidden)]
145 pub references_outer_names: bool,
146 #[cfg(debug_assertions)]
147 #[serde(skip)]
148 balance_depth: i32,
149 #[cfg(debug_assertions)]
150 #[serde(skip)]
151 balance_nonlinear: u32,
152}
153
154impl Clone for Chunk {
155 fn clone(&self) -> Self {
156 Self {
157 code: self.code.clone(),
158 constants: self.constants.clone(),
159 constant_index: self.constant_index.clone(),
160 lines: self.lines.clone(),
161 columns: self.columns.clone(),
162 source_file: self.source_file.clone(),
163 current_col: self.current_col,
164 functions: self.functions.clone(),
165 local_slots: self.local_slots.clone(),
166 binding_types: self.binding_types.clone(),
167 references_outer_names: self.references_outer_names,
168 #[cfg(debug_assertions)]
169 balance_depth: self.balance_depth,
170 #[cfg(debug_assertions)]
171 balance_nonlinear: self.balance_nonlinear,
172 }
173 }
174}
175
176#[derive(Debug, Clone, PartialEq, Eq, Hash)]
177enum ConstantKey {
178 Int(i64),
179 Float(u64),
180 String(String),
181 Bool(bool),
182 Nil,
183 Duration(i64),
184}
185
186impl From<&Constant> for ConstantKey {
187 fn from(value: &Constant) -> Self {
188 match value {
189 Constant::Int(value) => Self::Int(*value),
190 Constant::Float(value) => Self::Float(value.to_bits()),
191 Constant::String(value) => Self::String(value.clone()),
192 Constant::Bool(value) => Self::Bool(*value),
193 Constant::Nil => Self::Nil,
194 Constant::Duration(value) => Self::Duration(*value),
195 }
196 }
197}
198
199#[cfg(debug_assertions)]
200#[derive(Clone, Copy)]
201pub(crate) struct BalanceProbe {
202 depth: i32,
203 nonlinear: u32,
204}
205
206impl Default for Chunk {
207 fn default() -> Self {
208 Self::new()
209 }
210}
211
212pub(crate) struct ChunkParts {
218 pub(crate) code: Vec<u8>,
219 pub(crate) constants: Vec<Constant>,
220 pub(crate) lines: Vec<u32>,
221 pub(crate) columns: Vec<u32>,
222 pub(crate) source_file: Option<String>,
223 pub(crate) functions: Vec<Arc<CompiledFunction>>,
224 pub(crate) local_slots: Vec<LocalSlotInfo>,
225 pub(crate) binding_types: Vec<BindingTypeSlot>,
226 pub(crate) references_outer_names: bool,
227}
228
229impl Chunk {
230 pub(crate) fn from_artifact_parts(parts: ChunkParts) -> Self {
231 let ChunkParts {
232 code,
233 constants,
234 lines,
235 columns,
236 source_file,
237 functions,
238 local_slots,
239 binding_types,
240 references_outer_names,
241 } = parts;
242 Self {
243 code,
244 constants,
245 constant_index: None,
246 lines,
247 columns,
248 source_file,
249 current_col: 0,
250 functions,
251 local_slots,
252 binding_types,
253 references_outer_names,
254 #[cfg(debug_assertions)]
255 balance_depth: 0,
256 #[cfg(debug_assertions)]
257 balance_nonlinear: 0,
258 }
259 }
260
261 pub fn new() -> Self {
262 Self {
263 code: Vec::new(),
264 constants: Vec::new(),
265 constant_index: Some(HashMap::new()),
266 lines: Vec::new(),
267 columns: Vec::new(),
268 source_file: None,
269 current_col: 0,
270 functions: Vec::new(),
271 local_slots: Vec::new(),
272 binding_types: Vec::new(),
273 references_outer_names: false,
274 #[cfg(debug_assertions)]
275 balance_depth: 0,
276 #[cfg(debug_assertions)]
277 balance_nonlinear: 0,
278 }
279 }
280
281 pub fn set_column(&mut self, column: u32) {
282 self.current_col = column;
283 }
284
285 pub fn add_constant(&mut self, constant: Constant) -> u16 {
286 let index = self.constant_index.get_or_insert_with(|| {
287 self.constants
288 .iter()
289 .enumerate()
290 .filter_map(|(index, value)| {
291 u16::try_from(index)
292 .ok()
293 .map(|index| (ConstantKey::from(value), index))
294 })
295 .collect()
296 });
297 let key = ConstantKey::from(&constant);
298 if let Some(existing) = index.get(&key) {
299 return *existing;
300 }
301 let slot =
302 u16::try_from(self.constants.len()).expect("constant pool exceeded u16 operand space");
303 self.constants.push(constant);
304 index.insert(key, slot);
305 slot
306 }
307
308 pub fn emit(&mut self, op: Op, line: u32) {
309 self.note_balance(op, 0);
310 self.push_bytes(&[op as u8], line);
311 if reads_outer_name(op) {
312 self.references_outer_names = true;
313 }
314 }
315
316 pub fn emit_u16(&mut self, op: Op, value: u16, line: u32) {
317 self.note_balance(op, value);
318 self.push_bytes(&[op as u8, (value >> 8) as u8, value as u8], line);
319 if reads_outer_name(op)
322 || (op == Op::Closure
323 && self
324 .functions
325 .get(usize::from(value))
326 .is_some_and(|function| function.chunk.references_outer_names))
327 {
328 self.references_outer_names = true;
329 }
330 }
331
332 pub fn emit_u16_operands(&mut self, op: Op, values: &[u16], line: u32) {
335 debug_assert_eq!(op.operands().len(), values.len());
336 debug_assert!(op.operands().iter().all(|operand| operand.width() == 2));
337 self.note_balance(op, values.first().copied().unwrap_or_default());
338 let mut bytes = Vec::with_capacity(op.instruction_len());
339 bytes.push(op as u8);
340 for value in values {
341 bytes.extend_from_slice(&value.to_be_bytes());
342 }
343 self.push_bytes(&bytes, line);
344 if reads_outer_name(op) {
345 self.references_outer_names = true;
346 }
347 }
348
349 pub fn emit_u8(&mut self, op: Op, value: u8, line: u32) {
350 self.note_balance(op, u16::from(value));
351 self.push_bytes(&[op as u8, value], line);
352 if reads_outer_name(op) {
353 self.references_outer_names = true;
354 }
355 }
356
357 pub fn emit_call_builtin(&mut self, id: BuiltinId, name: u16, argc: u8, line: u32) {
358 self.note_balance(Op::CallBuiltin, u16::from(argc));
359 let mut bytes = vec![Op::CallBuiltin as u8];
360 bytes.extend_from_slice(&id.raw().to_be_bytes());
361 bytes.extend_from_slice(&name.to_be_bytes());
362 bytes.push(argc);
363 self.push_bytes(&bytes, line);
364 self.references_outer_names = true;
365 }
366
367 pub fn emit_call_builtin_spread(&mut self, id: BuiltinId, name: u16, line: u32) {
368 let mut bytes = vec![Op::CallBuiltinSpread as u8];
369 bytes.extend_from_slice(&id.raw().to_be_bytes());
370 bytes.extend_from_slice(&name.to_be_bytes());
371 self.push_bytes(&bytes, line);
372 self.references_outer_names = true;
373 }
374
375 pub fn emit_method_call(&mut self, name: u16, argc: u8, line: u32) {
376 self.emit_method_call_inner(Op::MethodCall, name, argc, line);
377 }
378
379 pub fn emit_method_call_opt(&mut self, name: u16, argc: u8, line: u32) {
380 self.emit_method_call_inner(Op::MethodCallOpt, name, argc, line);
381 }
382
383 fn emit_method_call_inner(&mut self, op: Op, name: u16, argc: u8, line: u32) {
384 self.note_balance(op, u16::from(argc));
385 self.push_bytes(&[op as u8, (name >> 8) as u8, name as u8, argc], line);
386 }
387
388 pub fn emit_set_local_slot_property(&mut self, property: u16, slot: u16, line: u32) {
389 self.note_balance(Op::SetLocalSlotProperty, 0);
390 self.push_bytes(
391 &[
392 Op::SetLocalSlotProperty as u8,
393 (property >> 8) as u8,
394 property as u8,
395 (slot >> 8) as u8,
396 slot as u8,
397 ],
398 line,
399 );
400 }
401
402 fn push_bytes(&mut self, bytes: &[u8], line: u32) {
403 self.code.extend_from_slice(bytes);
404 self.lines.extend(std::iter::repeat_n(line, bytes.len()));
405 self.columns
406 .extend(std::iter::repeat_n(self.current_col, bytes.len()));
407 }
408
409 pub fn current_offset(&self) -> usize {
410 self.code.len()
411 }
412
413 pub fn emit_jump(&mut self, op: Op, line: u32) -> usize {
414 self.note_balance(op, 0);
415 let patch = self.code.len() + 1;
416 self.push_bytes(&[op as u8, 0xff, 0xff], line);
417 patch
418 }
419
420 pub fn patch_jump(&mut self, patch: usize) {
421 self.patch_jump_to(patch, self.code.len());
422 }
423
424 pub fn patch_jump_to(&mut self, patch: usize, target: usize) {
425 let target = target as u16;
428 self.code[patch..patch + 2].copy_from_slice(&target.to_be_bytes());
429 }
430
431 pub fn read_u16(&self, position: usize) -> u16 {
432 u16::from_be_bytes([self.code[position], self.code[position + 1]])
433 }
434
435 pub(crate) fn add_local_slot(
436 &mut self,
437 name: String,
438 mutable: bool,
439 scope_depth: usize,
440 ) -> u16 {
441 let slot = u16::try_from(self.local_slots.len()).expect("local slot count exceeded u16");
442 self.local_slots.push(LocalSlotInfo {
443 name,
444 mutable,
445 scope_depth,
446 });
447 slot
448 }
449
450 pub(crate) fn add_binding_type(
456 &mut self,
457 name: &str,
458 type_expr: &TypeExpr,
459 nominal_type_names: &[String],
460 ) -> Option<u16> {
461 if let Some(index) = self.binding_types.iter().position(|slot| {
462 slot.name == name
463 && &slot.type_expr == type_expr
464 && slot.nominal_type_names == nominal_type_names
465 }) {
466 return u16::try_from(index).ok();
467 }
468 let index = u16::try_from(self.binding_types.len()).ok()?;
469 self.binding_types.push(BindingTypeSlot {
470 name: name.to_string(),
471 type_expr: type_expr.clone(),
472 nominal_type_names: nominal_type_names.to_vec(),
473 });
474 Some(index)
475 }
476
477 #[cfg(debug_assertions)]
478 pub(crate) fn balance_probe(&self) -> BalanceProbe {
479 BalanceProbe {
480 depth: self.balance_depth,
481 nonlinear: self.balance_nonlinear,
482 }
483 }
484
485 #[cfg(debug_assertions)]
486 pub(crate) fn balance_delta_since(&self, probe: BalanceProbe) -> Option<i32> {
487 (self.balance_nonlinear == probe.nonlinear).then_some(self.balance_depth - probe.depth)
488 }
489
490 #[cfg(debug_assertions)]
491 fn note_balance(&mut self, op: Op, count: u16) {
492 match stack_delta(op, count) {
493 Some(delta) => self.balance_depth += delta,
494 None => self.balance_nonlinear += 1,
495 }
496 }
497
498 #[cfg(not(debug_assertions))]
499 fn note_balance(&mut self, _op: Op, _count: u16) {}
500
501 pub fn disassemble(&self, name: &str) -> String {
502 let mut output = format!("== {name} ==\n");
503 let mut ip = 0;
504 while let Some(byte) = self.code.get(ip).copied() {
505 let offset = ip;
506 let line = self.lines.get(ip).copied().unwrap_or(0);
507 let Some(op) = Op::from_byte(byte) else { break };
508 ip += 1;
509 let rendered = self.disassemble_instruction(op, &mut ip);
510 debug_assert_eq!(
511 ip,
512 offset + op.instruction_len(),
513 "disassembler operand width drifted for {}",
514 op.name(),
515 );
516 output.push_str(&format!("{offset:04} [{line:>4}] {rendered}\n"));
517 }
518 output
519 }
520
521 fn disassemble_instruction(&self, op: Op, ip: &mut usize) -> String {
522 let label = opcode_label(op.name());
523 let read_u16 = |position: usize| {
524 self.code
525 .get(position..position + 2)
526 .map(|bytes| u16::from_be_bytes([bytes[0], bytes[1]]))
527 };
528 if matches!(
529 op,
530 Op::Constant
531 | Op::GetVar
532 | Op::DefLet
533 | Op::DefVar
534 | Op::DefCell
535 | Op::SetVar
536 | Op::GetProperty
537 | Op::GetPropertyOpt
538 | Op::Import
539 ) {
540 let Some(index) = read_u16(*ip) else {
541 return label;
542 };
543 *ip += 2;
544 return match self.constants.get(index as usize) {
545 Some(value) => format!("{label} {index:>4} ({value})"),
546 None => format!("{label} {index:>4}"),
547 };
548 }
549 if op == Op::SetProperty {
550 let Some(property) = read_u16(*ip) else {
551 return label;
552 };
553 let Some(binding) = read_u16(*ip + 2) else {
554 return label;
555 };
556 *ip += 4;
557 let render = |index: u16| {
558 self.constants
559 .get(index as usize)
560 .map(ToString::to_string)
561 .unwrap_or_default()
562 };
563 return format!(
564 "{label} property {property:>4} ({}) binding {binding:>4} ({})",
565 render(property),
566 render(binding)
567 );
568 }
569 if matches!(
570 op,
571 Op::GetLocalSlot
572 | Op::DefLocalSlot
573 | Op::SetLocalSlot
574 | Op::SetLocalSlotSubscript
575 | Op::ConcatAssignLocal
576 ) {
577 let Some(slot) = read_u16(*ip) else {
578 return label;
579 };
580 *ip += 2;
581 return match self.local_slots.get(slot as usize) {
582 Some(info) => format!("{label} {slot:>4} ({})", info.name),
583 None => format!("{label} {slot:>4}"),
584 };
585 }
586 if op == Op::SetLocalSlotProperty {
587 let Some(property) = read_u16(*ip) else {
588 return label;
589 };
590 let Some(slot) = read_u16(*ip + 2) else {
591 return label;
592 };
593 *ip += 4;
594 let property = self
595 .constants
596 .get(property as usize)
597 .map(ToString::to_string)
598 .unwrap_or_default();
599 return format!("{label} {slot:>4} {property}");
600 }
601 if matches!(op, Op::MethodCall | Op::MethodCallOpt) {
602 let Some(name) = read_u16(*ip) else {
603 return label;
604 };
605 let Some(argc) = self.code.get(*ip + 2).copied() else {
606 return label;
607 };
608 *ip += 3;
609 let name = self
610 .constants
611 .get(name as usize)
612 .map(ToString::to_string)
613 .unwrap_or_default();
614 return format!("{label} {argc:>4} ({name})");
615 }
616 if matches!(op, Op::Call | Op::TailCall) {
617 let Some(argc) = self.code.get(*ip).copied() else {
618 return label;
619 };
620 *ip += 1;
621 return format!("{label} {argc:>4}");
622 }
623 let width = instruction_len(op, &self.code[(*ip).saturating_sub(1)..]).unwrap_or(1);
624 if width == 3 {
625 let Some(value) = read_u16(*ip) else {
626 return label;
627 };
628 *ip += 2;
629 return format!("{label} {value:>4}");
630 }
631 if width > 1 {
632 *ip = (*ip).saturating_add(width - 1).min(self.code.len());
633 }
634 label
635 }
636}
637
638fn opcode_label(name: &str) -> String {
639 let mut output = String::new();
640 for (index, ch) in name.chars().enumerate() {
641 if index > 0 && ch.is_ascii_uppercase() {
642 output.push('_');
643 }
644 output.push(ch.to_ascii_uppercase());
645 }
646 output
647}
648
649fn reads_outer_name(op: Op) -> bool {
650 matches!(
651 op,
652 Op::GetVar
653 | Op::SetVar
654 | Op::Call
655 | Op::TailCall
656 | Op::Pipe
657 | Op::CheckType
658 | Op::CallSpread
659 | Op::CallBuiltin
660 | Op::CallBuiltinSpread
661 )
662}
663
664pub fn instruction_len(op: Op, _remaining: &[u8]) -> Option<usize> {
665 Some(op.instruction_len())
666}
667
668#[cfg(debug_assertions)]
669fn stack_delta(op: Op, count: u16) -> Option<i32> {
670 use Op::*;
671 let count = i32::from(count);
672 Some(match op {
673 Constant | Nil | True | False | RootHarness | GetVar | GetArgc | GetLocalSlot | Closure
674 | Dup => 1,
675 DefLet | DefVar | DefCell | SetVar | DefLocalSlot | SetLocalSlot | SetProperty
676 | SetLocalSlotProperty | ConcatAssignLocal | Pop => -1,
677 Negate | Not | GetProperty | GetPropertyOpt | CheckType | TryUnwrap | TryWrapOk | Swap
678 | PushScope | PopScope | PopIterator | PopHandler | AssertBindingType => 0,
679 Add | Sub | Mul | Div | Mod | Pow | AddInt | SubInt | MulInt | DivInt | ModInt
680 | AddFloat | SubFloat | MulFloat | DivFloat | ModFloat | Equal | NotEqual | Less
681 | Greater | LessEqual | GreaterEqual | EqualInt | NotEqualInt | LessInt | GreaterInt
682 | LessEqualInt | GreaterEqualInt | EqualFloat | NotEqualFloat | LessFloat
683 | GreaterFloat | LessEqualFloat | GreaterEqualFloat | EqualBool | NotEqualBool
684 | EqualString | NotEqualString | Contains | Subscript | SubscriptOpt => -1,
685 IterInit => -1,
686 Slice | SetSubscript | SetLocalSlotSubscript => -2,
687 BuildList | Concat | CallBuiltin => 1 - count,
688 BuildDict => 1 - 2 * count,
689 Call | MethodCall | MethodCallOpt => -count,
690 Jump
691 | JumpIfFalse
692 | JumpIfTrue
693 | IterNext
694 | Return
695 | TailCall
696 | Throw
697 | ThrowDeclared
698 | Rethrow
699 | TryCatchSetup
700 | TryCatchPreserve
701 | Spawn
702 | Pipe
703 | Parallel
704 | ParallelMap
705 | ParallelMapStream
706 | ParallelSettle
707 | SyncMutexEnter
708 | SyncMutexEnterKeyed
709 | TaskScopeEnter
710 | TaskScopeExit
711 | Import
712 | SelectiveImport
713 | NamespaceImport
714 | NamespaceImportMembers
715 | DeadlineSetup
716 | DeadlineEnd
717 | BuildEnum
718 | MatchEnum
719 | Yield
720 | CallSpread
721 | CallBuiltinSpread
722 | MethodCallSpread => return None,
723 })
724}