///|
let bpf_ld_w_abs : Int = 0x20

///|
let bpf_jmp_jeq_k : Int = 0x15

///|
let bpf_jmp_jge_k : Int = 0x35

///|
let bpf_ret_k : Int = 0x06

///|
let seccomp_data_nr_offset : UInt = 0U

///|
let seccomp_data_arch_offset : UInt = 4U

///|
fn action_value(action : SeccompAction) -> UInt {
  match action {
    KillProcess => 0x80000000U
    Trap => 0x00030000U
    Errno(errno) => 0x00050000U | (errno.reinterpret_as_uint() & 0x0000ffffU)
    Log => 0x7ffc0000U
    Allow => 0x7fff0000U
  }
}

///|
fn stmt(code : Int, k : UInt) -> BpfInstruction {
  { code, jt: 0, jf: 0, k, }
}

///|
fn jump(code : Int, k : UInt, jt : Int, jf : Int) -> BpfInstruction {
  { code, jt, jf, k, }
}

///|
/// Compile an unambiguous policy into a linear classic-BPF seccomp program.
pub fn compile(
  policy : Policy,
  architecture : Architecture,
) -> SandboxPlan raise CompileError {
  let diagnostics = validate_policy(policy)
  if !diagnostics.is_empty() {
    raise InvalidPolicy(diagnostics)
  }
  let instructions : Array[BpfInstruction] = [
    stmt(bpf_ld_w_abs, seccomp_data_arch_offset),
    jump(bpf_jmp_jeq_k, architecture.audit_value(), 1, 0),
    stmt(bpf_ret_k, action_value(KillProcess)),
    stmt(bpf_ld_w_abs, seccomp_data_nr_offset),
  ]
  // AUDIT_ARCH_X86_64 also identifies the x32 ABI. Its syscall numbers carry
  // bit 30, so reject that ABI before applying native x86_64 rules.
  if architecture == X86_64 {
    instructions.push(jump(bpf_jmp_jge_k, 0x40000000U, 0, 1))
    instructions.push(stmt(bpf_ret_k, action_value(KillProcess)))
  }
  for rule in policy.syscall_rules {
    let number = match syscall_number(architecture, rule.name) {
      Some(number) => number
      None => raise UnknownSyscall(architecture~, name=rule.name)
    }
    instructions.push(jump(bpf_jmp_jeq_k, number.reinterpret_as_uint(), 0, 1))
    instructions.push(stmt(bpf_ret_k, action_value(rule.action)))
  }
  instructions.push(stmt(bpf_ret_k, action_value(policy.default_action)))
  let program = { architecture, instructions, }
  verify_program(program)
  {
    name: policy.name,
    architecture,
    default_action: policy.default_action,
    syscall_rules: policy.syscall_rules,
    seccomp: program,
    path_rules: policy.path_rules,
    limits: policy.limits,
    require_landlock: policy.require_landlock,
  }
}

///|
/// Verify bounds and termination properties needed by the generated program.
pub fn verify_program(program : SeccompProgram) -> Unit raise CompileError {
  let len = program.instructions.length()
  if len == 0 {
    raise InvalidProgram("program is empty")
  }
  if len > 4096 {
    raise InvalidProgram("program exceeds Linux BPF_MAXINSNS")
  }
  for index, instruction in program.instructions {
    if instruction.code == bpf_jmp_jeq_k || instruction.code == bpf_jmp_jge_k {
      let true_target = index + 1 + instruction.jt
      let false_target = index + 1 + instruction.jf
      if instruction.jt < 0 ||
        instruction.jf < 0 ||
        true_target >= len ||
        false_target >= len {
        raise InvalidProgram("jump at instruction \{index} escapes program")
      }
    }
  }
  if program.instructions[len - 1].code != bpf_ret_k {
    raise InvalidProgram("program must end with RET")
  }
}

///|
/// Pack instructions as `[code, jt, jf, k, ...]` for the native runtime shim.
pub fn SeccompProgram::to_words(self : SeccompProgram) -> FixedArray[UInt] {
  let words : Array[UInt] = []
  for instruction in self.instructions {
    words.push(instruction.code.reinterpret_as_uint())
    words.push(instruction.jt.reinterpret_as_uint())
    words.push(instruction.jf.reinterpret_as_uint())
    words.push(instruction.k)
  }
  FixedArray::from_array(words)
}

///|
pub fn SeccompProgram::disassemble(self : SeccompProgram) -> String {
  let lines : Array[String] = []
  for index, instruction in self.instructions {
    let op = if instruction.code == bpf_ld_w_abs {
      "LD.W.ABS"
    } else if instruction.code == bpf_jmp_jeq_k {
      "JEQ.K"
    } else if instruction.code == bpf_jmp_jge_k {
      "JGE.K"
    } else if instruction.code == bpf_ret_k {
      "RET.K"
    } else {
      "UNKNOWN"
    }
    lines.push(
      "\{index}: \{op} k=\{instruction.k} jt=\{instruction.jt} jf=\{instruction.jf}",
    )
  }
  lines.join("\n")
}