plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
9b45c3f
— Peter John
2026-11-02T15:39:26+05:30
Report an unclosed delimiter at its opener; hint suggests a bare return body
- plum-cli/tests/compile_tests.rs +33 -0
- plum-core/src/loader.rs +29 -2
- plum-core/tests/loader_test.rs +19 -2
plum-cli/tests/compile_tests.rs
CHANGED
|
@@ -74,3 +74,36 @@ fn compileWithImportResolvesViaLibPath() {
|
|
|
74
74
|
let bytes = std::fs::read(out_path).expect("output wasm not found");
|
|
75
75
|
assert_eq!(&bytes[0..4], b"\0asm");
|
|
76
76
|
}
|
|
77
|
+
|
|
78
|
+
/// The body the parse-error hint for `()` suggests must itself compile, with
|
|
79
|
+
/// only the imports the failing file already had.
|
|
80
|
+
#[test]
|
|
81
|
+
fn returnBodySuggestedByTheParseErrorHintCompiles() {
|
|
82
|
+
let dir = freshTempDir("return_hint");
|
|
83
|
+
let src_path = dir.join("h.plum");
|
|
84
|
+
std::fs::write(
|
|
85
|
+
&src_path,
|
|
86
|
+
"import std/Float\n\nexport fun update(w: Float, dt: Float) =\n return\n",
|
|
87
|
+
)
|
|
88
|
+
.unwrap();
|
|
89
|
+
let out_path = dir.join("h.wasm");
|
|
90
|
+
let repo = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
91
|
+
.parent()
|
|
92
|
+
.unwrap();
|
|
93
|
+
let output = Command::new(plumBin())
|
|
94
|
+
.args([
|
|
95
|
+
"compile",
|
|
96
|
+
src_path.to_str().unwrap(),
|
|
97
|
+
"-o",
|
|
98
|
+
out_path.to_str().unwrap(),
|
|
99
|
+
"--lib-path",
|
|
100
|
+
repo.to_str().unwrap(),
|
|
101
|
+
])
|
|
102
|
+
.output()
|
|
103
|
+
.expect("failed to run plum compile");
|
|
104
|
+
assert!(
|
|
105
|
+
output.status.success(),
|
|
106
|
+
"{}",
|
|
107
|
+
String::from_utf8_lossy(&output.stderr)
|
|
108
|
+
);
|
|
109
|
+
}
|
plum-core/src/loader.rs
CHANGED
|
@@ -263,7 +263,7 @@ impl SyntaxError {
|
|
|
263
263
|
if tok == "()" {
|
|
264
264
|
msg.push_str(
|
|
265
265
|
"\n hint: `()` is not a value in Plum; a body with nothing to do \
|
|
266
|
-
|
|
266
|
+
is `return` (or `todo` while it is unimplemented)",
|
|
267
267
|
);
|
|
268
268
|
}
|
|
269
269
|
msg
|
|
@@ -285,7 +285,8 @@ impl SyntaxError {
|
|
|
285
285
|
/// `y := 3` blames `y`);
|
|
286
286
|
/// - an `ERROR` that only wraps tokens the parser had already accepted (it
|
|
287
287
|
/// gave up on them after seeing what came next) blames the first token
|
|
288
|
-
/// after it, or end of file when nothing follows
|
|
288
|
+
/// after it, or end of file when nothing follows (reported as the
|
|
289
|
+
/// innermost unclosed delimiter when the file ends inside one).
|
|
289
290
|
pub fn syntaxErrors(root: tree_sitter::Node, text: &str) -> Vec<SyntaxError> {
|
|
290
291
|
let mut out: Vec<SyntaxError> = Vec::new();
|
|
291
292
|
collectSyntaxErrors(root, root, text, &mut out);
|
|
@@ -344,6 +345,13 @@ fn locateError(err: tree_sitter::Node, root: tree_sitter::Node, text: &str) -> S
|
|
|
344
345
|
leafStartingAtOrAfter(root, err.end_byte())
|
|
345
346
|
};
|
|
346
347
|
let eof = || {
|
|
348
|
+
if let Some(open) = innermostUnclosed(root) {
|
|
349
|
+
return SyntaxError {
|
|
350
|
+
start: open.start_position(),
|
|
351
|
+
end: open.end_position(),
|
|
352
|
+
kind: SyntaxErrorKind::Unclosed(open.kind().to_string()),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
347
355
|
let end = lastLeaf(root).map_or(root.end_position(), |l| l.end_position());
|
|
348
356
|
SyntaxError {
|
|
349
357
|
start: end,
|
|
@@ -407,6 +415,25 @@ fn closerOf(kind: &str) -> Option<&'static str> {
|
|
|
407
415
|
}
|
|
408
416
|
}
|
|
409
417
|
|
|
418
|
+
/// The last opening delimiter still open at the end of the file, if any.
|
|
419
|
+
fn innermostUnclosed(root: tree_sitter::Node) -> Option<tree_sitter::Node> {
|
|
420
|
+
let mut stack: Vec<tree_sitter::Node> = Vec::new();
|
|
421
|
+
let mut at = 0;
|
|
422
|
+
while let Some(leaf) = leafStartingAtOrAfter(root, at) {
|
|
423
|
+
if closerOf(leaf.kind()).is_some() {
|
|
424
|
+
stack.push(leaf);
|
|
425
|
+
} else if matches!(leaf.kind(), ")" | "]" | "}") {
|
|
426
|
+
if let Some(top) = stack.last() {
|
|
427
|
+
if closerOf(top.kind()) == Some(leaf.kind()) {
|
|
428
|
+
stack.pop();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
at = leaf.end_byte();
|
|
433
|
+
}
|
|
434
|
+
stack.pop()
|
|
435
|
+
}
|
|
436
|
+
|
|
410
437
|
/// Whether `open` has a matching `closer` later in the file, counting nesting.
|
|
411
438
|
fn isClosed(root: tree_sitter::Node, open: tree_sitter::Node, closer: &str) -> bool {
|
|
412
439
|
let mut depth = 1usize;
|
plum-core/tests/loader_test.rs
CHANGED
|
@@ -283,7 +283,10 @@ fn parseErrorBlamesTheTokenNotTheWholeFunction() {
|
|
|
283
283
|
err.contains("at line 4, column 3: unexpected '()'"),
|
|
284
284
|
"{err}"
|
|
285
285
|
);
|
|
286
|
+
assert!(
|
|
286
|
-
|
|
287
|
+
err.contains("hint:") && err.contains("is `return`"),
|
|
288
|
+
"{err}"
|
|
289
|
+
);
|
|
287
290
|
assert!(!err.contains("export fun"), "{err}");
|
|
288
291
|
}
|
|
289
292
|
|
|
@@ -305,6 +308,18 @@ fn parseErrorOnMissingClosingDelimiter() {
|
|
|
305
308
|
assert!(err.contains("at line 3, column 3: unexpected 'y'"), "{err}");
|
|
306
309
|
}
|
|
307
310
|
|
|
311
|
+
#[test]
|
|
312
|
+
fn unclosedOpenerAtEndOfFileIsReportedAtTheOpener() {
|
|
313
|
+
let err = parseErrorOf("import std/Int\n\nfun f(a: Int) -> Int =\n g(a, 2\n");
|
|
314
|
+
assert!(err.contains("at line 4, column 4: unclosed '('"), "{err}");
|
|
315
|
+
let err = parseErrorOf("fun f() =\n x := [1, 2\n");
|
|
316
|
+
assert!(err.contains("at line 2, column 8: unclosed '['"), "{err}");
|
|
317
|
+
let err = parseErrorOf("fun f() =\n x := g(h(1), 2\n");
|
|
318
|
+
assert!(err.contains("at line 2, column 9: unclosed '('"), "{err}");
|
|
319
|
+
let err = parseErrorOf("fun f() =\n x := { 1 + 2\n");
|
|
320
|
+
assert!(err.contains("at line 2, column 8: unclosed '{'"), "{err}");
|
|
321
|
+
}
|
|
322
|
+
|
|
308
323
|
#[test]
|
|
309
324
|
fn parseErrorAtEndOfFile() {
|
|
310
325
|
let err = parseErrorOf("fun f() =\n x := 1 +\n");
|
|
@@ -313,8 +328,10 @@ fn parseErrorAtEndOfFile() {
|
|
|
313
328
|
"{err}"
|
|
314
329
|
);
|
|
315
330
|
let err = parseErrorOf("fun f() =\n x := foo(1, 2");
|
|
331
|
+
assert!(err.contains("at line 2, column 11: unclosed '('"), "{err}");
|
|
332
|
+
let err = parseErrorOf("fun f() =\n if true\n x := 1\n else\n");
|
|
316
333
|
assert!(
|
|
317
|
-
err.contains("at line
|
|
334
|
+
err.contains("at line 4, column 7: unexpected end of file"),
|
|
318
335
|
"{err}"
|
|
319
336
|
);
|
|
320
337
|
}
|