Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions include/naab/string_interpolation.h
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,20 @@ inline std::string formatInterpolationError(const std::string& expr,
const std::string& file,
int line, int column) {
std::string first = inner_error.substr(0, inner_error.find("\n\n"));
// The inner parser only saw the extracted expression, so its "line 1,
// column N" is a position inside ${...}. Say that, not a file position.
const std::string prefix = "Parse error at line 1, column ";
// The inner parser only saw the extracted expression, so its column is a
// position inside ${...}. Its LINE is not: the evaluators shift the inner
// tokens to the string's line (so runtime errors point at the right file
// line), which means the inner parse error reads "line <file line>,
// column <offset in the expression>" -- a file line paired with a column
// that is not one. Matching only "line 1" missed every string not on line
// 1 and printed exactly that mix. Report the column as what it is.
const std::string prefix = "Parse error at line ";
if (first.compare(0, prefix.size(), prefix) == 0) {
size_t colon = first.find(':', prefix.size());
size_t col_kw = first.find(", column ", prefix.size());
size_t colon = col_kw == std::string::npos ? col_kw : first.find(':', col_kw);
if (colon != std::string::npos) {
first = "at character " + first.substr(prefix.size(), colon - prefix.size()) +
size_t col_start = col_kw + 9; // strlen(", column ")
first = "at character " + first.substr(col_start, colon - col_start) +
" of the expression:" + first.substr(colon + 1);
}
}
Expand Down
43 changes: 43 additions & 0 deletions src/stdlib/string_impl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,49 @@ interpreter::NaabVal StringModule::call(
" Example: string.index_of(\"hello\", \"ll\") // 2\n"
);
}
// Names from JavaScript and Python that an LLM (or a person) reaches for.
// The generic "did you mean" picks by edit distance, which sent `slice`
// to split(); these say the equivalent directly.
if (function_name == "slice" || function_name == "substr") {
throw std::runtime_error(
"Unknown string function: " + function_name + "\n\n"
" Did you mean: string.substring(s, start, end)? The end index is exclusive.\n"
" Example: string.substring(\"hello\", 1, 3) // \"el\"\n"
);
}
if (function_name == "includes") {
throw std::runtime_error(
"Unknown string function: includes\n\n"
" Did you mean: string.contains()?\n"
" Example: string.contains(\"hello\", \"ell\") // true\n"
);
}
if (function_name == "padStart" || function_name == "padEnd" ||
function_name == "rjust" || function_name == "ljust") {
bool left = function_name == "padStart" || function_name == "rjust";
throw std::runtime_error(
"Unknown string function: " + function_name + "\n\n"
" Did you mean: string." + std::string(left ? "pad_left" : "pad_right") +
"(s, width, fill)?\n"
" Example: string." + std::string(left ? "pad_left" : "pad_right") +
"(\"7\", 3, \"0\") // \"" + std::string(left ? "007" : "700") + "\"\n"
);
}
if (function_name == "trimStart" || function_name == "trimEnd" ||
function_name == "trimLeft" || function_name == "trimRight") {
throw std::runtime_error(
"Unknown string function: " + function_name + "\n\n"
" Did you mean: string.trim()? It trims both ends.\n"
" Example: string.trim(\" hi \") // \"hi\"\n"
);
}
if (function_name == "replaceAll") {
throw std::runtime_error(
"Unknown string function: replaceAll\n\n"
" Did you mean: string.replace()? It already replaces every occurrence.\n"
" Example: string.replace(\"a-b-c\", \"-\", \"+\") // \"a+b+c\"\n"
);
}
if (function_name == "startsWith") {
throw std::runtime_error(
"Unknown string function: startsWith\n\n"
Expand Down
40 changes: 40 additions & 0 deletions tests/parser/test_dogfood_hints.sh
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,46 @@ for case_ in "T-01:t_let:let (control)" "T-02:t_dict:dict literal value" "T-03:t
fi
done

echo "=== I: interpolation parse errors report the column as a column ==="
# #258 shifted the inner tokens to the string's FILE line (so runtime errors
# inside ${...} point at the right line), which moved the inner parse error
# off "line 1" -- and the rewrite to "character N of the expression" matched
# only "line 1", so round 2 of repo-sentinel printed "Parser said: Parse error
# at line 2, column 11": a file line paired with an offset inside ${...}.
printf 'main {\n let js = "x ${c.replace(/a/g, 1)} y"\n}\n' > "$WORK/i_line2.naab"
out="$(run "" i_line2.naab)"
case "$out" in
*"Parser said: at character"*"of the expression"*)
if [[ "$out" == *"Parser said: Parse error at line"* ]]; then
fail I-01 "still reports the inner parse error as a file line"
else
pass I-01 "string on line 2: the inner column is reported as a character offset"
fi ;;
*) fail I-01 "inner parse error not rewritten" "$(grep 'Parser said' <<<"$out")" ;;
esac

echo "=== S: string names from other languages point at the NAAb one ==="
# F-007: `string.slice` suggested split() (edit distance). These say the
# equivalent directly. S-00 is the control that a real function still works.
# Match the suggestion itself ("Did you mean: string.X"): the old message
# listed every function after "Available:", so a loose match on the name
# passed S-01 on the build that suggested split().
for case_ in "S-01:slice:string.substring" "S-02:includes:string.contains" \
"S-03:padStart:string.pad_left" "S-04:replaceAll:string.replace" \
"S-05:trimStart:string.trim"; do
id="${case_%%:*}"; rest="${case_#*:}"; fn="${rest%%:*}"; want="${rest#*:}"
printf 'use string\nmain {\n print(string.%s("abc", 1, 2))\n}\n' "$fn" > "$WORK/s_$fn.naab"
out="$(run "" "s_$fn.naab")"
case "$out" in
*"Did you mean: $want"*) pass "$id" "string.$fn -> $want" ;;
*) fail "$id" "string.$fn gives no useful suggestion" "$(grep -m2 'Did you mean\|Error' <<<"$out")" ;;
esac
done
printf 'use string\nmain {\n print(string.substring("hello", 1, 3))\n}\n' > "$WORK/s_ok.naab"
out="$(run "" s_ok.naab)"
if grep -qx 'el' <<<"$out"; then pass S-00 "control: string.substring still works"
else fail S-00 "control: string.substring broke" "$(head -2 <<<"$out")"; fi

echo ""
echo "Results: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ]
Loading