plum

#treesitter#compiler#wasm

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 for the record only; errors in them
117
+ // Interpolated parts are typed and recorded; an error in one is the
118
- // were never part of this check.
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 Ok(PlumType::TApp(n, targs)) = ity {
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("return type mismatch"))
447
+ .find(|e| e.message.contains("has no method 'size'"))
446
448
  .unwrap();
447
- assert_eq!((e.line, e.column), (9, 1), "{:?}", errs);
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