///|
/// One operation executed by the optional stack-based bytecode backend.
pub(all) enum BytecodeOp {
  PushConstant(Int)
  LoadName(String)
  DefineName(String)
  StoreName(String)
  Duplicate
  Pop
  EnterScope
  ExitScope
  UnaryOp(UnaryOperator)
  BinaryOp(BinaryOperator)
  BuildList(Int)
  BuildMap(Int)
  GetSubscript
  SetSubscript
  GetProperty(String)
  SetProperty(String)
  InvokeMethod(String, Int)
  EvaluateSemantic(Expr)
  ExecuteSemantic(Stmt)
  Jump(Int)
  JumpIfFalse(Int)
  JumpIfFalseKeep(Int)
  JumpIfTrueKeep(Int)
  SetLast
  ReturnValue
  ReturnLast
} derive(Eq, Debug)

///|
pub extend BytecodeOp with Eq::{equal, not_equal}

///|
pub extend BytecodeOp with Debug::{to_repr}

///|
pub(all) struct BytecodeInstruction {
  op : BytecodeOp
  span : SourceSpan
} derive(Eq, Debug)

///|
pub extend BytecodeInstruction with Eq::{equal, not_equal}

///|
pub extend BytecodeInstruction with Debug::{to_repr}

///|
/// Immutable compiled program. Frequently used procedural operations lower to
/// native stack instructions; object/module/Fiber constructs use semantic
/// escape instructions so every supported Wren construct remains executable.
pub(all) struct BytecodeProgram {
  source_name : String
  constants : Array[Value]
  instructions : Array[BytecodeInstruction]
}

///|
pub(all) enum BytecodeCompileResult {
  BytecodeCompiled(BytecodeProgram)
  BytecodeCompileFailed(Array[Diagnostic])
}

///|
pub(all) enum BytecodeRunResult {
  BytecodeCompleted(Value, Int)
  BytecodeFailed(Diagnostic, Int)
}

///|
/// Dynamic counters collected by the bytecode profiler.
pub(all) struct BytecodeProfile {
  result : BytecodeRunResult
  executed_instructions : Int
  max_stack_depth : Int
  loads : Int
  stores : Int
  branches : Int
  arithmetic_operations : Int
  calls : Int
  semantic_fallbacks : Int
}

///|
/// One executed instruction captured for debugger frontends.
pub(all) struct BytecodeTraceEvent {
  instruction_pointer : Int
  step : Int
  stack_depth : Int
  instruction : BytecodeInstruction
}

///|
pub(all) struct BytecodeTrace {
  result : BytecodeRunResult
  events : Array[BytecodeTraceEvent]
}

///|
priv struct BytecodeProfiler {
  mut max_stack_depth : Int
  mut loads : Int
  mut stores : Int
  mut branches : Int
  mut arithmetic_operations : Int
  mut calls : Int
  mut semantic_fallbacks : Int
  capture_trace : Bool
  events : Array[BytecodeTraceEvent]
}

///|
fn BytecodeProfiler::new(capture_trace : Bool) -> BytecodeProfiler {
  {
    max_stack_depth: 0,
    loads: 0,
    stores: 0,
    branches: 0,
    arithmetic_operations: 0,
    calls: 0,
    semantic_fallbacks: 0,
    capture_trace,
    events: [],
  }
}

///|
fn BytecodeProfiler::observe(
  self : BytecodeProfiler,
  operation : BytecodeOp,
  stack_depth : Int,
  instruction_pointer : Int,
  step : Int,
  instruction : BytecodeInstruction,
) -> Unit {
  if stack_depth > self.max_stack_depth {
    self.max_stack_depth = stack_depth
  }
  match operation {
    LoadName(_) => self.loads = self.loads + 1
    DefineName(_) | StoreName(_) => self.stores = self.stores + 1
    Jump(_) | JumpIfFalse(_) | JumpIfFalseKeep(_) | JumpIfTrueKeep(_) =>
      self.branches = self.branches + 1
    UnaryOp(_) | BinaryOp(_) =>
      self.arithmetic_operations = self.arithmetic_operations + 1
    GetSubscript
    | SetSubscript
    | GetProperty(_)
    | SetProperty(_)
    | InvokeMethod(_, _) => self.calls = self.calls + 1
    EvaluateSemantic(_) | ExecuteSemantic(_) =>
      self.semantic_fallbacks = self.semantic_fallbacks + 1
    _ => ()
  }
  if self.capture_trace {
    self.events.push({ instruction_pointer, step, stack_depth, instruction, })
  }
}

