From 32fecca45d44cdaae9fa2dbfed13c2f9bec92827 Mon Sep 17 00:00:00 2001 From: Mark Thom Date: Tue, 23 Oct 2018 22:53:50 -0600 Subject: [PATCH] add write variants --- README.md | 3 + src/prolog/heap_print.rs | 190 +++++++++++------------ src/prolog/instructions.rs | 9 +- src/prolog/lib/builtins.pl | 35 ++++- src/prolog/machine/machine_state.rs | 11 +- src/prolog/machine/machine_state_impl.rs | 29 ++-- src/prolog/machine/mod.rs | 6 +- src/prolog/machine/system_calls.rs | 45 ++++-- src/prolog/machine/term_expansion.rs | 8 +- 9 files changed, 181 insertions(+), 155 deletions(-) diff --git a/README.md b/README.md index c62fbf7f..65d0099c 100644 --- a/README.md +++ b/README.md @@ -172,7 +172,10 @@ The following predicates are built-in to rusty-wam. * `throw/1` * `true/0` * `var/1` +* `write/1` +* `write_canonical/1` * `writeq/1` +* `write_term/2` ## Tutorial To enter a multi-clause predicate, the directive "[user]" is used. diff --git a/src/prolog/heap_print.rs b/src/prolog/heap_print.rs index 0b83b406..69ae852b 100644 --- a/src/prolog/heap_print.rs +++ b/src/prolog/heap_print.rs @@ -32,29 +32,6 @@ pub enum TokenOrRedirect { HeadTailSeparator, } -pub trait HCValueFormatter { - // this function belongs to the display predicate formatter, which it uses - // to format all clauses. - fn format_struct(&self, arity: usize, name: ClauseName, state_stack: &mut Vec) - { - state_stack.push(TokenOrRedirect::Close); - - for _ in 0 .. arity { - state_stack.push(TokenOrRedirect::Redirect); - state_stack.push(TokenOrRedirect::Comma); - } - - state_stack.pop(); - state_stack.push(TokenOrRedirect::Open); - - state_stack.push(TokenOrRedirect::Atom(name)); - } - - // this can be overloaded to handle special cases, falling back on the default of - // format_struct when convenient. - fn format_clause(&self, &mut HCPreOrderIterator, usize, ClauseType, &mut Vec); -} - pub trait HCValueOutputter { type Output; @@ -110,9 +87,6 @@ impl HCValueOutputter for PrinterOutputter { } } -// the 'classic' display corresponding to the display predicate. -pub struct WriteqFormatter {} - #[inline] fn is_numbered_var(ct: &ClauseType, arity: usize) -> bool { arity == 1 && if let &ClauseType::Named(ref name, _) = ct { @@ -145,68 +119,17 @@ impl MachineState { } } -fn print_op(ct: ClauseType, fixity: Fixity, state_stack: &mut Vec) { - match fixity { - Fixity::Post => { - state_stack.push(TokenOrRedirect::Op(ct.name(), fixity)); - state_stack.push(TokenOrRedirect::CompositeRedirect(DirectedOp::Right(ct.name()))); - }, - Fixity::Pre => { - state_stack.push(TokenOrRedirect::CompositeRedirect(DirectedOp::Left(ct.name()))); - state_stack.push(TokenOrRedirect::Op(ct.name(), fixity)); - }, - Fixity::In => { - state_stack.push(TokenOrRedirect::CompositeRedirect(DirectedOp::Left(ct.name()))); - state_stack.push(TokenOrRedirect::Op(ct.name(), fixity)); - state_stack.push(TokenOrRedirect::CompositeRedirect(DirectedOp::Right(ct.name()))); - } - } -} - -impl HCValueFormatter for WriteqFormatter { - fn format_clause(&self, iter: &mut HCPreOrderIterator, arity: usize, - ct: ClauseType, state_stack: &mut Vec) - { - if let Some(fixity) = ct.fixity() { - return print_op(ct, fixity, state_stack); - } else if is_numbered_var(&ct, arity) { - let addr = iter.stack().last().cloned().unwrap(); - - // 7.10.4 - if let Some(var) = iter.machine_st.numbervar(addr) { - iter.stack().pop(); - state_stack.push(TokenOrRedirect::NumberedVar(var)); - return; - } - } - - self.format_struct(arity, ct.name(), state_stack); - } -} - -pub struct TermFormatter {} - -impl HCValueFormatter for TermFormatter { - fn format_clause(&self, _: &mut HCPreOrderIterator, arity: usize, ct: ClauseType, - state_stack: &mut Vec) - { - if let Some(fixity) = ct.fixity() { - print_op(ct, fixity, state_stack); - } else { - self.format_struct(arity, ct.name(), state_stack); - } - } -} - type ReverseHeapVarDict<'a> = HashMap>; -pub struct HCPrinter<'a, Formatter, Outputter> { - formatter: Formatter, +pub struct HCPrinter<'a, Outputter> { outputter: Outputter, machine_st: &'a MachineState, state_stack: Vec, heap_locs: ReverseHeapVarDict<'a>, - printed_vars: HashSet + printed_vars: HashSet, + pub(crate) numbervars: bool, + pub(crate) quoted: bool, + pub(crate) ignore_ops: bool } macro_rules! push_space_if_amb { @@ -315,29 +238,83 @@ fn ambiguity_check(atom: &str, op: &Option) -> Option } } -impl<'a, Formatter: HCValueFormatter, Outputter: HCValueOutputter> - HCPrinter<'a, Formatter, Outputter> +impl<'a, Outputter: HCValueOutputter> HCPrinter<'a, Outputter> { - pub fn new(machine_st: &'a MachineState, fmt: Formatter, output: Outputter) -> Self + pub fn new(machine_st: &'a MachineState, output: Outputter) -> Self { - HCPrinter { formatter: fmt, - outputter: output, + HCPrinter { outputter: output, machine_st, state_stack: vec![], heap_locs: ReverseHeapVarDict::new(), - printed_vars: HashSet::new() } + printed_vars: HashSet::new(), + numbervars: true, + quoted: true, + ignore_ops: false } } - pub fn from_heap_locs(machine_st: &'a MachineState, fmt: Formatter, - output: Outputter, heap_locs: &'a HeapVarDict) + pub fn from_heap_locs(machine_st: &'a MachineState, output: Outputter, + heap_locs: &'a HeapVarDict) -> Self { - let mut printer = Self::new(machine_st, fmt, output); + let mut printer = Self::new(machine_st, output); printer.heap_locs = reverse_heap_locs(machine_st, heap_locs); printer } + fn enqueue_op(&mut self, ct: ClauseType, fixity: Fixity) { + match fixity { + Fixity::Post => { + self.state_stack.push(TokenOrRedirect::Op(ct.name(), fixity)); + self.state_stack.push(TokenOrRedirect::CompositeRedirect(DirectedOp::Right(ct.name()))); + }, + Fixity::Pre => { + self.state_stack.push(TokenOrRedirect::CompositeRedirect(DirectedOp::Left(ct.name()))); + self.state_stack.push(TokenOrRedirect::Op(ct.name(), fixity)); + }, + Fixity::In => { + self.state_stack.push(TokenOrRedirect::CompositeRedirect(DirectedOp::Left(ct.name()))); + self.state_stack.push(TokenOrRedirect::Op(ct.name(), fixity)); + self.state_stack.push(TokenOrRedirect::CompositeRedirect(DirectedOp::Right(ct.name()))); + } + } + } + + fn format_struct(&mut self, arity: usize, name: ClauseName) + { + self.state_stack.push(TokenOrRedirect::Close); + + for _ in 0 .. arity { + self.state_stack.push(TokenOrRedirect::Redirect); + self.state_stack.push(TokenOrRedirect::Comma); + } + + self.state_stack.pop(); + self.state_stack.push(TokenOrRedirect::Open); + + self.state_stack.push(TokenOrRedirect::Atom(name)); + } + + fn format_clause(&mut self, iter: &mut HCPreOrderIterator, arity: usize, ct: ClauseType) + { + if let Some(fixity) = ct.fixity() { + if !self.ignore_ops { + return self.enqueue_op(ct, fixity); + } + } else if self.numbervars && is_numbered_var(&ct, arity) { + let addr = iter.stack().last().cloned().unwrap(); + + // 7.10.4 + if let Some(var) = iter.machine_st.numbervar(addr) { + iter.stack().pop(); + self.state_stack.push(TokenOrRedirect::NumberedVar(var)); + return; + } + } + + self.format_struct(arity, ct.name()); + } + fn offset_as_string(&self, addr: Addr) -> Option { match addr { Addr::HeapCell(h) | Addr::Lis(h) | Addr::Str(h) => @@ -397,7 +374,7 @@ impl<'a, Formatter: HCValueFormatter, Outputter: HCValueOutputter> match atom.as_str() { "" => self.outputter.append("''"), ";" | "!" => self.outputter.append(atom.as_str()), - s => if fixity.is_some() || non_quoted_token(s.chars()) { + s => if fixity.is_some() || !self.quoted || non_quoted_token(s.chars()) { self.outputter.append(atom.as_str()) } else { self.outputter.push_char('\''); @@ -424,7 +401,7 @@ impl<'a, Formatter: HCValueFormatter, Outputter: HCValueOutputter> _ => self.outputter.append(&format!("\\x{:x}", c as u32)) }; } - + fn print_constant(&mut self, c: Constant, op: &Option) { match c { Constant::Atom(ref atom, Some(fixity)) => { @@ -444,11 +421,14 @@ impl<'a, Formatter: HCValueFormatter, Outputter: HCValueOutputter> }), Constant::Char(c) if non_quoted_token(once(c)) => self.print_char(c), - Constant::Char(c) => { - self.outputter.push_char('\''); - self.print_char(c); - self.outputter.push_char('\''); - }, + Constant::Char(c) => + if self.quoted { + self.outputter.push_char('\''); + self.print_char(c); + self.outputter.push_char('\''); + } else { + self.print_char(c); + }, Constant::EmptyList => self.outputter.append("[]"), Constant::Number(Number::Float(fl)) => @@ -519,7 +499,7 @@ impl<'a, Formatter: HCValueFormatter, Outputter: HCValueOutputter> } let ct = ClauseType::from(name.clone(), arity, Some(fixity)); - self.formatter.format_clause(iter, arity, ct, &mut self.state_stack); + self.format_clause(iter, arity, ct); if op.is_some() { self.state_stack.push(TokenOrRedirect::Open); @@ -528,7 +508,7 @@ impl<'a, Formatter: HCValueFormatter, Outputter: HCValueOutputter> HeapCellValue::NamedStr(arity, name, fixity) => push_space_if_amb!(self, name.as_str(), &op, { let ct = ClauseType::from(name, arity, fixity); - self.formatter.format_clause(iter, arity, ct, &mut self.state_stack) + self.format_clause(iter, arity, ct); }), HeapCellValue::Addr(Addr::Con(Constant::EmptyList)) => if !self.at_cdr("") { @@ -581,17 +561,21 @@ impl<'a, Formatter: HCValueFormatter, Outputter: HCValueOutputter> TokenOrRedirect::Open => self.outputter.append("("), TokenOrRedirect::OpenList(delimit) => - if !self.at_cdr(", ") { + if self.ignore_ops { + self.format_struct(2, clause_name!(".")); + } else if !self.at_cdr(", ") { self.outputter.append("["); } else { delimit.set(false); }, TokenOrRedirect::CloseList(delimit) => - if delimit.get() { + if !self.ignore_ops && delimit.get() { self.outputter.append("]"); }, TokenOrRedirect::HeadTailSeparator => - self.outputter.append(" | "), + if !self.ignore_ops { + self.outputter.append(" | "); + }, TokenOrRedirect::Comma => self.outputter.append(", ") } diff --git a/src/prolog/instructions.rs b/src/prolog/instructions.rs index 0a7723e4..50437cca 100644 --- a/src/prolog/instructions.rs +++ b/src/prolog/instructions.rs @@ -242,7 +242,8 @@ pub enum SystemClauseType { SkipMaxList, Succeed, TermVariables, - UnwindStack + UnwindStack, + WriteTerm } impl SystemClauseType { @@ -282,6 +283,7 @@ impl SystemClauseType { &SystemClauseType::Succeed => clause_name!("$succeed"), &SystemClauseType::TermVariables => clause_name!("$term_variables"), &SystemClauseType::UnwindStack => clause_name!("$unwind_stack"), + &SystemClauseType::WriteTerm => clause_name!("$write_term"), } } @@ -317,6 +319,7 @@ impl SystemClauseType { ("$skip_max_list", 4) => Some(SystemClauseType::SkipMaxList), ("$term_variables", 2) => Some(SystemClauseType::TermVariables), ("$unwind_stack", 0) => Some(SystemClauseType::UnwindStack), + ("$write_term", 4) => Some(SystemClauseType::WriteTerm), _ => None } } @@ -329,7 +332,6 @@ pub enum BuiltInClauseType { Compare, CompareTerm(CompareTermQT), CyclicTerm, - Writeq, CopyTerm, Eq, Functor, @@ -381,7 +383,6 @@ impl BuiltInClauseType { &BuiltInClauseType::Compare => clause_name!("compare"), &BuiltInClauseType::CompareTerm(qt) => clause_name!(qt.name()), &BuiltInClauseType::CyclicTerm => clause_name!("cyclic_term"), - &BuiltInClauseType::Writeq => clause_name!("writeq"), &BuiltInClauseType::CopyTerm => clause_name!("copy_term"), &BuiltInClauseType::Eq => clause_name!("=="), &BuiltInClauseType::Functor => clause_name!("functor"), @@ -402,7 +403,6 @@ impl BuiltInClauseType { &BuiltInClauseType::Compare => 2, &BuiltInClauseType::CompareTerm(_) => 2, &BuiltInClauseType::CyclicTerm => 1, - &BuiltInClauseType::Writeq => 1, &BuiltInClauseType::CopyTerm => 2, &BuiltInClauseType::Eq => 2, &BuiltInClauseType::Functor => 3, @@ -428,7 +428,6 @@ impl BuiltInClauseType { ("@=<", 2) => Some(BuiltInClauseType::CompareTerm(CompareTermQT::LessThanOrEqual)), ("\\=@=", 2) => Some(BuiltInClauseType::CompareTerm(CompareTermQT::NotEqual)), ("=@=", 2) => Some(BuiltInClauseType::CompareTerm(CompareTermQT::Equal)), - ("writeq", 1) => Some(BuiltInClauseType::Writeq), ("copy_term", 2) => Some(BuiltInClauseType::CopyTerm), ("==", 2) => Some(BuiltInClauseType::Eq), ("functor", 3) => Some(BuiltInClauseType::Functor), diff --git a/src/prolog/lib/builtins.pl b/src/prolog/lib/builtins.pl index 9ec86330..e6cfa3fb 100644 --- a/src/prolog/lib/builtins.pl +++ b/src/prolog/lib/builtins.pl @@ -7,8 +7,8 @@ (==)/2, (\==)/2, (@=<)/2, (@>=)/2, (@<)/2, (@>)/2, (=@=)/2, (\=@=)/2, (:)/2, call_with_inference_limit/3, catch/3, current_prolog_flag/2, expand_term/2, set_prolog_flag/2, - term_variables/2, - setup_call_cleanup/3, throw/1, true/0, false/0]). + setup_call_cleanup/3, term_variables/2, throw/1, true/0, false/0, + write/1, write_canonical/1, writeq/1, write_term/2]). /* this is an implementation specific declarative operator used to implement call_with_inference_limit/3 and setup_call_cleanup/3. switches to the default trust_me and retry_me_else. Indexing choice @@ -206,6 +206,37 @@ get_args([Arg|Args], Func, I0, N) :- '$call_with_default_policy'(I1 is I0 + 1), '$call_with_default_policy'(get_args(Args, Func, I1, N)). +% write, write_canonical, writeq, write_term. +is_write_option(Functor) :- + Functor =.. [Name, Arg | Args], + ( Args == [], Arg == true -> true + ; Args == [], Arg == false -> true + ; throw(error(domain_error(write_option, Functor), write_term/2)) ), % 8.14.2.3 e) + ( Name == ignore_ops -> true + ; Name == quoted -> true + ; Name == numbervars -> true + ; throw(error(domain_error(write_option, Functor), write_term/2)) ). % 8.14.2.3 e) + +inst_member_or([X|Xs], Y, _) :- + ( nonvar(X), is_write_option(X) -> ( Y = X, ! ; inst_member_or(Xs, Y, _) ) + ; throw(instantiation_error) ). % 8.14.2.3 b) +inst_member_or([], Y, Y). + +write_term(Term, Options) :- + '$skip_max_list'(_, -1, Options, Options0), + ( Options0 == [] -> true + ; throw(error(type_error(list, Options), write_term/2)) ), % 8.14.2.3 c) + inst_member_or(Options, ignore_ops(IgnoreOps), ignore_ops(false)), + inst_member_or(Options, numbervars(NumberVars), numbervars(false)), + inst_member_or(Options, quoted(Quoted), quoted(false)), + '$write_term'(Term, IgnoreOps, NumberVars, Quoted). + +write(Term) :- write_term(Term, [numbervars(true)]). + +write_canonical(Term) :- write_term(Term, [ignore_ops(true), quoted(true)]). + +writeq(Term) :- write_term(Term, [quoted(true), numbervars(true)]). + % expand_term. expand_term(Term0, Term) :- '$expand_term'(Term0, Term). diff --git a/src/prolog/machine/machine_state.rs b/src/prolog/machine/machine_state.rs index a01fd6d9..5c478c16 100644 --- a/src/prolog/machine/machine_state.rs +++ b/src/prolog/machine/machine_state.rs @@ -4,7 +4,6 @@ use prolog_parser::string_list::*; use prolog::instructions::*; use prolog::and_stack::*; use prolog::copier::*; -use prolog::heap_print::*; use prolog::machine::IndexStore; use prolog::machine::machine_errors::*; use prolog::num::{BigInt, BigUint, Zero, One}; @@ -541,14 +540,6 @@ pub(crate) trait CallPolicy: Any { return_from_clause!(machine_st.last_call, machine_st) }, - &BuiltInClauseType::Writeq => { - let output = machine_st.print_term(machine_st[temp_v!(1)].clone(), - WriteqFormatter {}, - PrinterOutputter::new()); - - println!("{}", output.result()); - return_from_clause!(machine_st.last_call, machine_st) - }, &BuiltInClauseType::CopyTerm => { machine_st.duplicate_term(); return_from_clause!(machine_st.last_call, machine_st) @@ -671,7 +662,7 @@ pub(crate) trait CallPolicy: Any { machine_st.execute_inlined(&inlined), ClauseType::Op(..) | ClauseType::Named(..) => { let module = name.owning_module(); - + if let Some(idx) = indices.get_code_index((name.clone(), arity), module) { self.context_call(machine_st, name, arity, idx, indices)?; } else { diff --git a/src/prolog/machine/machine_state_impl.rs b/src/prolog/machine/machine_state_impl.rs index 9465b3ab..9d2beb5e 100644 --- a/src/prolog/machine/machine_state_impl.rs +++ b/src/prolog/machine/machine_state_impl.rs @@ -119,10 +119,10 @@ impl MachineState { } pub(super) - fn print_var_eq(&self, var: Rc, addr: Addr, var_dir: &HeapVarDict, - fmt: Fmt, mut output: Outputter) - -> Outputter - where Fmt: HCValueFormatter, Outputter: HCValueOutputter + fn print_var_eq(&self, var: Rc, addr: Addr, var_dir: &HeapVarDict, + mut output: Outputter) + -> Outputter + where Outputter: HCValueOutputter { let orig_len = output.len(); @@ -131,7 +131,9 @@ impl MachineState { output.append(var.as_str()); output.append(" = "); - let printer = HCPrinter::from_heap_locs(&self, fmt, output, var_dir); + let mut printer = HCPrinter::from_heap_locs(&self, output, var_dir); + printer.numbervars = false; + let mut output = printer.print(addr); let bad_ending = format!("= {}", &var); @@ -144,20 +146,11 @@ impl MachineState { } pub(super) - fn print_exception(&self, addr: Addr, var_dir: &HeapVarDict, - fmt: Fmt, output: Outputter) - -> Outputter - where Fmt: HCValueFormatter, Outputter: HCValueOutputter + fn print_exception(&self, addr: Addr, var_dir: &HeapVarDict, output: Outputter) + -> Outputter + where Outputter: HCValueOutputter { - let printer = HCPrinter::from_heap_locs(&self, fmt, output, var_dir); - printer.print(addr) - } - - pub(super) - fn print_term(&self, addr: Addr, fmt: Fmt, output: Outputter) -> Outputter - where Fmt: HCValueFormatter, Outputter: HCValueOutputter - { - let printer = HCPrinter::new(&self, fmt, output); + let printer = HCPrinter::from_heap_locs(&self, output, var_dir); printer.print(addr) } diff --git a/src/prolog/machine/mod.rs b/src/prolog/machine/mod.rs index 0370a58b..36df5819 100644 --- a/src/prolog/machine/mod.rs +++ b/src/prolog/machine/mod.rs @@ -348,7 +348,6 @@ impl Machine { let error_str = self.machine_st.print_exception(Addr::HeapCell(h), &heap_locs, - TermFormatter {}, PrinterOutputter::new()) .result(); @@ -401,9 +400,8 @@ impl Machine { let mut sorted_vars: Vec<(&Rc, &Addr)> = var_dir.iter().collect(); sorted_vars.sort_by_key(|ref v| v.0); - for (var, addr) in sorted_vars { - let fmt = TermFormatter {}; - output = self.machine_st.print_var_eq(var.clone(), addr.clone(), var_dir, fmt, output); + for (var, addr) in sorted_vars { + output = self.machine_st.print_var_eq(var.clone(), addr.clone(), var_dir, output); } output diff --git a/src/prolog/machine/system_calls.rs b/src/prolog/machine/system_calls.rs index a86835b6..0616d769 100644 --- a/src/prolog/machine/system_calls.rs +++ b/src/prolog/machine/system_calls.rs @@ -1,6 +1,7 @@ use prolog_parser::ast::*; use prolog::heap_iter::*; +use prolog::heap_print::*; use prolog::instructions::*; use prolog::machine::IndexStore; use prolog::machine::machine_errors::*; @@ -443,31 +444,31 @@ impl MachineState { &SystemClauseType::TermVariables => { let a1 = self[temp_v!(1)].clone(); let mut vars = Vec::new(); - + { - let iter = HCPreOrderIterator::new(self, a1); - + let iter = HCPreOrderIterator::new(self, a1); + for item in iter { - match item { + match item { HeapCellValue::Addr(Addr::HeapCell(h)) => - vars.push(Ref::HeapCell(h)), + vars.push(Ref::HeapCell(h)), HeapCellValue::Addr(Addr::StackCell(fr, sc)) => - vars.push(Ref::StackCell(fr, sc)), + vars.push(Ref::StackCell(fr, sc)), _ => {} } } } - + let mut h = self.heap.h; let outcome = Addr::HeapCell(h); let mut seen_vars = HashSet::new(); - + for r in vars { if seen_vars.contains(&r) { continue; } - + self.heap.push(HeapCellValue::Addr(Addr::Lis(h+1))); self.heap.push(HeapCellValue::Addr(r.as_addr())); @@ -481,7 +482,31 @@ impl MachineState { let a2 = self[temp_v!(2)].clone(); self.unify(a2, outcome); }, - &SystemClauseType::UnwindStack => self.unwind_stack() + &SystemClauseType::UnwindStack => self.unwind_stack(), + &SystemClauseType::WriteTerm => { + let addr = self[temp_v!(1)].clone(); + + let ignore_ops = self[temp_v!(2)].clone(); + let numbervars = self[temp_v!(3)].clone(); + let quoted = self[temp_v!(4)].clone(); + + let mut printer = HCPrinter::new(&self, PrinterOutputter::new()); + + if let &Addr::Con(Constant::Atom(ref name, ..)) = &ignore_ops { + printer.ignore_ops = name.as_str() == "true"; + } + + if let &Addr::Con(Constant::Atom(ref name, ..)) = &numbervars { + printer.numbervars = name.as_str() == "true"; + } + + if let &Addr::Con(Constant::Atom(ref name, ..)) = "ed { + printer.quoted = name.as_str() == "true"; + } + + let mut output = printer.print(addr); + println!("{}", output.result()); + } }; self.set_p(); diff --git a/src/prolog/machine/term_expansion.rs b/src/prolog/machine/term_expansion.rs index eb1e9be4..e815711a 100644 --- a/src/prolog/machine/term_expansion.rs +++ b/src/prolog/machine/term_expansion.rs @@ -124,9 +124,11 @@ impl MachineState { self.reset(); Ok(None) } else { - let mut output = self.print_term(Addr::HeapCell(h), - WriteqFormatter {}, - PrinterOutputter::new()); + let mut output = { + let mut printer = HCPrinter::new(&self, PrinterOutputter::new()); + printer.print(Addr::HeapCell(h)) + }; + output.push_char('.'); self.reset(); -- 2.54.0