Skip to main content

nwnrs_nwscript/
nwasm.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    error::Error,
4    fmt,
5    fmt::Write,
6};
7
8use crate::{
9    LangSpec, NcsAuxCode, NcsInstruction, NcsOpcode, NcsReadError, Ndb, NdbFunction, NdbLine,
10    decode_ncs_instructions,
11};
12
13/// One decoded disassembly line.
14#[derive(#[automatically_derived]
impl ::core::fmt::Debug for NcsAsmLine {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field4_finish(f, "NcsAsmLine",
            "offset", &self.offset, "label", &self.label, "instruction",
            &self.instruction, "extra", &&self.extra)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for NcsAsmLine {
    #[inline]
    fn clone(&self) -> NcsAsmLine {
        NcsAsmLine {
            offset: ::core::clone::Clone::clone(&self.offset),
            label: ::core::clone::Clone::clone(&self.label),
            instruction: ::core::clone::Clone::clone(&self.instruction),
            extra: ::core::clone::Clone::clone(&self.extra),
        }
    }
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for NcsAsmLine {
    #[inline]
    fn eq(&self, other: &NcsAsmLine) -> bool {
        self.offset == other.offset && self.label == other.label &&
                self.instruction == other.instruction &&
            self.extra == other.extra
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for NcsAsmLine {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<usize>;
        let _: ::core::cmp::AssertParamIsEq<Option<String>>;
        let _: ::core::cmp::AssertParamIsEq<String>;
    }
}Eq)]
15pub struct NcsAsmLine {
16    /// Byte offset of the instruction within the NCS code section.
17    pub offset:      usize,
18    /// Optional synthetic label placed at this instruction offset.
19    pub label:       Option<String>,
20    /// Rendered instruction name.
21    pub instruction: String,
22    /// Rendered operand text.
23    pub extra:       String,
24}
25
26#[derive(#[automatically_derived]
impl ::core::fmt::Debug for DecodedAsmLine {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field3_finish(f,
            "DecodedAsmLine", "line", &self.line, "opcode", &self.opcode,
            "jump_target", &&self.jump_target)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for DecodedAsmLine {
    #[inline]
    fn clone(&self) -> DecodedAsmLine {
        DecodedAsmLine {
            line: ::core::clone::Clone::clone(&self.line),
            opcode: ::core::clone::Clone::clone(&self.opcode),
            jump_target: ::core::clone::Clone::clone(&self.jump_target),
        }
    }
}Clone)]
27struct DecodedAsmLine {
28    line:        NcsAsmLine,
29    opcode:      NcsOpcode,
30    jump_target: Option<usize>,
31}
32
33/// Options controlling NCS disassembly rendering.
34#[derive(#[automatically_derived]
#[allow(clippy::struct_excessive_bools)]
impl ::core::fmt::Debug for NcsDisassemblyOptions {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        let names: &'static _ =
            &["internal_names", "max_string_length", "labels", "offsets",
                        "local_offsets", "source_weave"];
        let values: &[&dyn ::core::fmt::Debug] =
            &[&self.internal_names, &self.max_string_length, &self.labels,
                        &self.offsets, &self.local_offsets, &&self.source_weave];
        ::core::fmt::Formatter::debug_struct_fields_finish(f,
            "NcsDisassemblyOptions", names, values)
    }
}Debug, #[automatically_derived]
#[allow(clippy::struct_excessive_bools)]
impl ::core::clone::Clone for NcsDisassemblyOptions {
    #[inline]
    fn clone(&self) -> NcsDisassemblyOptions {
        let _: ::core::clone::AssertParamIsClone<bool>;
        let _: ::core::clone::AssertParamIsClone<usize>;
        *self
    }
}Clone, #[automatically_derived]
#[allow(clippy::struct_excessive_bools)]
impl ::core::marker::Copy for NcsDisassemblyOptions { }Copy, #[automatically_derived]
#[allow(clippy::struct_excessive_bools)]
impl ::core::cmp::PartialEq for NcsDisassemblyOptions {
    #[inline]
    fn eq(&self, other: &NcsDisassemblyOptions) -> bool {
        self.internal_names == other.internal_names &&
                            self.labels == other.labels && self.offsets == other.offsets
                    && self.local_offsets == other.local_offsets &&
                self.source_weave == other.source_weave &&
            self.max_string_length == other.max_string_length
    }
}PartialEq, #[automatically_derived]
#[allow(clippy::struct_excessive_bools)]
impl ::core::cmp::Eq for NcsDisassemblyOptions {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<bool>;
        let _: ::core::cmp::AssertParamIsEq<usize>;
    }
}Eq, #[automatically_derived]
#[allow(clippy::struct_excessive_bools)]
impl ::core::hash::Hash for NcsDisassemblyOptions {
    #[inline]
    fn hash<__H: ::core::hash::Hasher>(&self, state: &mut __H) {
        ::core::hash::Hash::hash(&self.internal_names, state);
        ::core::hash::Hash::hash(&self.max_string_length, state);
        ::core::hash::Hash::hash(&self.labels, state);
        ::core::hash::Hash::hash(&self.offsets, state);
        ::core::hash::Hash::hash(&self.local_offsets, state);
        ::core::hash::Hash::hash(&self.source_weave, state)
    }
}Hash)]
35#[allow(clippy::struct_excessive_bools)]
36pub struct NcsDisassemblyOptions {
37    /// Render upstream internal enum names instead of canonical mnemonics.
38    pub internal_names:    bool,
39    /// Maximum string payload shown before truncation markers are appended.
40    pub max_string_length: usize,
41    /// Emit synthetic labels for jump targets.
42    pub labels:            bool,
43    /// Include instruction byte offsets in rendered text output.
44    pub offsets:           bool,
45    /// Include per-function local offsets when NDB debug info is available.
46    pub local_offsets:     bool,
47    /// Weave source lines into output when debug line info and source text are
48    /// available.
49    pub source_weave:      bool,
50}
51
52impl Default for NcsDisassemblyOptions {
53    fn default() -> Self {
54        Self {
55            internal_names:    false,
56            max_string_length: 15,
57            labels:            true,
58            offsets:           true,
59            local_offsets:     true,
60            source_weave:      true,
61        }
62    }
63}
64
65/// Errors returned while rendering or parsing NCS asm text.
66#[derive(#[automatically_derived]
impl ::core::fmt::Debug for NcsAsmError {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        match self {
            NcsAsmError::Read(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Read",
                    &__self_0),
            NcsAsmError::InvalidExtra {
                offset: __self_0,
                opcode: __self_1,
                auxcode: __self_2,
                message: __self_3 } =>
                ::core::fmt::Formatter::debug_struct_field4_finish(f,
                    "InvalidExtra", "offset", __self_0, "opcode", __self_1,
                    "auxcode", __self_2, "message", &__self_3),
            NcsAsmError::Parse { line: __self_0, message: __self_1 } =>
                ::core::fmt::Formatter::debug_struct_field2_finish(f, "Parse",
                    "line", __self_0, "message", &__self_1),
        }
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for NcsAsmError {
    #[inline]
    fn clone(&self) -> NcsAsmError {
        match self {
            NcsAsmError::Read(__self_0) =>
                NcsAsmError::Read(::core::clone::Clone::clone(__self_0)),
            NcsAsmError::InvalidExtra {
                offset: __self_0,
                opcode: __self_1,
                auxcode: __self_2,
                message: __self_3 } =>
                NcsAsmError::InvalidExtra {
                    offset: ::core::clone::Clone::clone(__self_0),
                    opcode: ::core::clone::Clone::clone(__self_1),
                    auxcode: ::core::clone::Clone::clone(__self_2),
                    message: ::core::clone::Clone::clone(__self_3),
                },
            NcsAsmError::Parse { line: __self_0, message: __self_1 } =>
                NcsAsmError::Parse {
                    line: ::core::clone::Clone::clone(__self_0),
                    message: ::core::clone::Clone::clone(__self_1),
                },
        }
    }
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for NcsAsmError {
    #[inline]
    fn eq(&self, other: &NcsAsmError) -> bool {
        let __self_discr = ::core::intrinsics::discriminant_value(self);
        let __arg1_discr = ::core::intrinsics::discriminant_value(other);
        __self_discr == __arg1_discr &&
            match (self, other) {
                (NcsAsmError::Read(__self_0), NcsAsmError::Read(__arg1_0)) =>
                    __self_0 == __arg1_0,
                (NcsAsmError::InvalidExtra {
                    offset: __self_0,
                    opcode: __self_1,
                    auxcode: __self_2,
                    message: __self_3 }, NcsAsmError::InvalidExtra {
                    offset: __arg1_0,
                    opcode: __arg1_1,
                    auxcode: __arg1_2,
                    message: __arg1_3 }) =>
                    __self_0 == __arg1_0 && __self_1 == __arg1_1 &&
                            __self_2 == __arg1_2 && __self_3 == __arg1_3,
                (NcsAsmError::Parse { line: __self_0, message: __self_1 },
                    NcsAsmError::Parse { line: __arg1_0, message: __arg1_1 }) =>
                    __self_0 == __arg1_0 && __self_1 == __arg1_1,
                _ => unsafe { ::core::intrinsics::unreachable() }
            }
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for NcsAsmError {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<NcsReadError>;
        let _: ::core::cmp::AssertParamIsEq<usize>;
        let _: ::core::cmp::AssertParamIsEq<NcsOpcode>;
        let _: ::core::cmp::AssertParamIsEq<NcsAuxCode>;
        let _: ::core::cmp::AssertParamIsEq<String>;
    }
}Eq)]
67pub enum NcsAsmError {
68    /// Binary decoding failed before text rendering could begin.
69    Read(NcsReadError),
70    /// One instruction payload did not match the expected opcode shape.
71    InvalidExtra {
72        /// Instruction byte offset in the NCS code section.
73        offset:  usize,
74        /// Opcode whose extra data was malformed.
75        opcode:  NcsOpcode,
76        /// Auxcode whose extra data was malformed.
77        auxcode: NcsAuxCode,
78        /// Human-readable explanation.
79        message: String,
80    },
81    /// One line of textual asm could not be parsed.
82    Parse {
83        /// One-based line number in the text input.
84        line:    usize,
85        /// Human-readable explanation.
86        message: String,
87    },
88}
89
90impl fmt::Display for NcsAsmError {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        match self {
93            Self::Read(error) => error.fmt(f),
94            Self::InvalidExtra {
95                offset,
96                opcode,
97                auxcode,
98                message,
99            } => f.write_fmt(format_args!("invalid {0}.{1} payload at byte {2}: {3}",
        opcode.internal_name(), auxcode.internal_name(), offset, message))write!(
100                f,
101                "invalid {}.{} payload at byte {}: {}",
102                opcode.internal_name(),
103                auxcode.internal_name(),
104                offset,
105                message
106            ),
107            Self::Parse {
108                line,
109                message,
110            } => {
111                f.write_fmt(format_args!("invalid NCS asm on line {0}: {1}", line, message))write!(f, "invalid NCS asm on line {line}: {message}")
112            }
113        }
114    }
115}
116
117impl Error for NcsAsmError {}
118
119impl From<NcsReadError> for NcsAsmError {
120    fn from(value: NcsReadError) -> Self {
121        Self::Read(value)
122    }
123}
124
125impl NcsOpcode {
126    /// Returns the upstream internal opcode constant name used by `nwasm`.
127    #[must_use]
128    pub fn internal_name(self) -> &'static str {
129        match self {
130            Self::Assignment => "ASSIGNMENT",
131            Self::RunstackAdd => "RUNSTACK_ADD",
132            Self::RunstackCopy => "RUNSTACK_COPY",
133            Self::Constant => "CONSTANT",
134            Self::ExecuteCommand => "EXECUTE_COMMAND",
135            Self::LogicalAnd => "LOGICAL_AND",
136            Self::LogicalOr => "LOGICAL_OR",
137            Self::InclusiveOr => "INCLUSIVE_OR",
138            Self::ExclusiveOr => "EXCLUSIVE_OR",
139            Self::BooleanAnd => "BOOLEAN_AND",
140            Self::Equal => "EQUAL",
141            Self::NotEqual => "NOT_EQUAL",
142            Self::Geq => "GEQ",
143            Self::Gt => "GT",
144            Self::Lt => "LT",
145            Self::Leq => "LEQ",
146            Self::ShiftLeft => "SHIFT_LEFT",
147            Self::ShiftRight => "SHIFT_RIGHT",
148            Self::UShiftRight => "USHIFT_RIGHT",
149            Self::Add => "ADD",
150            Self::Sub => "SUB",
151            Self::Mul => "MUL",
152            Self::Div => "DIV",
153            Self::Modulus => "MODULUS",
154            Self::Negation => "NEGATION",
155            Self::OnesComplement => "ONES_COMPLEMENT",
156            Self::ModifyStackPointer => "MODIFY_STACK_POINTER",
157            Self::StoreIp => "STORE_IP",
158            Self::Jmp => "JMP",
159            Self::Jsr => "JSR",
160            Self::Jz => "JZ",
161            Self::Ret => "RET",
162            Self::DeStruct => "DE_STRUCT",
163            Self::BooleanNot => "BOOLEAN_NOT",
164            Self::Decrement => "DECREMENT",
165            Self::Increment => "INCREMENT",
166            Self::Jnz => "JNZ",
167            Self::AssignmentBase => "ASSIGNMENT_BASE",
168            Self::RunstackCopyBase => "RUNSTACK_COPY_BASE",
169            Self::DecrementBase => "DECREMENT_BASE",
170            Self::IncrementBase => "INCREMENT_BASE",
171            Self::SaveBasePointer => "SAVE_BASE_POINTER",
172            Self::RestoreBasePointer => "RESTORE_BASE_POINTER",
173            Self::StoreState => "STORE_STATE",
174            Self::NoOperation => "NO_OPERATION",
175        }
176    }
177}
178
179impl NcsAuxCode {
180    /// Returns the upstream internal auxcode constant name used by `nwasm`.
181    #[must_use]
182    pub fn internal_name(self) -> &'static str {
183        match self {
184            Self::None => "NONE",
185            Self::TypeVoid => "TYPE_VOID",
186            Self::TypeCommand => "TYPE_COMMAND",
187            Self::TypeInteger => "TYPE_INTEGER",
188            Self::TypeFloat => "TYPE_FLOAT",
189            Self::TypeString => "TYPE_STRING",
190            Self::TypeObject => "TYPE_OBJECT",
191            Self::TypeEngst0 => "TYPE_ENGST0",
192            Self::TypeEngst1 => "TYPE_ENGST1",
193            Self::TypeEngst2 => "TYPE_ENGST2",
194            Self::TypeEngst3 => "TYPE_ENGST3",
195            Self::TypeEngst4 => "TYPE_ENGST4",
196            Self::TypeEngst5 => "TYPE_ENGST5",
197            Self::TypeEngst6 => "TYPE_ENGST6",
198            Self::TypeEngst7 => "TYPE_ENGST7",
199            Self::TypeEngst8 => "TYPE_ENGST8",
200            Self::TypeEngst9 => "TYPE_ENGST9",
201            Self::TypeTypeIntegerInteger => "TYPETYPE_INTEGER_INTEGER",
202            Self::TypeTypeFloatFloat => "TYPETYPE_FLOAT_FLOAT",
203            Self::TypeTypeObjectObject => "TYPETYPE_OBJECT_OBJECT",
204            Self::TypeTypeStringString => "TYPETYPE_STRING_STRING",
205            Self::TypeTypeStructStruct => "TYPETYPE_STRUCT_STRUCT",
206            Self::TypeTypeIntegerFloat => "TYPETYPE_INTEGER_FLOAT",
207            Self::TypeTypeFloatInteger => "TYPETYPE_FLOAT_INTEGER",
208            Self::TypeTypeEngst0Engst0 => "TYPETYPE_ENGST0_ENGST0",
209            Self::TypeTypeEngst1Engst1 => "TYPETYPE_ENGST1_ENGST1",
210            Self::TypeTypeEngst2Engst2 => "TYPETYPE_ENGST2_ENGST2",
211            Self::TypeTypeEngst3Engst3 => "TYPETYPE_ENGST3_ENGST3",
212            Self::TypeTypeEngst4Engst4 => "TYPETYPE_ENGST4_ENGST4",
213            Self::TypeTypeEngst5Engst5 => "TYPETYPE_ENGST5_ENGST5",
214            Self::TypeTypeEngst6Engst6 => "TYPETYPE_ENGST6_ENGST6",
215            Self::TypeTypeEngst7Engst7 => "TYPETYPE_ENGST7_ENGST7",
216            Self::TypeTypeEngst8Engst8 => "TYPETYPE_ENGST8_ENGST8",
217            Self::TypeTypeEngst9Engst9 => "TYPETYPE_ENGST9_ENGST9",
218            Self::TypeTypeVectorVector => "TYPETYPE_VECTOR_VECTOR",
219            Self::TypeTypeVectorFloat => "TYPETYPE_VECTOR_FLOAT",
220            Self::TypeTypeFloatVector => "TYPETYPE_FLOAT_VECTOR",
221            Self::EvalInplace => "EVAL_INPLACE",
222            Self::EvalPostplace => "EVAL_POSTPLACE",
223        }
224    }
225}
226
227impl NcsInstruction {
228    /// Returns the upstream `nwasm` instruction name.
229    #[must_use]
230    pub fn canonical_name(&self, internal: bool) -> String {
231        let mut name = if internal {
232            self.opcode.internal_name().to_string()
233        } else {
234            self.opcode.canonical_name().to_string()
235        };
236        let aux = if internal {
237            Some(self.auxcode.internal_name())
238        } else {
239            self.auxcode.canonical_name()
240        };
241        if let Some(aux) = aux {
242            name.push('.');
243            name.push_str(aux);
244        }
245        name
246    }
247
248    /// Renders the decoded operand payload using upstream `nwasm` formatting
249    /// rules.
250    ///
251    /// # Errors
252    ///
253    /// Returns [`NcsAsmError`] if formatting the operand fails.
254    pub fn extra_string(&self, max_string_length: usize) -> Result<String, NcsAsmError> {
255        extra_string_for_instruction(self, 0, None, &BTreeMap::new(), max_string_length)
256    }
257}
258
259/// Decodes a full `NCS` stream into instruction-shaped disassembly lines.
260///
261/// # Errors
262///
263/// Returns [`NcsAsmError`] if the stream cannot be decoded.
264pub fn disassemble_ncs(
265    bytes: &[u8],
266    langspec: Option<&LangSpec>,
267    options: NcsDisassemblyOptions,
268) -> Result<Vec<NcsAsmLine>, NcsAsmError> {
269    Ok(decode_asm_lines(bytes, langspec, options)?
270        .into_iter()
271        .map(|line| line.line)
272        .collect())
273}
274
275fn decode_asm_lines(
276    bytes: &[u8],
277    langspec: Option<&LangSpec>,
278    options: NcsDisassemblyOptions,
279) -> Result<Vec<DecodedAsmLine>, NcsAsmError> {
280    let instructions = decode_ncs_instructions(bytes)?;
281    let labels = if options.labels {
282        collect_jump_labels(&instructions)
283    } else {
284        BTreeMap::new()
285    };
286    let mut offset = 0usize;
287    let mut lines = Vec::with_capacity(instructions.len());
288
289    for instruction in instructions {
290        let jump_target = jump_target_for_instruction(&instruction, offset);
291        let extra = extra_string_for_instruction(
292            &instruction,
293            offset,
294            langspec,
295            &labels,
296            options.max_string_length,
297        )?;
298        lines.push(DecodedAsmLine {
299            line: NcsAsmLine {
300                offset,
301                label: labels.get(&offset).cloned(),
302                instruction: instruction.canonical_name(options.internal_names),
303                extra,
304            },
305            opcode: instruction.opcode,
306            jump_target,
307        });
308        offset += instruction.encoded_len();
309    }
310
311    Ok(lines)
312}
313
314/// Renders a full `NCS` stream into stable human-readable disassembly text.
315///
316/// # Errors
317///
318/// Returns [`NcsAsmError`] if decoding or rendering fails.
319pub fn render_ncs_disassembly(
320    bytes: &[u8],
321    langspec: Option<&LangSpec>,
322    options: NcsDisassemblyOptions,
323) -> Result<String, NcsAsmError> {
324    let lines = decode_asm_lines(bytes, langspec, options)?;
325    Ok(render_disassembly_lines(
326        &lines.into_iter().map(|line| line.line).collect::<Vec<_>>(),
327        options,
328    ))
329}
330
331/// Renders a full `NCS` stream into NDB-aware disassembly text.
332///
333/// # Errors
334///
335/// Returns [`NcsAsmError`] if decoding or rendering fails.
336pub fn render_ncs_disassembly_with_ndb(
337    bytes: &[u8],
338    langspec: Option<&LangSpec>,
339    ndb: Option<&Ndb>,
340    source_files: Option<&BTreeMap<String, Vec<String>>>,
341    options: NcsDisassemblyOptions,
342) -> Result<String, NcsAsmError> {
343    let lines = decode_asm_lines(bytes, langspec, options)?;
344    Ok(render_disassembly_with_ndb_lines(
345        &lines,
346        ndb,
347        source_files,
348        options,
349    ))
350}
351
352/// Parses assembleable NCS asm text into decoded instructions.
353///
354/// # Errors
355///
356/// Returns [`NcsAsmError`] if parsing fails.
357pub fn assemble_ncs_text(
358    text: &str,
359    langspec: Option<&LangSpec>,
360) -> Result<Vec<NcsInstruction>, NcsAsmError> {
361    let mut labels = BTreeMap::<String, usize>::new();
362    let mut pending_labels = Vec::<String>::new();
363    let mut parsed = Vec::<ParsedAsmInstruction>::new();
364
365    for (index, raw_line) in text.lines().enumerate() {
366        let line_number = index + 1;
367        let line = strip_asm_line(raw_line);
368        if line.is_empty() || is_function_header_line(line) {
369            continue;
370        }
371        if let Some(label) = line.strip_suffix(':') {
372            let label = label.trim();
373            if label.is_empty() {
374                return Err(NcsAsmError::Parse {
375                    line:    line_number,
376                    message: "empty label".to_string(),
377                });
378            }
379            if labels.contains_key(label) || pending_labels.iter().any(|pending| pending == label) {
380                return Err(NcsAsmError::Parse {
381                    line:    line_number,
382                    message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("duplicate label {0:?}", label))
    })format!("duplicate label {label:?}"),
383                });
384            }
385            pending_labels.push(label.to_string());
386            continue;
387        }
388
389        let line = strip_rendered_offsets(line);
390        let (instruction_name, extra) =
391            split_instruction_line(line).ok_or_else(|| NcsAsmError::Parse {
392                line:    line_number,
393                message: "missing instruction mnemonic".to_string(),
394            })?;
395        let (opcode, auxcode) = parse_instruction_name(instruction_name, line_number)?;
396        let operand = parse_instruction_operand(opcode, auxcode, extra, langspec, line_number)?;
397        let instruction_index = parsed.len();
398        for label in pending_labels.drain(..) {
399            labels.insert(label, instruction_index);
400        }
401        parsed.push(ParsedAsmInstruction {
402            opcode,
403            auxcode,
404            operand,
405            line: line_number,
406        });
407    }
408
409    if !pending_labels.is_empty() {
410        return Err(NcsAsmError::Parse {
411            line:    text.lines().count().max(1),
412            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("label {0:?} does not precede an instruction",
                pending_labels.first().cloned().unwrap_or_default()))
    })format!(
413                "label {:?} does not precede an instruction",
414                pending_labels.first().cloned().unwrap_or_default()
415            ),
416        });
417    }
418
419    let instruction_offsets = parsed
420        .iter()
421        .scan(0usize, |offset, instruction| {
422            let current = *offset;
423            *offset += instruction.encoded_len();
424            Some(current)
425        })
426        .collect::<Vec<_>>();
427
428    let label_offsets = labels
429        .into_iter()
430        .map(|(label, index)| {
431            let offset =
432                instruction_offsets
433                    .get(index)
434                    .copied()
435                    .ok_or_else(|| NcsAsmError::Parse {
436                        line:    parsed.get(index).map_or(1, |entry| entry.line),
437                        message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("label {0:?} resolved past end of instruction stream",
                label))
    })format!("label {label:?} resolved past end of instruction stream"),
438                    })?;
439            Ok((label, offset))
440        })
441        .collect::<Result<BTreeMap<_, _>, NcsAsmError>>()?;
442
443    parsed
444        .into_iter()
445        .zip(instruction_offsets)
446        .map(|(instruction, offset)| instruction.build(offset, &label_offsets))
447        .collect()
448}
449
450/// Parses assembleable NCS asm text and encodes it into bytecode without the
451/// fixed NCS file header.
452///
453/// # Errors
454///
455/// Returns [`NcsAsmError`] if parsing or encoding fails.
456pub fn assemble_ncs_bytes(text: &str, langspec: Option<&LangSpec>) -> Result<Vec<u8>, NcsAsmError> {
457    Ok(crate::encode_ncs_instructions(&assemble_ncs_text(
458        text, langspec,
459    )?))
460}
461
462/// Renders already-decoded disassembly lines into plain text.
463#[must_use]
464pub fn render_disassembly_lines(lines: &[NcsAsmLine], options: NcsDisassemblyOptions) -> String {
465    let mut rendered = Vec::new();
466
467    for line in lines {
468        if options.labels
469            && let Some(label) = &line.label
470        {
471            rendered.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}:", label))
    })format!("{label}:"));