///|
priv struct BytecodeBuilder {
  constants : Array[Value]
  instructions : Array[BytecodeInstruction]
}

///|
fn BytecodeBuilder::new() -> BytecodeBuilder {
  { constants: [], instructions: [], }
}

///|
fn BytecodeBuilder::emit(
  self : BytecodeBuilder,
  op : BytecodeOp,
  span : SourceSpan,
) -> Int {
  let index = self.instructions.length()
  self.instructions.push({ op, span, })
  index
}

///|
fn BytecodeBuilder::constant(self : BytecodeBuilder, value : Value) -> Int {
  let index = self.constants.length()
  self.constants.push(value)
  index
}

///|
fn BytecodeBuilder::patch_jump(
  self : BytecodeBuilder,
  index : Int,
  target : Int,
) -> Unit {
  let instruction = self.instructions[index]
  let op = match instruction.op {
    Jump(_) => Jump(target)
    JumpIfFalse(_) => JumpIfFalse(target)
    JumpIfFalseKeep(_) => JumpIfFalseKeep(target)
    JumpIfTrueKeep(_) => JumpIfTrueKeep(target)
    other => other
  }
  self.instructions[index] = { op, span: instruction.span, }
}

///|
fn BytecodeBuilder::compile_expression(
  self : BytecodeBuilder,
  expression : Expr,
) -> Unit {
  match expression {
    Literal(value, span) =>
      ignore(self.emit(PushConstant(self.constant(value)), span))
    Variable(name, span) => ignore(self.emit(LoadName(name), span))
    Assign(name, value, span) => {
      self.compile_expression(value)
      ignore(self.emit(Duplicate, span))
      ignore(self.emit(StoreName(name), span))
    }
    Grouping(inner, _) => self.compile_expression(inner)
    Unary(operator, value, span) => {
      self.compile_expression(value)
      ignore(self.emit(UnaryOp(operator), span))
    }
    Binary(left, LogicalAnd, right, span) => {
      self.compile_expression(left)
      let skip = self.emit(JumpIfFalseKeep(-1), span)
      ignore(self.emit(Pop, span))
      self.compile_expression(right)
      self.patch_jump(skip, self.instructions.length())
    }
    Binary(left, LogicalOr, right, span) => {
      self.compile_expression(left)
      let skip = self.emit(JumpIfTrueKeep(-1), span)
      ignore(self.emit(Pop, span))
      self.compile_expression(right)
      self.patch_jump(skip, self.instructions.length())
    }
    Binary(left, operator, right, span) => {
      self.compile_expression(left)
      self.compile_expression(right)
      ignore(self.emit(BinaryOp(operator), span))
    }
    Conditional(condition, truthy, falsey, span) => {
      self.compile_expression(condition)
      let false_jump = self.emit(JumpIfFalse(-1), span)
      self.compile_expression(truthy)
      let end_jump = self.emit(Jump(-1), span)
      self.patch_jump(false_jump, self.instructions.length())
      self.compile_expression(falsey)
      self.patch_jump(end_jump, self.instructions.length())
    }
    ListLiteral(items, span) => {
      for item in items {
        self.compile_expression(item)
      }
      ignore(self.emit(BuildList(items.length()), span))
    }
    MapLiteral(entries, span) => {
      for entry in entries {
        self.compile_expression(entry.key)
        self.compile_expression(entry.value)
      }
      ignore(self.emit(BuildMap(entries.length()), span))
    }
    SubscriptGet(receiver, index, span) => {
      self.compile_expression(receiver)
      self.compile_expression(index)
      ignore(self.emit(GetSubscript, span))
    }
    SubscriptSet(receiver, index, value, span) => {
      self.compile_expression(receiver)
      self.compile_expression(index)
      self.compile_expression(value)
      ignore(self.emit(SetSubscript, span))
    }
    PropertyGet(receiver, name, span) => {
      self.compile_expression(receiver)
      ignore(self.emit(GetProperty(name), span))
    }
    PropertySet(receiver, name, value, span) => {
      self.compile_expression(receiver)
      self.compile_expression(value)
      ignore(self.emit(SetProperty(name), span))
    }
    MethodCall(receiver, name, arguments, span) => {
      self.compile_expression(receiver)
      for argument in arguments {
        self.compile_expression(argument)
      }
      ignore(self.emit(InvokeMethod(name, arguments.length()), span))
    }
    FieldGet(_, span)
    | FieldAssign(_, _, span)
    | StaticFieldGet(_, span)
    | StaticFieldAssign(_, _, span)
    | SuperConstructorCall(_, span)
    | FunctionLiteral(_, _, span)
    | FiberCallResume(_, _, _, span)
    | FiberSequenceResume(_, _, _, _, _, _, _, _, span) =>
      ignore(self.emit(EvaluateSemantic(expression), span))
  }
}

