From eb36181e45f205f28a33641913da1a8d840eae0c Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sun, 28 Jun 2026 15:06:39 +0800 Subject: [PATCH] fix(codegen): escape C++ string literals (quotes, backslash, newline) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StringLiteral was emitted as std::string("") without escaping. Pine strings containing double quotes — common in JSON alert_message / webhook templates, e.g. '{"type":"bot"}' — produced invalid C++: std::string("{"type":"bot"}") // -> error: invalid suffix on literal breaking many real-world strategies (3commas/DCA/webhook bots). Route the main StringLiteral visitor and the const-string resolution path through the existing _cpp_string_escape helper (escapes \, ", \n, \r). Adds regression tests. No corpus regression: re-transpiling all 258 corpus probes is byte-identical with vs without this change (corpus strings contain no escapable characters, so it is a no-op for them). Co-Authored-By: Claude Opus 4.8 (1M context) --- pineforge_codegen/codegen/base.py | 2 +- pineforge_codegen/codegen/visit_expr.py | 2 +- tests/test_codegen_string_escape.py | 25 +++++++++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 tests/test_codegen_string_escape.py diff --git a/pineforge_codegen/codegen/base.py b/pineforge_codegen/codegen/base.py index 8e90f59..7b1f037 100644 --- a/pineforge_codegen/codegen/base.py +++ b/pineforge_codegen/codegen/base.py @@ -1258,7 +1258,7 @@ def _resolve_known(self, arg_str: str) -> str: if isinstance(val, (int, float)): return str(val) if isinstance(val, str): - return f'std::string("{val}")' + return f'std::string("{self._cpp_string_escape(val)}")' # Also resolve bar field references if arg_str in BAR_FIELDS: return BAR_FIELDS[arg_str] diff --git a/pineforge_codegen/codegen/visit_expr.py b/pineforge_codegen/codegen/visit_expr.py index eae35ec..494c3b8 100644 --- a/pineforge_codegen/codegen/visit_expr.py +++ b/pineforge_codegen/codegen/visit_expr.py @@ -180,7 +180,7 @@ def _visit_expr(self, node: ASTNode | None) -> str: if isinstance(node, NumberLiteral): return str(node.value) if isinstance(node, StringLiteral): - return f'std::string("{node.value}")' + return f'std::string("{self._cpp_string_escape(node.value)}")' if isinstance(node, BoolLiteral): return "true" if node.value else "false" if isinstance(node, NaLiteral): diff --git a/tests/test_codegen_string_escape.py b/tests/test_codegen_string_escape.py new file mode 100644 index 0000000..5b25f96 --- /dev/null +++ b/tests/test_codegen_string_escape.py @@ -0,0 +1,25 @@ +"""Regression: C++ string-literal escaping. + +Pine strings may contain characters that are special inside a C++ string +literal — double quotes (common in JSON alert/webhook templates), backslashes, +and newlines. These must be escaped when emitted, or the generated C++ fails to +compile ("invalid suffix on literal" / unterminated string). +""" + +from pineforge_codegen import transpile + + +def _cpp(body: str) -> str: + return transpile('//@version=6\nstrategy("t")\n' + body + "\n") + + +def test_string_with_embedded_double_quotes_is_escaped(): + cpp = _cpp("msg = '{\"type\":\"bot\",\"id\":\"42\"}'\nplot(str.length(msg))") + # The raw inner quotes must be backslash-escaped in the C++ literal. + assert r'\"type\"' in cpp + assert 'std::string("{"type"' not in cpp # the broken (unescaped) form + + +def test_string_with_backslash_is_escaped(): + cpp = _cpp("p = 'a\\\\b'\nplot(str.length(p))") + assert "\\\\" in cpp