472        }
473
474        let mut row = String::new();
475        if options.offsets {
476            let _ = row.write_fmt(format_args!("{0:04}", line.offset))write!(row, "{:04}", line.offset);
477            row.push_str(": ");
478        }
479
480        row.push_str(&line.instruction);
481        if !line.extra.is_empty() {
482            row.push(' ');
483            row.push_str(&line.extra);
484        }
485        rendered.push(row);
486    }
487
488    rendered.join("\n")
489}
490
491fn render_disassembly_with_ndb_lines(
492    lines: &[DecodedAsmLine],
493    ndb: Option<&Ndb>,
494    source_files: Option<&BTreeMap<String, Vec<String>>>,
495    options: NcsDisassemblyOptions,
496) -> String {
497    let Some(ndb) = ndb else {
498        return render_disassembly_lines(
499            &lines
500                .iter()
501                .map(|line| line.line.clone())
502                .collect::<Vec<_>>(),
503            options,
504        );
505    };
506
507    let functions = sorted_functions(ndb);
508    let mut rendered = Vec::new();
509    let mut current_function: Option<usize> = None;
510    let mut last_source: Option<(usize, usize)> = None;
511
512    for decoded in lines {
513        let line = &decoded.line;
514        let function_index = functions
515            .iter()
516            .position(|function| line_in_function(line.offset, function));
517
518        if function_index != current_function {
519            current_function = function_index;
520            last_source = None;
521
522            if let Some(index) = current_function
523                && let Some(function) = functions.get(index)
524            {
525                if !rendered.is_empty() {
526                    rendered.push(String::new());
527                }
528                rendered.push(render_function_header(function, ndb));
529            }
530        }
531
532        if options.labels
533            && let Some(label) = &line.label
534        {
535            rendered.push(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}:", label))
    })format!("{label}:"));