///|
fn BytecodeBuilder::compile_statement(
  self : BytecodeBuilder,
  statement : Stmt,
) -> Unit {
  match statement {
    ExpressionStatement(expression, span) => {
      self.compile_expression(expression)
      ignore(self.emit(SetLast, span))
    }
    VariableStatement(name, initializer, span) => {
      match initializer {
        Some(expression) => self.compile_expression(expression)
        None =>
          ignore(self.emit(PushConstant(self.constant(Value::null())), span))
      }
      ignore(self.emit(Duplicate, span))
      ignore(self.emit(DefineName(name), span))
      ignore(self.emit(SetLast, span))
    }
    BlockStatement(statements, span) => {
      ignore(self.emit(EnterScope, span))
      for child in statements {
        self.compile_statement(child)
      }
      ignore(self.emit(ExitScope, span))
    }
    IfStatement(condition, then_branch, else_branch, span) => {
      self.compile_expression(condition)
      let false_jump = self.emit(JumpIfFalse(-1), span)
      self.compile_statement(then_branch)
      let end_jump = self.emit(Jump(-1), span)
      self.patch_jump(false_jump, self.instructions.length())
      match else_branch {
        Some(branch) => self.compile_statement(branch)
        None => ()
      }
      self.patch_jump(end_jump, self.instructions.length())
    }
    WhileStatement(condition, body, span) => {
      let start = self.instructions.length()
      self.compile_expression(condition)
      let end_jump = self.emit(JumpIfFalse(-1), span)
      self.compile_statement(body)
      ignore(self.emit(Jump(start), span))
      self.patch_jump(end_jump, self.instructions.length())
    }
    ReturnStatement(value, span) => {
      match value {
        Some(expression) => self.compile_expression(expression)
        None =>
          ignore(self.emit(PushConstant(self.constant(Value::null())), span))
      }
      ignore(self.emit(ReturnValue, span))
    }
    ForStatement(_, _, _, span)
    | BreakStatement(span)
    | ContinueStatement(span)
    | ImportStatement(_, _, span)
    | ClassStatement(_, _, _, _, _, span) =>
      ignore(self.emit(ExecuteSemantic(statement), span))
  }
}

///|
/// Compile a named module through the complete hybrid bytecode backend.
pub fn compile_module_bytecode(
  source_name : String,
  source : String,
) -> BytecodeCompileResult {
  match parse_program(source) {
    ProgramParseFailed(issues) => BytecodeCompileFailed(issues)
    ProgramParsed(statements) => {
      let builder = BytecodeBuilder::new()
      for statement in statements {
        builder.compile_statement(statement)
      }
      ignore(
        builder.emit(
          ReturnLast,
          if statements.is_empty() {
            SourceSpan::empty()
          } else {
            statements[statements.length() - 1].span()
          },
        ),
      )
      BytecodeCompiled({
        source_name,
        constants: builder.constants,
        instructions: builder.instructions,
      })
    }
  }
}

