Skip to main content

assembler/
parser.rs

1//! Turn a sequence of input tokens into a data structure representing
2//! it.
3//!
4//! This data structure isn't a tree, because the logical
5//! structure of a TX-2 assembly-language program isn't tree-like.
6use std::{
7    collections::BTreeMap,
8    collections::BTreeSet,
9    fmt::Debug,
10    ops::{BitOr, Range, Shl},
11};
12
13pub(crate) mod helpers;
14mod symex;
15#[cfg(test)]
16mod tests;
17
18use chumsky::error::Rich;
19use chumsky::extension::v1::Ext;
20use chumsky::extension::v1::ExtParser;
21use chumsky::extra::{Full, ParserExtra};
22use chumsky::input::InputRef;
23use chumsky::input::{Emitter, MapExtra, Stream, ValueInput};
24
25use chumsky::Boxed;
26use chumsky::Parser;
27use chumsky::prelude::{Input, IterParser, Recursive, SimpleSpan, choice, just, one_of, recursive};
28use chumsky::select;
29
30use crate::collections::OneOrMore;
31
32use super::ast::{
33    ArithmeticExpression, Atom, CommaDelimitedFragment, Commas, CommasOrInstruction, ConfigValue,
34    Equality, EqualityValue, FragmentWithHold, HoldBit, InstructionFragment, LiteralValue,
35    Operator, Origin, RegisterContaining, RegistersContaining, SignedAtom, SpannedSymbolOrLiteral,
36    SymbolOrLiteral, Tag, TaggedProgramInstruction, UntaggedProgramInstruction,
37};
38use super::lexer::{self};
39use super::manuscript::{
40    MacroBodyLine, MacroDefinition, MacroDummyParameters, MacroInvocation, MacroParameter,
41    MacroParameterBindings, MacroParameterValue, ManuscriptLine, ManuscriptMetaCommand, SourceFile,
42    manuscript_lines_to_source_file,
43};
44use super::span::{Span, Spanned, span};
45use super::state::{NumeralMode, State};
46use super::symbol::SymbolName;
47use base::charset::Script;
48use base::prelude::*;
49use helpers::Sign;
50use symex::SymexSyllableRule;
51
52pub(crate) type ExtraWithoutContext<'a> = Full<Rich<'a, lexer::Token>, State<'a>, ()>;
53
54use lexer::Token as Tok;
55
56fn maybe_sign<'a, I>(
57    script_required: Script,
58) -> impl Parser<'a, I, Option<(Sign, Span)>, ExtraWithoutContext<'a>> + Clone
59where
60    I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
61{
62    choice((
63        just(Tok::Plus(script_required)).to(Sign::Plus),
64        just(Tok::Minus(script_required)).to(Sign::Minus),
65    ))
66    .map_with(|maybe_sign, extra| (maybe_sign, extra.span()))
67    .or_not()
68}
69
70#[derive(Debug, PartialEq, Eq, Clone)]
71enum BitDesignatorValidation {
72    Good(LiteralValue),
73    Suspect(u8, u8, LiteralValue),
74}
75
76fn make_bit_designator_literal(
77    script: Script,
78    quarter: u8,
79    bitnum: u8,
80    span: Span,
81) -> BitDesignatorValidation {
82    fn build(q: u64, b: u64) -> Unsigned36Bit {
83        // When used as a subscript, the quarter number goes into bits
84        // 3.6-3.5 (bits 23,22).  The bit number goes into bits
85        // 3.4-3.1 (bits 21-18).  However, subscript values are
86        // shifted left by 18 bits.  Meaning, if this is used as a
87        // normal-script value, it should not be shifted.
88        let qmod4: u64 = q % 4_u64;
89        Unsigned36Bit::ZERO.bitor(qmod4.shl(4_u32).bitor(b))
90    }
91    // Apparently-invalid bit designators should still be accepted.
92    // See for example the description of the SKM instruction (in
93    // chapter 3 of the Users Handbook which explains what the machine
94    // does with invalid bit designators.  See also the example in the
95    // table in section 6-2.4 of the Users Handbook.
96    //
97    // So we arrange to issue a warning message for this case.
98    let value = LiteralValue::from((span, script, build(quarter.into(), bitnum.into())));
99    match (quarter, bitnum) {
100        (1..=4, 1..=9) | (4, 10) => BitDesignatorValidation::Good(value),
101        _ => BitDesignatorValidation::Suspect(quarter, bitnum, value),
102    }
103}
104
105// I'm not really defining my own type here, this is just for
106// abbreviation purposes.
107type MyEmitter<'a, I> = Emitter<
108    <chumsky::extra::Full<chumsky::error::Rich<'a, lexer::Token>, State<'a>, ()> as ParserExtra<
109        'a,
110        I,
111    >>::Error,
112>;
113
114fn warn_bad_bitpos<'src, I>(
115    validated: BitDesignatorValidation,
116    extra: &mut MapExtra<'src, '_, I, ExtraWithoutContext<'src>>,
117    emitter: &mut MyEmitter<'src, I>,
118) -> LiteralValue
119where
120    I: Input<'src, Token = Tok, Span = Span> + ValueInput<'src>,
121{
122    match validated {
123        // This is a warning message only, because it's
124        // allowed to specify a nonexistent bit position (see
125        // description of SKM instruction).
126        BitDesignatorValidation::Suspect(q, b, literal) => {
127            emitter.emit(Rich::custom(
128                extra.span(),
129                format!("bit position {q}\u{00B7}{b} does not exist"),
130            ));
131            literal
132        }
133        BitDesignatorValidation::Good(literal) => literal,
134    }
135}
136
137fn bit_selector<'a, I>(
138    script_required: Script,
139) -> impl Parser<'a, I, LiteralValue, ExtraWithoutContext<'a>> + Clone
140where
141    I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
142{
143    select! {
144        Tok::BitPosition(script, quarter, bits) if script == script_required => (quarter, bits)
145    }
146    .try_map_with(move |(quarter, bit), extra| {
147        // Bit designators are always in decimal.  They end up in the "j bits" of the instruction word (bits 3.6 to 3.1).
148        // This is described in the Users Handbook on page 3-34 (in the description of the SKM instruction)
149        match quarter.as_str().parse::<u8>() {
150            Err(_) => Err(Rich::custom(
151                extra.span(),
152                format!("quarter {quarter} is not a valid decimal number"),
153            )),
154            Ok(q) => match bit.as_str().parse::<u8>() {
155                Ok(bit) => Ok(make_bit_designator_literal(
156                    script_required,
157                    q,
158                    bit,
159                    extra.span(),
160                )),
161                Err(_) => Err(Rich::custom(
162                    extra.span(),
163                    format!("bit position {bit} is not a valid decimal number"),
164                )),
165            },
166        }
167    })
168    .validate(warn_bad_bitpos)
169}
170
171fn literal<'a, I>(
172    script_required: Script,
173) -> impl Parser<'a, I, LiteralValue, ExtraWithoutContext<'a>> + Clone
174where
175    I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
176{
177    let plain_literal = {
178        let digits = select! {
179            Tok::Digits(script, n) if script == script_required => n,
180        };
181
182        digits.try_map_with(move |digits_token_payload, extra| {
183            let state: &State = extra.state();
184            let mode: NumeralMode = state.numeral_mode;
185            match digits_token_payload.make_num(mode) {
186                Ok(value) => Ok(LiteralValue::from((extra.span(), script_required, value))),
187                Err(e) => Err(Rich::custom(extra.span(), e.to_string())),
188            }
189        })
190    };
191    choice((bit_selector(script_required), plain_literal)).labelled("numeric literal")
192}
193
194fn here<'a, I>(
195    script_required: Script,
196) -> impl Parser<'a, I, SymbolOrLiteral, ExtraWithoutContext<'a>> + Clone
197where
198    I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
199{
200    select! {
201        Tok::Hash(script) if script == script_required => (),
202    }
203    .map_with(move |(), extra| SymbolOrLiteral::Here(script_required, extra.span()))
204}
205
206fn opcode_code(s: &str) -> Option<(Unsigned5Bit, Unsigned6Bit)> {
207    match s {
208        "IOS" => Some((u5!(0o00), u6!(0o04))),
209        "JMP" => Some((u5!(0o00), u6!(0o05))),
210        "BRC" => Some((u5!(0o01), u6!(0o05))),
211        "JPS" => Some((u5!(0o02), u6!(0o05))),
212        "BRS" => Some((u5!(0o03), u6!(0o05))),
213        "JPQ" => Some((u5!(0o14), u6!(0o05))),
214        "BPQ" => Some((u5!(0o15), u6!(0o05))),
215        "JES" => Some((u5!(0o16), u6!(0o05))),
216        "JPD" => Some((u5!(0o20), u6!(0o05))),
217        "BRD" => Some((u5!(0o21), u6!(0o05))),
218        "JDS" => Some((u5!(0o22), u6!(0o05))),
219        "BDS" => Some((u5!(0o23), u6!(0o05))),
220        "JPX" => Some((u5!(0o00), u6!(0o06))),
221        "JNX" => Some((u5!(0o00), u6!(0o07))),
222        "AUX" => Some((u5!(0o00), u6!(0o10))),
223        "RSX" => Some((u5!(0o00), u6!(0o11))),
224        "SKX" | "REX" | "SEX" => Some((u5!(0o00), u6!(0o12))),
225        "INX" => Some((u5!(0o02), u6!(0o12))),
226        "DEX" => Some((u5!(0o03), u6!(0o12))),
227        "SXD" => Some((u5!(0o04), u6!(0o12))),
228        "SXL" => Some((u5!(0o06), u6!(0o12))),
229        "SXG" => Some((u5!(0o07), u6!(0o12))),
230        "RXF" => Some((u5!(0o10), u6!(0o12))),
231        "RXD" => Some((u5!(0o20), u6!(0o12))),
232        "RFD" => Some((u5!(0o30), u6!(0o12))),
233        "EXX" => Some((u5!(0o00), u6!(0o14))),
234        "ADX" => Some((u5!(0o00), u6!(0o15))),
235        "DPX" => Some((u5!(0o00), u6!(0o16))),
236        "SKM" => Some((u5!(0o00), u6!(0o17))),
237        "MKC" => Some((u5!(0o01), u6!(0o17))),
238        "MKZ" => Some((u5!(0o02), u6!(0o17))),
239        "MKN" => Some((u5!(0o03), u6!(0o17))),
240        "SKU" => Some((u5!(0o10), u6!(0o17))),
241        "SUC" => Some((u5!(0o11), u6!(0o17))),
242        "SUZ" => Some((u5!(0o12), u6!(0o17))),
243        "SUN" => Some((u5!(0o13), u6!(0o17))),
244        "SKZ" => Some((u5!(0o20), u6!(0o17))),
245        "SZC" => Some((u5!(0o21), u6!(0o17))),
246        "SZZ" => Some((u5!(0o22), u6!(0o17))),
247        "SZN" => Some((u5!(0o23), u6!(0o17))),
248        "SKN" => Some((u5!(0o30), u6!(0o17))),
249        "SNC" => Some((u5!(0o31), u6!(0o17))),
250        "SNZ" => Some((u5!(0o32), u6!(0o17))),
251        "SNN" => Some((u5!(0o33), u6!(0o17))),
252        "CYR" => Some((u5!(0o04), u6!(0o17))),
253        "MCR" => Some((u5!(0o05), u6!(0o17))),
254        "MZR" => Some((u5!(0o06), u6!(0o17))),
255        "MNR" => Some((u5!(0o07), u6!(0o17))),
256        "SNR" => Some((u5!(0o34), u6!(0o17))),
257        "SZR" => Some((u5!(0o24), u6!(0o17))),
258        "SUR" => Some((u5!(0o14), u6!(0o17))),
259        "LDE" => Some((u5!(0o00), u6!(0o20))),
260        "SPF" => Some((u5!(0o00), u6!(0o21))),
261        "SPG" => Some((u5!(0o00), u6!(0o22))),
262        "LDA" => Some((u5!(0o00), u6!(0o24))),
263        "LDB" => Some((u5!(0o00), u6!(0o25))),
264        "LDC" => Some((u5!(0o00), u6!(0o26))),
265        "LDD" => Some((u5!(0o00), u6!(0o27))),
266        "STE" => Some((u5!(0o00), u6!(0o30))),
267        "FLF" => Some((u5!(0o00), u6!(0o31))),
268        "FLG" => Some((u5!(0o00), u6!(0o32))),
269        "STA" => Some((u5!(0o00), u6!(0o34))),
270        "STB" => Some((u5!(0o00), u6!(0o35))),
271        "STC" => Some((u5!(0o00), u6!(0o36))),
272        "STD" => Some((u5!(0o00), u6!(0o37))),
273        "ITE" => Some((u5!(0o00), u6!(0o40))),
274        "ITA" => Some((u5!(0o00), u6!(0o41))),
275        "UNA" => Some((u5!(0o00), u6!(0o42))),
276        "SED" => Some((u5!(0o00), u6!(0o43))),
277        "JOV" => Some((u5!(0o00), u6!(0o45))),
278        "JPA" => Some((u5!(0o00), u6!(0o46))),
279        "JNA" => Some((u5!(0o00), u6!(0o47))),
280        "EXA" => Some((u5!(0o00), u6!(0o54))),
281        "INS" => Some((u5!(0o00), u6!(0o55))),
282        "COM" => Some((u5!(0o00), u6!(0o56))),
283        "TSD" => Some((u5!(0o00), u6!(0o57))),
284        "CYA" => Some((u5!(0o00), u6!(0o60))),
285        "CYB" => Some((u5!(0o00), u6!(0o61))),
286        "CAB" => Some((u5!(0o00), u6!(0o62))),
287        "NOA" => Some((u5!(0o00), u6!(0o64))),
288        "DSA" => Some((u5!(0o00), u6!(0o65))),
289        "NAB" => Some((u5!(0o00), u6!(0o66))),
290        "ADD" => Some((u5!(0o00), u6!(0o67))),
291        "SCA" => Some((u5!(0o00), u6!(0o70))),
292        "SCB" => Some((u5!(0o00), u6!(0o71))),
293        "SAB" => Some((u5!(0o00), u6!(0o72))),
294        "TLY" => Some((u5!(0o00), u6!(0o74))),
295        "DIV" => Some((u5!(0o00), u6!(0o75))),
296        "MUL" => Some((u5!(0o00), u6!(0o76))),
297        "SUB" => Some((u5!(0o00), u6!(0o77))),
298        _ => None,
299    }
300}
301
302pub(super) fn symbol_or_literal<'a, I>(
303    script_required: Script,
304) -> impl Parser<'a, I, SymbolOrLiteral, ExtraWithoutContext<'a>> + Clone
305where
306    I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
307{
308    choice((
309        literal(script_required).map(SymbolOrLiteral::Literal),
310        symex::symex_syllable(script_required).map_with(move |name, extra| {
311            SymbolOrLiteral::Symbol(script_required, SymbolName::from(name), extra.span())
312        }),
313    ))
314    .labelled(match script_required {
315        Script::Super => "superscript single-syllable symbol or literal",
316        Script::Normal => "single-syllable symbol or literal",
317        Script::Sub => "subscript single-syllable symbol or literal",
318    })
319}
320
321fn opcode_to_literal(code: Unsigned6Bit, cfgbits: Unsigned5Bit, span: Span) -> LiteralValue {
322    let bits = Unsigned36Bit::ZERO
323        .bitor(u64::from(code).shl(24))
324        .bitor(u64::from(cfgbits).shl(30))
325        .bitor(helpers::opcode_auto_hold_bit(code));
326    LiteralValue::from((span, Script::Normal, bits))
327}
328
329pub(super) fn opcode<'a, I>() -> impl Parser<'a, I, LiteralValue, ExtraWithoutContext<'a>> + Clone
330where
331    I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
332{
333    symex::symex_syllable(Script::Normal)
334        .filter(|mnemonic| opcode_code(mnemonic).is_some())
335        .try_map_with(|mnemonic, extra| match opcode_code(mnemonic.as_str()) {
336            Some((cfgbits, code)) => Ok(opcode_to_literal(code, cfgbits, extra.span())),
337            None => Err(Rich::custom(
338                extra.span(),
339                format!("'{mnemonic}' is not an opcode mnemonic"),
340            )),
341        })
342        .labelled("opcode")
343}
344
345fn named_symbol<'a, I>(
346    rule: SymexSyllableRule,
347    script_required: Script,
348) -> impl Parser<'a, I, SymbolName, ExtraWithoutContext<'a>> + Clone
349where
350    I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
351{
352    symex::parse_symex(rule, script_required)
353}
354
355pub(super) fn operator<'a, I>(
356    script_required: Script,
357) -> impl Parser<'a, I, Operator, ExtraWithoutContext<'a>> + Clone
358where
359    I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
360{
361    select! {
362        // Solidus ("/") is used for divide.  See section 6-2.7
363        // "Word Assembly" for details.
364        Tok::Solidus(script) if script_required == script => Operator::Divide,
365        Tok::Plus(Script::Normal) => Operator::Add,
366        Tok::Times(got) if got == script_required => Operator::Multiply,
367        Tok::LogicalOr(got) if got == script_required => Operator::LogicalOr,
368        Tok::LogicalAnd(got) if got == script_required => Operator::LogicalAnd,
369        Tok::Minus(got) if script_required == got => Operator::Subtract,
370        Tok::Plus(got) if script_required == got => Operator::Add,
371    }
372    .labelled("arithmetic operator")
373}
374
375fn asterisk_indirection_fragment<'srcbody, I>()
376-> impl Parser<'srcbody, I, InstructionFragment, ExtraWithoutContext<'srcbody>> + Clone
377where
378    I: Input<'srcbody, Token = Tok, Span = Span> + ValueInput<'srcbody>,
379{
380    just(Tok::Asterisk(Script::Normal))
381        .map_with(|_, extra| InstructionFragment::DeferredAddressing(extra.span()))
382}
383
384/// The pipe construct is described in section 6-2.8 "SPECIAL SYMBOLS"
385/// of the Users Handbook.
386///
387/// "ADXₚ|ₜQ" should be equivalent to "ADXₚ{Qₜ}*".  So during
388/// evaluation we will need to generate an RC-word containing Qₜ.
389fn make_pipe_construct(
390    (p, (t, maybe_spanned_q)): (
391        SpannedSymbolOrLiteral,
392        (SpannedSymbolOrLiteral, Option<(InstructionFragment, Span)>),
393    ),
394) -> InstructionFragment {
395    // The variable names here are taken from the example in the
396    // documentation comment.
397    let tqspan = span(
398        t.span.start..if let Some((_, q_span)) = maybe_spanned_q {
399            q_span.end
400        } else {
401            t.span.end
402        },
403    );
404
405    let t: CommaDelimitedFragment = CommaDelimitedFragment {
406        span: t.span,
407        leading_commas: None,
408        holdbit: HoldBit::Unspecified,
409        fragment: InstructionFragment::Arithmetic(ArithmeticExpression::from(Atom::from(t.item))),
410        trailing_commas: None,
411    };
412    let qt: UntaggedProgramInstruction = if let Some((q, q_span)) = maybe_spanned_q {
413        UntaggedProgramInstruction::from(OneOrMore::with_tail(
414            CommaDelimitedFragment {
415                span: q_span,
416                holdbit: HoldBit::Unspecified,
417                leading_commas: None,
418                fragment: q,
419                trailing_commas: None,
420            },
421            vec![t],
422        ))
423    } else {
424        UntaggedProgramInstruction::from(OneOrMore::new(t))
425    };
426
427    let rc_word_value: RegisterContaining = RegisterContaining::from(TaggedProgramInstruction {
428        span: tqspan,
429        tags: Vec::new(),
430        instruction: qt,
431    });
432    InstructionFragment::PipeConstruct {
433        index: p,
434        rc_word_span: tqspan,
435        rc_word_value,
436    }
437}
438
439/// Macro terminators are described in section 6-4.5 of the TX-2 User Handbook.
440fn macro_terminator<'a, I>() -> impl Parser<'a, I, Tok, ExtraWithoutContext<'a>>
441where
442    I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
443{
444    // This list of 16 allowed terminators is exhaustive, see section
445    // 6-4.5 of the TX-2 User Handbook.
446    //
447    // ☛ , = →  | ⊃ ≡ ~ < > ∩ ∪ / × ∨ ∧
448    //
449    // The second symbol, in my scanned copy of the Users Handbook
450    // (page 6-31 of the Nov 1963 Users Handbook), looks like either a
451    // comma or a dot/full-stop/period.  Since a dot is valid in a
452    // symex name, and because the symbol seems to be taller than it
453    // is wide, I'm going to assume it is a comma.
454    //
455    // They are actually hard to distinguish in the copy of the Users
456    // Handbook I have.  But, looking at page 011 of Leonard
457    // Kleinrock's listing for his network simulator, the `HP OS`
458    // macro is definitely using as a separator a symbol that lives on
459    // the line.  If you look a little further below on the same
460    // page, the third instruction in the body of the `MV MX` macro
461    // body contains both a dot and a comma.  The dot is definitely
462    // above the line and looks rounder.  So I conclude that the
463    // separator character is a comma.
464    choice((
465        just(Tok::Hand(Script::Normal)),
466        just(Tok::Comma(Script::Normal)),
467        just(Tok::Equals(Script::Normal)),
468        just(Tok::Arrow(Script::Normal)),
469        just(Tok::Pipe(Script::Normal)),
470        just(Tok::ProperSuperset(Script::Normal)),
471        just(Tok::IdenticalTo(Script::Normal)),
472        just(Tok::Tilde(Script::Normal)),
473        just(Tok::LessThan(Script::Normal)),
474        just(Tok::GreaterThan(Script::Normal)),
475        just(Tok::Intersection(Script::Normal)),
476        just(Tok::Union(Script::Normal)),
477        just(Tok::Solidus(Script::Normal)),
478        just(Tok::Times(Script::Normal)),
479        just(Tok::LogicalOr(Script::Normal)),
480        just(Tok::LogicalAnd(Script::Normal)),
481    ))
482    .labelled("macro terminator")
483}
484
485// Exposed for testing.
486fn macro_definition_dummy_parameter<'a, I>()
487-> impl Parser<'a, I, MacroParameter, ExtraWithoutContext<'a>>
488where
489    I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
490{
491    // The TX-2 Users Handbook section 6-4.4 ("DUMMY PARAMETERS")
492    // doesn't disallow spaces in macro argument names.
493    (macro_terminator().then(named_symbol(SymexSyllableRule::Multiple, Script::Normal))).map_with(
494        |(terminator, symbol), extra| MacroParameter {
495            name: symbol,
496            span: extra.span(),
497            preceding_terminator: terminator,
498        },
499    )
500}
501
502fn macro_definition_dummy_parameters<'a, I>()
503-> impl Parser<'a, I, MacroDummyParameters, ExtraWithoutContext<'a>>
504where
505    I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
506{
507    choice((
508        macro_definition_dummy_parameter()
509            .repeated()
510            .at_least(1)
511            .collect::<Vec<_>>()
512            .map(MacroDummyParameters::OneOrMore),
513        macro_terminator().map(MacroDummyParameters::Zero),
514    ))
515}
516
517/// Macros are described in section 6-4 of the TX-2 User Handbook.
518fn macro_definition<'a, 'b, I>(
519    grammar: &Grammar<'a, 'b, I>,
520) -> impl Parser<'a, I, MacroDefinition, ExtraWithoutContext<'a>> + use<'a, 'b, I>
521where
522    I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
523{
524    named_metacommand(Metacommand::DefineMacro)
525        .ignore_then(
526            named_symbol(SymexSyllableRule::Multiple, Script::Normal).labelled("macro name"), // the macro's name (# is not allowed)
527        )
528        .then(macro_definition_dummy_parameters())
529        .then_ignore(end_of_line())
530        .then(
531            (macro_body_line(grammar).then_ignore(end_of_line()))
532                .repeated()
533                .collect()
534                .labelled("macro body"),
535        )
536        .then_ignore(named_metacommand(Metacommand::EndMacroDefinition))
537        // We don't parse end-of-line here because all metacommands are supposed
538        // to be followed by end-of-line.
539        .map_with(|((name, args), body), extra| {
540            let definition = MacroDefinition {
541                name,
542                params: args,
543                body,
544                span: extra.span(),
545            };
546            extra.state().define_macro(definition.clone());
547            definition
548        })
549}
550
551fn macro_body_line<'a, 'b, I>(
552    grammar: &Grammar<'a, 'b, I>,
553) -> impl Parser<'a, I, MacroBodyLine, ExtraWithoutContext<'a>> + use<'a, 'b, I>
554where
555    I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
556{
557    choice((
558        macro_invocation().map(MacroBodyLine::Expansion),
559        grammar.assignment.clone().map(MacroBodyLine::Equality),
560        grammar
561            .tagged_program_instruction
562            .clone()
563            .map(MacroBodyLine::Instruction),
564    ))
565}
566
567fn arithmetic_expression_in_any_script_allowing_spaces<'a, I>()
568-> impl Parser<'a, I, (Span, Script, ArithmeticExpression), ExtraWithoutContext<'a>>
569where
570    I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
571{
572    let g = grammar();
573    choice((
574        g.normal_arithmetic_expression_allowing_spaces
575            .map_with(|expr, extra| (extra.span(), Script::Normal, expr)),
576        g.subscript_arithmetic_expression_allowing_spaces
577            .map_with(|expr, extra| (extra.span(), Script::Sub, expr)),
578        g.superscript_arithmetic_expression_allowing_spaces
579            .map_with(|expr, extra| (extra.span(), Script::Super, expr)),
580    ))
581}
582
583fn defined_macro_name<'src, I>() -> impl Parser<'src, I, MacroDefinition, ExtraWithoutContext<'src>>
584where
585    I: Input<'src, Token = Tok, Span = Span> + ValueInput<'src>,
586{
587    fn mapping<'a>(
588        name: &SymbolName,
589        state: &State,
590        span: Span,
591    ) -> Result<MacroDefinition, chumsky::error::Rich<'a, lexer::Token>> {
592        match state.get_macro_definition(name) {
593            None => Err(Rich::custom(span, format!("unknown macro {name}"))),
594            Some(macro_def) => Ok(macro_def.clone()),
595        }
596    }
597
598    symex::parse_symex(SymexSyllableRule::OneOnly, Script::Normal).try_map_with(|name, extra| {
599        let span: Span = extra.span();
600        let state: &State = extra.state();
601        mapping(&name, state, span)
602    })
603}
604
605type ParsedMacroArg = Option<(Script, ArithmeticExpression)>;
606
607#[derive(Clone)]
608struct MacroInvocationParser<'src, I>
609where
610    I: Input<'src, Token = Tok, Span = Span> + ValueInput<'src>,
611{
612    expr_parser: Boxed<'src, 'src, I, (Span, ParsedMacroArg), ExtraWithoutContext<'src>>,
613    defined_macro_name_parser: Boxed<'src, 'src, I, MacroDefinition, ExtraWithoutContext<'src>>,
614}
615
616impl<'src, I> Default for MacroInvocationParser<'src, I>
617where
618    I: Input<'src, Token = Tok, Span = Span> + ValueInput<'src>,
619{
620    fn default() -> Self {
621        Self {
622            expr_parser: arithmetic_expression_in_any_script_allowing_spaces()
623                .or_not()
624                .map_with(|got, extra| match got {
625                    Some((span, script, expr)) => (span, Some((script, expr))),
626                    None => (extra.span(), None),
627                })
628                .boxed(),
629            defined_macro_name_parser: defined_macro_name().boxed(),
630        }
631    }
632}
633
634fn macro_invocation<'src, I>() -> impl Parser<'src, I, MacroInvocation, ExtraWithoutContext<'src>>
635where
636    I: Input<'src, Token = Tok, Span = Span> + ValueInput<'src>,
637{
638    Ext(MacroInvocationParser::default())
639}
640
641impl<'src, I> ExtParser<'src, I, MacroInvocation, ExtraWithoutContext<'src>>
642    for MacroInvocationParser<'src, I>
643where
644    I: Input<'src, Token = Tok, Span = Span> + ValueInput<'src>,
645{
646    fn parse(
647        &self,
648        inp: &mut InputRef<'src, '_, I, ExtraWithoutContext<'src>>,
649    ) -> Result<
650        MacroInvocation,
651        <Full<Rich<'src, Tok>, State<'src>, ()> as ParserExtra<'src, I>>::Error,
652    > {
653        let before = inp.cursor();
654        let macro_def: MacroDefinition = inp.parse(&self.defined_macro_name_parser)?;
655        let param_defs: Vec<MacroParameter> = match macro_def.params {
656            MacroDummyParameters::Zero(ref expected) => {
657                if let Some(got) = inp.next_maybe().as_deref() {
658                    if got == expected {
659                        Vec::new()
660                    } else {
661                        return Err(Rich::custom(
662                            inp.span_since(&before),
663                            format!(
664                                "expected macro name {} to be followed a terminator {} but got {}",
665                                macro_def.name, expected, got
666                            ),
667                        ));
668                    }
669                } else {
670                    return Err(Rich::custom(
671                        inp.span_since(&before),
672                        format!(
673                            "expected macro name {} to be followed a terminator {}",
674                            macro_def.name, expected
675                        ),
676                    ));
677                }
678            }
679            MacroDummyParameters::OneOrMore(ref params) => params.clone(),
680        };
681        let mut param_values: MacroParameterBindings = Default::default();
682        for param_def in param_defs {
683            let before = inp.cursor();
684            if let Some(got) = inp.next_maybe().as_deref() {
685                let span = inp.span_since(&before);
686                if got == &param_def.preceding_terminator {
687                    match inp.parse(&self.expr_parser)? {
688                        (span, Some((script, expr))) => {
689                            param_values.insert(
690                                param_def.name,
691                                span,
692                                Some(MacroParameterValue::Value(script, expr)),
693                            );
694                        }
695                        (span, None) => {
696                            // Record the fact that this parameter was missing.
697                            param_values.insert(param_def.name, span, None);
698                        }
699                    }
700                } else {
701                    return Err(Rich::custom(
702                        span,
703                        format!(
704                            "in invocation of macro {}, expected macro terminator {} before parameter {} but got {}",
705                            macro_def.name, param_def.preceding_terminator, param_def.name, got
706                        ),
707                    ));
708                }
709            } else {
710                let span = inp.span_since(&before);
711                return Err(Rich::custom(
712                    span,
713                    format!(
714                        "in invocation of macro {}, expected macro terminator {} before parameter {}",
715                        macro_def.name, param_def.preceding_terminator, param_def.name
716                    ),
717                ));
718            }
719        }
720        Ok(MacroInvocation {
721            macro_def,
722            param_values,
723        })
724    }
725}
726
727#[derive(Debug, PartialEq, Eq, Clone, Copy)]
728enum Metacommand {
729    Decimal,
730    Octal,
731    /// A ☛☛PUNCH meta command.
732    Punch,
733    DefineMacro,
734    EndMacroDefinition,
735}
736
737fn named_metacommand<'a, I>(which: Metacommand) -> impl Parser<'a, I, (), ExtraWithoutContext<'a>>
738where
739    I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
740{
741    let name_match = move |actual: &str| -> bool {
742        match which {
743            Metacommand::Decimal => {
744                matches!(actual, "DECIMAL" | "DECIMA" | "DECIM" | "DECI" | "DEC")
745            }
746            Metacommand::Octal => matches!(actual, "OCTAL" | "OCTA" | "OCT" | "OC"),
747            Metacommand::Punch => matches!(actual, "PUNCH" | "PUNC" | "PUN" | "PU"),
748            Metacommand::DefineMacro => actual == "DEF",
749            Metacommand::EndMacroDefinition => matches!(actual, "EMD" | "EM"),
750        }
751    };
752
753    let matching_metacommand_name = select! {
754        Tok::SymexSyllable(Script::Normal, name) if name_match(name.as_str()) => (),
755    };
756
757    just([Tok::Hand(Script::Normal), Tok::Hand(Script::Normal)])
758        .ignored()
759        .then_ignore(matching_metacommand_name)
760}
761
762fn metacommand<'a, 'b, I>(
763    grammar: &Grammar<'a, 'b, I>,
764) -> impl Parser<'a, I, ManuscriptMetaCommand, ExtraWithoutContext<'a>> + use<'a, 'b, I>
765where
766    I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
767{
768    fn punch<'a, I>() -> impl Parser<'a, I, ManuscriptMetaCommand, ExtraWithoutContext<'a>>
769    where
770        I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
771    {
772        // We currently have a limitation in the interpretation of
773        // "AA" in the PUNCH metacommand.  The documentation clearly
774        // states that this should be an honest tag.  We currently
775        // accept only numeric literals.
776        named_metacommand(Metacommand::Punch)
777            .ignore_then(literal(Script::Normal).or_not())
778            .try_map(|aa, span| match helpers::punch_address(aa) {
779                Ok(punch) => Ok(ManuscriptMetaCommand::Punch(punch)),
780                Err(msg) => Err(Rich::custom(span, msg)),
781            })
782            .labelled("PUNCH command")
783    }
784
785    fn base_change<'a, I>() -> impl Parser<'a, I, ManuscriptMetaCommand, ExtraWithoutContext<'a>>
786    where
787        I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
788    {
789        choice((
790            named_metacommand(Metacommand::Decimal)
791                .to(ManuscriptMetaCommand::BaseChange(NumeralMode::Decimal)),
792            named_metacommand(Metacommand::Octal)
793                .to(ManuscriptMetaCommand::BaseChange(NumeralMode::Octal)),
794        ))
795        .labelled("base-change metacommand")
796    }
797
798    choice((
799        base_change(),
800        punch(),
801        macro_definition(grammar).map(ManuscriptMetaCommand::Macro),
802    ))
803    .labelled("metacommand")
804}
805
806pub(crate) fn instructions_with_comma_counts<I>(it: I) -> Vec<CommaDelimitedFragment>
807where
808    I: Iterator<Item = CommasOrInstruction>,
809{
810    /// Fold operation which estabishes an alternating pattern of
811    /// commas and instructions.
812    ///
813    /// Invariant: acc is non-empty, begins and ends with C(_)
814    /// and does not contain a consecutive pair of C(_) or a
815    /// consecutive pair of Inst(_).
816    fn fold_step(
817        mut acc: Vec<CommasOrInstruction>,
818        item: CommasOrInstruction,
819    ) -> Vec<CommasOrInstruction> {
820        fn null_instruction(span: Span) -> FragmentWithHold {
821            FragmentWithHold {
822                span,
823                holdbit: HoldBit::Unspecified,
824                fragment: InstructionFragment::Null(span),
825            }
826        }
827
828        match acc.last_mut() {
829            Some(CommasOrInstruction::C(tail_comma)) => match item {
830                CommasOrInstruction::C(maybe_commas) => {
831                    if tail_comma.is_none() {
832                        *tail_comma = maybe_commas;
833                    } else {
834                        let null_inst_span: Span = match (tail_comma, &maybe_commas) {
835                            (_, Some(ic)) => span(ic.span().start..ic.span().start),
836                            (Some(tc), _) => span(tc.span().start..tc.span().start),
837                            (None, None) => {
838                                unreachable!(
839                                    "should be no need to interpose a null instruction between two instances of zero commas"
840                                );
841                            }
842                        };
843                        acc.push(CommasOrInstruction::I(null_instruction(null_inst_span)));
844                        acc.push(CommasOrInstruction::C(maybe_commas));
845                    }
846                }
847                CommasOrInstruction::I(inst) => {
848                    acc.push(CommasOrInstruction::I(inst));
849                    acc.push(CommasOrInstruction::C(None));
850                }
851            },
852            Some(CommasOrInstruction::I(_)) => unreachable!("invariant was broken"),
853            None => unreachable!("invariant was not established"),
854        }
855        assert!(matches!(acc.first(), Some(CommasOrInstruction::C(_))));
856        assert!(matches!(acc.last(), Some(CommasOrInstruction::C(_))));
857        acc
858    }
859
860    let mut it = it.peekable();
861
862    let initial_accumulator: Vec<CommasOrInstruction> = vec![CommasOrInstruction::C({
863        match it.peek() {
864            None => {
865                return Vec::new();
866            }
867            Some(CommasOrInstruction::I(_)) => None,
868            Some(CommasOrInstruction::C(maybe_commas)) => {
869                let c = maybe_commas.clone();
870                it.next();
871                c
872            }
873        }
874    })];
875
876    let tmp = it.fold(initial_accumulator, fold_step);
877    let mut output: Vec<CommaDelimitedFragment> = Vec::with_capacity(tmp.len() / 2 + 1);
878    let mut it = tmp.into_iter().peekable();
879    loop {
880        let maybe_before_count = it.next();
881        let maybe_inst = it.next();
882        match (maybe_before_count, maybe_inst) {
883            (None, _) => {
884                break;
885            }
886            (Some(CommasOrInstruction::C(before_commas)), Some(CommasOrInstruction::I(inst))) => {
887                let after_commas: Option<Commas> = match it.peek() {
888                    Some(CommasOrInstruction::C(commas)) => commas.clone(),
889                    None => None,
890                    Some(CommasOrInstruction::I(_)) => {
891                        unreachable!("fold_step did not maintain its invariant")
892                    }
893                };
894                output.push(CommaDelimitedFragment::new(
895                    before_commas,
896                    inst,
897                    after_commas,
898                ));
899            }
900            (Some(CommasOrInstruction::C(_)), None) => {
901                // No instructions in the input.
902                break;
903            }
904            (Some(CommasOrInstruction::I(_)), _)
905            | (Some(CommasOrInstruction::C(_)), Some(CommasOrInstruction::C(_))) => {
906                unreachable!("fold_step did not maintain its invariant");
907            }
908        }
909    }
910    output
911}
912
913fn tag_definition<'a, I>() -> impl Parser<'a, I, Tag, ExtraWithoutContext<'a>> + Clone
914where
915    I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
916{
917    named_symbol(SymexSyllableRule::Multiple, Script::Normal)
918        .map_with(|name, extra| Tag {
919            name,
920            span: extra.span(),
921        })
922        .then_ignore(just(Tok::Arrow(Script::Normal)))
923        .labelled("tag definition")
924}
925
926fn commas<'a, I>() -> impl Parser<'a, I, Commas, ExtraWithoutContext<'a>> + Clone
927where
928    I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
929{
930    just(Tok::Comma(Script::Normal))
931        .repeated()
932        .at_least(1)
933        .at_most(3)
934        .count()
935        .map_with(|count, extra| {
936            let span = extra.span();
937            match count {
938                1 => Commas::One(span),
939                2 => Commas::Two(span),
940                3 => Commas::Three(span),
941                _ => unreachable!(),
942            }
943        })
944}
945
946fn maybe_hold<'a, I>() -> impl Parser<'a, I, Option<HoldBit>, ExtraWithoutContext<'a>> + Clone
947where
948    I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
949{
950    choice((
951        one_of(Tok::Hold).to(HoldBit::Hold),
952        just(Tok::NotHold).to(HoldBit::NotHold),
953    ))
954    .or_not()
955    .labelled("instruction hold bit")
956}
957
958struct Grammar<'a, 'b, I>
959where
960    I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
961{
962    assignment: Boxed<'a, 'b, I, Equality, ExtraWithoutContext<'a>>,
963    tagged_program_instruction: Boxed<'a, 'b, I, TaggedProgramInstruction, ExtraWithoutContext<'a>>,
964    normal_arithmetic_expression_allowing_spaces:
965        Boxed<'a, 'b, I, ArithmeticExpression, ExtraWithoutContext<'a>>,
966    subscript_arithmetic_expression_allowing_spaces:
967        Boxed<'a, 'b, I, ArithmeticExpression, ExtraWithoutContext<'a>>,
968    superscript_arithmetic_expression_allowing_spaces:
969        Boxed<'a, 'b, I, ArithmeticExpression, ExtraWithoutContext<'a>>,
970    #[cfg(test)]
971    instruction_fragment: Boxed<'a, 'b, I, InstructionFragment, ExtraWithoutContext<'a>>,
972}
973
974fn grammar<'a: 'b, 'b, I>() -> Grammar<'a, 'b, I>
975where
976    I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
977{
978    const ALLOW_SPACES: bool = true;
979
980    let mut comma_delimited_instructions = Recursive::declare();
981    let tagged_program_instruction = (tag_definition()
982        .repeated()
983        .collect()
984        .then(comma_delimited_instructions.clone()))
985    .map_with(
986        |(tags, fragments): (Vec<Tag>, OneOrMore<CommaDelimitedFragment>), extra| {
987            let span: Span = extra.span();
988            if let Some(t) = tags.first() {
989                assert_eq!(t.span.start, span.start);
990            }
991            TaggedProgramInstruction {
992                span: extra.span(),
993                tags,
994                instruction: UntaggedProgramInstruction::from(fragments),
995            }
996        },
997    )
998    .labelled(
999        "optional tag definition followed by a (possibly comma-delimited) program instructions",
1000    );
1001
1002    // Parse {E} where E is some expression.  Since tags are
1003    // allowed inside RC-blocks, we should parse E as a
1004    // TaggedProgramInstruction.  But if we try to do that without
1005    // using recursive() we will blow the stack, unfortunately.
1006    let register_containing = tagged_program_instruction
1007        .clone()
1008        .delimited_by(
1009            just(Tok::LeftBrace(Script::Normal)),
1010            just(Tok::RightBrace(Script::Normal)),
1011        )
1012        .map_with(|tagged_instruction, extra| {
1013            Atom::RcRef(
1014                extra.span(),
1015                RegistersContaining::from_words(OneOrMore::new(RegisterContaining::from(
1016                    tagged_instruction,
1017                ))),
1018            )
1019        })
1020        .labelled("RC-word");
1021
1022    let arith_expr = |allow_spaces: bool, script_required: Script| {
1023        {
1024            let symex_syllable_rule = if allow_spaces {
1025                SymexSyllableRule::Multiple
1026            } else {
1027                SymexSyllableRule::OneOnly
1028            };
1029            // We use recursive here to prevent the parser blowing the stack
1030            // when trying to parse inputs which have parentheses - that is,
1031            // inputs that require recursion.
1032            recursive(move |arithmetic_expr| {
1033                // Parse (E) where E is some expression.
1034                let parenthesised_arithmetic_expression = arithmetic_expr // this is the recursive call
1035                    .clone()
1036                    .delimited_by(
1037                        just(Tok::LeftParen(script_required)),
1038                        just(Tok::RightParen(script_required)),
1039                    )
1040                    .map_with(move |expr, extra| {
1041                        Atom::Parens(extra.span(), script_required, Box::new(expr))
1042                    })
1043                    .labelled("parenthesised arithmetic expression");
1044
1045                // Parse a literal, symbol, #, or (recursively) an expression in parentheses.
1046                let naked_atom = choice((
1047                    literal(script_required).map(Atom::from),
1048                    opcode().map(Atom::from),
1049                    here(script_required).map(Atom::SymbolOrLiteral),
1050                    named_symbol(symex_syllable_rule, script_required).map_with(
1051                        move |symbol_name, extra| {
1052                            Atom::SymbolOrLiteral(SymbolOrLiteral::Symbol(
1053                                script_required,
1054                                symbol_name,
1055                                extra.span(),
1056                            ))
1057                        },
1058                    ),
1059                    register_containing,
1060                    parenthesised_arithmetic_expression,
1061                ))
1062                .boxed();
1063
1064                let signed_atom = maybe_sign(script_required).then(naked_atom).map_with(
1065                    |(possible_sign, magnitude), extra| SignedAtom {
1066                        span: extra.span(),
1067                        negated: matches!(possible_sign, Some((Sign::Minus, _))),
1068                        magnitude,
1069                    },
1070                );
1071
1072                // Parse an arithmetic operator (e.g. plus, times) followed by an atom.
1073                let operator_with_signed_atom = operator(script_required).then(signed_atom.clone());
1074
1075                // An arithmetic expression is a signed atom followed by zero or
1076                // more pairs of (arithmetic operator, signed atom).
1077                signed_atom
1078                    .then(operator_with_signed_atom.repeated().collect())
1079                    .map(|(head, tail)| ArithmeticExpression::with_tail(head, tail))
1080            })
1081        }
1082        .labelled("arithmetic expression")
1083    };
1084
1085    // Parse a values (symbolic or literal) or arithmetic expression.
1086    //
1087    // BAT² is not an identifier but a sequence[1] whose value is
1088    // computed by OR-ing the value of the symex BAT with the value of
1089    // the literal "²" (which is 2<<30, or 0o20_000_000_000).  But BAT²
1090    // is itself not an arithmetic_expression (because there is a script
1091    // change).
1092    //
1093    // You could argue that (BAT²) should be parsed as an atom.  Right
1094    // now that doesn't work because all the elements of an expression
1095    // (i.e. everything within the parens) need to have the same script.
1096    let program_instruction_fragment = recursive(|program_instruction_fragment| {
1097        // Parse the pipe-construct described in the User Handbook
1098        // section 2-2.8 "SPECIAL SYMBOLS" as "ₚ|ₜ" (though in reality
1099        // the pipe should also be subscript and in their example they
1100        // use a subscript q).
1101        //
1102        // The Handbook is not explicit on whether the "ₚ" or "ₜ" can
1103        // contain spaces.  We will assume not, for simplicity (at
1104        // least for the time being).
1105        //
1106        // "ADXₚ|ₜQ" should be equvialent to ADXₚ{Qₜ}*.  So we need to
1107        // generate an RC-word containing Qₜ.
1108        //
1109        // The Sketchpad code (file_name=102726903-05-01-acc.pdf, page
1110        // 16) contains a line which uses the pipe construct without a
1111        // value for Q: "INX ₚ|ₜ" (p is @sub_beta@ and t is
1112        // @sub_alpha@).  For this case we assume the value zero.
1113        let spanned_rc_word_fragment = symbol_or_literal(Script::Sub) // this is p
1114            .map_with(|p, extra| SpannedSymbolOrLiteral {
1115                item: p,
1116                span: extra.span(),
1117            })
1118            .boxed();
1119
1120        let q = program_instruction_fragment
1121            .clone()
1122            .map_with(|q, extra| (q, extra.span()));
1123        let tq = symbol_or_literal(Script::Sub) // this is t
1124            .map_with(|t, extra| SpannedSymbolOrLiteral {
1125                item: t,
1126                span: extra.span(),
1127            })
1128            .then(q.or_not())
1129            .boxed();
1130        let pipe_construct = spanned_rc_word_fragment
1131            .then_ignore(just(Tok::Pipe(Script::Sub)))
1132            .then(tq)
1133            .map(make_pipe_construct)
1134            .labelled("pipe construct");
1135
1136        let single_script_fragment = |script_required| {
1137            arith_expr.clone()(true, script_required).map(InstructionFragment::from)
1138        };
1139
1140        // A configuration syllable is not permitted to contain spaces
1141        // (per section 6-1.2 "INSTRUCTION WORDS" of the Users
1142        // Handbook).  So we need to prevent symex matching accepting
1143        // spaces.
1144        let config_value = choice((
1145            just(Tok::DoublePipe(Script::Normal)).ignore_then(
1146                arith_expr.clone()(false, Script::Normal).map(|expr| ConfigValue {
1147                    already_superscript: false,
1148                    expr,
1149                }),
1150            ),
1151            arith_expr.clone()(false, Script::Super).map(|expr| ConfigValue {
1152                already_superscript: true,
1153                expr,
1154            }),
1155        ))
1156        .try_map_with(|config_val, extra| {
1157            let span: Span = extra.span();
1158            let range: Range<usize> = span.into();
1159            let slice: &str = &extra.state().body[range];
1160
1161            if slice.contains(' ') {
1162                // Spaces in configuration values are prohibited by
1163                // the rule given in the Users handbook, section 6-2.1.
1164                Err(Rich::custom(
1165                    span,
1166                    format!("configuration value '{slice}' should not contain spaces"),
1167                ))
1168            } else {
1169                Ok(InstructionFragment::Config(config_val))
1170            }
1171        })
1172        .labelled("configuration value");
1173
1174        choice((
1175            pipe_construct,
1176            single_script_fragment(Script::Normal),
1177            single_script_fragment(Script::Sub),
1178            asterisk_indirection_fragment(),
1179            config_value,
1180        ))
1181        .labelled("program instruction")
1182    });
1183
1184    comma_delimited_instructions.define({
1185        let untagged_program_instruction = maybe_hold()
1186            .then(program_instruction_fragment.clone())
1187            .map_with(
1188                |(maybe_hold, fragment): (Option<HoldBit>, InstructionFragment), extra| {
1189                    FragmentWithHold {
1190                        span: extra.span(),
1191                        holdbit: maybe_hold.unwrap_or(HoldBit::Unspecified),
1192                        fragment,
1193                    }
1194                },
1195            );
1196
1197        choice((
1198            commas().map(|c| CommasOrInstruction::C(Some(c))),
1199            untagged_program_instruction
1200                .clone()
1201                .map(CommasOrInstruction::I),
1202        ))
1203        .repeated()
1204        .at_least(1)
1205        .collect::<Vec<CommasOrInstruction>>()
1206        .map(|ci_vec| instructions_with_comma_counts(ci_vec.into_iter()))
1207        .map(|cdfs| match OneOrMore::try_from_iter(cdfs.into_iter()) {
1208            Ok(cdfs) => cdfs,
1209            Err(_) => {
1210                unreachable!("instructions_with_comma_counts generated an empty output");
1211            }
1212        })
1213    });
1214
1215    // Assginments are called "equalities" in the TX-2 Users Handbook.
1216    // See section 6-2.2, "SYMEX DEFINITON - TAGS - EQUALITIES -
1217    // AUTOMATIC ASSIGNMENT".
1218    let assignment = (symex::parse_symex(SymexSyllableRule::Multiple, Script::Normal)
1219        .then_ignore(just(Tok::Equals(Script::Normal)))
1220        .then(comma_delimited_instructions.clone().map_with(
1221            |val: OneOrMore<CommaDelimitedFragment>, extra| {
1222                EqualityValue::from((extra.span(), UntaggedProgramInstruction::from(val)))
1223            },
1224        )))
1225    .map_with(|(name, value), extra| Equality {
1226        span: extra.span(),
1227        name,
1228        value,
1229    })
1230    .labelled("equality (assignment)");
1231
1232    let tagged_program_instruction = tagged_program_instruction.clone();
1233
1234    Grammar {
1235        assignment: assignment.boxed(),
1236        tagged_program_instruction: tagged_program_instruction.boxed(),
1237        normal_arithmetic_expression_allowing_spaces: arith_expr.clone()(
1238            ALLOW_SPACES,
1239            Script::Normal,
1240        )
1241        .boxed(),
1242        superscript_arithmetic_expression_allowing_spaces: arith_expr.clone()(
1243            ALLOW_SPACES,
1244            Script::Super,
1245        )
1246        .boxed(),
1247        subscript_arithmetic_expression_allowing_spaces: arith_expr.clone()(
1248            ALLOW_SPACES,
1249            Script::Sub,
1250        )
1251        .boxed(),
1252        #[cfg(test)]
1253        instruction_fragment: program_instruction_fragment.boxed(),
1254    }
1255}
1256
1257#[cfg(test)]
1258fn tagged_instruction<'a, I>()
1259-> impl Parser<'a, I, TaggedProgramInstruction, ExtraWithoutContext<'a>>
1260where
1261    I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
1262{
1263    grammar().tagged_program_instruction
1264}
1265
1266fn manuscript_line<'a, I>() -> impl Parser<'a, I, ManuscriptLine, ExtraWithoutContext<'a>>
1267where
1268    I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
1269{
1270    fn execute_metacommand(state: &mut NumeralMode, cmd: &ManuscriptMetaCommand) {
1271        match cmd {
1272            ManuscriptMetaCommand::Punch(_) | ManuscriptMetaCommand::Macro(_) => {
1273                // Instead of executing this metacommand as we parse it,
1274                // we simply return it as part of the parser output, and
1275                // it is executed by the driver.
1276            }
1277            ManuscriptMetaCommand::BaseChange(new_base) => state.set_numeral_mode(*new_base),
1278        }
1279    }
1280
1281    fn parse_and_execute_metacommand<'a, 'b, I>(
1282        grammar: &Grammar<'a, 'b, I>,
1283    ) -> impl Parser<'a, I, ManuscriptLine, ExtraWithoutContext<'a>> + use<'a, 'b, I>
1284    where
1285        I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
1286    {
1287        metacommand(grammar)
1288            .map_with(|cmd, extra| {
1289                execute_metacommand(&mut extra.state().numeral_mode, &cmd);
1290                ManuscriptLine::Meta(cmd)
1291            })
1292            .labelled("metacommand")
1293    }
1294
1295    fn build_code_line(
1296        (maybe_origin, statement): (Option<Origin>, TaggedProgramInstruction),
1297    ) -> ManuscriptLine {
1298        match maybe_origin {
1299            None => ManuscriptLine::StatementOnly(statement),
1300            Some(origin) => ManuscriptLine::OriginAndStatement(origin, statement),
1301        }
1302    }
1303
1304    fn line<'a, I>() -> impl Parser<'a, I, ManuscriptLine, ExtraWithoutContext<'a>>
1305    where
1306        I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
1307    {
1308        fn origin<'a, I>() -> impl Parser<'a, I, Origin, ExtraWithoutContext<'a>>
1309        where
1310            I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
1311        {
1312            /// An address expression is a literal value or a symex.  That is I
1313            /// think it's not required that an arithmetic expression
1314            /// (e.g. "5+BAR") be accepted in an origin notation (such as
1315            /// "<something>|").
1316            fn literal_address_expression<'a, I>()
1317            -> impl Parser<'a, I, Origin, ExtraWithoutContext<'a>>
1318            where
1319                I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
1320            {
1321                literal(Script::Normal)
1322                    .then_ignore(just(Tok::Pipe(Script::Normal)))
1323                    .try_map(|lit, span| match Address::try_from(lit.value()) {
1324                        Ok(addr) => Ok(Origin::Literal(span, addr)),
1325                        Err(e) => Err(Rich::custom(span, format!("not a valid address: {e}"))),
1326                    })
1327                    .labelled("literal address expression")
1328            }
1329
1330            fn symbolic_address_expression<'a, I>()
1331            -> impl Parser<'a, I, Origin, ExtraWithoutContext<'a>>
1332            where
1333                I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
1334            {
1335                named_symbol(SymexSyllableRule::Multiple, Script::Normal)
1336                    .then_ignore(just(Tok::Pipe(Script::Normal)))
1337                    .map_with(|name, extra| Origin::Symbolic(extra.span(), name))
1338                    .labelled("symbolic address expression")
1339            }
1340
1341            // An origin specification is an expression followed by a
1342            // (normal-case) pipe symbol.
1343            choice((literal_address_expression(), symbolic_address_expression()))
1344                .labelled("origin specification")
1345        }
1346
1347        let grammar = grammar();
1348
1349        let optional_origin_with_statement = origin()
1350            .or_not()
1351            .then(grammar.tagged_program_instruction.clone())
1352            .map(build_code_line)
1353            .labelled("statement with origin");
1354
1355        // TODO: also allowed: "T1->T2->ORIGIN|"
1356        let origin_only = origin().map(ManuscriptLine::OriginOnly);
1357        let tags_only = tag_definition()
1358            .repeated()
1359            .at_least(1)
1360            .collect()
1361            .map(ManuscriptLine::TagsOnly);
1362        let equality = grammar.assignment.clone().map(ManuscriptLine::Eq);
1363
1364        choice((
1365            // We have to parse an assignment first here, in order to
1366            // accept "FOO=2" as an assignment rather than the instruction
1367            // fragment "FOO" followed by a syntax error.
1368            equality,
1369            macro_invocation().map(ManuscriptLine::Macro),
1370            // Ignore whitespace after the metacommand but not before it.
1371            parse_and_execute_metacommand(&grammar),
1372            optional_origin_with_statement,
1373            // Because we prefer to parse a statement if one exists,
1374            // the origin_only alternative has to appear after the
1375            // alternative which parses a statement.
1376            origin_only,
1377            // Similarly for lines containing only tag efinitions.
1378            tags_only,
1379        ))
1380    }
1381
1382    line()
1383}
1384
1385fn end_of_line<'a, I>() -> impl Parser<'a, I, (), ExtraWithoutContext<'a>>
1386where
1387    I: Input<'a, Token = Tok, Span = Span>,
1388{
1389    let one_end_of_line = just(Tok::Newline).labelled("end-of-line").ignored();
1390
1391    one_end_of_line
1392        .repeated()
1393        .at_least(1)
1394        .ignored()
1395        .labelled("comment or end-of-line")
1396}
1397
1398fn terminated_manuscript_line<'a, I>()
1399-> impl Parser<'a, I, Option<(Span, ManuscriptLine)>, ExtraWithoutContext<'a>>
1400where
1401    I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
1402{
1403    // If we support INSERT, DELETE, REPLACE, we will need to
1404    // separate the processing of the metacommands and the
1405    // generation of the assembled code.
1406    manuscript_line()
1407        .or_not()
1408        .map_with(|maybe_line, extra| maybe_line.map(|line| (extra.span(), line)))
1409        .then_ignore(end_of_line())
1410}
1411
1412pub(crate) fn source_file<'a, I>() -> impl Parser<'a, I, SourceFile, ExtraWithoutContext<'a>>
1413where
1414    I: Input<'a, Token = Tok, Span = Span> + ValueInput<'a>,
1415{
1416    fn inconsistency_error<'src>(
1417        span: Span,
1418        name: &SymbolName,
1419        what: &str,
1420    ) -> chumsky::error::Rich<'src, lexer::Token> {
1421        Rich::custom(
1422            span,
1423            format!("internal error: inconsistent parser state for macro {name}: {what}"),
1424        )
1425    }
1426
1427    fn check_consistent<'a>(
1428        macro_def_in_source_file: Option<&MacroDefinition>,
1429        macro_def_in_state: Option<&MacroDefinition>,
1430    ) -> Result<(), chumsky::error::Rich<'a, lexer::Token>> {
1431        match (macro_def_in_source_file, macro_def_in_state) {
1432            (None, None) => {
1433                panic!("all_name is incorrect");
1434            }
1435            (None, Some(state_def)) => Err(inconsistency_error(
1436                state_def.span,
1437                &state_def.name,
1438                "missing from SourceFile output",
1439            )),
1440            (Some(source_file_def), None) => Err(inconsistency_error(
1441                source_file_def.span,
1442                &source_file_def.name,
1443                "missing from State",
1444            )),
1445            (Some(source_file_def), Some(state_def)) => {
1446                if source_file_def == state_def {
1447                    Ok(())
1448                } else {
1449                    Err(inconsistency_error(
1450                        source_file_def.span,
1451                        &source_file_def.name,
1452                        "inconsistently defined",
1453                    ))
1454                }
1455            }
1456        }
1457    }
1458
1459    terminated_manuscript_line()
1460        .repeated()
1461        .collect()
1462        .try_map_with(|lines: Vec<Option<(Span, ManuscriptLine)>>, extra| {
1463            // Filter out empty lines.
1464            let lines: Vec<(Span, ManuscriptLine)> = lines.into_iter().flatten().collect();
1465            let source_file: SourceFile = manuscript_lines_to_source_file(lines)?;
1466            let state_macros: BTreeMap<SymbolName, MacroDefinition> =
1467                extra.state().macros().clone();
1468            let all_names: BTreeSet<&SymbolName> = source_file
1469                .macros
1470                .keys()
1471                .chain(state_macros.keys())
1472                .collect();
1473            for name in all_names {
1474                check_consistent(
1475                    source_file.macros.get(name),
1476                    extra.state().macros().get(name),
1477                )?;
1478            }
1479            Ok(source_file)
1480        })
1481}
1482
1483type Mig<I, O> = chumsky::input::MappedInput<
1484    Tok,
1485    SimpleSpan,
1486    chumsky::input::Stream<std::vec::IntoIter<(Tok, SimpleSpan)>>,
1487    fn(I) -> O,
1488>;
1489pub(crate) type Mi = Mig<(Tok, SimpleSpan), (Tok, SimpleSpan)>;
1490
1491pub(crate) fn tokenize_and_parse_with<'a, P, T, F>(
1492    input: &'a str,
1493    mut setup: F,
1494    parser: P,
1495) -> (Option<T>, Vec<Rich<'a, Tok>>)
1496where
1497    F: FnMut(&mut State),
1498    P: Parser<'a, Mi, T, ExtraWithoutContext<'a>>,
1499{
1500    let mut state = State::new(input, NumeralMode::default());
1501    setup(&mut state);
1502
1503    // These conversions are adapted from the Logos example in the
1504    // Chumsky documentation.
1505    let scanner = lexer::Lexer::new(input).spanned();
1506    let tokens: Vec<(Tok, SimpleSpan)> = scanner
1507        .map(|item: (Tok, Range<usize>)| -> (Tok, Span) {
1508            match item {
1509                (Tok::Tab, span) => {
1510                    // The basic problem here is that the TX-2's
1511                    // M4 assembler allows a space to occur in the
1512                    // middle of a symex.  We implement this in
1513                    // the parser by returning the individual
1514                    // parts from the lexer and having the parser
1515                    // join them together.  The lexer doesn't
1516                    // return spaces.  In order to prevent the
1517                    // parser joining together "XY\tZ" in a
1518                    // similar way we would need to return TAB as
1519                    // a lexeme.  The problem with doing that
1520                    // though is that the parser would have to
1521                    // permit the TAB token between regular
1522                    // tokens everywhere in the grammar except
1523                    // between two symex components.  That would
1524                    // make the grammar difficult to maintain (and
1525                    // difficult to specify without bugs).
1526                    (Tok::Error(lexer::ErrorTokenKind::Tab), span.into())
1527                }
1528                (tok, span) => {
1529                    // Turn the `Range<usize>` spans logos gives us into
1530                    // chumsky's `SimpleSpan` via `Into`, because it's
1531                    // easier to work with
1532                    (tok, span.into())
1533                }
1534            }
1535        })
1536        .collect();
1537    let end_span: SimpleSpan = SimpleSpan::new(
1538        0,
1539        tokens.iter().map(|(_, span)| span.end).max().unwrap_or(0),
1540    );
1541    let token_stream: Mi = Stream::from_iter(tokens).map(end_span, |unchanged| unchanged);
1542    parser
1543        .parse_with_state(token_stream, &mut state)
1544        .into_output_errors()
1545}
1546
1547pub(crate) fn parse_source_file(
1548    source_file_body: &str,
1549    setup: fn(&mut State),
1550) -> (Option<SourceFile>, Vec<Rich<'_, Tok>>) {
1551    let parser = source_file();
1552    tokenize_and_parse_with(source_file_body, setup, parser)
1553}
1554
1555// Local Variables:
1556// mode: rustic
1557// lsp-rust-analyzer-server-display-inlay-hints: nil
1558// End: