From 98ed7c62afb118c181008c1ec6903a57489c1ff0 Mon Sep 17 00:00:00 2001 From: Priyanshu Dangare Date: Mon, 23 Mar 2026 19:26:18 +0530 Subject: [PATCH 1/3] feat: add STRINGIFY() builtin macro STRINGIFY(expr) turns any macro argument into a string token containing the space-joined text of the argument tokens. Modelled after substitute_concat; handles nested parentheses. Example: %define X 42 say STRINGIFY(X + 1); // => "X + 1" --- src/pre_processor.rs | 63 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/pre_processor.rs b/src/pre_processor.rs index e73f0290..b94194b5 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,65 @@ 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(); + match &token { + Token::LParen => { + parens += 1; + parts.push(token.to_string()); + self.remove_token(span); + } + Token::RParen => { + if parens == 0 { + self.remove_token(span); + break; + } + parens -= 1; + parts.push(token.to_string()); + self.remove_token(span); + } + _ => { + 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); From dff1670c21fcb448c8b5e1fe10f8bb2323985bdc Mon Sep 17 00:00:00 2001 From: Priyanshu Dangare Date: Tue, 24 Mar 2026 03:44:45 +0530 Subject: [PATCH 2/3] docs: document STRINGIFY built-in macro --- docs/language/macros.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) 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 )" +``` From 000cc8251c19c6759f230201cddb6e82dfbda8ce Mon Sep 17 00:00:00 2001 From: Priyanshu Dangare Date: Tue, 24 Mar 2026 04:10:22 +0530 Subject: [PATCH 3/3] refactor: simplify token to string conversions --- src/lexer/token.rs | 14 +++++++------- src/pre_processor.rs | 29 ++++++++++------------------- 2 files changed, 17 insertions(+), 26 deletions(-) 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 b94194b5..daf68a7e 100644 --- a/src/pre_processor.rs +++ b/src/pre_processor.rs @@ -361,26 +361,17 @@ impl<'a> PreProcessor<'a, '_> { let mut parens: i32 = 0; loop { let token = get_token(&self.tokens[*self.i]).clone(); - match &token { - Token::LParen => { - parens += 1; - parts.push(token.to_string()); - self.remove_token(span); - } - Token::RParen => { - if parens == 0 { - self.remove_token(span); - break; - } - parens -= 1; - parts.push(token.to_string()); - self.remove_token(span); - } - _ => { - parts.push(token.to_string()); - self.remove_token(span); - } + 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();