///|
/// Compile one anonymous source through the complete hybrid bytecode backend.
pub fn compile_bytecode(source : String) -> BytecodeCompileResult {
  compile_module_bytecode("", source)
}

///|
fn bytecode_failure(
  code : DiagnosticCode,
  message : String,
  span : SourceSpan,
  steps : Int,
) -> BytecodeRunResult {
  BytecodeFailed(
    Diagnostic::new(Runtime, code, message, span).with_source(""),
    steps,
  )
}

///|
fn pop_bytecode_value(
  stack : Array[Value],
  span : SourceSpan,
  steps : Int,
) -> (Value?, BytecodeRunResult?) {
  match stack.pop() {
    Some(value) => (Some(value), None)
    None =>
      (
        None,
        Some(
          bytecode_failure(
            InvalidOperand,
            "bytecode stack underflow",
            span,
            steps,
          ),
        ),
      )
  }
}

///|
/// Execute compiled bytecode with a deterministic instruction budget.
fn execute_bytecode(
  program : BytecodeProgram,
  max_steps : Int,
  profiler : BytecodeProfiler?,
  loader : (String, String) -> ModuleLoadResult,
  foreign_methods : Array[ForeignMethodBinding],
  foreign_classes : Array[ForeignClassBinding],
) -> BytecodeRunResult {
  if max_steps <= 0 {
    return bytecode_failure(
      ExecutionLimit,
      "bytecode step limit must be positive",
      SourceSpan::empty(),
      0,
    )
  }
  let environment = Environment::new()
  bind_core_globals(environment)
  let context = RuntimeContext::new_with_loader(
    max_steps,
    program.source_name,
    loader,
    foreign_methods,
    foreign_classes,
  )
  context.begin_operation()
  context.current_source = program.source_name
  let stack : Array[Value] = []
  let mut last = Value::null()
  let mut ip = 0
  let mut steps = 0
  while ip >= 0 && ip < program.instructions.length() {
    let instruction = program.instructions[ip]
    match profiler {
      Some(value) =>
        value.observe(
          instruction.op,
          stack.length(),
          ip,
          steps + 1,
          instruction,
        )
      None => ()
    }
    steps = steps + 1
    if steps > max_steps {
      return bytecode_failure(
        ExecutionLimit,
        "bytecode execution step limit exceeded",
        instruction.span,
        steps,
      )
    }
    ip = ip + 1
    match instruction.op {
      PushConstant(index) =>
        if index < 0 || index >= program.constants.length() {
          return bytecode_failure(
            InvalidIndex,
            "invalid bytecode constant index",
            instruction.span,
            steps,
          )
        } else {
          stack.push(program.constants[index])
        }
      LoadName(name) =>
        match environment.get(name) {
          Some(value) => stack.push(value)
          None =>
            return bytecode_failure(
              UndefinedVariable,
              "undefined variable '\{name}'",
              instruction.span,
              steps,
            )
        }
      DefineName(name) => {
        let (value, failure) = pop_bytecode_value(
          stack,
          instruction.span,
          steps,
        )
        match failure {
          Some(result) => return result
          None => ()
        }
        if !environment.bind(name, value.unwrap()) {
          return bytecode_failure(
            DuplicateVariable,
            "variable '\{name}' is already declared in this scope",
            instruction.span,
            steps,
          )
        }
      }
      StoreName(name) => {
        let (value, failure) = pop_bytecode_value(
          stack,
          instruction.span,
          steps,
        )
        match failure {
          Some(result) => return result
          None => ()
        }
        if !environment.assign(name, value.unwrap()) {
          return bytecode_failure(
            UndefinedVariable,
            "undefined variable '\{name}'",
            instruction.span,
            steps,
          )
        }
      }
      Duplicate =>
        if stack.is_empty() {
          return bytecode_failure(
            InvalidOperand,
            "bytecode stack underflow",
            instruction.span,
            steps,
          )
        } else {
          stack.push(stack[stack.length() - 1])
        }
      Pop => ignore(stack.pop())
      EnterScope => environment.push_scope()
      ExitScope => ignore(environment.pop_scope())
      UnaryOp(operator) => {
        let (value, failure) = pop_bytecode_value(
          stack,
          instruction.span,
          steps,
        )
        match failure {
          Some(result) => return result
          None => ()
        }
        match
          evaluate_unary(operator, value.unwrap(), instruction.span, context) {
          EvaluationSucceeded(result) => stack.push(result)
          EvaluationFailed(issue) => return BytecodeFailed(issue, steps)
        }
      }
      BinaryOp(operator) => {
        let (right, right_failure) = pop_bytecode_value(
          stack,
          instruction.span,
          steps,
        )
        match right_failure {
          Some(result) => return result
          None => ()
        }
        let (left, left_failure) = pop_bytecode_value(
          stack,
          instruction.span,
          steps,
        )
        match left_failure {
          Some(result) => return result
          None => ()
        }
        match
          evaluate_binary_values(
            operator,
            left.unwrap(),
            right.unwrap(),
            instruction.span,
            context,
          ) {
          EvaluationSucceeded(result) => stack.push(result)
          EvaluationFailed(issue) => return BytecodeFailed(issue, steps)
        }
      }
      BuildList(count) => {
        if count < 0 || stack.length() < count {
          return bytecode_failure(
            InvalidOperand,
            "invalid bytecode list item count",
            instruction.span,
            steps,
          )
        }
        let reversed : Array[Value] = []
        for item_index = 0; item_index < count; item_index = item_index + 1 {
          reversed.push(stack.pop().unwrap())
        }
        let items : Array[Value] = []
        let mut index = reversed.length() - 1
        while index >= 0 {
          items.push(reversed[index])
          index = index - 1
        }
        stack.push(context.new_list(items))
      }
      BuildMap(count) => {
        if count < 0 || stack.length() < count * 2 {
          return bytecode_failure(
            InvalidOperand,
            "invalid bytecode map entry count",
            instruction.span,
            steps,
          )
        }
        let keys : Array[Value] = []
        let values : Array[Value] = []
        for entry_index = 0; entry_index < count; entry_index = entry_index + 1 {
          values.push(stack.pop().unwrap())
          keys.push(stack.pop().unwrap())
        }
        let map = context.new_map()
        let identifier = match map {
          MapValue(value) => value
          _ => -1
        }
        let mut index = count - 1
        while index >= 0 {
          if !is_valid_map_key(keys[index]) {
            return BytecodeFailed(
              Diagnostic::new(
                Runtime,
                InvalidMapKey,
                "Map keys must be immutable values, received \{keys[index].type_name()}",
                instruction.span,
              ).with_source(""),
              steps,
            )
          }
          ignore(context.set_map_entry(identifier, keys[index], values[index]))
          index = index - 1
        }
        stack.push(map)
      }
      GetSubscript => {
        if stack.length() < 2 {
          return bytecode_failure(
            InvalidOperand,
            "bytecode stack underflow while reading subscript",
            instruction.span,
            steps,
          )
        }
        let index = stack.pop().unwrap()
        let receiver = stack.pop().unwrap()
        match read_subscript(receiver, index, instruction.span, context) {
          EvaluationSucceeded(value) => stack.push(value)
          EvaluationFailed(issue) => return BytecodeFailed(issue, steps)
        }
      }
      SetSubscript => {
        if stack.length() < 3 {
          return bytecode_failure(
            InvalidOperand,
            "bytecode stack underflow while writing subscript",
            instruction.span,
            steps,
          )
        }
        let value = stack.pop().unwrap()
        let index = stack.pop().unwrap()
        let receiver = stack.pop().unwrap()
        match
          write_subscript(receiver, index, value, instruction.span, context) {
          EvaluationSucceeded(result) => stack.push(result)
          EvaluationFailed(issue) => return BytecodeFailed(issue, steps)
        }
      }
      GetProperty(name) => {
        if stack.is_empty() {
          return bytecode_failure(
            InvalidOperand,
            "bytecode stack underflow while reading property",
            instruction.span,
            steps,
          )
        }
        let receiver = stack.pop().unwrap()
        match read_property(receiver, name, instruction.span, context) {
          EvaluationSucceeded(value) => stack.push(value)
          EvaluationFailed(issue) => return BytecodeFailed(issue, steps)
        }
      }
      SetProperty(name) => {
        if stack.length() < 2 {
          return bytecode_failure(
            InvalidOperand,
            "bytecode stack underflow while writing property",
            instruction.span,
            steps,
          )
        }
        let value = stack.pop().unwrap()
        let receiver = stack.pop().unwrap()
        match
          invoke_method(
            receiver,
            name + "=",
            [value],
            instruction.span,
            context,
          ) {
          EvaluationSucceeded(result) => stack.push(result)
          EvaluationFailed(issue) => return BytecodeFailed(issue, steps)
        }
      }
      InvokeMethod(name, arity) => {
        if arity < 0 || stack.length() < arity + 1 {
          return bytecode_failure(
            InvalidOperand,
            "invalid bytecode method arity",
            instruction.span,
            steps,
          )
        }
        let reversed : Array[Value] = []
        for argument_index = 0
            argument_index < arity
            argument_index = argument_index + 1 {
          reversed.push(stack.pop().unwrap())
        }
        let arguments : Array[Value] = []
        let mut index = reversed.length() - 1
        while index >= 0 {
          arguments.push(reversed[index])
          index = index - 1
        }
        let receiver = stack.pop().unwrap()
        match
          invoke_method(receiver, name, arguments, instruction.span, context) {
          EvaluationSucceeded(value) => stack.push(value)
          EvaluationFailed(issue) => return BytecodeFailed(issue, steps)
        }
      }
      EvaluateSemantic(expression) =>
        match evaluate_expression_in(expression, environment, context) {
          EvaluationSucceeded(value) => stack.push(value)
          EvaluationFailed(issue) => return BytecodeFailed(issue, steps)
        }
      ExecuteSemantic(statement) =>
        match execute_statement_in(statement, environment, context) {
          StatementCompleted(value) => last = value
          StatementReturned(value, _) =>
            return BytecodeCompleted(value, steps + context.steps)
          StatementFailed(issue) =>
            return BytecodeFailed(issue, steps + context.steps)
          StatementBroken(span) =>
            return bytecode_failure(
              BreakOutsideLoop,
              "break cannot cross a bytecode semantic boundary",
              span,
              steps + context.steps,
            )
          StatementContinued(span) =>
            return bytecode_failure(
              ContinueOutsideLoop,
              "continue cannot cross a bytecode semantic boundary",
              span,
              steps + context.steps,
            )
        }
      Jump(target) => ip = target
      JumpIfFalse(target) => {
        let (value, failure) = pop_bytecode_value(
          stack,
          instruction.span,
          steps,
        )
        match failure {
          Some(result) => return result
          None => ()
        }
        if !value.unwrap().is_truthy() {
          ip = target
        }
      }
      JumpIfFalseKeep(target) =>
        if stack.is_empty() {
          return bytecode_failure(
            InvalidOperand,
            "bytecode stack underflow",
            instruction.span,
            steps,
          )
        } else if !stack[stack.length() - 1].is_truthy() {
          ip = target
        }
      JumpIfTrueKeep(target) =>
        if stack.is_empty() {
          return bytecode_failure(
            InvalidOperand,
            "bytecode stack underflow",
            instruction.span,
            steps,
          )
        } else if stack[stack.length() - 1].is_truthy() {
          ip = target
        }
      SetLast => {
        let (value, failure) = pop_bytecode_value(
          stack,
          instruction.span,
          steps,
        )
        match failure {
          Some(result) => return result
          None => last = value.unwrap()
        }
      }
      ReturnValue => {
        let (value, failure) = pop_bytecode_value(
          stack,
          instruction.span,
          steps,
        )
        match failure {
          Some(result) => return result
          None => return BytecodeCompleted(value.unwrap(), steps)
        }
      }
      ReturnLast => return BytecodeCompleted(last, steps)
    }
  }
  BytecodeCompleted(last, steps)
}