536        }
537
538        let mut row = String::new();
539        if options.offsets {
540            let _ = row.write_fmt(format_args!("{0:04}", line.offset))write!(row, "{:04}", line.offset);
541            if options.local_offsets
542                && let Some(index) = current_function
543                && let Some(function) = functions.get(index)
544            {
545                let local = local_offset(line.offset, function);
546                row.push(' ');
547                let _ = row.write_fmt(format_args!("{0:04}", local))write!(row, "{local:04}");
548            }
549            row.push_str(": ");
550        }
551
552        row.push_str(&line.instruction);
553        let rendered_extra = render_ndb_aware_extra(decoded, &functions).unwrap_or(&line.extra);
554        if !rendered_extra.is_empty() {
555            row.push(' ');
556            row.push_str(rendered_extra);
557        }
558
559        if options.source_weave
560            && let Some((source_key, source_text)) =
561                source_line_for_offset(line.offset, ndb, source_files, &mut last_source)
562            && !source_text.is_empty()
563        {
564            row.push_str(" | ");
565            row.push_str(&source_key);
566            row.push_str(" | ");
567            row.push_str(&source_text);
568        }
569
570        rendered.push(row);
571    }
572
573    rendered.join("\n")
574}
575
576#[derive(#[automatically_derived]
impl ::core::fmt::Debug for ParsedAsmOperand {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        match self {
            ParsedAsmOperand::None =>
                ::core::fmt::Formatter::write_str(f, "None"),
            ParsedAsmOperand::Bytes(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Bytes",
                    &__self_0),
            ParsedAsmOperand::Jump(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Jump",
                    &__self_0),
        }
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for ParsedAsmOperand {
    #[inline]
    fn clone(&self) -> ParsedAsmOperand {
        match self {
            ParsedAsmOperand::None => ParsedAsmOperand::None,
            ParsedAsmOperand::Bytes(__self_0) =>
                ParsedAsmOperand::Bytes(::core::clone::Clone::clone(__self_0)),
            ParsedAsmOperand::Jump(__self_0) =>
                ParsedAsmOperand::Jump(::core::clone::Clone::clone(__self_0)),
        }
    }
}Clone)]
577enum ParsedAsmOperand {
578    None,
579    Bytes(Vec<u8>),
580    Jump(AsmJumpTarget),
581}
582
583#[derive(#[automatically_derived]
impl ::core::fmt::Debug for AsmJumpTarget {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        match self {
            AsmJumpTarget::Offset(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Offset",
                    &__self_0),
            AsmJumpTarget::Label(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Label",
                    &__self_0),
        }
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for AsmJumpTarget {
    #[inline]
    fn clone(&self) -> AsmJumpTarget {
        match self {
            AsmJumpTarget::Offset(__self_0) =>
                AsmJumpTarget::Offset(::core::clone::Clone::clone(__self_0)),
            AsmJumpTarget::Label(__self_0) =>
                AsmJumpTarget::Label(::core::clone::Clone::clone(__self_0)),
        }
    }
}Clone)]
584enum AsmJumpTarget {
585    Offset(usize),
586    Label(String),
587}
588
589#[derive(#[automatically_derived]
impl ::core::fmt::Debug for ParsedAsmInstruction {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field4_finish(f,
            "ParsedAsmInstruction", "opcode", &self.opcode, "auxcode",
            &self.auxcode, "operand", &self.operand, "line", &&self.line)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for ParsedAsmInstruction {
    #[inline]
    fn clone(&self) -> ParsedAsmInstruction {
        ParsedAsmInstruction {
            opcode: ::core::clone::Clone::clone(&self.opcode),
            auxcode: ::core::clone::Clone::clone(&self.auxcode),
            operand: ::core::clone::Clone::clone(&self.operand),
            line: ::core::clone::Clone::clone(&self.line),
        }
    }
}Clone)]
590struct ParsedAsmInstruction {
591    opcode:  NcsOpcode,
592    auxcode: NcsAuxCode,
593    operand: ParsedAsmOperand,
594    line:    usize,
595}
596
597impl ParsedAsmInstruction {
598    fn encoded_len(&self) -> usize {
599        2 + match &self.operand {
600            ParsedAsmOperand::None => 0,
601            ParsedAsmOperand::Bytes(bytes) => bytes.len(),
602            ParsedAsmOperand::Jump(_target) => 4,
603        }
604    }
605
606    fn build(
607        self,
608        offset: usize,
609        labels: &BTreeMap<String, usize>,
610    ) -> Result<NcsInstruction, NcsAsmError> {
611        let extra = match self.operand {
612            ParsedAsmOperand::None => Vec::new(),
613            ParsedAsmOperand::Bytes(bytes) => bytes,
614            ParsedAsmOperand::Jump(target) => {
615                let absolute_target = match target {
616                    AsmJumpTarget::Offset(offset) => offset,
617                    AsmJumpTarget::Label(label) => {
618                        labels
619                            .get(&label)
620                            .copied()
621                            .ok_or_else(|| NcsAsmError::Parse {
622                                line:    self.line,
623                                message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("unknown jump label {0:?}", label))
    })format!("unknown jump label {label:?}"),
624                            })?
625                    }
626                };
627                let relative = i64::try_from(absolute_target)
628                    .ok()
629                    .and_then(|target| i64::try_from(offset).ok().map(|origin| target - origin))
630                    .and_then(|relative| i32::try_from(relative).ok())
631                    .ok_or_else(|| NcsAsmError::Parse {
632                        line:    self.line,
633                        message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("jump target {0} is out of range for byte offset {1}",
                absolute_target, offset))
    })format!(
634                            "jump target {absolute_target} is out of range for byte offset \
635                             {offset}"
636                        ),
637                    })?;
638                relative.to_be_bytes().to_vec()
639            }
640        };
641
642        Ok(NcsInstruction {
643            opcode: self.opcode,
644            auxcode: self.auxcode,
645            extra,
646        })
647    }
648}
649
650fn render_ndb_aware_extra<'a>(
651    decoded: &'a DecodedAsmLine,
652    functions: &[&'a NdbFunction],
653) -> Option<&'a str> {
654    if decoded.opcode != NcsOpcode::Jsr {
655        return None;
656    }
657    let target = decoded.jump_target?;
658    functions
659        .iter()
660        .find(|function| line_in_function(target, function))
661        .map(|function| function.label.as_str())
662}
663
664fn extra_string_for_instruction(
665    instruction: &NcsInstruction,
666    offset: usize,
667    langspec: Option<&LangSpec>,
668    labels: &BTreeMap<usize, String>,
669    max_string_length: usize,
670) -> Result<String, NcsAsmError> {
671    let extra = instruction.extra.as_slice();
672    match instruction.opcode {
673        NcsOpcode::Constant => match instruction.auxcode {
674            NcsAuxCode::TypeString => {
675                let value = decode_prefixed_string(extra, offset, instruction)?;
676                Ok(truncate_nwasm_string(&value, max_string_length))
677            }
678            NcsAuxCode::TypeInteger => Ok(read_i32(extra, offset, instruction)?.to_string()),
679            NcsAuxCode::TypeObject => {
680                Ok(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("0x{0:08X}",
                read_i32(extra, offset, instruction)?))
    })format!("0x{:08X}", read_i32(extra, offset, instruction)?))
681            }
682            NcsAuxCode::TypeFloat => Ok(read_f32(extra, offset, instruction)?.to_string()),
683            NcsAuxCode::TypeEngst2 => Ok(read_u32(extra, offset, instruction)?.to_string()),
684            NcsAuxCode::TypeEngst7 => {
685                let value = decode_prefixed_string(extra, offset, instruction)?;
686                Ok(value.escape_default().to_string())
687            }
688            _ => Ok(String::new()),
689        },
690        NcsOpcode::Jz | NcsOpcode::Jmp | NcsOpcode::Jsr | NcsOpcode::Jnz => {
691            let target = jump_target(offset, read_i32(extra, offset, instruction)?);
692            Ok(format_jump_target(instruction.opcode, target, labels))
693        }
694        NcsOpcode::StoreState => Ok(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}, {1}",
                read_i32_part(extra, 0, offset, instruction)?,
                read_i32_part(extra, 4, offset, instruction)?))
    })format!(
695            "{}, {}",
696            read_i32_part(extra, 0, offset, instruction)?,
697            read_i32_part(extra, 4, offset, instruction)?,
698        )),
699        NcsOpcode::ModifyStackPointer
700        | NcsOpcode::Increment
701        | NcsOpcode::Decrement
702        | NcsOpcode::IncrementBase
703        | NcsOpcode::DecrementBase => Ok(read_i32(extra, offset, instruction)?.to_string()),
704        NcsOpcode::ExecuteCommand => {
705            let builtin_id = read_u16_part(extra, 0, offset, instruction)?;
706            let argc = read_u8_part(extra, 2, offset, instruction)?;
707            let _ = langspec;
708            Ok(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}, {1}", builtin_id, argc))
    })format!("{builtin_id}, {argc}"))
709        }
710        NcsOpcode::RunstackCopy | NcsOpcode::RunstackCopyBase => Ok(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}, {1}",
                read_i32_part(extra, 0, offset, instruction)?,
                read_i16_part(extra, 4, offset, instruction)?))
    })format!(
711            "{}, {}",
712            read_i32_part(extra, 0, offset, instruction)?,
713            read_i16_part(extra, 4, offset, instruction)?,
714        )),
715        NcsOpcode::Assignment | NcsOpcode::AssignmentBase => Ok(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}, {1}",
                read_i32_part(extra, 0, offset, instruction)?,
                read_u16_part(extra, 4, offset, instruction)?))
    })format!(
716            "{}, {}",
717            read_i32_part(extra, 0, offset, instruction)?,
718            read_u16_part(extra, 4, offset, instruction)?,
719        )),
720        NcsOpcode::DeStruct => Ok(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}, {1}, {2}",
                read_u16_part(extra, 0, offset, instruction)?,
                read_u16_part(extra, 2, offset, instruction)?,
                read_u16_part(extra, 4, offset, instruction)?))
    })format!(
721            "{}, {}, {}",
722            read_u16_part(extra, 0, offset, instruction)?,
723            read_u16_part(extra, 2, offset, instruction)?,
724            read_u16_part(extra, 4, offset, instruction)?,
725        )),
726        NcsOpcode::Equal | NcsOpcode::NotEqual
727            if instruction.auxcode == NcsAuxCode::TypeTypeStructStruct =>
728        {
729            Ok(read_u16(extra, offset, instruction)?.to_string())
730        }
731        _ if extra.is_empty() => Ok(String::new()),
732        _ => Err(NcsAsmError::InvalidExtra {
733            offset,
734            opcode: instruction.opcode,
735            auxcode: instruction.auxcode,
736            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("unsupported extra payload {0:?}",
                extra))
    })format!("unsupported extra payload {extra:?}"),
737        }),
738    }
739}
740
741fn strip_asm_line(line: &str) -> &str {
742    line.split_once(" | ")
743        .map_or(line, |(head, _tail)| head)
744        .trim()
745}
746
747fn is_function_header_line(line: &str) -> bool {
748    line.ends_with(']') && line.contains('(') && line.contains("):")
749}
750
751fn strip_rendered_offsets(line: &str) -> &str {
752    let Some((prefix, rest)) = line.split_once(": ") else {
753        return line;
754    };
755    if prefix
756        .chars()
757        .all(|ch| ch.is_ascii_digit() || ch.is_ascii_whitespace())
758        && prefix.chars().any(|ch| ch.is_ascii_digit())
759    {
760        rest.trim_start()
761    } else {
762        line
763    }
764}
765
766fn split_instruction_line(line: &str) -> Option<(&str, &str)> {
767    let trimmed = line.trim();
768    if trimmed.is_empty() {
769        return None;
770    }
771    if let Some((instruction, rest)) = trimmed.split_once(char::is_whitespace) {
772        Some((instruction, rest.trim()))
773    } else {
774        Some((trimmed, ""))
775    }
776}
777
778fn parse_instruction_name(name: &str, line: usize) -> Result<(NcsOpcode, NcsAuxCode), NcsAsmError> {
779    let (opcode_name, aux_name) = name
780        .split_once('.')
781        .map_or((name, None), |(opcode, aux)| (opcode, Some(aux)));
782    let opcode = parse_opcode_name(opcode_name).ok_or_else(|| NcsAsmError::Parse {
783        line,
784        message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("unknown instruction mnemonic {0:?}",
                opcode_name))
    })format!("unknown instruction mnemonic {opcode_name:?}"),
785    })?;
786    let auxcode = if let Some(aux_name) = aux_name {
787        parse_aux_name(aux_name).ok_or_else(|| NcsAsmError::Parse {
788            line,
789            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("unknown instruction auxcode {0:?}",
                aux_name))
    })format!("unknown instruction auxcode {aux_name:?}"),
