Skip to main content

mech_interpreter/
functions.rs

1use crate::*;
2use crate::tracing::{
3  format_trace,
4  format_trace_args,
5  summarize_function_pattern,
6  summarize_function_value,
7  summarize_values_with_kinds,
8};
9#[cfg(all(feature = "kind_annotation", feature = "enum"))]
10use std::collections::HashSet;
11use crate::*;
12
13// Functions
14// ============================================================================
15
16
17// Frames
18// ----------------------------------------------------------------------------
19
20#[derive(Clone, PartialEq, Eq, Debug)]
21pub enum FrameState {
22  Running,
23  Suspended,
24  Completed,
25}
26
27// One activation record on the call stack. Every user-function invocation gets
28// its own Frame so locals and the instruction pointer don't bleed across calls.
29#[derive(Clone)]
30pub struct Frame {
31  plan: Plan,
32  ip: usize,              // index of the next instruction to execute
33  locals: SymbolTableRef, // variables local to this invocation
34  out: Option<Value>,     // value yielded by a coroutine, if any
35  state: FrameState,      // Running / Suspended / Completed
36}
37
38// The call stack is a simple growable list of frames; the last entry is current.
39#[derive(Clone)]
40pub struct Stack {
41  frames: Vec<Frame>,
42}
43
44// Registers a user-written function so it can be called by name later.
45// Hashes the name to a u64 id used as the lookup key throughout the runtime.
46pub fn function_define(fxn_def: &FunctionDefine, p: &Interpreter) -> MResult<FunctionDefinition> {
47  let fxn_name_id = fxn_def.name.hash();
48  let mut new_fxn = FunctionDefinition::new(fxn_name_id, fxn_def.name.to_string(), fxn_def.clone());
49
50  // Record declared input arguments and their kind annotations.
51  for input_arg in &fxn_def.input {
52    new_fxn
53      .input
54      .insert(input_arg.name.hash(), input_arg.kind.clone());
55  }
56
57  // Record declared output arguments and their kind annotations.
58  for output_arg in &fxn_def.output {
59    new_fxn
60      .output
61      .insert(output_arg.name.hash(), output_arg.kind.clone());
62  }
63
64  // Store the definition and register the human-readable name string in both
65  // dictionaries so error messages and debug output can print it.
66  let functions = p.functions();
67  let mut functions_brrw = functions.borrow_mut();
68  functions_brrw
69    .user_functions
70    .insert(fxn_name_id, new_fxn.clone());
71  functions_brrw
72    .dictionary
73    .borrow_mut()
74    .insert(fxn_name_id, fxn_def.name.to_string());
75  p.state
76    .borrow()
77    .dictionary
78    .borrow_mut()
79    .insert(fxn_name_id, fxn_def.name.to_string());
80
81  Ok(new_fxn)
82}
83
84// Calls
85// ----------------------------------------------------------------------------
86
87// Dispatches a function call to whichever implementation is available:
88// user-defined functions first, then built-in functions, then native compiled
89// functions. Returns an error if the name is not found in any registry.
90pub fn function_call(fxn_call: &FunctionCall, env: Option<&Environment>, p: &Interpreter) -> MResult<Value> {
91  let functions = p.functions();
92  let fxn_name_id = fxn_call.name.hash();
93
94  // User-defined function: evaluate arguments then run the interpreted body.
95  if let Some(user_fxn) = { functions.borrow().user_functions.get(&fxn_name_id).cloned() } {
96    let mut input_arg_values = vec![];
97    for (_, arg_expr) in fxn_call.args.iter() {
98      input_arg_values.push(expression(arg_expr, env, p)?);
99    }
100    return execute_user_function(&user_fxn, &input_arg_values, p);
101  }
102
103  // Pre-compiled built-in functions.
104  if { functions.borrow().functions.contains_key(&fxn_name_id) } {
105    todo!();
106  }
107
108  // Native function compiler: the compiler picks a concrete implementation
109  // based on the runtime argument types, then we execute it immediately.
110  let fxn_compiler = {
111    functions
112      .borrow()
113      .function_compilers
114      .get(&fxn_name_id)
115      .copied()
116  };
117  match fxn_compiler {
118    Some(fxn_compiler) => {
119      let mut input_arg_values = vec![];
120      for (_, arg_expr) in fxn_call.args.iter() {
121        input_arg_values.push(expression(arg_expr, env, p)?);
122      }
123      trace_println!(
124        p,
125        "{}",
126        format_trace(
127          "fn",
128          format!(
129            "native {}({})",
130            fxn_call.name.to_string(),
131            format_trace_args(&input_arg_values)
132          ),
133        )
134      );
135      execute_native_function_compiler(fxn_compiler, &input_arg_values, p)
136    }
137    // No implementation found under this name at all.
138    None => Err(MechError::new(
139      MissingFunctionError {
140        function_id: fxn_name_id,
141      },
142      None,
143    )
144    .with_compiler_loc()
145    .with_tokens(fxn_call.name.tokens())),
146  }
147}
148
149// Asks a native function compiler to select the right concrete implementation
150// for the given argument types, runs it once to produce an initial value, then
151// pushes it onto the reactive plan so it re-runs when its inputs change.
152pub fn execute_native_function_compiler(
153  fxn_compiler: &'static dyn NativeFunctionCompiler,
154  input_arg_values: &Vec<Value>,
155  p: &Interpreter,
156) -> MResult<Value> {
157  let plan = p.plan();
158  match fxn_compiler.compile(input_arg_values) {
159    Ok(mut new_fxn) => {
160      trace_println!(
161        p,
162        "{}",
163        format_trace(
164          "arm",
165          format!(
166            "selected {} args=[{}]",
167            new_fxn
168              .to_string()
169              .lines()
170              .next()
171              .unwrap_or("<unknown-arm>"),
172            format_trace_args(input_arg_values)
173          ),
174        )
175      );
176      let mut plan_brrw = plan.borrow_mut();
177      new_fxn.solve();                   // run the function once to initialise its output
178      let result = new_fxn.out();
179      trace_println!(
180        p,
181        "{}",
182        format_trace("arm", format!("result {}", summarize_function_value(&result)))
183      );
184      plan_brrw.push(new_fxn);          // keep it in the plan for reactive re-evaluation
185      Ok(result)
186    }
187    Err(err) => Err(err),
188  }
189}
190
191// Executes a user-defined function. Handles argument count validation,
192// optional matrix broadcasting, match-arm dispatch, and plain statement bodies.
193// Logs entry/exit (or failure) via the trace machinery.
194fn execute_user_function(
195  fxn_def: &FunctionDefinition,
196  input_arg_values: &Vec<Value>,
197  p: &Interpreter,
198) -> MResult<Value> {
199  // Reject calls with the wrong number of arguments before doing anything else.
200  if input_arg_values.len() != fxn_def.input.len() {
201    return Err(MechError::new(
202      IncorrectNumberOfArguments {
203        expected: fxn_def.input.len(),
204        found: input_arg_values.len(),
205      },
206      None,
207    )
208    .with_compiler_loc()
209    .with_tokens(fxn_def.code.name.tokens()));
210  }
211
212  // If the function takes a single matrix argument and the element kind matches
213  // the output kind, broadcast element-wise instead of running the body once.
214  #[cfg(feature = "matrix")]
215  if let Some(result) = try_broadcast_user_function(fxn_def, input_arg_values, p)? {
216    return Ok(result);
217  }
218
219  trace_println!(
220    p,
221    "{}",
222    format_trace(
223      "fn",
224      format!(
225        "enter {}({})",
226        fxn_def.name,
227        format_trace_args(input_arg_values)
228      ),
229    )
230  );
231
232  // Choose execution strategy: match-arm body vs. plain statement body.
233  let output = if !fxn_def.code.match_arms.is_empty() {
234    // Match-arm body: loop to support tail-call optimisation. Each iteration
235    // opens a fresh scope, binds the current arguments, runs the arms, then
236    // either returns the result or loops with a new argument set.
237    let mut current_args: Vec<Value> = input_arg_values.clone();
238    loop {
239      let scope = FunctionScope::enter(p);
240      bind_function_inputs(fxn_def, &current_args, p)?;
241      let step: FunctionCallStep = execute_function_match_arms(fxn_def, &current_args, p)?;
242      drop(scope);
243      match step {
244        FunctionCallStep::Return(value) => break Ok(value),
245        // Tail call: swap in the new args and go around again without growing
246        // the Rust call stack.
247        FunctionCallStep::TailCall(next_args) => {
248          current_args = next_args;
249        }
250      }
251    }
252  } else {
253    // Plain statement body: run statements in order, then collect named outputs.
254    let scope = FunctionScope::enter(p);
255    bind_function_inputs(fxn_def, input_arg_values, p)?;
256    for statement_node in &fxn_def.code.statements {
257      statement(statement_node, None, p)?;
258    }
259    let result = collect_function_output(p, fxn_def);
260    drop(scope);
261    result
262  };
263
264  match output {
265    Ok(value) => {
266      trace_println!(
267        p,
268        "{}",
269        format_trace(
270          "fn",
271          format!("exit  {} => {}", fxn_def.name, summarize_function_value(&value))
272        )
273      );
274      Ok(value)
275    }
276    Err(err) => {
277      trace_println!(
278        p,
279        "{}",
280        format_trace("fn", format!("fail  {} => {:?}", fxn_def.name, err))
281      );
282      Err(err)
283    }
284  }
285}
286
287// The outcome of executing one match arm. Either we have a final value, or
288// we identified a tail call and carry its new arguments for the next iteration.
289enum FunctionCallStep {
290  Return(Value),
291  TailCall(Vec<Value>),
292}
293
294// If the function is single-input / single-output with matching scalar kinds,
295// and the actual argument is a matrix, run the function on each element and
296// reassemble the result into a matrix of the same shape.
297// Returns None if any condition for broadcasting isn't met, so the caller can
298// fall through to normal execution.
299#[cfg(feature = "matrix")]
300fn try_broadcast_user_function(
301  fxn_def: &FunctionDefinition,
302  input_arg_values: &Vec<Value>,
303  p: &Interpreter,
304) -> MResult<Option<Value>> {
305  if input_arg_values.len() != 1
306    || fxn_def.code.output.len() != 1
307    || fxn_def.code.input.len() != 1
308  {
309    return Ok(None);
310  }
311
312  let source = detach_value(&input_arg_values[0]);
313  if !source.is_matrix() {
314    return Ok(None);
315  }
316
317  // Resolve the declared input and output kinds from their annotations.
318  // Without kind_annotation feature we can't know the element type, so bail.
319  #[cfg(feature = "kind_annotation")]
320  let (input_kind, output_kind) = {
321    let input_kind = kind_annotation(&fxn_def.code.input[0].kind.kind, p)?
322      .to_value_kind(&p.state.borrow().kinds)?;
323    let output_kind = kind_annotation(&fxn_def.code.output[0].kind.kind, p)?
324      .to_value_kind(&p.state.borrow().kinds)?;
325    (input_kind, output_kind)
326  };
327
328  #[cfg(not(feature = "kind_annotation"))]
329  let (input_kind, output_kind) = {
330    return Ok(None);
331  };
332
333  // Only broadcast when input and output kinds are the same scalar kind.
334  // If the input is already a matrix kind, don't recurse.
335  if input_kind != output_kind || matches!(input_kind, ValueKind::Matrix(_, _)) {
336    return Ok(None);
337  }
338
339  let Some(elements) = crate::patterns::matrix_like_values(&source) else {
340    return Ok(None);
341  };
342
343  // Apply the function element-wise, then reassemble into the original shape.
344  let mut outputs = Vec::with_capacity(elements.len());
345  for element in elements {
346    outputs.push(execute_user_function(fxn_def, &vec![element], p)?);
347  }
348
349  let shape = source.shape();
350  Ok(Some(build_typed_matrix_from_values(
351    &output_kind,
352    outputs,
353    shape[0],
354    shape[1],
355  )))
356}
357
358// Assembles a list of scalar Values into a typed matrix.
359// TODO add more types
360#[cfg(feature = "matrix")]
361fn build_typed_matrix_from_values(
362  output_kind: &ValueKind,
363  outputs: Vec<Value>,
364  rows: usize,
365  cols: usize,
366) -> Value {
367  match output_kind {
368    #[cfg(feature = "f64")]
369    ValueKind::F64 => Value::MatrixF64(f64::to_matrix(
370      outputs
371        .into_iter()
372        .map(|value| {
373          value
374            .as_f64()
375            .expect("Expected f64 output")
376            .borrow()
377            .clone()
378        })
379        .collect::<Vec<f64>>(),
380      rows,
381      cols,
382    )),
383    _ => Value::MatrixValue(Value::to_matrix(outputs, rows, cols)),
384  }
385}
386
387// Tries each match arm in order against the current arguments. Handles:
388//   - enum exhaustiveness checking (kind_annotation + enum features)
389//   - tail-call detection (arm body is a recursive call with same arity)
390//   - output kind coercion
391// Returns an error if no arm matched.
392fn execute_function_match_arms(
393  fxn_def: &FunctionDefinition,
394  input_arg_values: &Vec<Value>,
395  p: &Interpreter,
396) -> MResult<FunctionCallStep> {
397
398  // Exhaustiveness check: when the single input is an enum type and there is
399  // no wildcard arm, every variant must be covered or we report which ones
400  // are missing before even attempting to run.
401  #[cfg(all(feature = "kind_annotation", feature = "enum"))]
402  {
403    let has_wildcard = fxn_def
404      .code
405      .match_arms
406      .iter()
407      .any(|arm| matches!(arm.pattern, Pattern::Wildcard));
408    if !has_wildcard && fxn_def.input.len() == 1 {
409      if let Some((_, kind_annotation_node)) = fxn_def.input.iter().next() {
410        let input_kind = kind_annotation(&kind_annotation_node.kind, p)?
411          .to_value_kind(&p.state.borrow().kinds)?;
412        if let ValueKind::Enum(enum_id, _) = input_kind {
413          let state_brrw = p.state.borrow();
414          if let Some(enum_def) = state_brrw.enums.get(&enum_id) {
415            // Collect every variant name that appears in the written arms.
416            let mut covered_variants: HashSet<u64> = HashSet::new();
417            for arm in &fxn_def.code.match_arms {
418              match &arm.pattern {
419                #[cfg(feature = "atom")]
420                Pattern::TupleStruct(tuple_struct) => {
421                  covered_variants.insert(tuple_struct.name.hash());
422                }
423                Pattern::Expression(expr) => {
424                  if let Expression::Literal(Literal::Atom(atom)) = expr {
425                    covered_variants.insert(atom.name.hash());
426                  }
427                }
428                _ => {}
429              }
430            }
431            let all_covered = enum_def
432              .variants
433              .iter()
434              .all(|(variant_id, _)| covered_variants.contains(variant_id));
435            if !all_covered {
436              // Build a readable list of the missing variant patterns.
437              let missing_patterns = enum_def
438                .variants
439                .iter()
440                .filter(|(variant_id, _)| !covered_variants.contains(variant_id))
441                .map(|(variant_id, payload_kind)| {
442                  let variant_name = enum_def
443                    .names
444                    .borrow()
445                    .get(variant_id)
446                    .cloned()
447                    .unwrap_or_else(|| variant_id.to_string());
448                  if payload_kind.is_some() {
449                    format!(":{}(...)", variant_name)
450                  } else {
451                    format!(":{}", variant_name)
452                  }
453                })
454                .collect::<Vec<String>>();
455              return Err(MechError::new(
456                FunctionMatchNonExhaustiveError {
457                  function_name: fxn_def.name.clone(),
458                  missing_patterns,
459                },
460                None,
461              )
462              .with_compiler_loc()
463              .with_tokens(fxn_def.code.name.tokens()));
464            }
465          }
466        }
467      }
468    }
469  }
470
471  // Try each arm in source order; the first one whose pattern matches wins.
472  for (arm_idx, arm) in fxn_def.code.match_arms.iter().enumerate() {
473    let mut env = Environment::new();
474    let matched = crate::patterns::pattern_matches_arguments(
475      &arm.pattern,
476      input_arg_values,
477      &mut env,
478      p,
479    )?;
480    trace_println!(p, "{}", {
481      let args_summary = summarize_values_with_kinds(input_arg_values);
482      let pattern_summary = summarize_function_pattern(&arm.pattern);
483      let marker = if matched { "✓" } else { "X" };
484      format_trace(
485        "match",
486        format!(
487          "arm[{arm_idx}] test pattern={pattern_summary} args=[{args_summary}] {marker}"
488        ),
489      )
490    });
491    if matched {
492      // Tail-call optimisation: if the arm body is a direct recursive call
493      // with the same arity, return new arguments instead of recursing.
494      if let Expression::FunctionCall(fxn_call) = &arm.expression {
495        if fxn_call.name.hash() == fxn_def.code.name.hash() {
496          let mut tail_args = Vec::with_capacity(fxn_call.args.len());
497          for (_, arg_expr) in fxn_call.args.iter() {
498            tail_args.push(expression(arg_expr, Some(&env), p)?);
499          }
500          if tail_args.len() == fxn_def.input.len() {
501            trace_println!(
502              p,
503              "{}",
504              format_trace(
505                "match",
506                format!("arm[{arm_idx}] tail-call {}", fxn_def.name)
507              )
508            );
509            return Ok(FunctionCallStep::TailCall(tail_args));
510          }
511        }
512      }
513      // Normal arm: evaluate the expression and coerce to the declared output kind.
514      let out = expression(&arm.expression, Some(&env), p)?;
515      let coerced = coerce_function_output_kind(detach_value(&out), fxn_def, p)?;
516      trace_println!(
517        p,
518        "{}",
519        format_trace(
520          "match",
521          format!(
522            "arm[{arm_idx}] out  value={} kind={}",
523            summarize_function_value(&coerced),
524            coerced.kind().to_string()
525          )
526        )
527      );
528      return Ok(FunctionCallStep::Return(coerced));
529    }
530  }
531  // No arm matched — this is a runtime error; the function has no defined output.
532  Err(MechError::new(
533    FunctionOutputUndefinedError {
534      output_id: fxn_def.id,
535    },
536    None,
537  )
538  .with_compiler_loc()
539  .with_tokens(fxn_def.code.name.tokens()))
540}
541
542// Coerces a match-arm result to the function's declared output kind.
543// If no output annotation exists, or conversion fails, the value is returned as-is.
544#[cfg(feature = "kind_annotation")]
545fn coerce_function_output_kind(
546  value: Value,
547  fxn_def: &FunctionDefinition,
548  p: &Interpreter,
549) -> MResult<Value> {
550  if fxn_def.output.is_empty() {
551    return Ok(value);
552  }
553  let Some((_, output_kind_annotation)) = fxn_def.output.get_index(0) else {
554    return Ok(value);
555  };
556  let target_kind =
557    kind_annotation(&output_kind_annotation.kind, p)?.to_value_kind(&p.state.borrow().kinds)?;
558  return Ok(value.convert_to(&target_kind).unwrap_or(value));
559}
560
561// RAII guard that swaps in a fresh symbol table and plan for the duration of a
562// function call, then restores the previous ones on drop. This is what gives
563// each function its own local variable namespace.
564struct FunctionScope {
565  state: Ref<ProgramState>,
566  previous_symbols: SymbolTableRef,
567  previous_plan: Plan,
568  previous_environment: Option<SymbolTableRef>,
569}
570
571impl FunctionScope {
572  fn enter(p: &Interpreter) -> Self {
573    let state = p.state.clone();
574    let mut state_brrw = state.borrow_mut();
575    // A new symbol table that shares the global name dictionary so that
576    // lookups by hash still resolve to human-readable names.
577    let mut local_symbols = SymbolTable::new();
578    local_symbols.dictionary = state_brrw.dictionary.clone();
579    let local_symbols = Ref::new(local_symbols);
580    let previous_symbols = std::mem::replace(&mut state_brrw.symbol_table, local_symbols);
581    let previous_plan = std::mem::replace(&mut state_brrw.plan, Plan::new());
582    let previous_environment = state_brrw.environment.take();
583    drop(state_brrw);
584
585    Self {
586      state,
587      previous_symbols,
588      previous_plan,
589      previous_environment,
590    }
591  }
592}
593
594// Restore the caller's symbol table, plan, and environment when the scope ends.
595impl Drop for FunctionScope {
596  fn drop(&mut self) {
597    let mut state_brrw = self.state.borrow_mut();
598    state_brrw.symbol_table = self.previous_symbols.clone();
599    state_brrw.plan = self.previous_plan.clone();
600    state_brrw.environment = self.previous_environment.clone();
601  }
602}
603
604// Function Definitions
605// ----------------------------------------------------------------------------
606
607// Binds each argument value to the corresponding local variable name.
608// With kind_annotation: validates and coerces argument types, including
609// special handling for enum types where coercion rules differ.
610fn bind_function_inputs(
611  fxn_def: &FunctionDefinition,
612  input_arg_values: &Vec<Value>,
613  p: &Interpreter,
614) -> MResult<()> {
615  let scoped_state = p.state.borrow();
616  for ((arg_id, input_kind_annotation), input_value) in
617    fxn_def.input.iter().zip(input_arg_values.iter())
618  {
619    // Look up the human-readable argument name for error messages.
620    let arg_name = fxn_def
621      .code
622      .input
623      .iter()
624      .find(|arg| arg.name.hash() == *arg_id)
625      .map(|arg| arg.name.to_string())
626      .unwrap_or_else(|| arg_id.to_string());
627
628    let bound_value = {
629      #[cfg(feature = "kind_annotation")]
630      {
631        let target_kind = kind_annotation(&input_kind_annotation.kind, p)?
632          .to_value_kind(&p.state.borrow().kinds)?;
633        let detached_input = detach_value(input_value);
634
635        // Enum arguments are checked for membership rather than converted,
636        // because coercion semantics don't apply across enum variants.
637        #[cfg(all(feature = "enum", feature = "atom"))]
638        if let ValueKind::Enum(enum_id, _) = &target_kind {
639          let state_brrw = p.state.borrow();
640          if enum_value_matches(detached_input.clone(), *enum_id, &state_brrw) {
641            detached_input.clone()
642          } else {
643            return Err(MechError::new(
644              FunctionInputTypeMismatchError {
645                function_name: fxn_def.name.clone(),
646                argument_name: arg_name.clone(),
647                expected: target_kind.clone(),
648                found: detached_input.kind(),
649              },
650              None,
651            )
652            .with_compiler_loc()
653            .with_tokens(input_kind_annotation.tokens()));
654          }
655        } else {
656          // Non-enum: attempt type conversion; error if it can't be done.
657          detached_input
658            .clone()
659            .convert_to(&target_kind)
660            .ok_or_else(|| {
661              MechError::new(
662                FunctionInputTypeMismatchError {
663                  function_name: fxn_def.name.clone(),
664                  argument_name: arg_name.clone(),
665                  expected: target_kind.clone(),
666                  found: detached_input.kind(),
667                },
668                None,
669              )
670              .with_compiler_loc()
671              .with_tokens(input_kind_annotation.tokens())
672            })?
673        }
674        #[cfg(not(all(feature = "enum", feature = "atom")))]
675        detached_input
676          .clone()
677          .convert_to(&target_kind)
678          .ok_or_else(|| {
679            MechError::new(
680              FunctionInputTypeMismatchError {
681                function_name: fxn_def.name.clone(),
682                argument_name: arg_name.clone(),
683                expected: target_kind.clone(),
684                found: detached_input.kind(),
685              },
686              None,
687            )
688            .with_compiler_loc()
689            .with_tokens(input_kind_annotation.tokens())
690          })?
691      }
692      // Without kind_annotation: accept the value as-is, just detach any reference.
693      #[cfg(not(feature = "kind_annotation"))]
694      {
695        detach_value(input_value)
696      }
697    };
698    scoped_state.save_symbol(*arg_id, arg_name, bound_value, false);
699  }
700  Ok(())
701}
702
703// Returns true if `value` is a valid member of the enum identified by `enum_id`.
704// Handles bare atom variants and tuple-struct variants (atom tag + payload).
705#[cfg(all(feature = "enum", feature = "atom"))]
706fn enum_value_matches(value: Value, enum_id: u64, state: &ProgramState) -> bool {
707  let enum_def = match state.enums.get(&enum_id) {
708    Some(enm) => enm,
709    None => return false,
710  };
711  match value {
712    // Bare atom: check that the atom's id is a known payload-less variant.
713    Value::Atom(atom) => {
714      let variant_id = atom.borrow().id();
715      enum_def
716        .variants
717        .iter()
718        .any(|(known_variant, payload_kind)| {
719          *known_variant == variant_id && payload_kind.is_none()
720        })
721    }
722    // Tuple-struct variant: a 2-element tuple of (atom-tag, payload).
723    // The tag must match a known variant and the payload must satisfy the
724    // declared payload kind, recursing for nested enums.
725    #[cfg(feature = "tuple")]
726    Value::Tuple(tuple_val) => {
727      let tuple_brrw = tuple_val.borrow();
728      if tuple_brrw.elements.len() != 2 {
729        return false;
730      }
731      let tag = match tuple_brrw.elements[0].as_ref() {
732        Value::Atom(atom) => atom.borrow().id(),
733        _ => return false,
734      };
735      let payload = tuple_brrw.elements[1].as_ref().clone();
736      let (_, declared_payload_kind) = match enum_def
737        .variants
738        .iter()
739        .find(|(known_variant, _)| *known_variant == tag)
740      {
741        Some(entry) => entry,
742        None => return false,
743      };
744      match declared_payload_kind {
745        Some(Value::Kind(expected_kind)) => match expected_kind {
746          // Nested enum payload: recurse.
747          ValueKind::Enum(inner_enum_id, _) => {
748            enum_value_matches(payload, *inner_enum_id, state)
749          }
750          // Scalar payload: accept exact match or a convertible value.
751          _ => {
752            payload.kind() == expected_kind.clone()
753              || payload.convert_to(expected_kind).is_some()
754          }
755        },
756        _ => false,
757      }
758    }
759    _ => false,
760  }
761}
762
763// Reads each declared output variable out of the local symbol table and
764// returns them as a single Value. Multiple outputs are wrapped in a Tuple;
765// a single output is returned directly; zero outputs return Empty.
766fn collect_function_output(p: &Interpreter, fxn_def: &FunctionDefinition) -> MResult<Value> {
767  let symbols = p.symbols();
768  let symbols_brrw = symbols.borrow();
769  let mut outputs = vec![];
770
771  for output_arg in &fxn_def.code.output {
772    let output_id = output_arg.name.hash();
773    match symbols_brrw.get(output_id) {
774      Some(cell) => outputs.push(detach_value(&cell.borrow())),
775      None => {
776        return Err(
777          MechError::new(FunctionOutputUndefinedError { output_id }, None)
778            .with_compiler_loc()
779            .with_tokens(output_arg.tokens()),
780        );
781      }
782    }
783  }
784
785  Ok(match outputs.len() {
786    0 => Value::Empty,
787    1 => outputs.remove(0),
788    _ => Value::Tuple(Ref::new(MechTuple::from_vec(outputs))),
789  })
790}
791
792// Peels off any MutableReference wrappers to get to the underlying value.
793// Used before storing arguments or returning results so callers always see
794// plain owned values, not live references into other cells.
795pub(crate) fn detach_value(value: &Value) -> Value {
796  match value {
797    Value::MutableReference(reference) => detach_value(&reference.borrow()),
798    _ => value.clone(),
799  }
800}
801
802// Function Errors
803// ----------------------------------------------------------------------------
804
805// The called function name doesn't exist in any registry.
806#[derive(Debug, Clone)]
807pub struct MissingFunctionError {
808  pub function_id: u64,
809}
810
811impl MechErrorKind for MissingFunctionError {
812  fn name(&self) -> &str {
813    "MissingFunction"
814  }
815  fn message(&self) -> String {
816    format!("Function with id {} not found", self.function_id)
817  }
818}
819
820// A function's output variable was declared but never assigned during execution.
821#[derive(Debug, Clone)]
822pub struct FunctionOutputUndefinedError {
823  pub output_id: u64,
824}
825
826impl MechErrorKind for FunctionOutputUndefinedError {
827  fn name(&self) -> &str {
828    "FunctionOutputUndefined"
829  }
830  fn message(&self) -> String {
831    format!(
832      "Function output {} was declared but never defined",
833      self.output_id
834    )
835  }
836}
837
838// A match-arm function doesn't cover every variant of its enum input type.
839#[derive(Debug, Clone)]
840pub struct FunctionMatchNonExhaustiveError {
841  pub function_name: String,
842  pub missing_patterns: Vec<String>,
843}
844
845impl MechErrorKind for FunctionMatchNonExhaustiveError {
846  fn name(&self) -> &str {
847    "FunctionMatchNonExhaustive"
848  }
849
850  fn message(&self) -> String {
851    format!(
852      "Function '{}' has non-exhaustive match arms. Missing patterns: {}. Add the missing patterns or add a wildcard (`*`) arm.",
853      self.function_name,
854      self.missing_patterns.join(", ")
855    )
856  }
857}
858
859// A value passed to a function argument didn't match the declared kind and
860// couldn't be coerced to it.
861#[derive(Debug, Clone)]
862pub struct FunctionInputTypeMismatchError {
863  pub function_name: String,
864  pub argument_name: String,
865  pub expected: ValueKind,
866  pub found: ValueKind,
867}
868
869impl MechErrorKind for FunctionInputTypeMismatchError {
870  fn name(&self) -> &str {
871    "FunctionInputTypeMismatch"
872  }
873
874  fn message(&self) -> String {
875    format!(
876      "Function '{}' argument '{}' expected {}, found {}",
877      self.function_name, self.argument_name, self.expected, self.found
878    )
879  }
880}