// ops/mir_call.hako — NyVmOpMirCall (Core dispatcher)
// Phase-2 scope:
// - ArrayBox metadata (size) + basic methods (size/push/pop/get/set)
// - MapBox metadata (len) + methods len()/iterator()
// - Stable diagnostics for unsupported module functions / methods / closures
using selfhost.shared.json.core.json_cursor as JsonCursorBox
using selfhost.shared.common.string_helpers as StringHelpers
using selfhost.vm.core.state as NyVmState

static box NyVmOpMirCall {
  _fail(state, tag) {
    // Record a stable error tag so NyVmDispatcher can surface it as return value
    if state != null { state.set("last_error_tag", tag) }
    print(tag)
    return -1
  }

  _arr_key(recv_id) {
    using selfhost.shared.common.string_helpers as StringHelpers
    return "arrsize:r" + StringHelpers.int_to_str(recv_id)
  }
  _arr_val_key(recv_id, idx) {
    using selfhost.shared.common.string_helpers as StringHelpers
    return "arrval:r" + StringHelpers.int_to_str(recv_id) + ":" + StringHelpers.int_to_str(idx)
  }
  _map_key(recv_id) {
    using selfhost.shared.common.string_helpers as StringHelpers
    return "maplen:r" + StringHelpers.int_to_str(recv_id)
  }
  _map_entry_slot(recv_id, key) {
    using selfhost.shared.common.string_helpers as StringHelpers
    return "mapentry:r" + StringHelpers.int_to_str(recv_id) + ":" + key
  }

  _extract_mir_call_obj(inst_json) {
    local key = "\"mir_call\":"
    local p = inst_json.indexOf(key)
    if p < 0 { return "" }
    p = p + key.length()
    local end = JsonCursorBox.seek_obj_end(inst_json, p)
    if end < 0 { return "" }
    return inst_json.substring(p, end+1)
  }

  _read_str(s, key) {
    local p = JsonCursorBox.find_key_dual(s, "\""+key+"\":", r#"\""+key+"\":"#, 0)
    if p < 0 { return "" }
    p = s.indexOf(":", p)
    if p < 0 { return "" }
    p = p + 1
    loop(true) {
      local ch = s.substring(p,p+1)
      // Parser workaround: avoid increment inside multi-way OR
      local is_ws = 0
      if ch == " " { is_ws = 1 }
      if ch == "\n" { is_ws = 1 }
      if ch == "\r" { is_ws = 1 }
      if ch == "\t" { is_ws = 1 }
      if is_ws == 1 { p = p + 1 continue }
      break
    }
    if s.substring(p,p+1) != "\"" { return "" }
    local end = JsonCursorBox.scan_string_end(s, p)
    if end < 0 { return "" }
    return s.substring(p+1, end)
  }

  _read_vid_field(json, key, missing_tag, bad_tag) {
    local pos = JsonCursorBox.find_key_dual(json, "\""+key+"\":", r#"\""+key+"\":"#, 0)
    if pos < 0 { print(missing_tag) return null }
    local colon = json.indexOf(":", pos)
    if colon < 0 { print(bad_tag) return null }
    local digits = JsonCursorBox.digits_from(json, colon+1)
    if digits == "" { print(bad_tag) return null }
    return StringHelpers.to_i64(digits)
  }

  _read_optional_vid_field(json, key) {
    local pos = JsonCursorBox.find_key_dual(json, "\""+key+"\":", r#"\""+key+"\":"#, 0)
    if pos < 0 { return null }
    local colon = json.indexOf(":", pos)
    if colon < 0 { return null }
    local digits = JsonCursorBox.digits_from(json, colon+1)
    if digits == "" { return null }
    return StringHelpers.to_i64(digits)
  }

  // Read mir_call.flags.optionality: "default" | "optional" | "bang"
  _read_optionality(mir_call_json) {
    local fk = JsonCursorBox.find_key_dual(mir_call_json, "\"flags\":", r#"\"flags\":"#, 0)
    if fk < 0 { return "default" }
    local colon = mir_call_json.indexOf(":", fk)
    if colon < 0 { return "default" }
    local idx = colon + 1
    loop(true) {
      if idx >= mir_call_json.length() { return "default" }
      local ch = mir_call_json.substring(idx, idx+1)
      local is_ws = 0
      if ch == " " { is_ws = 1 }
      if ch == "\n" { is_ws = 1 }
      if ch == "\r" { is_ws = 1 }
      if ch == "\t" { is_ws = 1 }
      if is_ws == 1 { idx = idx + 1 continue }
      break
    }
    if idx >= mir_call_json.length() { return "default" }
    if mir_call_json.substring(idx, idx+1) != "{" { return "default" }
    local rb = JsonCursorBox.seek_obj_end(mir_call_json, idx)
    if rb < 0 { return "default" }
    local flags = mir_call_json.substring(idx, rb+1)
    local opt = me._read_str(flags, "optionality")
    if opt == "optional" { return "optional" }
    if opt == "bang" { return "bang" }
    if mir_call_json.indexOf("\"optionality\":\"bang\"") >= 0 { return "bang" }
    if mir_call_json.indexOf("\"optionality\":\"optional\"") >= 0 { return "optional" }
    return "default"
  }

  _read_dst(inst_json, context) {
    local missing = "[core/mir_call] " + context + " missing dst"
    local bad = "[core/mir_call] " + context + " bad dst"
    return me._read_vid_field(inst_json, "dst", missing, bad)
  }

  _read_receiver(callee, context) {
    local missing = "[core/mir_call] " + context + " missing receiver"
    local bad = "[core/mir_call] " + context + " bad receiver"
    return me._read_vid_field(callee, "receiver", missing, bad)
  }

  _scan_next_arg(m, i_ref) {
    loop(true) {
      local idx = i_ref.get(0)
      local ch = m.substring(idx, idx+1)
      // Parser workaround: avoid array-element assignment inside multi-way OR
      local is_whitespace = 0
      if ch == " " { is_whitespace = 1 }
      if ch == "\n" { is_whitespace = 1 }
      if ch == "\r" { is_whitespace = 1 }
      if ch == "\t" { is_whitespace = 1 }
      if is_whitespace == 1 { i_ref.set(0, idx + 1) continue }
      break
    }
    local idx = i_ref.get(0)
    if m.substring(idx, idx+1) == "]" { return "" }
    local digits = JsonCursorBox.digits_from(m, idx)
    if digits == "" { return "" }
    i_ref.set(0, idx + digits.length())
    loop(true) {
      local idx2 = i_ref.get(0)
      if idx2 >= m.length() { break }
      local ch2 = m.substring(idx2, idx2+1)
      // Apply workaround here too
      local is_ws = 0
      if ch2 == " " { is_ws = 1 }
      if ch2 == "\n" { is_ws = 1 }
      if ch2 == "\r" { is_ws = 1 }
      if ch2 == "\t" { is_ws = 1 }
      if is_ws == 1 { i_ref.set(0, idx2 + 1) continue }
      break
    }
    local idx3 = i_ref.get(0)
    if idx3 < m.length() && m.substring(idx3, idx3+1) == "," { i_ref.set(0, idx3 + 1) }
    return digits
  }

  _read_arg_vid(m, index, missing_tag, bad_tag) {
    local args_pos = JsonCursorBox.find_key_dual(m, "\"args\":[", r#"\"args\":\["#, 0)
    if args_pos < 0 { print(missing_tag) return null }
    local start = m.indexOf("[", args_pos)
    if start < 0 { print(bad_tag) return null }
    local i = new ArrayBox()
    i.push(start + 1)
    local current = 0
    loop(true) {
      local idx = i.get(0)
      if idx >= m.length() { break }
      loop(true) {
        local idx2 = i.get(0)
        if idx2 >= m.length() { break }
        local ch = m.substring(idx2, idx2+1)
        // Parser workaround: avoid array-element assignment inside multi-way OR
        local is_ws = 0
        if ch == " " { is_ws = 1 }
        if ch == "\n" { is_ws = 1 }
        if ch == "\r" { is_ws = 1 }
        if ch == "\t" { is_ws = 1 }
        if is_ws == 1 { i.set(0, idx2 + 1) continue }
        break
      }
      local idx3 = i.get(0)
      if idx3 >= m.length() { break }
      if m.substring(idx3, idx3+1) == "]" { break }
      local digits = me._scan_next_arg(m, i)
      if digits == "" { print(bad_tag) return null }
      if current == index { return StringHelpers.to_i64(digits) }
      current = current + 1
    }
    print(missing_tag)
    return null
  }

  _read_arg_vid_optional(m, index) {
    local args_pos = JsonCursorBox.find_key_dual(m, "\"args\":[", r#"\"args\":\["#, 0)
    if args_pos < 0 { return null }
    local start = m.indexOf("[", args_pos)
    if start < 0 { return null }
    local i = new ArrayBox()
    i.push(start + 1)
    local current = 0
    loop(true) {
      local idx = i.get(0)
      if idx >= m.length() { break }
      if m.substring(idx, idx+1) == "]" { break }
      local digits = me._scan_next_arg(m, i)
      if digits == "" { return null }
      if current == index { return StringHelpers.to_i64(digits) }
      current = current + 1
    }
    return null
  }

  _ensure_map_meta(state, recv_id) {
    local mem = state.get("mem")
    local len_key = me._map_key(recv_id)
    if mem.get(len_key) == null { mem.set(len_key, 0) }
  }

  _map_len_get(state, recv_id) {
    local mem = state.get("mem")
    local len = mem.get(me._map_key(recv_id))
    if len == null { return 0 }
    return len
  }

  _map_len_set(state, recv_id, len) {
    local mem = state.get("mem")
    mem.set(me._map_key(recv_id), len)
  }

  _handle_console(callee, m, state) {
    local name = me._read_str(callee, "name")
    if name == "" { return me._fail(state, "[core/mir_call] console missing name") }
    if !(name == "print" || name == "env.console.log" || name == "nyash.console.log" || name == "env.console.warn" || name == "nyash.console.warn" || name == "env.console.error" || name == "nyash.console.error") {
      return me._fail(state, "[core/mir_call] unsupported global: " + name)
    }
    local arg_vid = me._read_arg_vid(m, 0, "[core/mir_call] console missing arg", "[core/mir_call] console bad arg")
    if arg_vid == null { return -1 }
    local val = NyVmState.get_reg(state, arg_vid)
    using selfhost.vm.hakorune-vm.str_cast as StrCast
    print(StrCast.to_str(val))
    return 0
  }

  _handle_constructor(inst_json, callee, state) {
    local dst = me._read_dst(inst_json, "constructor")
    if dst == null { return -1 }
    local box_type = me._read_str(callee, "box_type")
    if box_type == "ArrayBox" {
      local mem = state.get("mem")
      mem.set(me._arr_key(dst), 0)
      NyVmState.set_reg(state, dst, dst)
      return 0
    }
    if box_type == "MapBox" {
      me._ensure_map_meta(state, dst)
      NyVmState.set_reg(state, dst, dst)
      return 0
    }
    if box_type == "StringBox" {
      return me._fail(state, "[core/mir_call/constructor_unsupported]")
    }
    return me._fail(state, "[core/mir_call/constructor_unsupported]")
  }

  _array_metadata_or_fail(state, recv_id, context) {
    local key = me._arr_key(recv_id)
    local mem = state.get("mem")
    local sz = mem.get(key)
    if sz == null { print("[core/mir_call] " + context + " missing array metadata") return null }
    // Return ArrayBox instead of array literal
    local result = new ArrayBox()
    result.push(mem)
    result.push(key)
    result.push(sz)
    return result
  }

  _handle_array_method(method, inst_json, m, state, recv_id) {
    if method == "size" {
      local meta = me._array_metadata_or_fail(state, recv_id, "method(size)")
      if meta == null { return -1 }
      local dst = me._read_dst(inst_json, "method(size)")
      if dst == null { return -1 }
      NyVmState.set_reg(state, dst, meta.get(2))
      return 0
    }
    if method == "push" {
      local meta = me._array_metadata_or_fail(state, recv_id, "method(push)")
      if meta == null { return -1 }
      local mem = meta.get(0)
      local sz = meta.get(2)
      local val_vid = me._read_arg_vid_optional(m, 0)
      local value = 0
      if val_vid != null { value = NyVmState.get_reg(state, val_vid) }
      mem.set(me._arr_val_key(recv_id, sz), value)
      sz = sz + 1
      mem.set(meta.get(1), sz)
      local dst_opt = me._read_optional_vid_field(inst_json, "dst")
      if dst_opt != null { NyVmState.set_reg(state, dst_opt, sz) }
      return 0
    }
    if method == "pop" {
      local meta = me._array_metadata_or_fail(state, recv_id, "method(pop)")
      if meta == null { return -1 }
      local mem = meta.get(0)
      local sz = meta.get(2)
      if sz <= 0 {
        // Empty → return null (no-op)
        local dst_empty = me._read_optional_vid_field(inst_json, "dst")
        if dst_empty != null { NyVmState.set_reg(state, dst_empty, null) }
        return 0
      }
      sz = sz - 1
      local val = mem.get(me._arr_val_key(recv_id, sz))
      mem.set(me._arr_val_key(recv_id, sz), null)
      mem.set(meta.get(1), sz)
      local dst_opt = me._read_optional_vid_field(inst_json, "dst")
      if dst_opt != null { NyVmState.set_reg(state, dst_opt, val) }
      return 0
    }
    if method == "clear" {
      // clear(): reset size metadata to 0; values are logically cleared (TTL: metadata-only)
      local meta = me._array_metadata_or_fail(state, recv_id, "method(clear)")
      if meta == null { return -1 }
      local mem = meta.get(0)
      mem.set(meta.get(1), 0)
      local dst_opt = me._read_optional_vid_field(inst_json, "dst")
      if dst_opt != null { NyVmState.set_reg(state, dst_opt, void) }
      return 0
    }
    if method == "get" {
      local idx_vid = me._read_arg_vid(m, 0, "[core/mir_call] method(get) missing index", "[core/mir_call] method(get) bad index")
      if idx_vid == null { return -1 }
      local meta = me._array_metadata_or_fail(state, recv_id, "method(get)")
      if meta == null { return -1 }
      local mem = meta.get(0)
      local idx = NyVmState.get_reg(state, idx_vid)
      if idx < 0 || idx >= meta.get(2) {
        // OOB → null
        local dst_oob = me._read_dst(inst_json, "method(get)")
        if dst_oob == null { return -1 }
        NyVmState.set_reg(state, dst_oob, null)
        return 0
      }
      local val = mem.get(me._arr_val_key(recv_id, idx))
      local dst = me._read_dst(inst_json, "method(get)")
      if dst == null { return -1 }
      NyVmState.set_reg(state, dst, val)
      return 0
    }
    if method == "set" {
      local meta = me._array_metadata_or_fail(state, recv_id, "method(set)")
      if meta == null { return -1 }
      local mem = meta.get(0)
      local idx_vid = me._read_arg_vid(m, 0, "[core/mir_call] method(set) missing index", "[core/mir_call] method(set) bad index")
      if idx_vid == null { return -1 }
      local val_vid = me._read_arg_vid(m, 1, "[core/mir_call] method(set) missing value", "[core/mir_call] method(set) bad value")
      if val_vid == null { return -1 }
      local idx = NyVmState.get_reg(state, idx_vid)
      if idx < 0 { return me._fail(state, "[core/array/oob_set]") }
      local value = NyVmState.get_reg(state, val_vid)
      // i > len → OOB fail; i == len → append; 0<=i<len → replace
      if idx > meta.get(2) { return me._fail(state, "[core/array/oob_set]") }
      if idx == meta.get(2) {
        // append
        mem.set(me._arr_val_key(recv_id, idx), value)
        mem.set(meta.get(1), idx + 1)
      } else {
        mem.set(me._arr_val_key(recv_id, idx), value)
      }
      // Return void
      local dst_opt = me._read_optional_vid_field(inst_json, "dst")
      if dst_opt != null { NyVmState.set_reg(state, dst_opt, void) }
      return 0
    }
    return me._fail(state, "[core/mir_call/method_unsupported]")
  }

  _handle_map_method(method, inst_json, m, state, recv_id) {
    me._ensure_map_meta(state, recv_id)
    local len = me._map_len_get(state, recv_id)
    local mem = state.get("mem")
    if method == "size" || method == "len" {
      local dst = me._read_dst(inst_json, "map " + method)
      if dst == null { return -1 }
      NyVmState.set_reg(state, dst, len)
      return 0
    }
    if method == "has" {
      local dst = me._read_dst(inst_json, "map has")
      if dst == null { return -1 }
      local key_vid = me._read_arg_vid(m, 0, "[core/mir_call] map has missing key", "[core/mir_call] map has bad key")
      if key_vid == null { return -1 }
      local key_val = NyVmState.get_reg(state, key_vid)
      using selfhost.vm.hakorune-vm.str_cast as StrCast
      local key = StrCast.to_str(key_val)
      local slot = me._map_entry_slot(recv_id, key)
      local has_key = mem.get(slot) != null
      local result = 0
      if has_key { result = 1 }
      NyVmState.set_reg(state, dst, result)
      return 0
    }
    if method == "keys" {
      local dst = me._read_dst(inst_json, "map keys")
      if dst == null { return -1 }
      // Build keys by scanning mem for receiver-specific entry slots (non-null values only)
      local keys_arr = new ArrayBox()
      local prefix = me._map_entry_slot(recv_id, "")
      // Iterate memory keys (MapBox.keys())
      local all_keys = mem.keys()
      local i = 0
      local n = all_keys.length()
      loop(i < n) {
        using selfhost.vm.hakorune-vm.str_cast as StrCast
        local k = StrCast.to_str(all_keys.get(i))
        // startsWith(prefix)
        local ok = 0
        if k.length() >= prefix.length() {
          if k.substring(0, prefix.length()) == prefix { ok = 1 }
        }
        if ok == 1 {
          local v = mem.get(k)
          if v != null {
            local tail = k.substring(prefix.length(), k.length())
            keys_arr.push(tail)
          }
        }
        i = i + 1
      }
      NyVmState.set_reg(state, dst, keys_arr)
      return 0
    }
    if method == "values" {
      local dst = me._read_dst(inst_json, "map values")
      if dst == null { return -1 }
      // Build values by scanning mem for receiver-specific entry slots
      local values_arr = new ArrayBox()
      local prefix = me._map_entry_slot(recv_id, "")
      local all_keys = mem.keys()
      local i = 0
      local n = all_keys.length()
      loop(i < n) {
        using selfhost.vm.hakorune-vm.str_cast as StrCast
        local k = StrCast.to_str(all_keys.get(i))
        local ok = 0
        if k.length() >= prefix.length() {
          if k.substring(0, prefix.length()) == prefix { ok = 1 }
        }
        if ok == 1 {
          local v = mem.get(k)
          if v != null { values_arr.push(v) }
        }
        i = i + 1
      }
      NyVmState.set_reg(state, dst, values_arr)
      return 0
    }
    if method == "iterator" {
      local dst = me._read_dst(inst_json, "map iterator")
      if dst == null { return -1 }
      NyVmState.set_reg(state, dst, len)
      return 0
    }
    if method == "delete" {
      // delete(key): if present, remove and decrement len; dst (optional) := receiver
      local key_vid = me._read_arg_vid(m, 0, "[core/mir_call] map delete missing key", "[core/mir_call] map delete bad key")
      if key_vid == null { return -1 }
      local key_val = NyVmState.get_reg(state, key_vid)
      using selfhost.vm.hakorune-vm.str_cast as StrCast
      local key = StrCast.to_str(key_val)
      local slot = me._map_entry_slot(recv_id, key)
      local deleted = mem.get(slot)
      if deleted != null {
        mem.set(slot, null)
        len = (len - 1)
        if len < 0 { len = 0 }
        me._map_len_set(state, recv_id, len)
      }
      local dst_opt = me._read_optional_vid_field(inst_json, "dst")
      if dst_opt != null { NyVmState.set_reg(state, dst_opt, deleted) }
      return 0
    }
    if method == "clear" {
      // clear(): set length to 0 and remove all entries for this map from mem
      local prefix = me._map_entry_slot(recv_id, "")
      local all_keys = mem.keys()
      local i = 0
      local n = all_keys.length()
      loop(i < n) {
        using selfhost.vm.hakorune-vm.str_cast as StrCast
        local k = StrCast.to_str(all_keys.get(i))
        local ok = 0
        if k.length() >= prefix.length() {
          if k.substring(0, prefix.length()) == prefix { ok = 1 }
        }
        if ok == 1 { mem.set(k, null) }
        i = i + 1
      }
      me._map_len_set(state, recv_id, 0)
      local dst_opt2 = me._read_optional_vid_field(inst_json, "dst")
      if dst_opt2 != null { NyVmState.set_reg(state, dst_opt2, void) }
      return 0
    }
    if method == "set" {
      local key_vid = me._read_arg_vid(m, 0, "[core/mir_call] map set missing key", "[core/mir_call] map set bad key")
      if key_vid == null { return -1 }
      local val_vid = me._read_arg_vid(m, 1, "[core/mir_call] map set missing value", "[core/mir_call] map set bad value")
      if val_vid == null { return -1 }
      local key_val = NyVmState.get_reg(state, key_vid)
      // Validate key: null/void are invalid（暗黙変換なし）
      if key_val == null || key_val == void { return me._fail(state, "[core/map/key_type]") }
      using selfhost.vm.hakorune-vm.str_cast as StrCast
      local key = StrCast.to_str(key_val)
      local slot = me._map_entry_slot(recv_id, key)
      local had = mem.get(slot) != null
      // Validate value: void は不正。null は許可（値として保存）。
      local v = NyVmState.get_reg(state, val_vid)
      if v == void { return me._fail(state, "[core/map/value_type]") }
      mem.set(slot, v)
      if !had {
        len = len + 1
        me._map_len_set(state, recv_id, len)
      }
      local dst_opt = me._read_optional_vid_field(inst_json, "dst")
      if dst_opt != null { NyVmState.set_reg(state, dst_opt, void) }
      return 0
    }
    if method == "get" {
      local key_vid = me._read_arg_vid(m, 0, "[core/mir_call] map get missing key", "[core/mir_call] map get bad key")
      if key_vid == null { return -1 }
      local key_val = NyVmState.get_reg(state, key_vid)
      using selfhost.vm.hakorune-vm.str_cast as StrCast
      local key = StrCast.to_str(key_val)
      local slot = me._map_entry_slot(recv_id, key)
      local value = mem.get(slot)
      local opt = me._read_optionality(m)
      if value == null {
        // default: missing → null
        local dst0 = me._read_dst(inst_json, "map get")
        if dst0 == null { return -1 }
        NyVmState.set_reg(state, dst0, null)
        return 0
      }
      local dst = me._read_dst(inst_json, "map get")
      if dst == null { return -1 }
      if opt == "optional" {
        local outm2 = new MapBox()
        outm2.set("present", 1)
        outm2.set("value", value)
        NyVmState.set_reg(state, dst, outm2)
      } else {
        NyVmState.set_reg(state, dst, value)
      }
      return 0
    }
    return me._fail(state, "[core/mir_call/method_unsupported]")
  }

  _normalize_method(method) {
    // Alias normalization: size|length|len -> size
    if method == "length" || method == "len" { return "size" }
    // substring|substr -> substring
    if method == "substr" { return "substring" }
    // charAt|at -> charAt
    if method == "at" { return "charAt" }
    return method
  }

  _handle_method(inst_json, callee, m, state) {
    local raw_method = me._read_str(callee, "method")
    if raw_method == "" { return me._fail(state, "[core/mir_call] method missing name") }
    local method = me._normalize_method(raw_method)
    local recv_id = me._read_receiver(callee, "method")
    if recv_id == null { return -1 }
    local mem = state.get("mem")
    local arr_key = me._arr_key(recv_id)
    local map_key = me._map_key(recv_id)
    local has_arr = mem.get(arr_key) != null
    local has_map = mem.get(map_key) != null
    // Map-specific delegation (support size/len/iterator/set/has/keys/values/delete/get/clear)
    if has_map && !has_arr && (method == "size" || method == "len" || method == "iterator" || method == "set" || method == "has" || method == "keys" || method == "values" || method == "delete" || method == "get" || method == "clear") {
      return me._handle_map_method(method, inst_json, m, state, recv_id)
    }
    if method == "size" || method == "len" || method == "iterator" || method == "has" || method == "keys" || method == "values" {
      // Map-specific methods without metadata should still succeed
      if !has_arr && !has_map {
        return me._handle_map_method(method, inst_json, m, state, recv_id)
      }
    }
    // String.size/0 — when receiver is not an Array/Map pseudo-id, treat receiver value as String
    if method == "size" && !has_arr && !has_map {
      local dst = me._read_dst(inst_json, "method(size)")
      if dst == null { return -1 }
      local recv_val = NyVmState.get_reg(state, recv_id)
      using selfhost.vm.hakorune-vm.str_cast as StrCast
      local s = StrCast.to_str(recv_val)
      NyVmState.set_reg(state, dst, s.length())
      return 0
    }
    // String.indexOf/1 — return first index or -1
    if method == "indexOf" && !has_arr && !has_map {
      local dst = me._read_dst(inst_json, "method(indexOf)")
      if dst == null { return -1 }
      local idx_vid = me._read_arg_vid(m, 0, "[core/mir_call] method(indexOf) missing needle", "[core/mir_call] method(indexOf) bad needle")
      if idx_vid == null { return -1 }
      local recv_val = NyVmState.get_reg(state, recv_id)
      using selfhost.vm.hakorune-vm.str_cast as StrCast
      local s = StrCast.to_str(recv_val)
      local needle_val = NyVmState.get_reg(state, idx_vid)
      using selfhost.vm.hakorune-vm.str_cast as StrCast
      local needle = StrCast.to_str(needle_val)
      local pos = s.indexOf(needle)
      if pos >= 0 {
        NyVmState.set_reg(state, dst, pos)
      } else {
        local opt = me._read_optionality(m)
        if opt == "optional" {
          NyVmState.set_reg(state, dst, null)
        } else if opt == "bang" {
          // Record stable error tag for dispatcher to surface as return value
          state.set("last_error_tag", "[core/mir_call] string indexOf not found")
          return -1
        } else {
          NyVmState.set_reg(state, dst, -1)
        }
      }
      return 0
    }
    // String.lastIndexOf/1 — return last index or -1
    if method == "lastIndexOf" && !has_arr && !has_map {
      local dst = me._read_dst(inst_json, "method(lastIndexOf)")
      if dst == null { return -1 }
      local idx_vid = me._read_arg_vid(m, 0, "[core/mir_call] method(lastIndexOf) missing needle", "[core/mir_call] method(lastIndexOf) bad needle")
      if idx_vid == null { return -1 }
      local recv_val = NyVmState.get_reg(state, recv_id)
      using selfhost.vm.hakorune-vm.str_cast as StrCast
      local s = StrCast.to_str(recv_val)
      local needle_val = NyVmState.get_reg(state, idx_vid)
      using selfhost.vm.hakorune-vm.str_cast as StrCast
      local needle = StrCast.to_str(needle_val)
      local pos = s.lastIndexOf(needle)
      NyVmState.set_reg(state, dst, pos)
      return 0
    }
    // String.substring/2 — return substring [start,end) with bounds check
    if method == "substring" && !has_arr && !has_map {
      local dst = me._read_dst(inst_json, "method(substring)")
      if dst == null { return -1 }
      local start_vid = me._read_arg_vid(m, 0, "[core/mir_call] method(substring) missing start", "[core/mir_call] method(substring) bad start")
      if start_vid == null { return -1 }
      local end_vid = me._read_arg_vid(m, 1, "[core/mir_call] method(substring) missing end", "[core/mir_call] method(substring) bad end")
      if end_vid == null { return -1 }
      local recv_val = NyVmState.get_reg(state, recv_id)
      using selfhost.vm.hakorune-vm.str_cast as StrCast
      local s = StrCast.to_str(recv_val)
      local start = NyVmState.get_reg(state, start_vid)
      local end = NyVmState.get_reg(state, end_vid)
      // Bounds check
      if start < 0 || end < 0 || start > s.length() || end > s.length() || start > end {
        return me._fail(state, "[core/string/bounds]")
      }
      local out = s.substring(start, end)
      NyVmState.set_reg(state, dst, out)
      return 0
    }
    // String.charAt/1 — return character at index with bounds check
    if method == "charAt" && !has_arr && !has_map {
      local dst = me._read_dst(inst_json, "method(charAt)")
      if dst == null { return -1 }
      local idx_vid = me._read_arg_vid(m, 0, "[core/mir_call] method(charAt) missing index", "[core/mir_call] method(charAt) bad index")
      if idx_vid == null { return -1 }
      local recv_val = NyVmState.get_reg(state, recv_id)
      using selfhost.vm.hakorune-vm.str_cast as StrCast
      local s = StrCast.to_str(recv_val)
      local idx = NyVmState.get_reg(state, idx_vid)
      // Bounds check
      if idx < 0 || idx >= s.length() {
        return me._fail(state, "[core/string/bounds]")
      }
      local ch = s.substring(idx, idx+1)
      NyVmState.set_reg(state, dst, ch)
      return 0
    }
    // String.replace/2 — replace first occurrence of pattern with replacement
    if method == "replace" && !has_arr && !has_map {
      local dst = me._read_dst(inst_json, "method(replace)")
      if dst == null { return -1 }
      local pattern_vid = me._read_arg_vid(m, 0, "[core/mir_call] method(replace) missing pattern", "[core/mir_call] method(replace) bad pattern")
      if pattern_vid == null { return -1 }
      local replacement_vid = me._read_arg_vid(m, 1, "[core/mir_call] method(replace) missing replacement", "[core/mir_call] method(replace) bad replacement")
      if replacement_vid == null { return -1 }
      local recv_val = NyVmState.get_reg(state, recv_id)
      using selfhost.vm.hakorune-vm.str_cast as StrCast
      local s = StrCast.to_str(recv_val)
      local pattern_val = NyVmState.get_reg(state, pattern_vid)
      using selfhost.vm.hakorune-vm.str_cast as StrCast
      local pattern = StrCast.to_str(pattern_val)
      local replacement_val = NyVmState.get_reg(state, replacement_vid)
      using selfhost.vm.hakorune-vm.str_cast as StrCast
      local replacement = StrCast.to_str(replacement_val)
      // Simple replace: find first occurrence and replace
      local pos = s.indexOf(pattern)
      local result = s
      if pos >= 0 {
        local before = s.substring(0, pos)
        local after = s.substring(pos + pattern.length(), s.length())
        result = before + replacement + after
      }
      NyVmState.set_reg(state, dst, result)
      return 0
    }
    return me._handle_array_method(method, inst_json, m, state, recv_id)
  }

  _handle_modulefn(inst_json, callee, m, state) {
    local name = me._read_str(callee, "name")
    if name == "" { return me._fail(state, "[core/mir_call] modulefn missing name") }
    // Accept legacy names without "/arity" suffix for a few supported helpers
    if name == "StringHelpers.int_to_str" { name = "StringHelpers.int_to_str/1" }
    if name == "StringHelpers.to_i64" { name = "StringHelpers.to_i64/1" }
    if name == "ArrayBox.len/0" {
      local arg_vid = me._read_arg_vid(m, 0, "[core/mir_call] modulefn len/0 missing arg", "[core/mir_call] modulefn len/0 bad arg")
      if arg_vid == null { return -1 }
      local mem = state.get("mem")
      local sz = mem.get(me._arr_key(arg_vid))
      if sz == null { sz = 0 }
      local dst = me._read_dst(inst_json, "modulefn len/0")
      if dst == null { return -1 }
      NyVmState.set_reg(state, dst, sz)
      return 0
    }
    if name == "ArrayBox.clear/0" {
      // Alias support: treat as receiver.method("clear")
      local arg_vid = me._read_first_arg(m)
      if arg_vid == null { return me._fail(state, "[core/mir_call] array clear missing arg") }
      // Reset metadata size to 0 (values are logically cleared; TTL: metadata-only)
      local mem = state.get("mem")
      mem.set(me._arr_key(arg_vid), 0)
      // Optional dst := void
      local dst_opt = me._read_optional_vid_field(inst_json, "dst")
      if dst_opt != null { NyVmState.set_reg(state, dst_opt, void) }
      return 0
    }
    if name == "MapBox.len/0" {
      local arg_vid = me._read_first_arg(m)
      if arg_vid == null { return me._fail(state, "[core/mir_call] map len missing arg") }
      me._ensure_map_meta(state, arg_vid)
      local dst = me._read_dst(inst_json, "modulefn MapBox.len/0")
      if dst == null { return -1 }
      NyVmState.set_reg(state, dst, me._map_len_get(state, arg_vid))
      return 0
    }
    if name == "StringHelpers.int_to_str/1" {
      // One-arg function: stringify the argument value
      local arg_vid = me._read_first_arg(m)
      if arg_vid == null { return me._fail(state, "[core/mir_call] int_to_str missing arg") }
      local v = NyVmState.get_reg(state, arg_vid)
      using selfhost.vm.hakorune-vm.str_cast as StrCast
      local s = StrCast.to_str(v)
      local dst = me._read_dst(inst_json, "modulefn StringHelpers.int_to_str/1")
      if dst == null { return -1 }
      NyVmState.set_reg(state, dst, s)
      return 0
    }
    if name == "StringHelpers.to_i64/1" {
      local arg_vid = me._read_first_arg(m)
      if arg_vid == null { return me._fail(state, "[core/mir_call] string to_i64 missing arg") }
      local v = NyVmState.get_reg(state, arg_vid)
      // Accept already-integer values; else convert numeric strings only
      local outv = 0
      using selfhost.vm.hakorune-vm.str_cast as StrCast
      if StrCast.to_str(v) == StrCast.to_str(StringHelpers.to_i64(v)) {
        // naive guard: to_i64 roundtrip textual equality — accept v
        outv = StringHelpers.to_i64(v)
      } else {
        // Fallback strict check: only digit strings with optional sign
        using selfhost.vm.hakorune-vm.str_cast as StrCast
        local s = StrCast.to_str(v)
        local i = 0
        if s.length() > 0 && (s.substring(0,1) == "-" || s.substring(0,1) == "+") { i = 1 }
        local ok = (s.length() > i)
        loop(i < s.length()) {
          local ch = s.substring(i,i+1)
          if !(ch >= "0" && ch <= "9") { ok = false break }
          i = i + 1
        }
        if !ok { return me._fail(state, "[core/mir_call] string to_i64 bad arg") }
        outv = StringHelpers.to_i64(s)
      }
      local dst = me._read_dst(inst_json, "modulefn StringHelpers.to_i64/1")
      if dst == null { return -1 }
      NyVmState.set_reg(state, dst, outv)
      return 0
    }
    if name == "MapBox.iterator/0" {
      return me._fail(state, "[core/mir_call] map iterator unsupported")
    }
    return me._fail(state, "[core/mir_call] modulefn unsupported: " + name)
  }

  _read_first_arg(mcall_json) {
    local k = JsonCursorBox.find_key_dual(mcall_json, "\"args\":[", r#"\"args\":\["#, 0)
    if k < 0 { return null }
    local lb = mcall_json.indexOf("[", k)
    if lb < 0 { return null }
    local i = lb + 1
    loop(true) {
      local ch = mcall_json.substring(i,i+1)
      // Parser workaround: avoid increment inside multi-way OR
      local is_ws = 0
      if ch == " " { is_ws = 1 }
      if ch == "\n" { is_ws = 1 }
      if ch == "\r" { is_ws = 1 }
      if ch == "\t" { is_ws = 1 }
      if is_ws == 1 { i = i + 1 continue }
      break
    }
    local ds = JsonCursorBox.digits_from(mcall_json, i)
    if ds == "" { return null }
    return StringHelpers.to_i64(ds)
  }

  handle(inst_json, state) {
    local m = me._extract_mir_call_obj(inst_json)
    if m == "" { return me._fail(state, "[core/mir_call] missing mir_call object") }
    local ck = JsonCursorBox.find_key_dual(m, "\"callee\":{", r#"\"callee\":\{"#, 0)
    if ck < 0 { return me._fail(state, "[core/mir_call] missing callee") }
    local brace = m.indexOf("{", ck)
    if brace < 0 { return me._fail(state, "[core/mir_call] bad callee") }
    local cb = JsonCursorBox.seek_obj_end(m, brace)
    if cb < 0 { return me._fail(state, "[core/mir_call] bad callee") }
    local callee = m.substring(brace, cb+1)
    local typ = me._read_str(callee, "type")
    if typ == "" { return me._fail(state, "[core/mir_call] callee missing type") }
    if typ == "Global" || typ == "Extern" {
      return me._handle_console(callee, m, state)
    } else if typ == "Constructor" {
      return me._handle_constructor(inst_json, callee, state)
    } else if typ == "Method" {
      return me._handle_method(inst_json, callee, m, state)
    } else if typ == "ModuleFunction" {
      return me._handle_modulefn(inst_json, callee, m, state)
    } else if typ == "Closure" {
      return me._fail(state, "[core/mir_call] unsupported callee type: Closure")
    }
    return me._fail(state, "[core/mir_call] unsupported callee type: " + typ)
  }
}

static box NyVmOpMirCallMain { main(args){ return 0 } }