790        })?
791    } else {
792        default_aux_for_opcode(opcode)
793    };
794    Ok((opcode, auxcode))
795}
796
797fn default_aux_for_opcode(opcode: NcsOpcode) -> NcsAuxCode {
798    match opcode {
799        NcsOpcode::Assignment
800        | NcsOpcode::AssignmentBase
801        | NcsOpcode::RunstackCopy
802        | NcsOpcode::RunstackCopyBase => NcsAuxCode::TypeVoid,
803        _ => NcsAuxCode::None,
804    }
805}
806
807fn parse_opcode_name(name: &str) -> Option<NcsOpcode> {
808    match name {
809        "CPDOWNSP" | "ASSIGNMENT" => Some(NcsOpcode::Assignment),
810        "RSADD" | "RUNSTACK_ADD" => Some(NcsOpcode::RunstackAdd),
811        "CPTOPSP" | "RUNSTACK_COPY" => Some(NcsOpcode::RunstackCopy),
812        "CONST" | "CONSTANT" => Some(NcsOpcode::Constant),
813        "ACTION" | "EXECUTE_COMMAND" => Some(NcsOpcode::ExecuteCommand),
814        "LOGAND" | "LOGICAL_AND" => Some(NcsOpcode::LogicalAnd),
815        "LOGOR" | "LOGICAL_OR" => Some(NcsOpcode::LogicalOr),
816        "INCOR" | "INCLUSIVE_OR" => Some(NcsOpcode::InclusiveOr),
817        "EXCOR" | "EXCLUSIVE_OR" => Some(NcsOpcode::ExclusiveOr),
818        "BOOLAND" | "BOOLEAN_AND" => Some(NcsOpcode::BooleanAnd),
819        "EQUAL" => Some(NcsOpcode::Equal),
820        "NEQUAL" | "NOT_EQUAL" => Some(NcsOpcode::NotEqual),
821        "GEQ" => Some(NcsOpcode::Geq),
822        "GT" => Some(NcsOpcode::Gt),
823        "LT" => Some(NcsOpcode::Lt),
824        "LEQ" => Some(NcsOpcode::Leq),
825        "SHLEFT" | "SHIFT_LEFT" => Some(NcsOpcode::ShiftLeft),
826        "SHRIGHT" | "SHIFT_RIGHT" => Some(NcsOpcode::ShiftRight),
827        "USHRIGHT" | "USHIFT_RIGHT" => Some(NcsOpcode::UShiftRight),
828        "ADD" => Some(NcsOpcode::Add),
829        "SUB" => Some(NcsOpcode::Sub),
830        "MUL" => Some(NcsOpcode::Mul),
831        "DIV" => Some(NcsOpcode::Div),
832        "MOD" | "MODULUS" => Some(NcsOpcode::Modulus),
833        "NEG" | "NEGATION" => Some(NcsOpcode::Negation),
834        "COMP" | "ONES_COMPLEMENT" => Some(NcsOpcode::OnesComplement),
835        "MOVSP" | "MODIFY_STACK_POINTER" => Some(NcsOpcode::ModifyStackPointer),
836        "STOREIP" | "STORE_IP" => Some(NcsOpcode::StoreIp),
837        "JMP" => Some(NcsOpcode::Jmp),
838        "JSR" => Some(NcsOpcode::Jsr),
839        "JZ" => Some(NcsOpcode::Jz),
840        "RET" => Some(NcsOpcode::Ret),
841        "DESTRUCT" | "DE_STRUCT" => Some(NcsOpcode::DeStruct),
842        "NOT" | "BOOLEAN_NOT" => Some(NcsOpcode::BooleanNot),
843        "DECSP" | "DECREMENT" => Some(NcsOpcode::Decrement),
844        "INCSP" | "INCREMENT" => Some(NcsOpcode::Increment),
845        "JNZ" => Some(NcsOpcode::Jnz),
846        "CPDOWNBP" | "ASSIGNMENT_BASE" => Some(NcsOpcode::AssignmentBase),
847        "CPTOPBP" | "RUNSTACK_COPY_BASE" => Some(NcsOpcode::RunstackCopyBase),
848        "DECBP" | "DECREMENT_BASE" => Some(NcsOpcode::DecrementBase),
849        "INCBP" | "INCREMENT_BASE" => Some(NcsOpcode::IncrementBase),
850        "SAVEBP" | "SAVE_BASE_POINTER" => Some(NcsOpcode::SaveBasePointer),
851        "RESTOREBP" | "RESTORE_BASE_POINTER" => Some(NcsOpcode::RestoreBasePointer),
852        "STORESTATE" | "STORE_STATE" => Some(NcsOpcode::StoreState),
853        "NOP" | "NO_OPERATION" => Some(NcsOpcode::NoOperation),
854        _ => None,
855    }
856}
857
858fn parse_aux_name(name: &str) -> Option<NcsAuxCode> {
859    match name {
860        "NONE" => Some(NcsAuxCode::None),
861        "TYPE_VOID" => Some(NcsAuxCode::TypeVoid),
862        "TYPE_COMMAND" => Some(NcsAuxCode::TypeCommand),
863        "I" | "TYPE_INTEGER" => Some(NcsAuxCode::TypeInteger),
864        "F" | "TYPE_FLOAT" => Some(NcsAuxCode::TypeFloat),
865        "S" | "TYPE_STRING" => Some(NcsAuxCode::TypeString),
866        "O" | "TYPE_OBJECT" => Some(NcsAuxCode::TypeObject),
867        "E0" | "TYPE_ENGST0" => Some(NcsAuxCode::TypeEngst0),
868        "E1" | "TYPE_ENGST1" => Some(NcsAuxCode::TypeEngst1),
869        "E2" | "TYPE_ENGST2" => Some(NcsAuxCode::TypeEngst2),
870        "E3" | "TYPE_ENGST3" => Some(NcsAuxCode::TypeEngst3),
871        "E4" | "TYPE_ENGST4" => Some(NcsAuxCode::TypeEngst4),
872        "E5" | "TYPE_ENGST5" => Some(NcsAuxCode::TypeEngst5),
873        "E6" | "TYPE_ENGST6" => Some(NcsAuxCode::TypeEngst6),
874        "E7" | "TYPE_ENGST7" => Some(NcsAuxCode::TypeEngst7),
875        "E8" | "TYPE_ENGST8" => Some(NcsAuxCode::TypeEngst8),
876        "E9" | "TYPE_ENGST9" => Some(NcsAuxCode::TypeEngst9),
877        "II" | "TYPETYPE_INTEGER_INTEGER" => Some(NcsAuxCode::TypeTypeIntegerInteger),
878        "FF" | "TYPETYPE_FLOAT_FLOAT" => Some(NcsAuxCode::TypeTypeFloatFloat),
879        "OO" | "TYPETYPE_OBJECT_OBJECT" => Some(NcsAuxCode::TypeTypeObjectObject),
880        "SS" | "TYPETYPE_STRING_STRING" => Some(NcsAuxCode::TypeTypeStringString),
881        "TT" | "TYPETYPE_STRUCT_STRUCT" => Some(NcsAuxCode::TypeTypeStructStruct),
882        "IF" | "TYPETYPE_INTEGER_FLOAT" => Some(NcsAuxCode::TypeTypeIntegerFloat),
883        "FI" | "TYPETYPE_FLOAT_INTEGER" => Some(NcsAuxCode::TypeTypeFloatInteger),
884        "E0E0" | "TYPETYPE_ENGST0_ENGST0" => Some(NcsAuxCode::TypeTypeEngst0Engst0),
885        "E1E1" | "TYPETYPE_ENGST1_ENGST1" => Some(NcsAuxCode::TypeTypeEngst1Engst1),
886        "E2E2" | "TYPETYPE_ENGST2_ENGST2" => Some(NcsAuxCode::TypeTypeEngst2Engst2),
887        "E3E3" | "TYPETYPE_ENGST3_ENGST3" => Some(NcsAuxCode::TypeTypeEngst3Engst3),
888        "E4E4" | "TYPETYPE_ENGST4_ENGST4" => Some(NcsAuxCode::TypeTypeEngst4Engst4),
889        "E5E5" | "TYPETYPE_ENGST5_ENGST5" => Some(NcsAuxCode::TypeTypeEngst5Engst5),
890        "E6E6" | "TYPETYPE_ENGST6_ENGST6" => Some(NcsAuxCode::TypeTypeEngst6Engst6),
891        "E7E7" | "TYPETYPE_ENGST7_ENGST7" => Some(NcsAuxCode::TypeTypeEngst7Engst7),
892        "E8E8" | "TYPETYPE_ENGST8_ENGST8" => Some(NcsAuxCode::TypeTypeEngst8Engst8),
893        "E9E9" | "TYPETYPE_ENGST9_ENGST9" => Some(NcsAuxCode::TypeTypeEngst9Engst9),
894        "VV" | "TYPETYPE_VECTOR_VECTOR" => Some(NcsAuxCode::TypeTypeVectorVector),
895        "VF" | "TYPETYPE_VECTOR_FLOAT" => Some(NcsAuxCode::TypeTypeVectorFloat),
896        "FV" | "TYPETYPE_FLOAT_VECTOR" => Some(NcsAuxCode::TypeTypeFloatVector),
897        "EVAL_INPLACE" => Some(NcsAuxCode::EvalInplace),
898        "EVAL_POSTPLACE" => Some(NcsAuxCode::EvalPostplace),
899        _ => None,
900    }
901}
902
903fn parse_instruction_operand(
904    opcode: NcsOpcode,
905    auxcode: NcsAuxCode,
906    extra: &str,
907    langspec: Option<&LangSpec>,
908    line: usize,
909) -> Result<ParsedAsmOperand, NcsAsmError> {
910    let extra = extra.trim();
911    match opcode {
912        NcsOpcode::Constant => parse_constant_operand(auxcode, extra, line),
913        NcsOpcode::Jmp | NcsOpcode::Jsr | NcsOpcode::Jz | NcsOpcode::Jnz => {
914            parse_jump_operand(extra, line)
915        }
916        NcsOpcode::StoreState => parse_store_state_operand(extra, line),
917        NcsOpcode::ModifyStackPointer
918        | NcsOpcode::Increment
919        | NcsOpcode::Decrement
920        | NcsOpcode::IncrementBase
921        | NcsOpcode::DecrementBase => parse_single_number_bytes::<i32>(extra, line),
922        NcsOpcode::ExecuteCommand => parse_action_operand(extra, langspec, line),
923        NcsOpcode::RunstackCopy | NcsOpcode::RunstackCopyBase => {
924            parse_runstack_copy_operand(extra, line)
925        }
926        NcsOpcode::Assignment | NcsOpcode::AssignmentBase => parse_assignment_operand(extra, line),
927        NcsOpcode::DeStruct => parse_destruct_operand(extra, line),
928        NcsOpcode::Equal | NcsOpcode::NotEqual if auxcode == NcsAuxCode::TypeTypeStructStruct => {
929            parse_single_number_bytes::<u16>(extra, line)
930        }
931        _ if extra.is_empty() => Ok(ParsedAsmOperand::None),
932        _ => Err(NcsAsmError::Parse {
933            line,
934            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("instruction {0} does not accept operands {1:?}",
                opcode, extra))
    })format!("instruction {opcode} does not accept operands {extra:?}"),
935        }),
936    }
937}
938
939fn parse_store_state_operand(extra: &str, line: usize) -> Result<ParsedAsmOperand, NcsAsmError> {
940    let parts = split_csv(extra);
941    let [first, second] = parts.as_slice() else {
942        return Err(NcsAsmError::Parse {
943            line,
944            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("expected `a, b`, found {0:?}",
                extra))
    })format!("expected `a, b`, found {extra:?}"),
945        });
946    };
947    let first = parse_i32_like(first, line)?;
948    let second = parse_i32_like(second, line)?;
949    Ok(ParsedAsmOperand::Bytes(
950        [
951            first.to_be_bytes().as_slice(),
952            second.to_be_bytes().as_slice(),
953        ]
954        .concat(),
955    ))
956}
957
958fn parse_constant_operand(
959    auxcode: NcsAuxCode,
960    extra: &str,
961    line: usize,
962) -> Result<ParsedAsmOperand, NcsAsmError> {
963    match auxcode {
964        NcsAuxCode::TypeString | NcsAuxCode::TypeEngst7 => {
965            let value = unescape_nwasm_string(extra, line)?;
966            let length = u16::try_from(value.len()).map_err(|_error| NcsAsmError::Parse {
967                line,
968                message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("string operand too long: {0} bytes",
                value.len()))
    })format!("string operand too long: {} bytes", value.len()),
969            })?;
970            Ok(ParsedAsmOperand::Bytes(
971                [length.to_be_bytes().as_slice(), value.as_bytes()].concat(),
972            ))
973        }
974        NcsAuxCode::TypeInteger => parse_single_number_bytes::<i32>(extra, line),
975        NcsAuxCode::TypeFloat => {
976            let value = extra.parse::<f32>().map_err(|error| NcsAsmError::Parse {
977                line,
978                message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("invalid float operand {0:?}: {1}",
                extra, error))
    })format!("invalid float operand {extra:?}: {error}"),
979            })?;
980            Ok(ParsedAsmOperand::Bytes(
981                value.to_bits().to_be_bytes().to_vec(),
982            ))
983        }
984        NcsAuxCode::TypeObject => {
985            let value = parse_i32_like(extra, line)?;
986            Ok(ParsedAsmOperand::Bytes(value.to_be_bytes().to_vec()))
987        }
988        NcsAuxCode::TypeEngst2 => {
989            let value = parse_u32_like(extra, line)?;
990            Ok(ParsedAsmOperand::Bytes(value.to_be_bytes().to_vec()))
991        }
992        _ if extra.is_empty() => Ok(ParsedAsmOperand::None),
993        _ => Err(NcsAsmError::Parse {
994            line,
995            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("unsupported CONST operand for auxcode {0:?}",
                auxcode))
    })format!("unsupported CONST operand for auxcode {auxcode:?}"),
996        }),
997    }
998}
999
1000fn parse_jump_operand(extra: &str, line: usize) -> Result<ParsedAsmOperand, NcsAsmError> {
1001    if extra.is_empty() {
1002        return Err(NcsAsmError::Parse {
1003            line,
1004            message: "missing jump target".to_string(),
1005        });
1006    }
1007    if let Some(offset) = parse_usize_like(extra) {
1008        return Ok(ParsedAsmOperand::Jump(AsmJumpTarget::Offset(offset)));
1009    }
1010    Ok(ParsedAsmOperand::Jump(AsmJumpTarget::Label(
1011        extra.to_string(),
1012    )))
1013}
1014
1015fn parse_action_operand(
1016    extra: &str,
1017    langspec: Option<&LangSpec>,
1018    line: usize,
1019) -> Result<ParsedAsmOperand, NcsAsmError> {
1020    let parts = split_csv(extra);
1021    match parts.as_slice() {
1022        [id, argc] => {
1023            let id = parse_u16_like(id, line)?;
1024            let argc = parse_u8_like(argc, line)?;
1025            Ok(ParsedAsmOperand::Bytes(
1026                [id.to_be_bytes().as_slice(), &[argc]].concat(),
1027            ))
1028        }
1029        [name] if !name.is_empty() => {
1030            let Some(spec) = langspec else {
1031                return Err(NcsAsmError::Parse {
1032                    line,
1033                    message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("ACTION operand {0:?} requires langspec or explicit `id, argc`",
                name))
    })format!(
1034                        "ACTION operand {name:?} requires langspec or explicit `id, argc`"
1035                    ),
1036                });
1037            };
1038            let (builtin_id, function) = spec
1039                .functions
1040                .iter()
1041                .enumerate()
1042                .find(|(_index, function)| function.name == *name)
1043                .ok_or_else(|| NcsAsmError::Parse {
1044                    line,
1045                    message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("unknown ACTION builtin {0:?}",
                name))
    })format!("unknown ACTION builtin {name:?}"),
1046                })?;
1047            let argc =
1048                u8::try_from(function.parameters.len()).map_err(|_error| NcsAsmError::Parse {
1049                    line,
1050                    message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("ACTION builtin {0:?} has too many parameters to infer argc",
                name))
    })format!(
1051                        "ACTION builtin {name:?} has too many parameters to infer argc"
1052                    ),
1053                })?;
1054            let builtin_id = u16::try_from(builtin_id).map_err(|_error| NcsAsmError::Parse {
1055                line,
1056                message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("ACTION builtin index out of range for {0:?}",
                name))
    })format!("ACTION builtin index out of range for {name:?}"),
1057            })?;
1058            Ok(ParsedAsmOperand::Bytes(
1059                [builtin_id.to_be_bytes().as_slice(), &[argc]].concat(),
1060            ))
1061        }
1062        _ => Err(NcsAsmError::Parse {
1063            line,
1064            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("invalid ACTION operand {0:?}",
                extra))
    })format!("invalid ACTION operand {extra:?}"),
