diff --git a/include/naab/string_interpolation.h b/include/naab/string_interpolation.h index 3ce94e2c..4a944c41 100644 --- a/include/naab/string_interpolation.h +++ b/include/naab/string_interpolation.h @@ -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 , + // column " -- 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); } } diff --git a/src/stdlib/string_impl.cpp b/src/stdlib/string_impl.cpp index bd4a2e1b..adf5a336 100644 --- a/src/stdlib/string_impl.cpp +++ b/src/stdlib/string_impl.cpp @@ -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" diff --git a/tests/parser/test_dogfood_hints.sh b/tests/parser/test_dogfood_hints.sh index d423a768..9dec7f16 100755 --- a/tests/parser/test_dogfood_hints.sh +++ b/tests/parser/test_dogfood_hints.sh @@ -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 ]