diff --git a/docs/language/macros.md b/docs/language/macros.md index 0a2c180a..20e73b30 100644 --- a/docs/language/macros.md +++ b/docs/language/macros.md @@ -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 )" +``` diff --git a/src/lexer/token.rs b/src/lexer/token.rs index de1967ab..3fee5a0d 100644 --- a/src/lexer/token.rs +++ b/src/lexer/token.rs @@ -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"), diff --git a/src/pre_processor.rs b/src/pre_processor.rs index e73f0290..daf68a7e 100644 --- a/src/pre_processor.rs +++ b/src/pre_processor.rs @@ -73,6 +73,10 @@ impl<'a> PreProcessor<'a, '_> { dirty = true; continue; } + if self.substitute_stringify(span)? { + dirty = true; + continue; + } *self.i += 1; } if dirty { @@ -333,6 +337,56 @@ impl<'a> PreProcessor<'a, '_> { Ok(true) } + fn substitute_stringify(&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 != "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 = 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 { let Token::Name(macro_name) = get_token(&self.tokens[*self.i]) else { return Ok(false);