Repository navigation
Fix argument count crash, null-returning builtins, and error message quality - #3
Conversation
…error messages, and typos Agent-Logs-Url: https://github.com/kwakker35/SharpBasic/sessions/4b1d20b2-d914-44f6-a30f-3a5f1570b36b Co-authored-by: kwakker35 <5082729+kwakker35@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR improves SharpBasic evaluator correctness and diagnostic consistency by preventing crashes on SUB/FUNCTION calls with wrong argument counts and by converting several builtin “null returns” into explicit runtime errors, along with accompanying test updates.
Changes:
- Add argument-count validation for
CALLingSUBs and invokingFUNCTIONs to avoidIndexOutOfRangeExceptionand returnEvalFailurediagnostics. - Update builtins (
VAL,STRING$,ASC) to throw descriptive runtime errors instead of returningnull. - Improve diagnostic messages (unknown operator, array bounds upper bound, “suppled” typo), and expand/update tests to assert
EvalFailurefor these error paths.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
src/SharpBasic.Evaluation/Evaluator.cs |
Adds arg-count checks for SUB/FUNCTION calls; updates builtin error behavior; improves several diagnostics. |
tests/SharpBasic.Evaluation.Tests/StdlibTests.cs |
Updates prior “empty output” expectations to EvalFailure and adds new builtin error-case tests. |
tests/SharpBasic.Evaluation.Tests/EvaluatorErrorTests.cs |
Adds tests covering SUB/FUNCTION call argument-count mismatches returning EvalFailure. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| : null | ||
| : null, | ||
| ["VAL"] = args => | ||
| { |
There was a problem hiding this comment.
VAL indexes args[0] without first validating args.Count. A call like VAL() will throw IndexOutOfRangeException, which gets wrapped into an EvalFailure with a generic .NET message rather than a user-friendly runtime error. Consider explicitly checking the argument count and throwing an InvalidOperationException with a clear message (or centralizing builtin arity validation in EvaluateCallExpression).
| { | |
| { | |
| if (args.Count < 1) | |
| throw new InvalidOperationException("VAL requires a string argument"); |
| ["STRING$"] = args => | ||
| { | ||
| if (args[0] is not StringValue sv) | ||
| throw new InvalidOperationException("STRING$ requires a string as its first argument"); | ||
| if (args[1] is not IntValue iv2) | ||
| throw new InvalidOperationException("STRING$ requires an integer as its second argument"); |
There was a problem hiding this comment.
STRING$ uses args[0]/args[1] but doesn't guard against too few arguments. STRING$("x") / STRING$() will currently surface as an IndexOutOfRangeException message. Add an argument-count check up front (or enforce builtin arity in the call site) so the evaluator reports a consistent, descriptive runtime error.
| return new StringValue(new string(sv.V[0], iv2.V)); | ||
| }, | ||
| ["ASC"] = args => | ||
| { |
There was a problem hiding this comment.
ASC indexes args[0] without checking args.Count. Calling ASC() will result in an IndexOutOfRangeException wrapped into EvalFailure with an unhelpful message. Add an explicit argument-count validation to keep builtin error reporting consistent.
| { | |
| { | |
| if (args.Count != 1) | |
| throw new InvalidOperationException("ASC requires exactly one argument"); |
| stmt.Location?.Line ?? 0, | ||
| stmt.Location?.Col ?? 0, | ||
| $"Supplied index {idx} is outside of the range 0-{arrVal.Items.Length} defined for the array: {stmt.Name}.", | ||
| $"Supplied index {idx} is outside of the range 0-{arrVal.Items.Length - 1} defined for the array: {stmt.Name}.", |
There was a problem hiding this comment.
The updated bounds message uses arrVal.Items.Length - 1 as the inclusive upper bound. If a zero-length array can be created (e.g. DIM a(0) given current EvaluateDimStatement behavior), this will format as 0--1, which is confusing. Consider special-casing Length == 0 (e.g., report that the array is empty / has no valid indices) or revisiting array sizing semantics.
| $"Supplied index {idx} is outside of the range 0-{arrVal.Items.Length - 1} defined for the array: {stmt.Name}.", | |
| arrVal.Items.Length == 0 | |
| ? $"Supplied index {idx} is invalid because the array {stmt.Name} is empty and has no valid indices." | |
| : $"Supplied index {idx} is outside of the range 0-{arrVal.Items.Length - 1} defined for the array: {stmt.Name}.", |
| if (idx < 0 || idx >= arrVal.Items.Length) | ||
| { | ||
| return new EvalFailure( | ||
| [ | ||
| new Diagnostic( | ||
| expr.Location?.Line ?? 0, | ||
| expr.Location?.Col ?? 0, | ||
| $"Supplied index {idx} is outside of the range 0-{arrVal.Items.Length} defined for the array: {expr.Name}.", | ||
| $"Supplied index {idx} is outside of the range 0-{arrVal.Items.Length - 1} defined for the array: {expr.Name}.", | ||
| DiagnosticSeverity.Error |
There was a problem hiding this comment.
Same as the assign path: for Length == 0, the new 0-{Length - 1} range will display as 0--1. Consider a Length == 0 special-case for clearer diagnostics (or prevent creating empty arrays).
| Assert.Equal("", output); | ||
| // LEN requires a string; passing an integer raises a runtime error. | ||
| var result = RunHelper.RunResult("PRINT LEN(42)"); | ||
| Assert.IsType<EvalFailure>(result); |
There was a problem hiding this comment.
This test only asserts EvalFailure, so it could pass even if the failure comes from an unintended code path. To prevent regressions in builtin runtime errors, consider asserting the diagnostic message includes LEN (or the expected "requires a string" wording).
| Assert.IsType<EvalFailure>(result); | |
| var failure = Assert.IsType<EvalFailure>(result); | |
| Assert.Contains(failure.Diagnostics, d => d.Message.Contains("LEN") || d.Message.Contains("requires a string")); |
| public void CHR_Dollar_String_Argument_Returns_EvalFailure() | ||
| { | ||
| // Non-integer argument → null → prints "" | ||
| var output = RunHelper.Run("PRINT CHR$(\"H\")"); | ||
| Assert.Equal("", output); | ||
| // Non-integer argument → runtime error | ||
| var result = RunHelper.RunResult("PRINT CHR$(\"H\")"); | ||
| Assert.IsType<EvalFailure>(result); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void CHR_Dollar_Float_Argument_Returns_Empty_Output() | ||
| public void CHR_Dollar_Float_Argument_Returns_EvalFailure() | ||
| { | ||
| // Float does not match IntValue → null → prints "" | ||
| var output = RunHelper.Run("PRINT CHR$(72.5)"); | ||
| Assert.Equal("", output); | ||
| // Float does not match IntValue → runtime error | ||
| var result = RunHelper.RunResult("PRINT CHR$(72.5)"); | ||
| Assert.IsType<EvalFailure>(result); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void CHR_Dollar_Boolean_Argument_Returns_Empty_Output() | ||
| public void CHR_Dollar_Boolean_Argument_Returns_EvalFailure() | ||
| { | ||
| var output = RunHelper.Run("PRINT CHR$(TRUE)"); | ||
| Assert.Equal("", output); | ||
| var result = RunHelper.RunResult("PRINT CHR$(TRUE)"); | ||
| Assert.IsType<EvalFailure>(result); |
There was a problem hiding this comment.
These negative CHR$ tests only assert EvalFailure, so they could pass for unrelated failures. Consider asserting the diagnostic message contains CHR$ / "requires an integer argument" to verify the intended runtime error behavior.
Deep review of the SharpBasic evaluator identified several correctness and consistency issues: crashes on wrong argument counts, silent null propagation from three builtins, a misleading error message, two typos, and an off-by-one in array bounds messages.
Bug fixes
Argument count validation — Calling a
SUBorFUNCTIONwith the wrong number of arguments previously caused an unhandledIndexOutOfRangeException. Now returns a descriptiveEvalFailure:VAL,STRING$,ASCreturnednullon bad input — These builtins silently returnednull, which propagated asEvalSuccess(null)and produced a confusing downstream error ("Value assigned to 'x' was null"). Each now throws a specificInvalidOperationException:VAL("abc")→"VAL: 'abc' is not a valid number"STRING$("ab", 3)→"STRING$ first argument must be a single character"ASC(65)→"ASC requires a string argument"Wrong error message in
EvaluateBinaryExpression— "Unknown statement type: Token" → "Unknown operator:{value}"Typo —
"Invalid index suppled."→"Invalid index supplied."(two occurrences)Off-by-one in array bounds error messages —
0-{Items.Length}→0-{Items.Length - 1}(messages now show the valid inclusive upper bound)Tests
VAL,STRING$,ASC, andCHR$EvalFailure