1065        }),
1066    }
1067}
1068
1069fn parse_runstack_copy_operand(extra: &str, line: usize) -> Result<ParsedAsmOperand, NcsAsmError> {
1070    let parts = split_csv(extra);
1071    let [offset, size] = parts.as_slice() else {
1072        return Err(NcsAsmError::Parse {
1073            line,
1074            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("expected `offset, size`, found {0:?}",
                extra))
    })format!("expected `offset, size`, found {extra:?}"),
1075        });
1076    };
1077    let offset = parse_i32_like(offset, line)?;
1078    let size = parse_i16_like(size, line)?;
1079    Ok(ParsedAsmOperand::Bytes(
1080        [
1081            offset.to_be_bytes().as_slice(),
1082            size.to_be_bytes().as_slice(),
1083        ]
1084        .concat(),
1085    ))
1086}
1087
1088fn parse_assignment_operand(extra: &str, line: usize) -> Result<ParsedAsmOperand, NcsAsmError> {
1089    let parts = split_csv(extra);
1090    let [offset, size] = parts.as_slice() else {
1091        return Err(NcsAsmError::Parse {
1092            line,
1093            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("expected `offset, size`, found {0:?}",
                extra))
    })format!("expected `offset, size`, found {extra:?}"),
1094        });
1095    };
1096    let offset = parse_i32_like(offset, line)?;
1097    let size = parse_u16_like(size, line)?;
1098    Ok(ParsedAsmOperand::Bytes(
1099        [
1100            offset.to_be_bytes().as_slice(),
1101            size.to_be_bytes().as_slice(),
1102        ]
1103        .concat(),
1104    ))
1105}
1106
1107fn parse_destruct_operand(extra: &str, line: usize) -> Result<ParsedAsmOperand, NcsAsmError> {
1108    let parts = split_csv(extra);
1109    let [first, second, third] = parts.as_slice() else {
1110        return Err(NcsAsmError::Parse {
1111            line,
1112            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("expected `a, b, c`, found {0:?}",
                extra))
    })format!("expected `a, b, c`, found {extra:?}"),
1113        });
1114    };
1115    let first = parse_u16_like(first, line)?;
1116    let second = parse_u16_like(second, line)?;
1117    let third = parse_u16_like(third, line)?;
1118    Ok(ParsedAsmOperand::Bytes(
1119        [
1120            first.to_be_bytes().as_slice(),
1121            second.to_be_bytes().as_slice(),
1122            third.to_be_bytes().as_slice(),
1123        ]
1124        .concat(),
1125    ))
1126}
1127
1128fn parse_single_number_bytes<T>(extra: &str, line: usize) -> Result<ParsedAsmOperand, NcsAsmError>
1129where
1130    T: ParseAsmNumber,
1131{
1132    let value = T::parse(extra, line)?;
1133    Ok(ParsedAsmOperand::Bytes(value.to_be_bytes_vec()))
1134}
1135
1136fn split_csv(input: &str) -> Vec<&str> {
1137    input.split(',').map(str::trim).collect()
1138}
1139
1140trait ParseAsmNumber {
1141    fn parse(input: &str, line: usize) -> Result<Self, NcsAsmError>
1142    where
1143        Self: Sized;
1144    fn to_be_bytes_vec(self) -> Vec<u8>;
1145}
1146
1147impl ParseAsmNumber for i32 {
1148    fn parse(input: &str, line: usize) -> Result<Self, NcsAsmError> {
1149        parse_i32_like(input, line)
1150    }
1151
1152    fn to_be_bytes_vec(self) -> Vec<u8> {
1153        self.to_be_bytes().to_vec()
1154    }
1155}
1156
1157impl ParseAsmNumber for u16 {
1158    fn parse(input: &str, line: usize) -> Result<Self, NcsAsmError> {
1159        parse_u16_like(input, line)
1160    }
1161
1162    fn to_be_bytes_vec(self) -> Vec<u8> {
1163        self.to_be_bytes().to_vec()
1164    }
1165}
1166
1167fn parse_i32_like(input: &str, line: usize) -> Result<i32, NcsAsmError> {
1168    let input = input.trim();
1169    if let Some(hex) = input
1170        .strip_prefix("0x")
1171        .or_else(|| input.strip_prefix("0X"))
1172    {
1173        u32::from_str_radix(hex, 16)
1174            .map(|value| i32::from_be_bytes(value.to_be_bytes()))
1175            .map_err(|error| NcsAsmError::Parse {
1176                line,
1177                message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("invalid hex i32 operand {0:?}: {1}",
                input, error))
    })format!("invalid hex i32 operand {input:?}: {error}"),
1178            })
1179    } else {
1180        input.parse::<i32>().map_err(|error| NcsAsmError::Parse {
1181            line,
1182            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("invalid i32 operand {0:?}: {1}",
                input, error))
    })format!("invalid i32 operand {input:?}: {error}"),
1183        })
1184    }
1185}
1186
1187fn parse_i16_like(input: &str, line: usize) -> Result<i16, NcsAsmError> {
1188    input
1189        .trim()
1190        .parse::<i16>()
1191        .map_err(|error| NcsAsmError::Parse {
1192            line,
1193            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("invalid i16 operand {0:?}: {1}",
                input, error))
    })format!("invalid i16 operand {input:?}: {error}"),
1194        })
1195}
1196
1197fn parse_u16_like(input: &str, line: usize) -> Result<u16, NcsAsmError> {
1198    let input = input.trim();
1199    if let Some(hex) = input
1200        .strip_prefix("0x")
1201        .or_else(|| input.strip_prefix("0X"))
1202    {
1203        u16::from_str_radix(hex, 16).map_err(|error| NcsAsmError::Parse {
1204            line,
1205            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("invalid hex u16 operand {0:?}: {1}",
                input, error))
    })format!("invalid hex u16 operand {input:?}: {error}"),
1206        })
1207    } else {
1208        input.parse::<u16>().map_err(|error| NcsAsmError::Parse {
1209            line,
1210            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("invalid u16 operand {0:?}: {1}",
                input, error))
    })format!("invalid u16 operand {input:?}: {error}"),
1211        })
1212    }
1213}
1214
1215fn parse_u8_like(input: &str, line: usize) -> Result<u8, NcsAsmError> {
1216    input
1217        .trim()
1218        .parse::<u8>()
1219        .map_err(|error| NcsAsmError::Parse {
1220            line,
1221            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("invalid u8 operand {0:?}: {1}",
                input, error))
    })format!("invalid u8 operand {input:?}: {error}"),
1222        })
1223}
1224
1225fn parse_u32_like(input: &str, line: usize) -> Result<u32, NcsAsmError> {
1226    let input = input.trim();
1227    if let Some(hex) = input
1228        .strip_prefix("0x")
1229        .or_else(|| input.strip_prefix("0X"))
1230    {
1231        u32::from_str_radix(hex, 16).map_err(|error| NcsAsmError::Parse {
1232            line,
1233            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("invalid hex u32 operand {0:?}: {1}",
                input, error))
    })format!("invalid hex u32 operand {input:?}: {error}"),
1234        })
1235    } else {
1236        input.parse::<u32>().map_err(|error| NcsAsmError::Parse {
1237            line,
1238            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("invalid u32 operand {0:?}: {1}",
                input, error))
    })format!("invalid u32 operand {input:?}: {error}"),
1239        })
1240    }
1241}
1242
1243fn parse_usize_like(input: &str) -> Option<usize> {
1244    let input = input.trim();
1245    input.parse::<usize>().ok().or_else(|| {
1246        input
1247            .strip_prefix("0x")
1248            .or_else(|| input.strip_prefix("0X"))
1249            .and_then(|hex| usize::from_str_radix(hex, 16).ok())
1250    })
1251}
1252
1253fn unescape_nwasm_string(input: &str, line: usize) -> Result<String, NcsAsmError> {
1254    let mut chars = input.chars().peekable();
1255    let mut output = String::new();
1256    while let Some(ch) = chars.next() {
1257        if ch != '\\' {
1258            output.push(ch);
1259            continue;
1260        }
1261        let escaped = chars.next().ok_or_else(|| NcsAsmError::Parse {
1262            line,
1263            message: "dangling string escape".to_string(),
1264        })?;
1265        match escaped {
1266            '\\' => output.push('\\'),
1267            '\'' => output.push('\''),
1268            '"' => output.push('"'),
1269            'n' => output.push('\n'),
1270            'r' => output.push('\r'),
1271            't' => output.push('\t'),
1272            '0' => output.push('\0'),
1273            'x' => {
1274                let hi = chars.next().ok_or_else(|| NcsAsmError::Parse {
1275                    line,
1276                    message: "incomplete \\xNN string escape".to_string(),
1277                })?;
1278                let lo = chars.next().ok_or_else(|| NcsAsmError::Parse {
1279                    line,
1280                    message: "incomplete \\xNN string escape".to_string(),
1281                })?;
1282                let value = u8::from_str_radix(&::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}{1}", hi, lo))
    })format!("{hi}{lo}"), 16).map_err(|error| {
1283                    NcsAsmError::Parse {
1284                        line,
1285                        message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("invalid \\x escape: {0}", error))
    })format!("invalid \\x escape: {error}"),
1286                    }
1287                })?;
1288                output.push(char::from(value));
1289            }
1290            'u' => {
1291                if chars.next() != Some('{') {
1292                    return Err(NcsAsmError::Parse {
1293                        line,
1294                        message: "expected `\\u{...}` escape".to_string(),
1295                    });
1296                }
1297                let mut digits = String::new();
1298                loop {
1299                    let next = chars.next().ok_or_else(|| NcsAsmError::Parse {
1300                        line,
1301                        message: "unterminated `\\u{...}` escape".to_string(),
1302                    })?;
1303                    if next == '}' {
1304                        break;
1305                    }
1306                    digits.push(next);
1307                }
1308                let value =
1309                    u32::from_str_radix(&digits, 16).map_err(|error| NcsAsmError::Parse {
1310                        line,
1311                        message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("invalid unicode escape: {0}",
                error))
    })format!("invalid unicode escape: {error}"),
1312                    })?;
1313                let ch = char::from_u32(value).ok_or_else(|| NcsAsmError::Parse {
1314                    line,
1315                    message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("invalid unicode scalar value {0:#x}",
                value))
    })format!("invalid unicode scalar value {value:#x}"),
1316                })?;
1317                output.push(ch);
1318            }
1319            other => {
1320                return Err(NcsAsmError::Parse {
1321                    line,
1322                    message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("unsupported string escape \\{0}",
                other))
    })format!("unsupported string escape \\{other}"),
1323                });
1324            }
1325        }
1326    }
1327    Ok(output)
1328}
1329
1330fn truncate_nwasm_string(value: &str, max_string_length: usize) -> String {
1331    let escaped = value
1332        .chars()
1333        .take(max_string_length)
1334        .flat_map(char::escape_default)
1335        .collect::<String>();
1336    if value.len() > max_string_length {
1337        ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{1}..{0}", value.len(), escaped))
    })format!("{escaped}..{}", value.len())
