From 3453643c118cfe04261e769f6268a825fc9b152a Mon Sep 17 00:00:00 2001 From: gingerBill Date: Thu, 13 Aug 2026 00:34:28 +0100 Subject: [PATCH] Improve `unquote_string_triple` to give better error messages on invalid literals --- src/parser.cpp | 45 ++++++++++++++- src/string.cpp | 145 ++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 169 insertions(+), 21 deletions(-) diff --git a/src/parser.cpp b/src/parser.cpp index bea018906..06f7a822e 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -845,6 +845,25 @@ gb_internal Ast *ast_uninit(AstFile *f, Token token) { } gb_internal ExactValue exact_value_from_token(AstFile *f, Token const &token) { + auto token_pos_at_offset = [](Token const &token, isize offset) -> TokenPos { + TokenPos pos = token.pos; + if (offset <= 0) { + return pos; + } + String s = token.string; + isize n = gb_min(offset, s.len); + for (isize i = 0; i < n; i++) { + if (s.text[i] == '\n') { + pos.line += 1; + pos.column = 1; + } else { + pos.column += 1; + } + pos.offset += 1; + } + return pos; + }; + String s = token.string; string_interner_insert(s); switch (token.kind) { @@ -857,8 +876,30 @@ gb_internal ExactValue exact_value_from_token(AstFile *f, Token const &token) { if (s.len >= 6 && ((s.text[0] == '"' && s.text[1] == '"' && s.text[2] == '"') || (s.text[0] == '`' && s.text[1] == '`' && s.text[2] == '`'))) { - if (!unquote_string_triple(ast_allocator(f), &s, string_contains_char(s, '\r'))) { - syntax_error(token, "Invalid multi-line string literal"); + TripleStringErrorKind terr = TripleStringError_None; + isize terr_off = -1; + if (!unquote_string_triple(ast_allocator(f), &s, string_contains_char(s, '\r'), &terr, &terr_off)) { + TokenPos pos = token_pos_at_offset(token, terr_off); + switch (terr) { + case TripleStringError_ContentOnOpeningLine: + syntax_error(pos, "A multi-line string literal must begin on the line after the opening delimiter"); + break; + case TripleStringError_ClosingNotOnOwnLine: + syntax_error(pos, "The closing delimiter of a multi-line string literal must be on its own line"); + break; + case TripleStringError_UnderIndented: + syntax_error(pos, "This line is indented less than the closing delimiter of the multi-line string literal"); + break; + case TripleStringError_IndentationMismatch: + syntax_error(pos, "The indentation of this line does not match the closing delimiter of the multi-line string literal"); + break; + case TripleStringError_InvalidEscape: + syntax_error(pos, "Invalid escape sequence in string literal"); + break; + default: + syntax_error(pos, "Invalid multi-line string literal"); + break; + } } } else if (!unquote_string(ast_allocator(f), &s, 0, s.text[0] == '`')) { syntax_error(token, "Invalid string literal"); diff --git a/src/string.cpp b/src/string.cpp index 551a6250c..76d03d55f 100644 --- a/src/string.cpp +++ b/src/string.cpp @@ -1360,34 +1360,54 @@ gb_internal bool triple_string_deindent(gbAllocator a, String body, String *out) return true; } -// Triple-quoted string literal, dispatching on the delimiter: -// `"` -> escapes processed; multi-line form is Java-style de-indented -// `\`` -> raw (no escapes); multi-line form is ALSO Java-style de-indented -// (carriage returns normalized), embedded single/double backticks -// are literal + +enum TripleStringErrorKind { + TripleStringError_None = 0, + TripleStringError_InvalidLiteral, // malformed delimiters (shouldn't occur for a real token) + TripleStringError_ContentOnOpeningLine, // text after the opening delimiter on its own line + TripleStringError_ClosingNotOnOwnLine, // closing delimiter is not alone on its line + TripleStringError_UnderIndented, // a content line is indented less than the closing delimiter + TripleStringError_IndentationMismatch, // a line's indentation whitespace differs from the closing delimiter + TripleStringError_InvalidEscape, // bad escape sequence (only reachable for `"""` strings) +}; + +// `out_err` receives the failure reason and `out_err_offset` a byte offset into +// the *token string* (i.e. into *s_ on entry) pointing at the offending +// character, or -1 when no precise location is available. Both are only +// meaningful when the function returns 0. // // 0 == failure // 1 == original memory // 2 == new allocation -gb_internal i32 unquote_string_triple(gbAllocator a, String *s_, bool has_carriage_return=false) { +gb_internal i32 unquote_string_triple(gbAllocator a, String *s_, bool has_carriage_return=false, + TripleStringErrorKind *out_err=nullptr, isize *out_err_offset=nullptr) { + if (out_err) *out_err = TripleStringError_None; + if (out_err_offset) *out_err_offset = -1; + + #define TRIPLE_FAIL(kind, off) do { \ + if (out_err) *out_err = (kind); \ + if (out_err_offset) *out_err_offset = (off); \ + return 0; \ + } while (0) + String s = *s_; isize n = s.len; if (n < 6) { - return 0; + TRIPLE_FAIL(TripleStringError_InvalidLiteral, 0); } u8 quote = s[0]; if (quote != '"' && quote != '`') { - return 0; + TRIPLE_FAIL(TripleStringError_InvalidLiteral, 0); } if (!(s[0] == quote && s[1] == quote && s[2] == quote) || !(s[n-1] == quote && s[n-2] == quote && s[n-3] == quote)) { - return 0; + TRIPLE_FAIL(TripleStringError_InvalidLiteral, 0); } - String body = make_string(s.text+3, s.len-6); bool raw = (quote == '`'); + String body = make_string(s.text+3, s.len-6); // body begins at token offset 3 - // Single-line form: no indentation handling. + // Single-line form: no indentation handling if (!string_contains_char(body, '\n')) { if (raw) { if (has_carriage_return) { @@ -1397,22 +1417,109 @@ gb_internal i32 unquote_string_triple(gbAllocator a, String *s_, bool has_carria *s_ = body; return 1; } - return unquote_string_triple_content(a, s_, body, has_carriage_return, false); + i32 r = unquote_string_triple_content(a, s_, body, has_carriage_return, false); + if (r == 0) { + TRIPLE_FAIL(TripleStringError_InvalidEscape, -1); + } + return r; } - // Multi-line form: Java-style de-indentation for both delimiters. - String di = {}; - if (!triple_string_deindent(a, body, &di)) { - return 0; + // Opening delimiter's line must be whitespace only. + isize first_nl = 0; + while (body.text[first_nl] != '\n') { + first_nl += 1; } + for (isize i = 0; i < first_nl; i++) { + u8 c = body.text[i]; + if (c != ' ' && c != '\t' && c != '\r') { + TRIPLE_FAIL(TripleStringError_ContentOnOpeningLine, 3 + i); + } + } + + // Closing delimiter's line: whitespace after the final newline is the indent prefix. + isize last_nl = body.len - 1; + while (body.text[last_nl] != '\n') { + last_nl -= 1; + } + String indent = make_string(body.text+last_nl+1, body.len-(last_nl+1)); + for (isize i = 0; i < indent.len; i++) { + u8 c = indent.text[i]; + if (c != ' ' && c != '\t') { + TRIPLE_FAIL(TripleStringError_ClosingNotOnOwnLine, 3 + (last_nl+1) + i); + } + } + + String content = make_string(body.text+first_nl+1, last_nl-(first_nl+1)); + isize content_base = 3 + (first_nl+1); // token offset of the content start + + u8 *dbuf = gb_alloc_array(a, u8, content.len+1); + isize dlen = 0; + isize li = 0; + for (;;) { + isize le = li; + while (le < content.len && content.text[le] != '\n') { + le += 1; + } + isize len = le - li; + if (len > 0 && content.text[li+len-1] == '\r') { + len -= 1; // normalize CRLF + } + + bool blank = true; + for (isize k = 0; k < len; k++) { + u8 c = content.text[li+k]; + if (c != ' ' && c != '\t') { + blank = false; + break; + } + } + if (!blank) { + isize ws = 0; + while (ws < len && (content.text[li+ws] == ' ' || content.text[li+ws] == '\t')) { + ws += 1; + } + isize ws_end_off = content_base + li + ws; + + for (isize k = 0; k < indent.len; k++) { + if (k >= len || content.text[li+k] != indent.text[k]) { + u8 lc = 0; + if (k < len) { + lc = content.text[li+k]; + } + gb_free(a, dbuf); + + if (k < len && (lc == ' ' || lc == '\t')) { + TRIPLE_FAIL(TripleStringError_IndentationMismatch, ws_end_off); + } + TRIPLE_FAIL(TripleStringError_UnderIndented, ws_end_off); + } + } + for (isize k = indent.len; k < len; k++) { + dbuf[dlen++] = content.text[li+k]; + } + } + if (le >= content.len) { + break; + } + dbuf[dlen++] = '\n'; + li = le + 1; + } + + String di = make_string(dbuf, dlen); if (raw) { - // Raw: the de-indented content is used verbatim (no escape processing). *s_ = di; return 2; } - // Processed: run escape sequences on the de-indented content. - return unquote_string_triple_content(a, s_, di, false, true); + i32 r = unquote_string_triple_content(a, s_, di, false, true); + if (r == 0) { + TRIPLE_FAIL(TripleStringError_InvalidEscape, -1); + } + return r; + + #undef TRIPLE_FAIL } + + gb_internal bool string_is_valid_identifier(String str) { if (str.len <= 0) return false;