diff --git a/docs/language/macros.md b/docs/language/macros.md index 0a2c180a..d5f6d8d2 100644 --- a/docs/language/macros.md +++ b/docs/language/macros.md @@ -106,6 +106,40 @@ all overloads for that name at once. %endif ``` +## Format Strings + +`FMT()` is a built-in macro that builds a string by interpolating values into a format string. The first argument must be a string literal containing `%s` placeholders. Each subsequent argument is substituted in order for each `%s`. + +```goboscript +FMT("Hello, %s! You are %s years old.", name, age) +``` + +This expands to a chain of `&` (join) operations: + +```goboscript +"Hello, " & name & "! You are " & age & " years old." +``` + +The number of arguments after the format string must exactly match the number of `%s` placeholders in the format string. Passing too few or too many arguments is a compile-time error. + +```goboscript +FMT("x = %s, y = %s", x, y) # correct: 2 placeholders, 2 arguments +FMT("x = %s", x, y) # error: 1 placeholder, 2 arguments given +FMT("x = %s, y = %s", x) # error: 2 placeholders, 1 argument given +``` + +If no placeholders are present (only one argument), `FMT` simply expands to that string literal: + +```goboscript +FMT("no placeholders") # expands to "no placeholders" +``` + +Use `%%` to include a literal `%` character in the output: + +```goboscript +FMT("%s%%", ratio) # expands to ratio & "%" +``` + ## Concatenate Tokens ```goboscript diff --git a/src/diagnostic/diagnostic_kind.rs b/src/diagnostic/diagnostic_kind.rs index c95f6dd1..130a97b0 100644 --- a/src/diagnostic/diagnostic_kind.rs +++ b/src/diagnostic/diagnostic_kind.rs @@ -83,6 +83,10 @@ pub enum DiagnosticKind { expected: usize, given: usize, }, + MacroTypeError { + macro_name: SmolStr, + message: SmolStr, + }, CommandFailed { stderr: Vec, }, @@ -223,6 +227,9 @@ impl DiagnosticKind { expected, given ) } + DiagnosticKind::MacroTypeError { macro_name, message } => { + format!("macro '{}': {}", macro_name, message) + } DiagnosticKind::CommandFailed { .. } => "command failed".to_string(), DiagnosticKind::ProcedureRedefinition(name) => { format!("procedure '{}' is already defined", name) @@ -437,6 +444,7 @@ impl From<&DiagnosticKind> for Level { | DiagnosticKind::ProcArgsCountMismatch { .. } | DiagnosticKind::FuncArgsCountMismatch { .. } | DiagnosticKind::MacroArgsCountMismatch { .. } + | DiagnosticKind::MacroTypeError { .. } | DiagnosticKind::CommandFailed { .. } | DiagnosticKind::ProcedureRedefinition(_) | DiagnosticKind::FunctionRedefinition(_) diff --git a/src/parser.rs b/src/parser.rs index 46aa9139..5a66cffa 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -68,12 +68,13 @@ fn parse_sprite(tokens: Vec) -> (Sprite, Vec) { pub fn parse(translation_unit: &TranslationUnit) -> (Sprite, Vec) { let (tokens, tokenize_diagnostics) = tokenize(translation_unit); let (tokens, preprocess_diagnostic) = preprocess(tokens); + let has_preprocess_error = preprocess_diagnostic.is_some(); let (sprite, parse_diagnostics) = parse_sprite(tokens); let all_diagnostics = tokenize_diagnostics .into_iter() .chain(preprocess_diagnostic) - .chain(parse_diagnostics) + .chain(if has_preprocess_error { vec![] } else { parse_diagnostics }) .collect(); (sprite, all_diagnostics) diff --git a/src/pre_processor.rs b/src/pre_processor.rs index 62c04220..edbbd310 100644 --- a/src/pre_processor.rs +++ b/src/pre_processor.rs @@ -70,6 +70,10 @@ impl<'a> PreProcessor<'a, '_> { if self.substitute_function_define(span, suppress)? { continue; } + if self.substitute_fmt(span)? { + dirty = true; + continue; + } if self.substitute_concat(span)? { dirty = true; continue; @@ -334,6 +338,145 @@ impl<'a> PreProcessor<'a, '_> { Ok(true) } + fn substitute_fmt(&mut self, span: &mut Span) -> Result { + let Token::Name(macro_name) = get_token(&self.tokens[*self.i]) else { + return Ok(false); + }; + let macro_name_span = get_span(&self.tokens[*self.i]); + if macro_name != "FMT" { + return Ok(false); + } + if self + .tokens + .get(*self.i + 1) + .is_none_or(|token| get_token(token) != &Token::LParen) + { + return Ok(false); + } + self.remove_token(span); + self.remove_token(span); + self.expect_no_eof()?; + let mut token = get_token(&self.tokens[*self.i]).clone(); + if token == Token::RParen { + return Err(Diagnostic { + kind: DiagnosticKind::MacroArgsCountMismatch { + expected: 1, + given: 0, + }, + span: macro_name_span, + }); + } + let mut args: Vec> = vec![]; + let mut arg_spans: Vec = vec![]; + let mut arg: Vec = vec![]; + let mut arg_start = get_span(&self.tokens[*self.i]).start; + let mut arg_end = arg_start; + let mut parens = 0i32; + loop { + let tok_span = get_span(&self.tokens[*self.i]); + match &token { + Token::LParen => { + parens += 1; + arg_end = tok_span.end; + arg.push(token); + } + Token::RParen if parens > 0 => { + parens -= 1; + arg_end = tok_span.end; + arg.push(token); + } + Token::RParen => { + arg_spans.push(arg_start..arg_end); + args.push(arg); + self.remove_token(span); + break; + } + Token::Comma if parens == 0 => { + arg_spans.push(arg_start..arg_end); + args.push(arg); + arg = vec![]; + self.remove_token(span); + token = get_token(&self.tokens[*self.i]).clone(); + arg_start = get_span(&self.tokens[*self.i]).start; + arg_end = arg_start; + continue; + } + _ => { + arg_end = tok_span.end; + arg.push(token); + } + } + self.remove_token(span); + token = get_token(&self.tokens[*self.i]).clone(); + } + self.apply_fmt(args, arg_spans, macro_name_span, span) + } + + fn apply_fmt( + &mut self, + args: Vec>, + arg_spans: Vec, + macro_name_span: Span, + span: &mut Span, + ) -> Result { + let Some([Token::Str(fmt)]) = args.first().map(|a| a.as_slice()) else { + return Err(Diagnostic { + kind: DiagnosticKind::MacroTypeError { + macro_name: "FMT".into(), + message: "format string argument must be a string literal".into(), + }, + span: arg_spans.first().cloned().unwrap_or(macro_name_span), + }); + }; + let fmt_str = fmt.clone(); + let sentinel = "\x00"; + let escaped = fmt_str.replace("%%", sentinel); + let parts: Vec = escaped + .split("%s") + .map(|s| SmolStr::from(s.replace(sentinel, "%"))) + .collect(); + let placeholders = parts.len() - 1; + let given = args.len() - 1; + if given != placeholders { + return Err(Diagnostic { + kind: DiagnosticKind::MacroArgsCountMismatch { + expected: placeholders + 1, + given: given + 1, + }, + span: macro_name_span, + }); + } + let mut output: Vec = vec![]; + for (idx, part) in parts.iter().enumerate() { + if !part.is_empty() { + if !output.is_empty() { + output.push(Token::Amp); + } + output.push(Token::Str(part.clone())); + } + if idx < args.len() - 1 { + let arg_tokens = &args[idx + 1]; + if !arg_tokens.is_empty() { + if !output.is_empty() { + output.push(Token::Amp); + } + output.extend(arg_tokens.iter().cloned()); + } + } + } + if output.is_empty() { + output.push(Token::Str(SmolStr::from(""))); + } + let inserted: Vec = output + .into_iter() + .map(|t| (macro_name_span.start, t, macro_name_span.end)) + .collect(); + let count = inserted.len(); + self.tokens.splice(*self.i..*self.i, inserted); + span.end += count; + Ok(true) + } + fn substitute_concat(&mut self, span: &mut Span) -> Result { let Token::Name(macro_name) = get_token(&self.tokens[*self.i]) else { return Ok(false);