1338    } else {
1339        escaped
1340    }
1341}
1342
1343fn collect_jump_labels(instructions: &[NcsInstruction]) -> BTreeMap<usize, String> {
1344    let mut offset = 0usize;
1345    let mut targets = BTreeSet::new();
1346    let mut labels = BTreeMap::new();
1347
1348    for instruction in instructions {
1349        if let Some(target) = jump_target_for_instruction(instruction, offset) {
1350            targets.insert((instruction.opcode as u8, target));
1351        }
1352        offset += instruction.encoded_len();
1353    }
1354
1355    for (opcode, target) in targets {
1356        let prefix = if opcode == NcsOpcode::Jsr as u8 {
1357            "sub"
1358        } else {
1359            "loc"
1360        };
1361        labels
1362            .entry(target)
1363            .or_insert_with(|| ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}_{1:04}", prefix, target))
    })format!("{prefix}_{target:04}"));
1364    }
1365
1366    labels
1367}
1368
1369fn jump_target_for_instruction(instruction: &NcsInstruction, offset: usize) -> Option<usize> {
1370    if !#[allow(non_exhaustive_omitted_patterns)] match instruction.opcode {
    NcsOpcode::Jmp | NcsOpcode::Jsr | NcsOpcode::Jz | NcsOpcode::Jnz => true,
    _ => false,
}matches!(
1371        instruction.opcode,
1372        NcsOpcode::Jmp | NcsOpcode::Jsr | NcsOpcode::Jz | NcsOpcode::Jnz
1373    ) {
1374        return None;
1375    }
1376    read_i32(&instruction.extra, offset, instruction)
1377        .ok()
1378        .map(|relative| jump_target(offset, relative))
1379}
1380
1381fn jump_target(offset: usize, relative: i32) -> usize {
1382    let base = i64::try_from(offset).ok().unwrap_or(i64::MAX);
1383    let target = base.saturating_add(i64::from(relative));
1384    usize::try_from(target.max(0)).ok().unwrap_or(0)
1385}
1386
1387fn format_jump_target(
1388    opcode: NcsOpcode,
1389    target: usize,
1390    labels: &BTreeMap<usize, String>,
1391) -> String {
1392    if let Some(label) = labels.get(&target) {
1393        label.clone()
1394    } else if opcode == NcsOpcode::Jsr {
1395        ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("sub_{0:04}", target))
    })format!("sub_{target:04}")
1396    } else {
1397        ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("loc_{0:04}", target))
    })format!("loc_{target:04}")
1398    }
1399}
1400
1401fn sorted_functions(ndb: &Ndb) -> Vec<&NdbFunction> {
1402    let mut functions = ndb.functions.iter().collect::<Vec<_>>();
1403    functions.sort_by_key(|function| function.binary_start);
1404    functions
1405}
1406
1407fn line_in_function(offset: usize, function: &NdbFunction) -> bool {
1408    let start = function.binary_start.saturating_sub(13) as usize;
1409    let end = function.binary_end.saturating_sub(13) as usize;
1410    (start..end).contains(&offset)
1411}
1412
1413fn local_offset(offset: usize, function: &NdbFunction) -> usize {
1414    let start = function.binary_start.saturating_sub(13) as usize;
1415    offset.saturating_sub(start)
1416}
1417
1418fn render_function_header(function: &NdbFunction, ndb: &Ndb) -> String {
1419    let args = function
1420        .args
1421        .iter()
1422        .map(ToString::to_string)
1423        .collect::<Vec<_>>()
1424        .join(", ");
1425    let start = function.binary_start.saturating_sub(13);
1426    let end = function.binary_end.saturating_sub(13);
1427    let location = source_location_for_function(function, ndb).map_or_else(
1428        || " ".to_string(),
1429        |(file, line)| ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!(" {1}.nss:{0} ",
                line.saturating_sub(1), file))
    })format!(" {file}.nss:{} ", line.saturating_sub(1)),
1430    );
1431
1432    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} {1}({2}):{3}[{4}:{5}]",
                function.return_type, function.label, args, location, start,
                end))
    })format!(
1433        "{} {}({}):{}[{}:{}]",
1434        function.return_type, function.label, args, location, start, end
1435    )
1436}
1437
1438fn source_location_for_function(function: &NdbFunction, ndb: &Ndb) -> Option<(String, usize)> {
1439    ndb.lines
1440        .iter()
1441        .find(|line| line.binary_start == function.binary_start)
1442        .and_then(|line| {
1443            ndb.files
1444                .get(line.file_num)
1445                .map(|file| (file.name.clone(), line.line_num))
1446        })
1447}
1448
1449fn source_line_for_offset(
1450    offset: usize,
1451    ndb: &Ndb,
1452    source_files: Option<&BTreeMap<String, Vec<String>>>,
1453    last_source: &mut Option<(usize, usize)>,
1454) -> Option<(String, String)> {
1455    let line = unique_line_for_offset(offset, &ndb.lines)?;
1456    let key = (line.file_num, line.line_num);
1457    if last_source.as_ref() == Some(&key) {
1458        return None;
1459    }
1460    *last_source = Some(key);
1461
1462    let file = ndb.files.get(line.file_num)?;
1463    let source_files = source_files?;
1464    let lines = source_files.get(&file.name)?;
1465    let text = lines
1466        .get(line.line_num.saturating_sub(1))?
1467        .trim()
1468        .to_string();
1469    Some((
1470        ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}.nss:{1}", file.name,
                line.line_num.saturating_sub(1)))
    })format!("{}.nss:{}", file.name, line.line_num.saturating_sub(1)),
1471        text,
1472    ))
1473}
1474
1475fn unique_line_for_offset(offset: usize, lines: &[NdbLine]) -> Option<&NdbLine> {
1476    let matches = lines
1477        .iter()
1478        .filter(|line| {
1479            let start = line.binary_start.saturating_sub(13) as usize;
1480            let end = line.binary_end.saturating_sub(13) as usize;
1481            (start..end).contains(&offset)
1482        })
1483        .collect::<Vec<_>>();
1484    if matches.len() == 1 {
1485        matches.into_iter().next()
1486    } else {
1487        None
1488    }
1489}
1490
1491fn decode_prefixed_string(
1492    extra: &[u8],
1493    offset: usize,
1494    instruction: &NcsInstruction,
1495) -> Result<String, NcsAsmError> {
1496    let length = usize::from(read_u16(extra, offset, instruction)?);
1497    let payload = extra
1498        .get(2..2 + length)
1499        .ok_or_else(|| NcsAsmError::InvalidExtra {
1500            offset,
1501            opcode: instruction.opcode,
1502            auxcode: instruction.auxcode,
1503            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("expected {0} string bytes",
                length))
    })format!("expected {length} string bytes"),
1504        })?;
1505    Ok(String::from_utf8_lossy(payload).to_string())
1506}
1507
1508fn read_u8_part(
1509    extra: &[u8],
1510    start: usize,
1511    offset: usize,
1512    instruction: &NcsInstruction,
1513) -> Result<u8, NcsAsmError> {
1514    extra
1515        .get(start)
1516        .copied()
1517        .ok_or_else(|| NcsAsmError::InvalidExtra {
1518            offset,
1519            opcode: instruction.opcode,
1520            auxcode: instruction.auxcode,
1521            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("expected byte at offset {0}",
                start))
    })format!("expected byte at offset {start}"),
1522        })
1523}
1524
1525fn read_u16(extra: &[u8], offset: usize, instruction: &NcsInstruction) -> Result<u16, NcsAsmError> {
1526    read_u16_part(extra, 0, offset, instruction)
1527}
1528
1529fn read_u16_part(
1530    extra: &[u8],
1531    start: usize,
1532    offset: usize,
1533    instruction: &NcsInstruction,
1534) -> Result<u16, NcsAsmError> {
1535    let bytes: [u8; 2] = extra
1536        .get(start..start + 2)
1537        .ok_or_else(|| NcsAsmError::InvalidExtra {
1538            offset,
1539            opcode: instruction.opcode,
1540            auxcode: instruction.auxcode,
1541            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("expected 2 bytes at offset {0}",
                start))
    })format!("expected 2 bytes at offset {start}"),
1542        })?
1543        .try_into()
1544        .map_err(|_error| NcsAsmError::InvalidExtra {
1545            offset,
1546            opcode: instruction.opcode,
1547            auxcode: instruction.auxcode,
1548            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("expected 2 bytes at offset {0}",
                start))
    })format!("expected 2 bytes at offset {start}"),
1549        })?;
1550    Ok(u16::from_be_bytes(bytes))
1551}
1552
1553fn read_i16_part(
1554    extra: &[u8],
1555    start: usize,
1556    offset: usize,
1557    instruction: &NcsInstruction,
1558) -> Result<i16, NcsAsmError> {
1559    let bytes: [u8; 2] = extra
1560        .get(start..start + 2)
1561        .ok_or_else(|| NcsAsmError::InvalidExtra {
1562            offset,
1563            opcode: instruction.opcode,
1564            auxcode: instruction.auxcode,
1565            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("expected 2 bytes at offset {0}",
                start))
    })format!("expected 2 bytes at offset {start}"),
1566        })?
1567        .try_into()
1568        .map_err(|_error| NcsAsmError::InvalidExtra {
1569            offset,
1570            opcode: instruction.opcode,
1571            auxcode: instruction.auxcode,
1572            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("expected 2 bytes at offset {0}",
                start))
    })format!("expected 2 bytes at offset {start}"),
1573        })?;
1574    Ok(i16::from_be_bytes(bytes))
1575}
1576
1577fn read_i32(extra: &[u8], offset: usize, instruction: &NcsInstruction) -> Result<i32, NcsAsmError> {
1578    read_i32_part(extra, 0, offset, instruction)
1579}
1580
1581fn read_i32_part(
1582    extra: &[u8],
1583    start: usize,
1584    offset: usize,
1585    instruction: &NcsInstruction,
1586) -> Result<i32, NcsAsmError> {
1587    let bytes: [u8; 4] = extra
1588        .get(start..start + 4)
1589        .ok_or_else(|| NcsAsmError::InvalidExtra {
1590            offset,
1591            opcode: instruction.opcode,
1592            auxcode: instruction.auxcode,
1593            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("expected 4 bytes at offset {0}",
                start))
    })format!("expected 4 bytes at offset {start}"),
1594        })?
1595        .try_into()
1596        .map_err(|_error| NcsAsmError::InvalidExtra {
1597            offset,
1598            opcode: instruction.opcode,
1599            auxcode: instruction.auxcode,
1600            message: ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("expected 4 bytes at offset {0}",
                start))
    })format!("expected 4 bytes at offset {start}"),
