plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
33fe838
— Peter John
2026-11-06T14:36:31+05:30
Report type errors inside string interpolation
compiler/checker/src/infer.rs
CHANGED
|
@@ -114,12 +114,12 @@ pub(crate) fn inferExprInner(
|
|
|
114
114
|
ast::Expr { kind: ast::ExprKind::Int(_), .. } => Ok(PlumType::TInt),
|
|
115
115
|
ast::Expr { kind: ast::ExprKind::Float(_), .. } => Ok(PlumType::TFloat),
|
|
116
116
|
ast::Expr { kind: ast::ExprKind::String(s), .. } => {
|
|
117
|
-
// Interpolated parts are typed
|
|
117
|
+
// Interpolated parts are typed and recorded; an error in one is the
|
|
118
|
-
//
|
|
118
|
+
// string's error (positioned at the failing node inside the part).
|
|
119
119
|
for part in &s.parts {
|
|
120
120
|
if let ast::StringPart::Interp(e) = part {
|
|
121
|
-
let ity = inferExprWith(e, env, ctx, None);
|
|
121
|
+
let ity = inferExprWith(e, env, ctx, None)?;
|
|
122
|
-
if let
|
|
122
|
+
if let PlumType::TApp(n, targs) = ity {
|
|
123
123
|
let gens = enumGenericsOf(&n, ctx);
|
|
124
124
|
if gens.len() == targs.len()
|
|
125
125
|
&& ctx.methods.contains_key(&(n.clone(), "toStr".to_string()))
|
compiler/checker/tests/checker/checker_tests.rs
CHANGED
|
@@ -440,11 +440,13 @@ fn unknown_method_on_heap_const_is_checker_error() {
|
|
|
440
440
|
fn recovered_call_failure_does_not_reposition_genuine_error() {
|
|
441
441
|
let src = "enum Box[T] =\n | Box(v: T)\n\nenum Cfg =\n | Cfg(a: Int)\n\n Cell = Box(1)\n\nfun f() -> Int =\n return \"{Cfg.Cell.size()}\"\n";
|
|
442
442
|
let errs = checkSrc(src).unwrap_err();
|
|
443
|
+
// The interpolated call is a genuine error now (it used to be dropped, leaving only
|
|
444
|
+
// the return mismatch): reported at the call inside the string.
|
|
443
445
|
let e = errs
|
|
444
446
|
.iter()
|
|
445
|
-
.find(|e| e.message.contains("
|
|
447
|
+
.find(|e| e.message.contains("has no method 'size'"))
|
|
446
448
|
.unwrap();
|
|
447
|
-
assert_eq!(
|
|
449
|
+
assert_eq!(e.line, 10, "{:?}", errs);
|
|
448
450
|
}
|
|
449
451
|
|
|
450
452
|
// ---------- constants: PascalCase, always `Type.Name` ----------
|
compiler/checker/tests/checker/diagnostic_tests.rs
CHANGED
|
@@ -119,3 +119,39 @@ fn payloadFreeVariantRejectsNamedArguments() {
|
|
|
119
119
|
assert_eq!((es[0].line, es[0].column), (4, 20), "{es:?}");
|
|
120
120
|
assert!(es[0].message.contains("variant 'Nothing': expected 0 arg(s), got 1"), "{es:?}");
|
|
121
121
|
}
|
|
122
|
+
|
|
123
|
+
const INTERP_TYPES: &str = "struct Dur(ms: Int) =\n fun toMillis(self) -> Int = self.ms\n\nstruct Tm(t: Int) =\n fun sub(self, d: Dur) -> Dur = Dur(ms: self.t - d.ms)\n\n";
|
|
124
|
+
|
|
125
|
+
/// A type error inside an interpolation is the type error, at the bad node inside the string.
|
|
126
|
+
#[test]
|
|
127
|
+
fn interpolationTypeErrorIsReportedAtTheBadCall() {
|
|
128
|
+
let src = format!("{INTERP_TYPES}fun f(a: Tm, b: Tm) -> Str = \"x {{b.sub(a).toMillis()}}\"\n");
|
|
129
|
+
let es = errors(&src);
|
|
130
|
+
assert_eq!(es.len(), 1, "{es:?}");
|
|
131
|
+
assert_eq!(es[0].kind, DiagKind::Type, "{es:?}");
|
|
132
|
+
assert!(es[0].message.contains("expected Dur, found Tm"), "{es:?}");
|
|
133
|
+
assert_eq!((es[0].line, es[0].column), (7, 34), "{es:?}");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// The same mistake outside a string gives the same wording.
|
|
137
|
+
#[test]
|
|
138
|
+
fn interpolationTypeErrorHasTheWordingOfThePlainExpression() {
|
|
139
|
+
let plain = errors(&format!("{INTERP_TYPES}fun f(a: Tm, b: Tm) -> Int = b.sub(a).toMillis()\n"));
|
|
140
|
+
let interp = errors(&format!("{INTERP_TYPES}fun f(a: Tm, b: Tm) -> Str = \"{{b.sub(a).toMillis()}}\"\n"));
|
|
141
|
+
assert_eq!(plain[0].message, interp[0].message);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/// Nested interpolation and a triple-quoted multi-line string report the bad part's own line.
|
|
145
|
+
#[test]
|
|
146
|
+
fn interpolationTypeErrorPositionsInNestedAndMultiLineStrings() {
|
|
147
|
+
let multi = format!("{INTERP_TYPES}fun f(a: Tm, b: Tm) -> Str =\n \"\"\"one\n two {{b.sub(a).t}}\"\"\"\n");
|
|
148
|
+
let es = errors(&multi);
|
|
149
|
+
assert_eq!(es.len(), 1, "{es:?}");
|
|
150
|
+
assert!(es[0].message.contains("expected Dur, found Tm"), "{es:?}");
|
|
151
|
+
assert_eq!(es[0].line, 9, "{es:?}");
|
|
152
|
+
let nested = format!("{INTERP_TYPES}fun f(a: Tm, b: Tm) -> Str = \"x {{\"in {{b.sub(a).toMillis()}}\"}}\"\n");
|
|
153
|
+
let es = errors(&nested);
|
|
154
|
+
assert_eq!(es.len(), 1, "{es:?}");
|
|
155
|
+
assert!(es[0].message.contains("expected Dur, found Tm"), "{es:?}");
|
|
156
|
+
assert_eq!((es[0].line, es[0].column), (7, 39), "{es:?}");
|
|
157
|
+
}
|
compiler/wasm-codegen/tests/wasm_codegen/codegen_tests.rs
CHANGED
|
@@ -1043,6 +1043,28 @@ fun main() -> Int =
|
|
|
1043
1043
|
assert_eq!(runMerged::<i64>("question_interp", src), 1);
|
|
1044
1044
|
}
|
|
1045
1045
|
|
|
1046
|
+
/// A valid method chain inside an interpolation still compiles and runs (the part's
|
|
1047
|
+
/// type errors are reported now, F-INTERP-1; a correct part must not be affected).
|
|
1048
|
+
#[test]
|
|
1049
|
+
fn valid_method_chain_inside_string_interpolation_runs() {
|
|
1050
|
+
let src = "\
|
|
1051
|
+
import std/Str
|
|
1052
|
+
import std/Int
|
|
1053
|
+
|
|
1054
|
+
struct A(n: Int) =
|
|
1055
|
+
fun c(self) -> B = B(m: self.n + 1)
|
|
1056
|
+
|
|
1057
|
+
struct B(m: Int) =
|
|
1058
|
+
fun b(self) -> Int = self.m * 10
|
|
1059
|
+
|
|
1060
|
+
fun main() -> Int =
|
|
1061
|
+
a := A(n: 4)
|
|
1062
|
+
s := \"v={a.c().b()}\"
|
|
1063
|
+
return s.length()
|
|
1064
|
+
";
|
|
1065
|
+
assert_eq!(runMerged::<i64>("interp_chain", src), 4);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1046
1068
|
// ---- mixed types via enums (T4) ----
|
|
1047
1069
|
|
|
1048
1070
|
/// Compiles `src` as a real file (so `import std/...` resolves against the repo's
|