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
26 changes: 26 additions & 0 deletions docs/language/macros.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,29 @@ all overloads for that name at once.
```goboscript
CONCAT(prefix, suffix) # becomes prefixsuffix
```

## Stringify Tokens

`STRINGIFY` is a built-in macro that converts its argument tokens into a string literal.
All tokens inside the parentheses are joined with spaces and produced as a single string value.

```goboscript
STRINGIFY(hello world) # becomes "hello world"
```

This is useful when you need to turn a macro expansion or a sequence of tokens into a
string at compile time:

```goboscript
%define VERSION 1 2 3

onflag {
say STRINGIFY(VERSION); # says "1 2 3"
}
```

Nested parentheses are supported and are included verbatim in the resulting string:

```goboscript
STRINGIFY(foo(bar, baz)) # becomes "foo ( bar , baz )"
```
14 changes: 7 additions & 7 deletions src/lexer/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,18 +241,18 @@ pub enum Token {
impl Display for Token {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Token::Name(name) => write!(f, "name{}", name),
Token::Name(name) => write!(f, "{}", name),
Token::Define => write!(f, "%define"),
Token::Undef => write!(f, "%undef"),
Token::Newline => writeln!(f),
Token::Backslash => write!(f, "\\"),
Token::Arg(name) => write!(f, "${}", name),
Token::Bin(value) => write!(f, "bin{}", value),
Token::Oct(value) => write!(f, "oct{}", value),
Token::Int(value) => write!(f, "int{}", value),
Token::Hex(value) => write!(f, "hex{}", value),
Token::Float(value) => write!(f, "float{}", value),
Token::Str(value) => write!(f, "str{}", value),
Token::Bin(value) => write!(f, "{}", value),
Token::Oct(value) => write!(f, "{}", value),
Token::Int(value) => write!(f, "{}", value),
Token::Hex(value) => write!(f, "{}", value),
Token::Float(value) => write!(f, "{}", value),
Token::Str(value) => write!(f, "\"{}\"", value),
Token::Costumes => write!(f, "costumes"),
Token::Sounds => write!(f, "sounds"),
Token::Local => write!(f, "local"),
Expand Down
54 changes: 54 additions & 0 deletions src/pre_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ impl<'a> PreProcessor<'a, '_> {
dirty = true;
continue;
}
if self.substitute_stringify(span)? {
dirty = true;
continue;
}
*self.i += 1;
}
if dirty {
Expand Down Expand Up @@ -333,6 +337,56 @@ impl<'a> PreProcessor<'a, '_> {
Ok(true)
}

fn substitute_stringify(&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 != "STRINGIFY" {
return Ok(false);
}
if self
.tokens
.get(*self.i + 1)
.is_none_or(|token| get_token(token) != &Token::LParen)
{
return Ok(false);
}
// Remove STRINGIFY and '('
self.remove_token(span);
self.remove_token(span);
self.expect_no_eof()?;
// Collect all tokens until the matching ')'
let mut parts: Vec<String> = vec![];
let mut parens: i32 = 0;
loop {
let token = get_token(&self.tokens[*self.i]).clone();
if token == Token::RParen && parens == 0 {
self.remove_token(span);
break;
}
match token {
Token::LParen => parens += 1,
Token::RParen => parens -= 1,
_ => {}
}
parts.push(token.to_string());
self.remove_token(span);
self.expect_no_eof()?;
}
let stringified: SmolStr = parts.join(" ").into();
self.tokens.insert(
*self.i,
(
macro_name_span.start,
Token::Str(stringified),
macro_name_span.end,
),
);
span.end += 1;
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