1601        })?;
1602    Ok(i32::from_be_bytes(bytes))
1603}
1604
1605fn read_u32(extra: &[u8], offset: usize, instruction: &NcsInstruction) -> Result<u32, NcsAsmError> {
1606    let bytes: [u8; 4] = extra
1607        .get(..4)
1608        .ok_or_else(|| NcsAsmError::InvalidExtra {
1609            offset,
1610            opcode: instruction.opcode,
1611            auxcode: instruction.auxcode,
1612            message: "expected 4 bytes".to_string(),
1613        })?
1614        .try_into()
1615        .map_err(|_error| NcsAsmError::InvalidExtra {
1616            offset,
1617            opcode: instruction.opcode,
1618            auxcode: instruction.auxcode,
1619            message: "expected 4 bytes".to_string(),
1620        })?;
1621    Ok(u32::from_be_bytes(bytes))
1622}
1623
1624fn read_f32(extra: &[u8], offset: usize, instruction: &NcsInstruction) -> Result<f32, NcsAsmError> {
1625    Ok(f32::from_bits(read_u32(extra, offset, instruction)?))
1626}
1627
1628#[cfg(test)]
1629mod tests {
1630    use std::collections::BTreeMap;
1631
1632    use super::{
1633        NcsAsmLine, NcsDisassemblyOptions, assemble_ncs_bytes, assemble_ncs_text, disassemble_ncs,
1634        render_ncs_disassembly, render_ncs_disassembly_with_ndb,
1635    };
1636    use crate::{
1637        BuiltinFunction, BuiltinType, LangSpec, NcsAuxCode, NcsInstruction, NcsOpcode, Ndb,
1638        NdbFile, NdbFunction, NdbLine, NdbType, encode_ncs_instructions,
1639    };
1640
1641    #[test]
1642    fn instruction_names_match_upstream_nwasm() {
1643        let instruction = NcsInstruction {
1644            opcode:  NcsOpcode::LogicalAnd,
1645            auxcode: NcsAuxCode::TypeTypeIntegerInteger,
1646            extra:   b"not-printed".to_vec(),
1647        };
1648
1649        assert_eq!(instruction.canonical_name(false), "LOGAND.II");
1650        assert_eq!(
1651            instruction.canonical_name(true),
1652            "LOGICAL_AND.TYPETYPE_INTEGER_INTEGER"
1653        );
1654    }
1655
1656    #[test]
1657    fn disassembles_action_names_with_langspec() -> Result<(), Box<dyn std::error::Error>> {
1658        let bytes = encode_ncs_instructions(&[
1659            NcsInstruction {
1660                opcode:  NcsOpcode::ExecuteCommand,
1661                auxcode: NcsAuxCode::None,
1662                extra:   [1_u16.to_be_bytes().as_slice(), &[2_u8]].concat(),
1663            },
1664            NcsInstruction {
1665                opcode:  NcsOpcode::Ret,
1666                auxcode: NcsAuxCode::None,
1667                extra:   Vec::new(),
1668            },
1669        ]);
1670        let spec = LangSpec {
1671            engine_num_structures: 0,
1672            engine_structures:     Vec::new(),
1673            constants:             Vec::new(),
1674            functions:             vec![
1675                BuiltinFunction {
1676                    name:        "First".to_string(),
1677                    return_type: BuiltinType::Void,
1678                    parameters:  Vec::new(),
1679                },
1680                BuiltinFunction {
1681                    name:        "DelayCommand".to_string(),
1682                    return_type: BuiltinType::Void,
1683                    parameters:  Vec::new(),
1684                },
1685            ],
1686        };
1687
1688        let lines = disassemble_ncs(&bytes, Some(&spec), NcsDisassemblyOptions::default())?;
1689        assert_eq!(
1690            lines,
1691            vec![
1692                NcsAsmLine {
1693                    offset:      0,
1694                    label:       None,
1695                    instruction: "ACTION".to_string(),
1696                    extra:       "1, 2".to_string(),
1697                },
1698                NcsAsmLine {
1699                    offset:      5,
1700                    label:       None,
1701                    instruction: "RET".to_string(),
1702                    extra:       String::new(),
1703                },
1704            ]
1705        );
1706        Ok(())
1707    }
1708
1709    #[test]
1710    fn disassembles_string_constants_like_upstream() -> Result<(), Box<dyn std::error::Error>> {
1711        let extra = [5_u16.to_be_bytes().as_slice(), b"hello"].concat();
1712        let bytes = encode_ncs_instructions(&[NcsInstruction {
1713            opcode: NcsOpcode::Constant,
1714            auxcode: NcsAuxCode::TypeString,
1715            extra,
1716        }]);
1717        let lines = disassemble_ncs(&bytes, None, NcsDisassemblyOptions::default())?;
1718
1719        assert_eq!(lines.len(), 1);
1720        let line = lines
1721            .first()
1722            .expect("string disassembly should produce one line");
1723        assert_eq!(line.instruction, "CONST.S");
1724        assert_eq!(line.extra, "hello");
1725        Ok(())
1726    }
1727
1728    #[test]
1729    fn renders_jump_targets_with_labels() -> Result<(), Box<dyn std::error::Error>> {
1730        let bytes = encode_ncs_instructions(&[
1731            NcsInstruction {
1732                opcode:  NcsOpcode::Jz,
1733                auxcode: NcsAuxCode::None,
1734                extra:   8_i32.to_be_bytes().to_vec(),
1735            },
1736            NcsInstruction {
1737                opcode:  NcsOpcode::Ret,
1738                auxcode: NcsAuxCode::None,
1739                extra:   Vec::new(),
1740            },
1741            NcsInstruction {
1742                opcode:  NcsOpcode::Ret,
1743                auxcode: NcsAuxCode::None,
1744                extra:   Vec::new(),
1745            },
1746        ]);
1747
1748        let rendered = render_ncs_disassembly(&bytes, None, NcsDisassemblyOptions::default())?;
1749        assert!(rendered.contains("0000: JZ loc_0008"));
1750        assert!(rendered.contains("loc_0008:"));
1751        Ok(())
1752    }
1753
1754    #[test]
1755    fn renders_function_headers_and_source_weaving_with_ndb()
1756    -> Result<(), Box<dyn std::error::Error>> {
1757        let bytes = encode_ncs_instructions(&[NcsInstruction {
1758            opcode:  NcsOpcode::Ret,
1759            auxcode: NcsAuxCode::None,
1760            extra:   Vec::new(),
1761        }]);
1762        let ndb = Ndb {
1763            files:     vec![NdbFile {
1764                name:    "test".to_string(),
1765                is_root: true,
1766            }],
1767            structs:   Vec::new(),
1768            functions: vec![NdbFunction {
1769                label:        "main".to_string(),
1770                binary_start: 13,
1771                binary_end:   15,
1772                return_type:  NdbType::Void,
1773                args:         Vec::new(),
1774            }],
1775            variables: Vec::new(),
1776            lines:     vec![NdbLine {
1777                file_num:     0,
1778                line_num:     1,
1779                binary_start: 13,
1780                binary_end:   15,
1781            }],
1782        };
1783        let mut sources = BTreeMap::new();
1784        sources.insert("test".to_string(), vec!["void main() {}".to_string()]);
1785
1786        let rendered = render_ncs_disassembly_with_ndb(
1787            &bytes,
1788            None,
1789            Some(&ndb),
1790            Some(&sources),
1791            NcsDisassemblyOptions::default(),
1792        )?;
1793
1794        assert!(rendered.contains("v main(): test.nss:0 [0:2]"));
1795        assert!(rendered.contains("0000 0000: RET | test.nss:0 | void main() {}"));
1796        Ok(())
1797    }
1798
1799    #[test]
1800    fn renders_jsr_targets_as_function_labels_with_ndb() -> Result<(), Box<dyn std::error::Error>> {
1801        let bytes = encode_ncs_instructions(&[
1802            NcsInstruction {
1803                opcode:  NcsOpcode::Jsr,
1804                auxcode: NcsAuxCode::None,
1805                extra:   6_i32.to_be_bytes().to_vec(),
1806            },
1807            NcsInstruction {
1808                opcode:  NcsOpcode::Ret,
1809                auxcode: NcsAuxCode::None,
1810                extra:   Vec::new(),
1811            },
1812            NcsInstruction {
1813                opcode:  NcsOpcode::Ret,
1814                auxcode: NcsAuxCode::None,
1815                extra:   Vec::new(),
1816            },
1817        ]);
1818        let ndb = Ndb {
1819            files:     vec![NdbFile {
1820                name:    "test".to_string(),
1821                is_root: true,
1822            }],
1823            structs:   Vec::new(),
1824            functions: vec![NdbFunction {
1825                label:        "helper".to_string(),
1826                binary_start: 19,
1827                binary_end:   21,
1828                return_type:  NdbType::Void,
1829                args:         Vec::new(),
1830            }],
1831            variables: Vec::new(),
1832            lines:     Vec::new(),
1833        };
1834
1835        let rendered = render_ncs_disassembly_with_ndb(
1836            &bytes,
1837            None,
1838            Some(&ndb),
1839            None,
1840            NcsDisassemblyOptions::default(),
1841        )?;
1842
1843        assert!(rendered.contains("0000: JSR helper"));
1844        Ok(())
1845    }
1846
1847    #[test]
1848    fn assemble_roundtrips_rendered_disassembly_to_identical_bytes()
1849    -> Result<(), Box<dyn std::error::Error>> {
1850        let bytes = encode_ncs_instructions(&[
1851            NcsInstruction {
1852                opcode:  NcsOpcode::Constant,
1853                auxcode: NcsAuxCode::TypeString,
1854                extra:   [6_u16.to_be_bytes().as_slice(), b"hi\\n[]"].concat(),
1855            },
1856            NcsInstruction {
1857                opcode:  NcsOpcode::AssignmentBase,
1858                auxcode: NcsAuxCode::TypeVoid,
1859                extra:   [
1860                    (-12_i32).to_be_bytes().as_slice(),
1861                    4_u16.to_be_bytes().as_slice(),
1862                ]
1863                .concat(),
1864            },
1865            NcsInstruction {
1866                opcode:  NcsOpcode::ExecuteCommand,
1867                auxcode: NcsAuxCode::None,
1868                extra:   [7_u16.to_be_bytes().as_slice(), &[1_u8]].concat(),
1869            },
1870            NcsInstruction {
1871                opcode:  NcsOpcode::Jsr,
1872                auxcode: NcsAuxCode::None,
1873                extra:   12_i32.to_be_bytes().to_vec(),
1874            },
1875            NcsInstruction {
1876                opcode:  NcsOpcode::Equal,
1877                auxcode: NcsAuxCode::TypeTypeStructStruct,
1878                extra:   12_u16.to_be_bytes().to_vec(),
1879            },
1880            NcsInstruction {
1881                opcode:  NcsOpcode::Ret,
1882                auxcode: NcsAuxCode::None,
1883                extra:   Vec::new(),
1884            },
1885            NcsInstruction {
1886                opcode:  NcsOpcode::Ret,
1887                auxcode: NcsAuxCode::None,
1888                extra:   Vec::new(),
1889            },
1890        ]);
1891
1892        let rendered = render_ncs_disassembly(
1893            &bytes,
1894            None,
1895            NcsDisassemblyOptions {
1896                max_string_length: usize::MAX,
1897                ..NcsDisassemblyOptions::default()
1898            },
1899        )?;
1900        let reassembled = assemble_ncs_bytes(&rendered, None)?;
1901
1902        assert_eq!(reassembled, bytes);
1903        Ok(())
1904    }
1905
1906    #[test]
1907    fn assemble_accepts_internal_names_and_explicit_auxcodes()
1908    -> Result<(), Box<dyn std::error::Error>> {
1909        let text = "\
19100000: ASSIGNMENT_BASE.TYPE_VOID -8, 4
19110008: EXECUTE_COMMAND.NONE 1, 2
19120013: RET.NONE
1913";
1914        let instructions = assemble_ncs_text(text, None)?;
1915
1916        assert_eq!(instructions.len(), 3);
1917        let first = instructions.first().expect("expected first instruction");
1918        let second = instructions.get(1).expect("expected second instruction");
1919        assert_eq!(first.opcode, NcsOpcode::AssignmentBase);
1920        assert_eq!(first.auxcode, NcsAuxCode::TypeVoid);
1921        assert_eq!(second.opcode, NcsOpcode::ExecuteCommand);
1922        assert_eq!(second.auxcode, NcsAuxCode::None);
1923        Ok(())
1924    }
1925}