diff --git a/src/protocol/input_source.rs b/src/protocol/input_source.rs index 0ea65391f07b7e6d4d08abf34978a09a61ff9ffd..93f133f9fd795e66c77b66bba97e7fad3b4f387e 100644 --- a/src/protocol/input_source.rs +++ b/src/protocol/input_source.rs @@ -454,6 +454,14 @@ impl ParseError { self.with_at_span(StatementKind::Info, source, span, msg) } + pub fn with_info_str_at_pos(self, source: &InputSource, pos: InputPosition, msg: &str) -> Self { + self.with_at_span( + StatementKind::Info, source, + InputSpan::from_positions(pos, pos.with_offset(1)), + msg.to_string() + ) + } + pub fn with_info_str_at_span(self, source: &InputSource, span: InputSpan, msg: &str) -> Self { self.with_at_span(StatementKind::Info, source, span, msg.to_string()) } diff --git a/src/protocol/parser/pass_definitions.rs b/src/protocol/parser/pass_definitions.rs index 5955dfdff242e285bdfef5ac867b75c9b5fcab40..b7f2f70ae093791e51e0fb9a5ec6555e0bf36ce4 100644 --- a/src/protocol/parser/pass_definitions.rs +++ b/src/protocol/parser/pass_definitions.rs @@ -3,13 +3,17 @@ use super::symbol_table::*; use super::{Module, ModuleCompilationPhase, PassCtx}; use super::tokens::*; use super::token_parsing::*; -use crate::protocol::input_source::{InputSource as InputSource, InputPosition as InputPosition, InputSpan, ParseError}; +use super::pass_definitions_types::*; + +use crate::protocol::input_source::{InputSource, InputPosition, InputSpan, ParseError}; use crate::collections::*; /// Parses all the tokenized definitions into actual AST nodes. pub(crate) struct PassDefinitions { // State associated with the definition currently being processed cur_definition: DefinitionId, + // Itty bitty parsing machines + type_parser: ParserTypeParser, // Temporary buffers of various kinds buffer: String, struct_fields: ScopedBuffer, @@ -25,6 +29,7 @@ impl PassDefinitions { pub(crate) fn new() -> Self { Self{ cur_definition: DefinitionId::new_invalid(), + type_parser: ParserTypeParser::new(), buffer: String::with_capacity(128), struct_fields: ScopedBuffer::new_reserved(128), enum_variants: ScopedBuffer::new_reserved(128), diff --git a/src/protocol/parser/pass_definitions_types.rs b/src/protocol/parser/pass_definitions_types.rs index a67c9f5f48d4dc486f18b7d97718ab603e6435c2..834a7415dbb763dae5b7ae347b2294c6442e85d1 100644 --- a/src/protocol/parser/pass_definitions_types.rs +++ b/src/protocol/parser/pass_definitions_types.rs @@ -1,12 +1,12 @@ -use crate::protocol::*; use crate::protocol::parser::*; +use crate::protocol::parser::token_parsing::*; struct Entry { element: ParserTypeElement, depth: i32, } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, PartialEq, Eq)] enum DepthKind { Tuple, // because we had a `(` token PolyArgs, // because we had a `<` token @@ -14,15 +14,20 @@ enum DepthKind { struct DepthElement { kind: DepthKind, + entry_index: u32, // in `entries` array of parser pos: InputPosition, } -/// Temporarily keep the error around in unevaluated format because sometimes -/// when we evaluate a `ParserType`, we perform some backtracking if it fails. -enum ParserTypeError { - ExpectedAType(InputPosition), - MessageAt(InputPosition, &'static str), - TwoMessageAt(InputPosition, &'static str, InputPosition, &'static str), +/// Current parsing state, for documentation's sake: types may be named, or may +/// be tuples. Named types may have polymorphic arguments (if the type +/// declaration allows) and a type may be turned into an array of that type by +/// postfixing a "[]". +enum ParseState { + TypeMaybePolyArgs, // just parsed a type, might have poly arguments + TypeNeverPolyArgs, // just parsed a type that cannot have poly arguments + PolyArgStart, // just opened a polymorphic argument list + TupleStart, // just opened a tuple list + ParsedComma, // just had a comma } /// Parsers tokens into `ParserType` instances (yes, the name of the struct is @@ -30,34 +35,58 @@ enum ParserTypeError { pub(crate) struct ParserTypeParser { entries: Vec, depths: Vec, + parse_state: ParseState, first_pos: InputPosition, last_pos: InputPosition, } impl ParserTypeParser { + pub(crate) fn new() -> Self { + return Self{ + entries: Vec::with_capacity(16), + depths: Vec::with_capacity(16), + parse_state: ParseState::TypeMaybePolyArgs, + first_pos: InputPosition{ line: 0, offset: 0 }, + last_pos: InputPosition{ line: 0, offset: 0 } + } + } + pub(crate) fn consume_parser_type( - &mut self, - source: &InputSource, iter: &mut TokenIter, - symbols: &SymbolTable, heap: &Heap, poly_vars: &[Identifier], - cur_scope: SymbolScope, wrapping_definition: DefinitionId, + &mut self, iter: &mut TokenIter, heap: &Heap, source: &InputSource, + symbols: &SymbolTable, poly_vars: &[Identifier], + wrapping_definition: DefinitionId, cur_scope: SymbolScope, allow_inference: bool, inside_angular_bracket: Option, - ) -> Result { - // Make sure we start in an empty state - debug_assert!(self.entries.is_empty()); - debug_assert!(self.depths.is_empty()); + ) -> Result { + // Prepare + self.entries.clear(); + self.depths.clear(); // Setup processing if let Some(bracket_pos) = inside_angular_bracket { - self.push_depth(DepthKind::PolyArgs, bracket_pos); + self.push_depth(DepthKind::PolyArgs, u32::MAX, bracket_pos); } - let first_element = match iter.next() { + let initial_state = match iter.next() { Some(TokenKind::Ident) => { + let element = Self::consume_parser_type_element( + iter, source, heap, symbols, wrapping_definition, poly_vars, cur_scope, allow_inference + )?; + self.first_pos = element.element_span.begin; + self.last_pos = element.element_span.end; + + self.entries.push(Entry{ + element, + depth: self.cur_depth(), + }); + ParseState::TypeMaybePolyArgs }, Some(TokenKind::OpenParen) => { let tuple_start_pos = iter.next_start_position(); - self.push_depth(DepthKind::Tuple, tuple_start_pos); + self.first_pos = tuple_start_pos; // last pos will be set later, this is a tuple + + let tuple_entry_index = self.entries.len() as u32; + self.push_depth(DepthKind::Tuple, tuple_entry_index, tuple_start_pos); self.entries.push(Entry{ element: ParserTypeElement{ element_span: InputSpan::from_positions(tuple_start_pos, tuple_start_pos), @@ -66,10 +95,159 @@ impl ParserTypeParser { depth: self.cur_depth(), }); iter.consume(); + + ParseState::PolyArgStart }, - _ => return Err(ParserTypeError::MessageAt(iter.last_valid_pos(), "expected a type")), + _ => return Err(ParseError::new_error_str_at_pos(source, iter.last_valid_pos(), "expected a type")), }; + self.parse_state = initial_state; + + // Depth stack and entries are initialized, continue until depth stack + // is empty, or until an unexpected set of tokens is encountered + while !self.depths.is_empty() { + let next = iter.next(); + + match self.parse_state { + ParseState::TypeMaybePolyArgs => { + // Allowed tokens: , < > >> ) [ + match next { + Some(TokenKind::Comma) => self.consume_comma(iter), + Some(TokenKind::OpenAngle) => self.consume_open_angle(iter), + Some(TokenKind::CloseAngle) => self.consume_close_angle(source, iter)?, + Some(TokenKind::ShiftRight) => self.consume_double_close_angle(source, iter)?, + Some(TokenKind::CloseParen) => self.consume_close_paren(source, iter)?, + Some(TokenKind::OpenSquare) => self.consume_square_parens(source, iter)?, + _ => return Err(ParseError::new_error_str_at_pos( + source, iter.last_valid_pos(), + "unexpected token: expected ',', '<', '>', '<<', ')' or '['" + )), + } + }, + ParseState::TypeNeverPolyArgs => { + // Allowed tokens: , > >> ) [ + match next { + Some(TokenKind::Comma) => self.consume_comma(iter), + Some(TokenKind::CloseAngle) => self.consume_close_angle(source, iter)?, + Some(TokenKind::ShiftRight) => self.consume_double_close_angle(source, iter)?, + Some(TokenKind::CloseParen) => self.consume_close_paren(source, iter)?, + Some(TokenKind::OpenSquare) => self.consume_square_parens(source, iter)?, + _ => return Err(ParseError::new_error_str_at_pos( + source, iter.last_valid_pos(), + "unexpected token: expected ',', '>', '>>', ')' or '['" + )), + } + }, + ParseState::PolyArgStart => { + // Allowed tokens: ident ( + match next { + Some(TokenKind::Ident) => self.consume_type_idents( + source, heap, symbols, wrapping_definition, poly_vars, cur_scope, allow_inference, iter + )?, + Some(TokenKind::OpenParen) => self.consume_open_paren(iter), + _ => return Err(ParseError::new_error_str_at_pos( + source, iter.last_valid_pos(), + "unexpected token: expected typename or '('" + )), + } + }, + ParseState::TupleStart => { + // Allowed tokens: ident ) + match next { + Some(TokenKind::Ident) => self.consume_type_idents( + source, heap, symbols, wrapping_definition, poly_vars, cur_scope, allow_inference, iter + )?, + Some(TokenKind::CloseParen) => self.consume_close_paren(source, iter)?, + _ => return Err(ParseError::new_error_str_at_pos( + source, iter.last_valid_pos(), + "unexpected token: expected typename or ')'" + )), + } + }, + ParseState::ParsedComma => { + // Allowed tokens: ident ( > >> ) + match next { + Some(TokenKind::Ident) => self.consume_type_idents( + source, heap, symbols, wrapping_definition, poly_vars, cur_scope, allow_inference, iter + )?, + Some(TokenKind::OpenParen) => self.consume_open_paren(iter), + Some(TokenKind::CloseAngle) => self.consume_close_angle(source, iter)?, + Some(TokenKind::ShiftRight) => self.consume_double_close_angle(source, iter)?, + Some(TokenKind::CloseParen) => self.consume_close_paren(source, iter)?, + _ => return Err(ParseError::new_error_str_at_pos( + source, iter.last_valid_pos(), + "unexpected token: expected typename, '(', '>', '>>' or ')'" + )) + } + } + } + } + + // If here then we have found the correct number of closing braces. + // However we might still have any number of array postfixed + if inside_angular_bracket.is_none() { + while Some(TokenKind::OpenSquare) == iter.next() { + self.consume_square_parens(source, iter)?; + } + } + + // Type should be completed. But we still need to check the polymorphic + // arguments and strip tuples with just one embedded type. + let num_entries = self.entries.len(); + + for el_index in 0..num_entries { + let cur_element = &self.entries[el_index]; + + // Peek ahead to see how many embedded types we have + let mut encountered_embedded = 0; + for peek_index in el_index + 1..num_entries { + let peek_element = &self.entries[peek_index]; + if peek_element.depth == cur_element.depth + 1 { + encountered_embedded += 1; + } else if peek_element.depth <= cur_element.depth { + break; + } + } + + // If we're dealing with a tuple then we don't need to determine if + // the number of embedded types is correct, we simply need to set it + // to whatever what was encountered. + if let ParserTypeVariant::Tuple(_) = cur_element.element.variant { + self.entries[el_index].element.variant = ParserTypeVariant::Tuple(encountered_embedded); + } else { + let expected_embedded = cur_element.element.variant.num_embedded() as u32; + if expected_embedded != encountered_embedded { + if encountered_embedded == 0 { + // Every polymorphic argument should be inferred + if !allow_inference { + return Err(ParseError::new_error_str_at_span( + source, cur_element.element.element_span, + "type inference is not allowed here" + )); + } + + // Insert missing types + let inserted_span = cur_element.element.element_span; + let inserted_depth = cur_element.depth + 1; + self.entries.reserve(expected_embedded as usize); + self.entries.insert(el_index + 1, Entry{ + element: ParserTypeElement{ + element_span: inserted_span, + variant: ParserTypeVariant::Inferred, + }, + depth: inserted_depth, + }); + } else { + // Mismatch in number of embedded types + return Err(Self::construct_poly_arg_mismatch_error( + source, cur_element.element.element_span, allow_inference, + expected_embedded, encountered_embedded + )); + } + } + } + } + // Convert the results from parsing into the `ParserType` let mut elements = Vec::with_capacity(self.entries.len()); debug_assert!(!self.entries.is_empty()); @@ -84,42 +262,346 @@ impl ParserTypeParser { }); } - /// Consumes an identifier that should resolve to some kind of type. There - /// may be trailing '::' tokens, commas, or polymorphic arguments. - fn consume_parser_type_ident( - &self, iter: &mut TokenIter, symbols: &SymbolTable, poly_vars: &[Identifier], - allow_inference: bool, - ) -> Result { + // --- Parsing Utilities + #[inline] + fn consume_type_idents( + &mut self, source: &InputSource, heap: &Heap, symbols: &SymbolTable, + wrapping_definition: DefinitionId, poly_vars: &[Identifier], + cur_scope: SymbolScope, allow_inference: bool, iter: &mut TokenIter + ) -> Result<(), ParseError> { + let element = Self::consume_parser_type_element( + iter, source, heap, symbols, wrapping_definition, poly_vars, cur_scope, allow_inference + )?; + let depth = self.cur_depth(); + self.last_pos = element.element_span.end; + self.entries.push(Entry{ element, depth }); + self.parse_state = ParseState::TypeMaybePolyArgs; + + return Ok(()); } #[inline] - fn push_depth(&mut self, kind: DepthKind, pos: InputPosition) { - self.depths.push(DepthElement{ kind, pos }); + fn consume_open_angle(&mut self, iter: &mut TokenIter) { + // Note: open angular bracket is only consumed when we just parsed an + // ident-based type. So the last element of the `entries` array is the + // one that this angular bracket starts the polymorphic arguments for. + let angle_start_pos = iter.next_start_position(); + let entry_index = (self.entries.len() - 1) as u32; + self.push_depth(DepthKind::PolyArgs, entry_index, angle_start_pos); + self.parse_state = ParseState::PolyArgStart; + + iter.consume(); + } + + #[inline] + fn consume_close_angle(&mut self, source: &InputSource, iter: &mut TokenIter) -> Result<(), ParseError> { + let (angle_start_pos, angle_end_pos) = iter.next_positions(); + self.last_pos = angle_end_pos; + self.pop_depth(source, DepthKind::PolyArgs, angle_start_pos)?; + self.parse_state = ParseState::TypeNeverPolyArgs; + + iter.consume(); + return Ok(()) } #[inline] - fn pop_depth(&mut self, kind: DepthKind, pos: InputPosition) -> Result<(), ParserTypeError> { + fn consume_double_close_angle(&mut self, source: &InputSource, iter: &mut TokenIter) -> Result<(), ParseError> { + let (angle_start_pos, angle_end_pos) = iter.next_positions(); + self.last_pos = angle_end_pos; + + self.pop_depth(source, DepthKind::PolyArgs, angle_start_pos)?; // first '>' in '>>'. + self.pop_depth(source, DepthKind::PolyArgs, angle_start_pos.with_offset(1))?; // second '>' in '>>' + self.parse_state = ParseState::TypeNeverPolyArgs; + + iter.consume(); // consume once, '>>' is one token + return Ok(()) + } + + #[inline] + fn consume_open_paren(&mut self, iter: &mut TokenIter) { + let paren_start_pos = iter.next_start_position(); + let cur_depth = self.cur_depth(); + let entry_index = self.entries.len() as u32; + self.entries.push(Entry{ + element: ParserTypeElement { + element_span: InputSpan::from_positions(paren_start_pos, paren_start_pos), + variant: ParserTypeVariant::Tuple(0), + }, + depth: cur_depth, + }); + + self.push_depth(DepthKind::Tuple, entry_index, paren_start_pos); + self.parse_state = ParseState::TupleStart; + + iter.consume(); + } + + #[inline] + fn consume_close_paren(&mut self, source: &InputSource, iter: &mut TokenIter) -> Result<(), ParseError> { + let (paren_start_pos, paren_end_pos) = iter.next_positions(); + self.last_pos = paren_end_pos; + let tuple_type_index = self.pop_depth(source, DepthKind::Tuple, paren_start_pos)?; + self.entries[tuple_type_index as usize].element.element_span.end = paren_end_pos.with_offset(1); + self.parse_state = ParseState::TypeNeverPolyArgs; + + iter.consume(); + return Ok(()) + } + + #[inline] + fn consume_comma(&mut self, iter: &mut TokenIter) { + iter.consume(); + self.parse_state = ParseState::ParsedComma; + } + + #[inline] + fn consume_square_parens(&mut self, source: &InputSource, iter: &mut TokenIter) -> Result<(), ParseError> { + // Consume the opening square paren that forms the postfixed array type + let array_start_pos = iter.next_start_position(); + iter.consume(); + if iter.next() != Some(TokenKind::CloseSquare) { + return Err(ParseError::new_error_str_at_pos( + source, iter.last_valid_pos(), + "unexpected token: expected ']'" + )); + } + + let (_, array_end_pos) = iter.next_positions(); + let array_span = InputSpan::from_positions(array_start_pos, array_end_pos); + self.last_pos = array_end_pos; + + // In the language we put the array specification after a type, in the + // type tree we need to make the array type the parent, so: + let insert_depth = self.cur_depth(); + let insert_at = self.entries.iter().rposition(|e| e.depth == insert_depth).unwrap(); + let num_embedded = self.entries[insert_at].element.variant.num_embedded(); + + self.entries.insert(insert_at, Entry{ + element: ParserTypeElement{ + element_span: array_span, + variant: ParserTypeVariant::Array, + }, + depth: insert_depth + }); + + // Need to increment the depth of the child types + self.entries[insert_at + 1].depth += 1; // element we applied the array type to + if num_embedded != 0 { + for index in insert_at + 2..self.entries.len() { + let element = &mut self.entries[index]; + if element.depth >= insert_depth + 1 { + element.depth += 1; + } else { + break; + } + } + } + + return Ok(()) + } + + /// Consumes a namespaced identifier that should resolve to some kind of + /// type. There may be commas or polymorphic arguments remaining after this + /// function has finished. + fn consume_parser_type_element( + iter: &mut TokenIter, source: &InputSource, heap: &Heap, symbols: &SymbolTable, + wrapping_definition: DefinitionId, poly_vars: &[Identifier], + mut scope: SymbolScope, allow_inference: bool, + ) -> Result { + use ParserTypeVariant as PTV; + let (mut type_text, mut type_span) = consume_any_ident(source, iter)?; + + let variant = match type_text { + KW_TYPE_MESSAGE => PTV::Message, + KW_TYPE_BOOL => PTV::Bool, + KW_TYPE_UINT8 => PTV::UInt8, + KW_TYPE_UINT16 => PTV::UInt16, + KW_TYPE_UINT32 => PTV::UInt32, + KW_TYPE_UINT64 => PTV::UInt64, + KW_TYPE_SINT8 => PTV::SInt8, + KW_TYPE_SINT16 => PTV::SInt16, + KW_TYPE_SINT32 => PTV::SInt32, + KW_TYPE_SINT64 => PTV::SInt64, + KW_TYPE_IN_PORT => PTV::Input, + KW_TYPE_OUT_PORT => PTV::Output, + KW_TYPE_CHAR => PTV::Character, + KW_TYPE_STRING => PTV::String, + KW_TYPE_INFERRED => { + if !allow_inference { + return Err(ParseError::new_error_str_at_span( + source, type_span, "type inference is not allowed here" + )); + } + + PTV::Inferred + }, + _ => { + // Must be some kind of symbolic type + let mut type_kind = None; + for (poly_idx, poly_var) in poly_vars.iter().enumerate() { + if poly_var.value.as_bytes() == type_text { + type_kind = Some(PTV::PolymorphicArgument(wrapping_definition, poly_idx as u32)); + } + } + + if type_kind.is_none() { + // Check symbol table for definition. To be fair, the language + // only allows a single namespace for now. That said: + let last_symbol = symbols.get_symbol_by_name(scope, type_text); + if last_symbol.is_none() { + return Err(ParseError::new_error_str_at_span( + source, type_span, "unknown type" + )); + } + let mut last_symbol = last_symbol.unwrap(); + + // Resolving scopes until we reach the intended type + loop { + match &last_symbol.variant { + SymbolVariant::Module(symbol_module) => { + // Expecting more identifiers + if Some(TokenKind::ColonColon) != iter.next() { + return Err(ParseError::new_error_str_at_span( + source, type_span, "expected a type but got a module" + )); + } + + consume_token(source, iter, TokenKind::ColonColon)?; + + // Consume next part of type and prepare for next + // lookup loop + let (next_text, next_span) = consume_any_ident(source, iter)?; + let old_text = type_text; + type_text = next_text; + type_span.end = next_span.end; + scope = SymbolScope::Module(symbol_module.root_id); + + let new_symbol = symbols.get_symbol_by_name_defined_in_scope(scope, type_text); + if new_symbol.is_none() { + // If the type is imported in the module then notify the programmer + // that imports do not leak outside of a module + let type_name = String::from_utf8_lossy(type_text); + let module_name = String::from_utf8_lossy(old_text); + let suffix = if symbols.get_symbol_by_name(scope, type_text).is_some() { + format!( + ". The module '{}' does import '{}', but these imports are not visible to other modules", + &module_name, &type_name + ) + } else { + String::new() + }; + + let message = format!("unknown type '{}' in module '{}'{}", type_name, module_name, suffix); + return Err(ParseError::new_error_at_span(source, next_span, message)); + } + + last_symbol = new_symbol.unwrap(); + }, + SymbolVariant::Definition(symbol_definition) => { + let num_poly_vars = heap[symbol_definition.definition_id].poly_vars().len(); + type_kind = Some(PTV::Definition(symbol_definition.definition_id, num_poly_vars as u32)); + break; + } + } + } + } + + debug_assert!(type_kind.is_some()); + type_kind.unwrap() + }, + }; + + Ok(ParserTypeElement{ element_span: type_span, variant }) + } + + // --- Parsing Depth Management + + #[inline] + fn push_depth(&mut self, kind: DepthKind, entry_index: u32, pos: InputPosition) { + self.depths.push(DepthElement{ kind, entry_index, pos }); + } + + #[inline] + fn pop_depth(&mut self, source: &InputSource, kind: DepthKind, pos: InputPosition) -> Result { if self.depths.is_empty() { // More closing parens than opening ones let message = match kind { DepthKind::Tuple => "unmatched ')'", DepthKind::PolyArgs => "unmatched '>'", }; - return Err(ParserTypeError::MessageAt(pos, message)) + return Err(ParseError::new_error_str_at_pos(source, pos, message)) } - let last = *self.depths.last().unwrap(); - if last != kind { - // Wrong kind of paren - let (message_second) = match kind { - DepthKind::Tuple => "unexpected closing" - } + let last = self.depths.last().unwrap(); + if last.kind != kind { + // Wrong kind of closing parens + let (encountered_message, matching_message) = match kind { + DepthKind::Tuple => ( + "unexpected closing ')'", + "expected a '>' to match this '<'" + ), + DepthKind::PolyArgs => ( + "unexpected closing '>'", + "expected a ')' to match this '('" + ), + }; + + return Err( + ParseError::new_error_str_at_pos(source, pos, encountered_message) + .with_info_str_at_pos(source, last.pos, matching_message) + ); } + + let popped = self.depths.pop().unwrap(); + return Ok(popped.entry_index); } #[inline] fn cur_depth(&self) -> i32 { return self.depths.len() as i32; } + + // --- Small Utilities + + fn construct_poly_arg_mismatch_error( + source: &InputSource, span: InputSpan, allow_inference: bool, + num_expected: u32, num_encountered: u32 + ) -> ParseError { + let type_name = String::from_utf8_lossy(source.section_at_span(span)); + + fn polymorphic_name_text(num: u32) -> &'static str { + if num == 1 { "polymorphic argument" } else { "polymorphic arguments" } + } + fn were_or_was(num: u32) -> &'static str { + if num == 1 { "was" } else { "were" } + } + + if num_expected == 0 { + return ParseError::new_error_at_span( + source, span, + format!( + "the type '{}' is not polymorphic, yet {} {} {} provided", + type_name, num_encountered, polymorphic_name_text(num_encountered), + were_or_was(num_encountered) + ) + ); + } + + let maybe_infer_text = if allow_inference { + " (or none, to perform implicit type inference)" + } else { + "" + }; + + return ParseError::new_error_at_span( + source, span, + format!( + "expected {} {}{} for the type '{}', but {} {} provided", + num_expected, polymorphic_name_text(num_expected), + maybe_infer_text, type_name, num_encountered, + were_or_was(num_encountered) + ) + ); + } } \ No newline at end of file