///|
/// Execute compiled bytecode with a deterministic instruction budget.
pub fn run_bytecode(
  program : BytecodeProgram,
  max_steps : Int,
) -> BytecodeRunResult {
  execute_bytecode(
    program,
    max_steps,
    None,
    fn(_, name) { ModuleLoadFailed("module '\{name}' is not available") },
    [],
    [],
  )
}

///|
/// Execute complete bytecode semantics with host-provided modules and foreign
/// bindings. Complex object operations currently use semantic escape
/// instructions while sharing the same environment, limits, and object heap.
pub fn run_bytecode_with_bindings(
  program : BytecodeProgram,
  max_steps : Int,
  loader : (String, String) -> ModuleLoadResult,
  foreign_methods : Array[ForeignMethodBinding],
  foreign_classes : Array[ForeignClassBinding],
) -> BytecodeRunResult {
  execute_bytecode(
    program,
    max_steps,
    None,
    loader,
    foreign_methods,
    foreign_classes,
  )
}

///|
pub fn run_bytecode_with_loader(
  program : BytecodeProgram,
  max_steps : Int,
  loader : (String, String) -> ModuleLoadResult,
) -> BytecodeRunResult {
  run_bytecode_with_bindings(program, max_steps, loader, [], [])
}

///|
/// Execute bytecode and return dynamic instruction, stack, branch and
/// variable-access counters for profiling and regression analysis.
pub fn profile_bytecode(
  program : BytecodeProgram,
  max_steps : Int,
) -> BytecodeProfile {
  let profiler = BytecodeProfiler::new(false)
  let result = execute_bytecode(
    program,
    max_steps,
    Some(profiler),
    fn(_, name) { ModuleLoadFailed("module '\{name}' is not available") },
    [],
    [],
  )
  let executed_instructions = match result {
    BytecodeCompleted(_, steps) | BytecodeFailed(_, steps) => steps
  }
  {
    result,
    executed_instructions,
    max_stack_depth: profiler.max_stack_depth,
    loads: profiler.loads,
    stores: profiler.stores,
    branches: profiler.branches,
    arithmetic_operations: profiler.arithmetic_operations,
    calls: profiler.calls,
    semantic_fallbacks: profiler.semantic_fallbacks,
  }
}

