Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions docs/language/macros.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/diagnostic/diagnostic_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ pub enum DiagnosticKind {
expected: usize,
given: usize,
},
MacroTypeError {
macro_name: SmolStr,
message: SmolStr,
},
CommandFailed {
stderr: Vec<u8>,
},
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -437,6 +444,7 @@ impl From<&DiagnosticKind> for Level {
| DiagnosticKind::ProcArgsCountMismatch { .. }
| DiagnosticKind::FuncArgsCountMismatch { .. }
| DiagnosticKind::MacroArgsCountMismatch { .. }
| DiagnosticKind::MacroTypeError { .. }
| DiagnosticKind::CommandFailed { .. }
| DiagnosticKind::ProcedureRedefinition(_)
| DiagnosticKind::FunctionRedefinition(_)
Expand Down
3 changes: 2 additions & 1 deletion src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,13 @@ fn parse_sprite(tokens: Vec<SpannedToken>) -> (Sprite, Vec<Diagnostic>) {
pub fn parse(translation_unit: &TranslationUnit) -> (Sprite, Vec<Diagnostic>) {
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)
Expand Down
143 changes: 143 additions & 0 deletions src/pre_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -334,6 +338,145 @@ impl<'a> PreProcessor<'a, '_> {
Ok(true)
}

fn substitute_fmt(&mut self, span: &mut Span) -> Result<bool, Diagnostic> {
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<Token>> = vec![];
let mut arg_spans: Vec<Span> = vec![];
let mut arg: Vec<Token> = 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<Vec<Token>>,
arg_spans: Vec<Span>,
macro_name_span: Span,
span: &mut Span,
) -> Result<bool, Diagnostic> {
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<SmolStr> = 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<Token> = 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<SpannedToken> = 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<bool, Diagnostic> {
let Token::Name(macro_name) = get_token(&self.tokens[*self.i]) else {
return Ok(false);
Expand Down
Loading