plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


54233aa — Peter John 2026-10-26T14:44:33+05:30
feat(loader): clear error for duplicate top-level constants with both locations
plum-core/src/ast.rs CHANGED
@@ -142,6 +142,8 @@ pub struct Fn {
142
142
  pub struct Const {
143
143
  pub name: String,
144
144
  pub value: Expr,
145
+ /// 1-based source line of the declaration (for duplicate diagnostics).
146
+ pub line: usize,
145
147
  }
146
148
 
147
149
  /// `test "description" { ... }` — compiled only by `plum test`, stripped from
plum-core/src/loader.rs CHANGED
@@ -330,6 +330,25 @@ fn mergeItems(
330
330
  continue;
331
331
  }
332
332
  let key = itemNameKey(item);
333
+ if let Item::Const(c) = item {
334
+ if let Some(prev) = items.iter().find_map(|i| match i {
335
+ Item::Const(p) if p.name == c.name => Some(p),
336
+ _ => None,
337
+ }) {
338
+ let prev_file = names
339
+ .get(&key)
340
+ .map(|f| f.display().to_string())
341
+ .unwrap_or_default();
342
+ return Err(format!(
343
+ "duplicate constant '{}': defined in {}:{} and {}:{}",
344
+ c.name,
345
+ prev_file,
346
+ prev.line,
347
+ file.display(),
348
+ c.line
349
+ ));
350
+ }
351
+ }
333
352
  if let Some(existing_file) = names.get(&key) {
334
353
  return Err(format!(
335
354
  "duplicate declaration '{}': declared in both '{}' and '{}'",
plum-core/src/parser.rs CHANGED
@@ -349,7 +349,11 @@ impl<'a> AstParser<'a> {
349
349
  self.parseExpression(unwrapped)
350
350
  })
351
351
  .unwrap_or(Expr::Int(0));
352
- Const { name, value }
352
+ Const {
353
+ name,
354
+ value,
355
+ line: node.start_position().row + 1,
356
+ }
353
357
  }
354
358
 
355
359
  fn parseTest(&self, node: Node) -> Test {
plum-core/tests/loader_test.rs CHANGED
@@ -270,3 +270,45 @@ fn nullable_bang_rejected() {
270
270
  let err = loadAndMerge(&entry, dir.path()).unwrap_err();
271
271
  assert!(err.contains("Missing, X"), "{err}");
272
272
  }
273
+
274
+ #[test]
275
+ fn duplicate_const_across_modules() {
276
+ let dir = TempDir::new().unwrap();
277
+ let lib_path = dir.path().join("libs");
278
+ writeLibFile(&lib_path, "helper", "module fixtures\n\nPI = 3.14\n");
279
+ let entry = writeLibFile(
280
+ &lib_path,
281
+ "main",
282
+ "module fixtures\n\nimport helper\n\nPI = 3.0\n",
283
+ );
284
+ let msg = loadAndMerge(&entry, &lib_path).unwrap_err();
285
+ assert!(msg.contains("duplicate constant 'PI': defined in "), "{}", msg);
286
+ assert!(msg.contains("main.plum:5") && msg.contains("helper.plum:3"), "{}", msg);
287
+ }
288
+
289
+ #[test]
290
+ fn duplicate_const_same_file() {
291
+ let dir = TempDir::new().unwrap();
292
+ let lib_path = dir.path().join("libs");
293
+ let entry = writeLibFile(
294
+ &lib_path,
295
+ "main",
296
+ "module fixtures\n\nPI = 3.0\nPI = 4.0\n",
297
+ );
298
+ let msg = loadAndMerge(&entry, &lib_path).unwrap_err();
299
+ assert!(msg.contains("duplicate constant 'PI'"), "{}", msg);
300
+ assert!(msg.contains("main.plum:3") && msg.contains("main.plum:4"), "{}", msg);
301
+ }
302
+
303
+ #[test]
304
+ fn distinct_consts_ok() {
305
+ let dir = TempDir::new().unwrap();
306
+ let lib_path = dir.path().join("libs");
307
+ writeLibFile(&lib_path, "helper", "module fixtures\n\nPI = 3.14\n");
308
+ let entry = writeLibFile(
309
+ &lib_path,
310
+ "main",
311
+ "module fixtures\n\nimport helper\n\nTAU = 6.28\n",
312
+ );
313
+ assert!(loadAndMerge(&entry, &lib_path).is_ok());
314
+ }