Improve unquote_string_triple to give better error messages on invalid literals

This commit is contained in:
gingerBill
2026-08-13 00:34:28 +01:00
parent c085a46185
commit 3453643c11
2 changed files with 169 additions and 21 deletions

View File

@@ -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");

View File

@@ -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;