///|
/// Execute while capturing an instruction-by-instruction trace. Source-level
/// debugger UIs can filter events by `instruction.span.line()` for breakpoints.
pub fn trace_bytecode(
  program : BytecodeProgram,
  max_steps : Int,
) -> BytecodeTrace {
  let profiler = BytecodeProfiler::new(true)
  let result = execute_bytecode(
    program,
    max_steps,
    Some(profiler),
    fn(_, name) { ModuleLoadFailed("module '\{name}' is not available") },
    [],
    [],
  )
  { result, events: profiler.events, }
}

///|
/// Render bytecode with instruction offsets and source positions. This is the
/// stable inspection surface used by debuggers and teaching tools.
pub fn disassemble_bytecode(program : BytecodeProgram) -> Array[String] {
  let lines : Array[String] = []
  for index = 0; index < program.instructions.length(); index = index + 1 {
    let instruction = program.instructions[index]
    lines.push(
      "\{index}  \{instruction.span.line()}:\{instruction.span.column()}  " +
      "\{Repr(instruction.op)}",
    )
  }
  lines
}

///|
/// Compile and execute source through the optional bytecode backend.
pub fn run_compiled(source : String, max_steps : Int) -> BytecodeRunResult {
  match compile_bytecode(source) {
    BytecodeCompiled(program) => run_bytecode(program, max_steps)
    BytecodeCompileFailed(issues) => BytecodeFailed(issues[0], 0)
  }
}