plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
02af96d
— Peter John
2026-11-06T17:04:10+05:30
Simpler language: modules
- compiler/checker/src/program.rs +6 -4
- compiler/checker/tests/checker/main.rs +1 -0
- compiler/checker/tests/checker/modules_tests.rs +114 -0
- compiler/core/src/ast.rs +6 -0
- compiler/core/src/diag.rs +31 -1
- compiler/core/src/lib.rs +1 -0
- compiler/core/src/loader.rs +27 -0
- compiler/core/src/modules.rs +376 -0
- compiler/core/src/parser.rs +5 -0
- compiler/core/tests/core/formatter_test.rs +9 -0
- compiler/core/tests/core/loader_test.rs +30 -8
- compiler/driver/src/lib.rs +1 -1
- compiler/lsp/src/completion.rs +35 -2
- compiler/lsp/src/main.rs +37 -2
- compiler/tooling/tree-sitter-plum/grammar.js +1 -1
- compiler/tooling/tree-sitter-plum/queries/plum/format.scm +3 -0
- compiler/tooling/tree-sitter-plum/queries/plum/highlights.scm +5 -0
- compiler/tooling/tree-sitter-plum/src/grammar.json +0 -0
- compiler/tooling/tree-sitter-plum/src/node-types.json +0 -0
- compiler/tooling/tree-sitter-plum/src/parser.c +0 -0
- compiler/tooling/tree-sitter-plum/test/corpus/const.txt +26 -0
- compiler/tooling/vscode-plum/syntaxes/plum.tmLanguage.json +14 -2
- compiler/wasm-codegen/src/driver.rs +2 -1
- tests/modules/constants.plum +30 -0
- tests/modules/host_extern.plum +11 -0
- tests/modules/mathx.plum +12 -0
- tests/modules/shapes.plum +10 -0
- tests/modules/two_modules.plum +34 -0
compiler/checker/src/program.rs
CHANGED
|
@@ -84,17 +84,19 @@ pub(crate) fn checkSourceFull(source: &ast::Source) -> (CheckResult<()>, TypedIn
|
|
|
84
84
|
(result, info)
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
/// The `Io` capability rules:
|
|
87
|
+
/// The `Io` capability rules: a module-level `extern fun` takes `io: Io` first, and the
|
|
88
|
+
/// primitive `Io` has no constructor.
|
|
88
89
|
pub(crate) fn ioCapabilityErrors(source: &ast::Source) -> Vec<CheckError> {
|
|
89
90
|
let mut errors = Vec::new();
|
|
90
|
-
// Host calls are
|
|
91
|
+
// Host calls are `extern fun` methods on `primitive Io`, or module-level `extern fun`s
|
|
92
|
+
// whose first parameter is the capability (`io: Io`).
|
|
91
93
|
for item in &source.items {
|
|
92
94
|
if let ast::Item::Fn(f) = item {
|
|
93
|
-
if f.is_extern && f.type_param.is_none() {
|
|
95
|
+
if f.is_extern && f.type_param.is_none() && !ast::takesIoFirst(f) {
|
|
94
96
|
errors.push(CheckError::at(
|
|
95
97
|
f.line,
|
|
96
98
|
f.column,
|
|
97
|
-
"host calls are declared as `extern fun` methods on `primitive Io`".to_string(),
|
|
99
|
+
"host calls are declared as `extern fun` methods on `primitive Io`, or as a module-level `extern fun` with `io: Io` as its first parameter".to_string(),
|
|
98
100
|
));
|
|
99
101
|
}
|
|
100
102
|
}
|
compiler/checker/tests/checker/main.rs
CHANGED
|
@@ -4,6 +4,7 @@ mod checker_tests;
|
|
|
4
4
|
mod closure_hint_test;
|
|
5
5
|
mod diagnostic_tests;
|
|
6
6
|
mod io_primitive_tests;
|
|
7
|
+
mod modules_tests;
|
|
7
8
|
mod explicit_generics_tests;
|
|
8
9
|
mod features;
|
|
9
10
|
mod fip_test;
|
compiler/checker/tests/checker/modules_tests.rs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
//! Modules: `import a/b` gives `b.f()` and `b.Const`; module-level constants and externs.
|
|
2
|
+
//! The module files live in `tests/modules/` (`mathx` and `shapes` both define `clamp`,
|
|
3
|
+
//! `twice` and `Limit`); the programs under test are written to a temp dir.
|
|
4
|
+
use plum_checker::checkProgram;
|
|
5
|
+
|
|
6
|
+
fn temp(name: &str, src: &str) -> std::path::PathBuf {
|
|
7
|
+
let dir = std::env::temp_dir().join(format!("plum_modules_{}_{}", name, std::process::id()));
|
|
8
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
9
|
+
let path = dir.join("main.plum");
|
|
10
|
+
std::fs::write(&path, src).unwrap();
|
|
11
|
+
path
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/// Loads and checks `src` (as `main.plum`, imports against the repo root); the errors are one text.
|
|
15
|
+
fn run(name: &str, src: &str) -> Result<(), String> {
|
|
16
|
+
let path = temp(name, src);
|
|
17
|
+
let loaded = plum_core::loadAndMergeDiag(&path, &plum_test_support::repoRoot());
|
|
18
|
+
let _ = std::fs::remove_dir_all(path.parent().unwrap());
|
|
19
|
+
let source = loaded.map_err(|ds| ds.iter().map(|d| d.to_string()).collect::<Vec<_>>().join(" | "))?;
|
|
20
|
+
checkProgram(&source).0.map_err(|ds| ds.iter().map(|d| d.message.clone()).collect::<Vec<_>>().join(" | "))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const BOTH: &str = "import std/Int\nimport tests/modules/mathx\nimport tests/modules/shapes\n\n";
|
|
24
|
+
|
|
25
|
+
#[test]
|
|
26
|
+
fn qualified_calls_and_constants_check() {
|
|
27
|
+
run("ok", &format!("{BOTH}fun f() -> Int =\n mathx.clamp(5) + shapes.clamp(5) + mathx.Limit + shapes.Limit\n")).unwrap();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#[test]
|
|
31
|
+
fn an_unqualified_use_of_a_name_two_modules_define_is_ambiguous() {
|
|
32
|
+
let e = run("ambig", &format!("{BOTH}fun f() -> Int =\n clamp(5)\n")).unwrap_err();
|
|
33
|
+
assert!(e.contains("'clamp' is defined in several modules (mathx, shapes)"), "{e}");
|
|
34
|
+
assert!(!e.contains('@'), "{e}");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#[test]
|
|
38
|
+
fn an_unqualified_constant_two_modules_define_is_ambiguous() {
|
|
39
|
+
let e = run("ambigc", &format!("{BOTH}fun f() -> Int =\n Limit\n")).unwrap_err();
|
|
40
|
+
assert!(e.contains("'Limit' is defined in several modules"), "{e}");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#[test]
|
|
44
|
+
fn a_file_prefers_its_own_definition_of_a_shared_name() {
|
|
45
|
+
run("own", "import std/Int\nimport tests/modules/mathx\n\nfun clamp(x: Int) -> Int = x\n\nfun f() -> Int =\n clamp(500) + mathx.clamp(500)\n").unwrap();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#[test]
|
|
49
|
+
fn an_unqualified_use_resolves_to_the_one_direct_import() {
|
|
50
|
+
run("direct", "import std/Int\nimport tests/modules/mathx\n\nfun f() -> Int =\n clamp(500)\n").unwrap();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#[test]
|
|
54
|
+
fn a_module_without_the_function_is_an_error() {
|
|
55
|
+
let e = run("nofn", &format!("{BOTH}fun f() -> Int =\n mathx.nope(1)\n")).unwrap_err();
|
|
56
|
+
assert!(e.contains("module 'mathx' has no function 'nope'"), "{e}");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#[test]
|
|
60
|
+
fn a_module_without_the_constant_is_an_error() {
|
|
61
|
+
let e = run("noconst", &format!("{BOTH}fun f() -> Int =\n mathx.Missing\n")).unwrap_err();
|
|
62
|
+
assert!(e.contains("module 'mathx' has no constant 'Missing'"), "{e}");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#[test]
|
|
66
|
+
fn errors_show_the_source_name_not_the_renamed_one() {
|
|
67
|
+
let e = run("srcname", "import std/Int\nimport tests/modules/mathx\n\nfun clamp(x: Int) -> Int = true\n").unwrap_err();
|
|
68
|
+
assert!(e.contains("clamp"), "{e}");
|
|
69
|
+
assert!(!e.contains('@'), "{e}");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[test]
|
|
73
|
+
fn a_local_named_like_a_module_wins() {
|
|
74
|
+
run("local", &format!("{BOTH}fun f(mathx: Int) -> Int =\n mathx + shapes.clamp(5)\n")).unwrap();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#[test]
|
|
78
|
+
fn constants_that_read_each_other_in_a_cycle_are_an_error() {
|
|
79
|
+
let e = run("cycle", "import std/Int\n\nA = B + 1\nB = A + 1\n\nfun f() -> Int = A\n").unwrap_err();
|
|
80
|
+
assert!(e.contains("cycle"), "{e}");
|
|
81
|
+
assert!(e.contains("A -> B -> A") || e.contains("B -> A -> B"), "{e}");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#[test]
|
|
85
|
+
fn a_constant_reading_itself_is_a_cycle() {
|
|
86
|
+
let e = run("self", "import std/Int\n\nA = A + 1\n\nfun f() -> Int = A\n").unwrap_err();
|
|
87
|
+
assert!(e.contains("cycle"), "{e}");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#[test]
|
|
91
|
+
fn constants_declared_out_of_order_check() {
|
|
92
|
+
run("order", "import std/Int\n\nC = B + 1\nB = A + 1\nA = 1\n\nfun f() -> Int = C\n").unwrap();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#[test]
|
|
96
|
+
fn an_extern_with_io_first_checks() {
|
|
97
|
+
run("ext", "import std/Io\nimport std/Float\n\nextern fun rawNowMillis(io: Io) -> Float\n\nfun f(io: Io) -> Float = rawNowMillis(io)\n").unwrap();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#[test]
|
|
101
|
+
fn an_extern_without_io_first_is_rejected() {
|
|
102
|
+
let e = run("extno", "import std/Float\n\nextern fun rawNowMillis() -> Float\n").unwrap_err();
|
|
103
|
+
assert!(e.contains("with `io: Io` as its first parameter"), "{e}");
|
|
104
|
+
let e = run("extint", "import std/Int\nimport std/Float\n\nextern fun rawNowMillis(x: Int) -> Float\n").unwrap_err();
|
|
105
|
+
assert!(e.contains("with `io: Io` as its first parameter"), "{e}");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#[test]
|
|
109
|
+
fn primitive_outside_std_is_rejected_for_a_built_in_and_for_an_extension() {
|
|
110
|
+
let e = run("primint", "primitive Int =\n fun f(self) -> Byte =\n self\n").unwrap_err();
|
|
111
|
+
assert!(e.contains("only allowed in std"), "{e}");
|
|
112
|
+
let e = run("primext", "import std/Int\n\nstruct Pt(x: Int)\n\nprimitive Pt =\n fun g(self) -> Int = 1\n").unwrap_err();
|
|
113
|
+
assert!(e.contains("only allowed in std"), "{e}");
|
|
114
|
+
}
|
compiler/core/src/ast.rs
CHANGED
|
@@ -328,6 +328,12 @@ pub const BUILTIN_PRIMITIVES: [&str; 7] = ["Int", "Float", "Bool", "Str", "Byte"
|
|
|
328
328
|
/// `primitive Io =` (std/Io.plum); extern methods are allowed nowhere else.
|
|
329
329
|
pub const IO_TYPE: &str = "Io";
|
|
330
330
|
|
|
331
|
+
/// Whether `f`'s first parameter is the `Io` capability (`io: Io`): what a module-level
|
|
332
|
+
/// `extern fun` needs, and what its host import does not receive.
|
|
333
|
+
pub fn takesIoFirst(f: &Fn) -> bool {
|
|
334
|
+
matches!(f.params.first(), Some(Param { ty: ParamType::Type(t), .. }) if t.name == IO_TYPE && t.generics.is_empty())
|
|
335
|
+
}
|
|
336
|
+
|
|
331
337
|
/// Separator of an associated item's mangled global name: `Float.Pi`.
|
|
332
338
|
pub const MEMBER_SEP: &str = ".";
|
|
333
339
|
|
compiler/core/src/diag.rs
CHANGED
|
@@ -32,7 +32,8 @@ pub struct Diagnostic {
|
|
|
32
32
|
|
|
33
33
|
impl Diagnostic {
|
|
34
34
|
pub fn new(kind: DiagKind, file: impl Into<String>, line: usize, column: usize, message: impl Into<String>) -> Self {
|
|
35
|
+
let message: String = message.into();
|
|
35
|
-
Diagnostic { file: file.into(), line, column, end_line: 0, end_column: 0, kind, message:
|
|
36
|
+
Diagnostic { file: file.into(), line, column, end_line: 0, end_column: 0, kind, message: sourceName(&message) }
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
/// The same diagnostic with the end of the failing node's span (1-based, exclusive).
|
|
@@ -58,3 +59,32 @@ impl fmt::Display for Diagnostic {
|
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
}
|
|
62
|
+
|
|
63
|
+
/// `text` with the module mark of renamed definitions removed (`clamp@mathx` -> `clamp`), so
|
|
64
|
+
/// messages show the names as written in the source (see `modules`).
|
|
65
|
+
pub fn sourceName(text: &str) -> String {
|
|
66
|
+
if !text.contains(crate::modules::MODULE_MARK) {
|
|
67
|
+
return text.to_string();
|
|
68
|
+
}
|
|
69
|
+
let isId = |c: char| c.is_alphanumeric() || c == '_';
|
|
70
|
+
let chars: Vec<char> = text.chars().collect();
|
|
71
|
+
let mut out = String::with_capacity(text.len());
|
|
72
|
+
let mut i = 0;
|
|
73
|
+
while i < chars.len() {
|
|
74
|
+
let c = chars[i];
|
|
75
|
+
let marked = c == crate::modules::MODULE_MARK
|
|
76
|
+
&& i > 0
|
|
77
|
+
&& isId(chars[i - 1])
|
|
78
|
+
&& chars.get(i + 1).is_some_and(|n| n.is_alphabetic() || *n == '_');
|
|
79
|
+
if marked {
|
|
80
|
+
i += 1;
|
|
81
|
+
while i < chars.len() && isId(chars[i]) {
|
|
82
|
+
i += 1;
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
out.push(c);
|
|
87
|
+
i += 1;
|
|
88
|
+
}
|
|
89
|
+
out
|
|
90
|
+
}
|
compiler/core/src/lib.rs
CHANGED
|
@@ -19,6 +19,7 @@ pub mod layout;
|
|
|
19
19
|
pub mod loader;
|
|
20
20
|
pub mod lower;
|
|
21
21
|
pub mod main_result;
|
|
22
|
+
pub mod modules;
|
|
22
23
|
pub mod parser;
|
|
23
24
|
pub mod purity;
|
|
24
25
|
pub mod rename;
|
compiler/core/src/loader.rs
CHANGED
|
@@ -67,12 +67,14 @@ pub fn loadAndMergeWithOverridesDiag(
|
|
|
67
67
|
collectImport(import, lib_path, overrides, &mut visited, &mut files)?;
|
|
68
68
|
}
|
|
69
69
|
shadowStdTypes(&mut files, lib_path)?;
|
|
70
|
+
resolveFileModules(&mut files, lib_path)?;
|
|
70
71
|
for f in &files {
|
|
71
72
|
mergeItems(&f.path, &f.source, &mut items, &mut names, &mut prims, lib_path)?;
|
|
72
73
|
}
|
|
73
74
|
let entry_source = files.swap_remove(0).source;
|
|
74
75
|
|
|
75
76
|
dropExtensions(&items, &mut prims)?;
|
|
77
|
+
crate::modules::orderConsts(&mut items)?;
|
|
76
78
|
let mut merged = Source {
|
|
77
79
|
file: entry_source.file,
|
|
78
80
|
module: entry_source.module,
|
|
@@ -192,6 +194,31 @@ fn collectImport(
|
|
|
192
194
|
Ok(())
|
|
193
195
|
}
|
|
194
196
|
|
|
197
|
+
/// Gives each file's `import a/b` the name `b` for that module's functions and constants
|
|
198
|
+
/// (see `modules`), renaming the ones several files define.
|
|
199
|
+
fn resolveFileModules(files: &mut [Loaded], lib_path: &Path) -> Loaded_<()> {
|
|
200
|
+
let index: HashMap<PathBuf, usize> = files.iter().enumerate().map(|(i, f)| (f.path.clone(), i)).collect();
|
|
201
|
+
let infos: Vec<crate::modules::ModuleInfo> = files
|
|
202
|
+
.iter()
|
|
203
|
+
.map(|f| crate::modules::ModuleInfo {
|
|
204
|
+
name: f.path.file_stem().map(|s| s.to_string_lossy().to_string()).unwrap_or_default(),
|
|
205
|
+
path: displayPath(&f.path),
|
|
206
|
+
imports: f
|
|
207
|
+
.source
|
|
208
|
+
.imports
|
|
209
|
+
.iter()
|
|
210
|
+
.filter_map(|imp| {
|
|
211
|
+
let target = resolveImport(imp, lib_path).ok()?;
|
|
212
|
+
let alias = imp.path.rsplit('/').next().unwrap_or(&imp.path).to_string();
|
|
213
|
+
Some((alias, *index.get(&target)?))
|
|
214
|
+
})
|
|
215
|
+
.collect(),
|
|
216
|
+
})
|
|
217
|
+
.collect();
|
|
218
|
+
let mut sources: Vec<&mut Source> = files.iter_mut().map(|f| &mut f.source).collect();
|
|
219
|
+
crate::modules::resolveModules(infos, &mut sources)
|
|
220
|
+
}
|
|
221
|
+
|
|
195
222
|
fn declaredTypes(source: &Source) -> Vec<String> {
|
|
196
223
|
source
|
|
197
224
|
.items
|
compiler/core/src/modules.rs
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
//! Modules. `import a/b` makes `b` a name for that file's top-level functions and
|
|
2
|
+
//! constants: `b.f()`, `b.Const`. Types and enum variants stay unqualified.
|
|
3
|
+
//!
|
|
4
|
+
//! Two files may define the same function or constant name. Such a name is renamed per
|
|
5
|
+
//! module while the files are merged (`clamp` in `mathx` becomes `clamp@mathx`), so every
|
|
6
|
+
//! later pass keeps seeing plain global names and needs no change; diagnostics print the
|
|
7
|
+
//! source name (see `diag::sourceName`). A name only one file defines keeps its name, and
|
|
8
|
+
//! stays callable unqualified (the fallback T5 removes).
|
|
9
|
+
//!
|
|
10
|
+
//! Resolution of a use in file F, for a name defined in several files:
|
|
11
|
+
//! - `m.f()` / `m.C` with `m` an import of F (and not a local of the function): that module's;
|
|
12
|
+
//! - an unqualified use: F's own definition, else the one F imports directly, else an error.
|
|
13
|
+
//!
|
|
14
|
+
//! A local named like an import wins over the module (the locals of a function are its
|
|
15
|
+
//! parameters and every binder in its body).
|
|
16
|
+
use crate::ast::*;
|
|
17
|
+
use crate::diag::{DiagKind, Diagnostic};
|
|
18
|
+
use crate::rename::{renameStmts, Name};
|
|
19
|
+
use crate::visit::{visitBlock, visitExpr};
|
|
20
|
+
use std::collections::{HashMap, HashSet};
|
|
21
|
+
|
|
22
|
+
/// Separates a source name from the module of a renamed definition: `clamp@mathx`.
|
|
23
|
+
pub const MODULE_MARK: char = '@';
|
|
24
|
+
|
|
25
|
+
/// What the loader knows about one file.
|
|
26
|
+
pub struct ModuleInfo {
|
|
27
|
+
/// The file's stem: the name its importers use.
|
|
28
|
+
pub name: String,
|
|
29
|
+
/// For diagnostics.
|
|
30
|
+
pub path: String,
|
|
31
|
+
/// `(name, file index)` of each direct import.
|
|
32
|
+
pub imports: Vec<(String, usize)>,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// A top-level function a module may rename: not a method, host import, export or `main`.
|
|
36
|
+
fn renamable(f: &Fn) -> bool {
|
|
37
|
+
f.type_param.is_none() && !f.is_extern && !f.is_export && !f.is_test && f.name != "main"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fn isModuleConst(c: &Const) -> bool {
|
|
41
|
+
!c.name.contains(MEMBER_SEP)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
struct World {
|
|
45
|
+
infos: Vec<ModuleInfo>,
|
|
46
|
+
/// Per file: every top-level fn name (any kind), and every module-level const name.
|
|
47
|
+
fns: Vec<HashSet<String>>,
|
|
48
|
+
consts: Vec<HashSet<String>>,
|
|
49
|
+
/// Names defined in two or more files (renamable fns / consts only).
|
|
50
|
+
fnCollide: HashSet<String>,
|
|
51
|
+
constCollide: HashSet<String>,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fn mangle(name: &str, module: &str) -> String {
|
|
55
|
+
format!("{name}{MODULE_MARK}{module}")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
impl World {
|
|
59
|
+
fn global(&self, file: usize, name: &str, collide: &HashSet<String>) -> String {
|
|
60
|
+
if collide.contains(name) {
|
|
61
|
+
mangle(name, &self.infos[file].name)
|
|
62
|
+
} else {
|
|
63
|
+
name.to_string()
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
pub fn resolveModules(infos: Vec<ModuleInfo>, sources: &mut [&mut Source]) -> Result<(), Vec<Diagnostic>> {
|
|
69
|
+
let n = sources.len();
|
|
70
|
+
let mut fns = vec![HashSet::new(); n];
|
|
71
|
+
let mut consts = vec![HashSet::new(); n];
|
|
72
|
+
let mut fnCount: HashMap<String, usize> = HashMap::new();
|
|
73
|
+
let mut constCount: HashMap<String, usize> = HashMap::new();
|
|
74
|
+
for (i, s) in sources.iter().enumerate() {
|
|
75
|
+
let mut ownFns = HashSet::new();
|
|
76
|
+
let mut ownConsts = HashSet::new();
|
|
77
|
+
for item in &s.items {
|
|
78
|
+
match item {
|
|
79
|
+
Item::Fn(f) if f.type_param.is_none() => {
|
|
80
|
+
if renamable(f) && ownFns.insert(f.name.clone()) {
|
|
81
|
+
*fnCount.entry(f.name.clone()).or_default() += 1;
|
|
82
|
+
}
|
|
83
|
+
fns[i].insert(f.name.clone());
|
|
84
|
+
}
|
|
85
|
+
Item::Const(c) if isModuleConst(c) => {
|
|
86
|
+
if ownConsts.insert(c.name.clone()) {
|
|
87
|
+
*constCount.entry(c.name.clone()).or_default() += 1;
|
|
88
|
+
}
|
|
89
|
+
consts[i].insert(c.name.clone());
|
|
90
|
+
}
|
|
91
|
+
_ => {}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
let collide = |m: &HashMap<String, usize>| -> HashSet<String> {
|
|
96
|
+
m.iter().filter(|(_, &c)| c > 1).map(|(k, _)| k.clone()).collect()
|
|
97
|
+
};
|
|
98
|
+
let world = World { fnCollide: collide(&fnCount), constCollide: collide(&constCount), infos, fns, consts };
|
|
99
|
+
// A program whose files import nothing has no `m.f()` to resolve.
|
|
100
|
+
if world.fnCollide.is_empty() && world.constCollide.is_empty() && world.infos.iter().all(|i| i.imports.is_empty()) {
|
|
101
|
+
return Ok(());
|
|
102
|
+
}
|
|
103
|
+
let mut errors = Vec::new();
|
|
104
|
+
for (i, s) in sources.iter_mut().enumerate() {
|
|
105
|
+
rewriteFile(&world, i, s, &mut errors);
|
|
106
|
+
}
|
|
107
|
+
if errors.is_empty() {
|
|
108
|
+
Ok(())
|
|
109
|
+
} else {
|
|
110
|
+
Err(errors)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#[derive(Clone, Copy)]
|
|
115
|
+
enum Kind {
|
|
116
|
+
Fn,
|
|
117
|
+
Const,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
struct Cx<'a> {
|
|
121
|
+
world: &'a World,
|
|
122
|
+
file: usize,
|
|
123
|
+
aliases: HashMap<String, usize>,
|
|
124
|
+
locals: HashSet<String>,
|
|
125
|
+
errors: &'a mut Vec<Diagnostic>,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
impl Cx<'_> {
|
|
129
|
+
fn error(&mut self, line: usize, column: usize, message: String) {
|
|
130
|
+
let path = self.world.infos[self.file].path.clone();
|
|
131
|
+
self.errors.push(Diagnostic::new(DiagKind::Resolution, path, line, column, message));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// Which file an unqualified use of the colliding `name` means; `None` after an error.
|
|
135
|
+
fn pick(&mut self, name: &str, kind: Kind, line: usize, column: usize) -> Option<usize> {
|
|
136
|
+
let world = self.world;
|
|
137
|
+
let defines = |f: usize| match kind {
|
|
138
|
+
Kind::Fn => world.fns[f].contains(name),
|
|
139
|
+
Kind::Const => world.consts[f].contains(name),
|
|
140
|
+
};
|
|
141
|
+
if defines(self.file) {
|
|
142
|
+
return Some(self.file);
|
|
143
|
+
}
|
|
144
|
+
let direct: Vec<usize> = world.infos[self.file]
|
|
145
|
+
.imports
|
|
146
|
+
.iter()
|
|
147
|
+
.map(|(_, t)| *t)
|
|
148
|
+
.filter(|t| *t != self.file && defines(*t))
|
|
149
|
+
.collect();
|
|
150
|
+
if direct.len() == 1 {
|
|
151
|
+
return Some(direct[0]);
|
|
152
|
+
}
|
|
153
|
+
let mut owners: Vec<String> =
|
|
154
|
+
(0..world.infos.len()).filter(|f| defines(*f)).map(|f| world.infos[f].name.clone()).collect();
|
|
155
|
+
owners.sort();
|
|
156
|
+
let hint = owners.first().cloned().unwrap_or_default();
|
|
157
|
+
self.error(
|
|
158
|
+
line,
|
|
159
|
+
column,
|
|
160
|
+
format!("'{name}' is defined in several modules ({}): write {hint}.{name}", owners.join(", ")),
|
|
161
|
+
);
|
|
162
|
+
None
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
fn visit(&mut self, e: &mut Expr) {
|
|
166
|
+
let (line, column) = (e.span.startLine, e.span.startColumn);
|
|
167
|
+
let world = self.world;
|
|
168
|
+
match &mut e.kind {
|
|
169
|
+
ExprKind::Attribute(a) => {
|
|
170
|
+
let ExprKind::Var(m) = &a.object.kind else { return };
|
|
171
|
+
if self.locals.contains(m) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
let Some(&target) = self.aliases.get(m) else { return };
|
|
175
|
+
let module = m.clone();
|
|
176
|
+
let replacement = match &a.attr {
|
|
177
|
+
AttrKind::Method(call) => {
|
|
178
|
+
if !world.fns[target].contains(&call.name) {
|
|
179
|
+
self.error(call.line, call.column, format!("module '{module}' has no function '{}'", call.name));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
let mut call = call.clone();
|
|
183
|
+
call.name = world.global(target, &call.name, &world.fnCollide);
|
|
184
|
+
Expr::FnCall(call)
|
|
185
|
+
}
|
|
186
|
+
AttrKind::Field(name) => {
|
|
187
|
+
if !world.consts[target].contains(name) {
|
|
188
|
+
self.error(line, column, format!("module '{module}' has no constant '{name}'"));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
Expr::TypeName(world.global(target, name, &world.constCollide))
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
*e = replacement.at(e);
|
|
195
|
+
}
|
|
196
|
+
ExprKind::FnCall(c) if world.fnCollide.contains(&c.name) && !self.locals.contains(&c.name) => {
|
|
197
|
+
let (name, l, col) = (c.name.clone(), c.line, c.column);
|
|
198
|
+
if let Some(t) = self.pick(&name, Kind::Fn, l, col) {
|
|
199
|
+
if let ExprKind::FnCall(c) = &mut e.kind {
|
|
200
|
+
c.name = world.global(t, &name, &world.fnCollide);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
ExprKind::Var(n) if world.fnCollide.contains(n.as_str()) && !self.locals.contains(n.as_str()) => {
|
|
205
|
+
let name = n.clone();
|
|
206
|
+
if let Some(t) = self.pick(&name, Kind::Fn, line, column) {
|
|
207
|
+
e.kind = ExprKind::Var(world.global(t, &name, &world.fnCollide));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
ExprKind::TypeName(n) if world.constCollide.contains(n.as_str()) => {
|
|
211
|
+
let name = n.clone();
|
|
212
|
+
if let Some(t) = self.pick(&name, Kind::Const, line, column) {
|
|
213
|
+
e.kind = ExprKind::TypeName(world.global(t, &name, &world.constCollide));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
_ => {}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// Every name a body declares: parameters, `:=` targets, loop binders, pattern names and
|
|
222
|
+
/// closure parameters.
|
|
223
|
+
fn bodyLocals(params: &[Param], stmts: &[Stmt]) -> HashSet<String> {
|
|
224
|
+
let mut out: HashSet<String> = params.iter().map(|p| p.name.clone()).collect();
|
|
225
|
+
let mut stmts = stmts.to_vec();
|
|
226
|
+
renameStmts(&mut stmts, &mut |n, kind| {
|
|
227
|
+
if matches!(kind, Name::Decl | Name::Pat) {
|
|
228
|
+
out.insert(n.clone());
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
out
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/// The names a function declares anywhere in its parameters and body (see `bodyLocals`).
|
|
235
|
+
pub fn fnLocals(f: &Fn) -> HashSet<String> {
|
|
236
|
+
let body = match &f.body {
|
|
237
|
+
FnBody::Block(b) => b.stmts.clone(),
|
|
238
|
+
FnBody::Expr(e) => vec![Stmt::Expr(e.clone(), 0, 0)],
|
|
239
|
+
FnBody::Extern => Vec::new(),
|
|
240
|
+
};
|
|
241
|
+
bodyLocals(&f.params, &body)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
fn rewriteFile(world: &World, file: usize, source: &mut Source, errors: &mut Vec<Diagnostic>) {
|
|
245
|
+
for item in &mut source.items {
|
|
246
|
+
match item {
|
|
247
|
+
Item::Fn(f) if renamable(f) && world.fnCollide.contains(&f.name) => {
|
|
248
|
+
f.name = mangle(&f.name, &world.infos[file].name);
|
|
249
|
+
}
|
|
250
|
+
Item::Const(c) if isModuleConst(c) && world.constCollide.contains(&c.name) => {
|
|
251
|
+
c.name = mangle(&c.name, &world.infos[file].name);
|
|
252
|
+
}
|
|
253
|
+
_ => {}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
let aliases: HashMap<String, usize> = world.infos[file]
|
|
257
|
+
.imports
|
|
258
|
+
.iter()
|
|
259
|
+
.filter(|(_, t)| *t != file)
|
|
260
|
+
.map(|(a, t)| (a.clone(), *t))
|
|
261
|
+
.collect();
|
|
262
|
+
let mut cx = Cx { world, file, aliases, locals: HashSet::new(), errors };
|
|
263
|
+
for item in &mut source.items {
|
|
264
|
+
match item {
|
|
265
|
+
Item::Fn(f) => {
|
|
266
|
+
cx.locals = fnLocals(f);
|
|
267
|
+
for p in &mut f.params {
|
|
268
|
+
if let Some(d) = &mut p.default {
|
|
269
|
+
visitExpr(d, &mut |e| cx.visit(e));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
match &mut f.body {
|
|
273
|
+
FnBody::Block(b) => visitBlock(b, &mut |e| cx.visit(e)),
|
|
274
|
+
FnBody::Expr(e) => visitExpr(e, &mut |e| cx.visit(e)),
|
|
275
|
+
FnBody::Extern => {}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
Item::Test(t) => {
|
|
279
|
+
cx.locals = bodyLocals(&[], &t.body.stmts);
|
|
280
|
+
visitBlock(&mut t.body, &mut |e| cx.visit(e));
|
|
281
|
+
}
|
|
282
|
+
Item::Const(c) => {
|
|
283
|
+
cx.locals.clear();
|
|
284
|
+
visitExpr(&mut c.value, &mut |e| cx.visit(e));
|
|
285
|
+
}
|
|
286
|
+
Item::Enum(_) => {}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/// Puts each constant after the constants its initializer reads (the slots of the
|
|
292
|
+
/// constants keep their places among the other items), and rejects a cycle.
|
|
293
|
+
pub fn orderConsts(items: &mut [Item]) -> Result<(), Vec<Diagnostic>> {
|
|
294
|
+
let slots: Vec<usize> = (0..items.len()).filter(|i| matches!(items[*i], Item::Const(_))).collect();
|
|
295
|
+
if slots.is_empty() {
|
|
296
|
+
return Ok(());
|
|
297
|
+
}
|
|
298
|
+
let index: HashMap<String, usize> = slots
|
|
299
|
+
.iter()
|
|
300
|
+
.enumerate()
|
|
301
|
+
.filter_map(|(k, s)| match &items[*s] {
|
|
302
|
+
Item::Const(c) => Some((c.name.clone(), k)),
|
|
303
|
+
_ => None,
|
|
304
|
+
})
|
|
305
|
+
.collect();
|
|
306
|
+
let deps: Vec<Vec<usize>> = slots
|
|
307
|
+
.iter()
|
|
308
|
+
.map(|s| {
|
|
309
|
+
let Item::Const(c) = &items[*s] else { unreachable!() };
|
|
310
|
+
let mut value = c.value.clone();
|
|
311
|
+
let mut found = Vec::new();
|
|
312
|
+
visitExpr(&mut value, &mut |e| {
|
|
313
|
+
if let ExprKind::TypeName(n) = &e.kind {
|
|
314
|
+
if let Some(k) = index.get(n) {
|
|
315
|
+
found.push(*k);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
found
|
|
320
|
+
})
|
|
321
|
+
.collect();
|
|
322
|
+
// 0 = new, 1 = on the stack, 2 = done.
|
|
323
|
+
let mut state = vec![0u8; slots.len()];
|
|
324
|
+
let mut order: Vec<usize> = Vec::new();
|
|
325
|
+
fn dfs(k: usize, deps: &[Vec<usize>], state: &mut [u8], order: &mut Vec<usize>, path: &mut Vec<usize>) -> Option<Vec<usize>> {
|
|
326
|
+
match state[k] {
|
|
327
|
+
2 => return None,
|
|
328
|
+
1 => {
|
|
329
|
+
let at = path.iter().position(|p| *p == k).unwrap_or(0);
|
|
330
|
+
let mut cycle = path[at..].to_vec();
|
|
331
|
+
cycle.push(k);
|
|
332
|
+
return Some(cycle);
|
|
333
|
+
}
|
|
334
|
+
_ => {}
|
|
335
|
+
}
|
|
336
|
+
state[k] = 1;
|
|
337
|
+
path.push(k);
|
|
338
|
+
for d in &deps[k] {
|
|
339
|
+
if let Some(c) = dfs(*d, deps, state, order, path) {
|
|
340
|
+
return Some(c);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
path.pop();
|
|
344
|
+
state[k] = 2;
|
|
345
|
+
order.push(k);
|
|
346
|
+
None
|
|
347
|
+
}
|
|
348
|
+
for k in 0..slots.len() {
|
|
349
|
+
if let Some(cycle) = dfs(k, &deps, &mut state, &mut order, &mut Vec::new()) {
|
|
350
|
+
let name = |k: usize| match &items[slots[k]] {
|
|
351
|
+
Item::Const(c) => c.name.clone(),
|
|
352
|
+
_ => String::new(),
|
|
353
|
+
};
|
|
354
|
+
let line = match &items[slots[cycle[0]]] {
|
|
355
|
+
Item::Const(c) => c.line,
|
|
356
|
+
_ => 0,
|
|
357
|
+
};
|
|
358
|
+
let text = cycle.iter().map(|k| name(*k)).collect::<Vec<_>>().join(" -> ");
|
|
359
|
+
return Err(vec![Diagnostic::new(
|
|
360
|
+
DiagKind::Resolution,
|
|
361
|
+
"",
|
|
362
|
+
line,
|
|
363
|
+
1,
|
|
364
|
+
format!("constants refer to each other in a cycle: {text}"),
|
|
365
|
+
)]);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if order.iter().enumerate().all(|(i, k)| i == *k) {
|
|
369
|
+
return Ok(());
|
|
370
|
+
}
|
|
371
|
+
let taken: Vec<Item> = slots.iter().map(|s| items[*s].clone()).collect();
|
|
372
|
+
for (slot, k) in slots.iter().zip(order) {
|
|
373
|
+
items[*slot] = taken[k].clone();
|
|
374
|
+
}
|
|
375
|
+
Ok(())
|
|
376
|
+
}
|
compiler/core/src/parser.rs
CHANGED
|
@@ -160,6 +160,11 @@ impl<'a> AstParser<'a> {
|
|
|
160
160
|
items.push(Item::Fn(self.parseFn(child)));
|
|
161
161
|
topLevelOrder.push(TopLevelKind::Other);
|
|
162
162
|
}
|
|
163
|
+
// A module-level constant `ArenaW = 800.0`: a global under its own name.
|
|
164
|
+
"const" => {
|
|
165
|
+
items.push(Item::Const(self.parseConst(child)));
|
|
166
|
+
topLevelOrder.push(TopLevelKind::Other);
|
|
167
|
+
}
|
|
163
168
|
"test" => {
|
|
164
169
|
items.push(Item::Test(self.parseTest(child)));
|
|
165
170
|
topLevelOrder.push(TopLevelKind::Test);
|
compiler/core/tests/core/formatter_test.rs
CHANGED
|
@@ -413,3 +413,12 @@ fn formatFnGenericsStayTight() {
|
|
|
413
413
|
assert!(out.contains("fun pair[A, Elem](a: A, b: Elem) -> A = a"), "{out}");
|
|
414
414
|
assert_eq!(formatSource(&out).expect("second"), out);
|
|
415
415
|
}
|
|
416
|
+
|
|
417
|
+
/// A module-level constant is its own line and keeps a blank line the source had.
|
|
418
|
+
#[test]
|
|
419
|
+
fn moduleLevelConstantsFormat() {
|
|
420
|
+
let input = "import std/Float\n\nArenaW = 800.0\nArenaH = 600.0\n\nArea = ArenaW * ArenaH\n\nfun f() -> Float =\n mathx.Limit\n";
|
|
421
|
+
let out = formatSource(input).expect("formats");
|
|
422
|
+
assert_eq!(out, input);
|
|
423
|
+
assert_eq!(formatSource(&out).expect("formats twice"), out);
|
|
424
|
+
}
|
compiler/core/tests/core/loader_test.rs
CHANGED
|
@@ -179,18 +179,13 @@ fn unresolvableImportIsAClearError() {
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
#[test]
|
|
182
|
-
fn
|
|
182
|
+
fn duplicateTopLevelNameInOneFileIsAClearError() {
|
|
183
183
|
let dir = TempDir::new().unwrap();
|
|
184
184
|
let lib_path = dir.path().join("libs");
|
|
185
|
-
writeLibFile(
|
|
186
|
-
&lib_path,
|
|
187
|
-
"helper",
|
|
188
|
-
"module fixtures\n\nfun sharedFn(u: Unit) -> Unit =\n u\n",
|
|
189
|
-
);
|
|
190
185
|
let entry = writeLibFile(
|
|
191
186
|
&lib_path,
|
|
192
187
|
"main",
|
|
193
|
-
"module fixtures\n\
|
|
188
|
+
"module fixtures\n\nfun sharedFn(u: Unit) -> Unit =\n u\n\nfun sharedFn(u: Unit) -> Unit =\n u\n",
|
|
194
189
|
);
|
|
195
190
|
|
|
196
191
|
let result = loadAndMergeDiag(&entry, &lib_path);
|
|
@@ -203,6 +198,34 @@ fn duplicateTopLevelNameAcrossFilesIsAClearError() {
|
|
|
203
198
|
);
|
|
204
199
|
}
|
|
205
200
|
|
|
201
|
+
#[test]
|
|
202
|
+
fn sameTopLevelNameInTwoModulesIsNotACollision() {
|
|
203
|
+
let dir = TempDir::new().unwrap();
|
|
204
|
+
let lib_path = dir.path().join("libs");
|
|
205
|
+
writeLibFile(&lib_path, "helper", "module fixtures\n\nfun sharedFn(u: Unit) -> Unit =\n u\n");
|
|
206
|
+
let entry = writeLibFile(
|
|
207
|
+
&lib_path,
|
|
208
|
+
"main",
|
|
209
|
+
"module fixtures\n\nimport helper\n\nfun sharedFn(u: Unit) -> Unit =\n helper.sharedFn(u)\n",
|
|
210
|
+
);
|
|
211
|
+
let merged = loadAndMergeDiag(&entry, &lib_path).expect("two modules may share a function name");
|
|
212
|
+
let names: Vec<String> = merged
|
|
213
|
+
.items
|
|
214
|
+
.iter()
|
|
215
|
+
.filter_map(|i| match i {
|
|
216
|
+
plum_core::ast::Item::Fn(f) => Some(f.name.clone()),
|
|
217
|
+
_ => None,
|
|
218
|
+
})
|
|
219
|
+
.collect();
|
|
220
|
+
assert_eq!(names.len(), 2);
|
|
221
|
+
assert_ne!(names[0], names[1], "{names:?}");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#[test]
|
|
225
|
+
fn diagnosticsShowTheSourceNameOfARenamedDefinition() {
|
|
226
|
+
assert_eq!(plum_core::diag::sourceName("fn 'clamp@mathx': bad; also [email protected] and x @ y"), "fn 'clamp': bad; also a.c and x @ y");
|
|
227
|
+
}
|
|
228
|
+
|
|
206
229
|
#[test]
|
|
207
230
|
fn sameMethodNameOnDifferentReceiversAcrossFilesIsNotACollision() {
|
|
208
231
|
let dir = TempDir::new().unwrap();
|
|
@@ -262,7 +285,6 @@ fn removedSyntaxIsAParseError() {
|
|
|
262
285
|
"fun f(u: Unit) -> Unit??\n u\n",
|
|
263
286
|
"fun f(u: Unit) -> Unit? =\n u\n",
|
|
264
287
|
"fun f(u: Unit) -> Unit? ! [X] =\n u\n",
|
|
265
|
-
"Pi = 3\n",
|
|
266
288
|
"extend Foo =\n fun f() -> Unit =\n todo\n",
|
|
267
289
|
] {
|
|
268
290
|
let dir = TempDir::new().unwrap();
|
compiler/driver/src/lib.rs
CHANGED
|
@@ -99,7 +99,7 @@ fn fusion(copy: &mut Source, env: &Env) -> bool {
|
|
|
99
99
|
env.errors.borrow_mut().extend(errors);
|
|
100
100
|
if env.explain {
|
|
101
101
|
for n in ¬es {
|
|
102
|
-
eprintln!("fusion: {
|
|
102
|
+
eprintln!("fusion: {}", plum_core::diag::sourceName(n));
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
!notes.is_empty()
|
compiler/lsp/src/completion.rs
CHANGED
|
@@ -75,7 +75,7 @@ pub fn topLevelItems(source: &ast::Source) -> Vec<CompletionItem> {
|
|
|
75
75
|
match item {
|
|
76
76
|
ast::Item::Fn(f) if f.type_param.is_none() => {
|
|
77
77
|
items.push(CompletionItem {
|
|
78
|
-
label: f.name
|
|
78
|
+
label: plum_core::diag::sourceName(&f.name),
|
|
79
79
|
kind: Some(CompletionItemKind::FUNCTION),
|
|
80
80
|
detail: Some("fn".to_string()),
|
|
81
81
|
..Default::default()
|
|
@@ -99,7 +99,7 @@ pub fn topLevelItems(source: &ast::Source) -> Vec<CompletionItem> {
|
|
|
99
99
|
}
|
|
100
100
|
ast::Item::Const(c) => {
|
|
101
101
|
items.push(CompletionItem {
|
|
102
|
-
label: c.name
|
|
102
|
+
label: plum_core::diag::sourceName(&c.name),
|
|
103
103
|
kind: Some(CompletionItemKind::CONSTANT),
|
|
104
104
|
..Default::default()
|
|
105
105
|
});
|
|
@@ -160,6 +160,39 @@ pub fn staticItems(source: &ast::Source, owner: &str) -> Vec<CompletionItem> {
|
|
|
160
160
|
items
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/// Completion items for `module.`: the top-level functions and constants of the file
|
|
164
|
+
/// `import .../module` names (read from its own text, so each keeps its source name).
|
|
165
|
+
pub fn moduleItems(libPath: &Path, importPath: &str) -> Vec<CompletionItem> {
|
|
166
|
+
let Ok(text) = std::fs::read_to_string(libPath.join(format!("{importPath}.plum"))) else {
|
|
167
|
+
return Vec::new();
|
|
168
|
+
};
|
|
169
|
+
let mut parser = tree_sitter::Parser::new();
|
|
170
|
+
if parser.set_language(&tree_sitter_plum::LANGUAGE.into()).is_err() {
|
|
171
|
+
return Vec::new();
|
|
172
|
+
}
|
|
173
|
+
let Some(tree) = parser.parse(&text, None) else { return Vec::new() };
|
|
174
|
+
let module = plum_core::AstParser::new(&text).parseSourceRaw(tree.root_node());
|
|
175
|
+
let mut items = Vec::new();
|
|
176
|
+
for item in &module.items {
|
|
177
|
+
match item {
|
|
178
|
+
ast::Item::Fn(f) if f.type_param.is_none() && !f.is_test => items.push(CompletionItem {
|
|
179
|
+
label: f.name.clone(),
|
|
180
|
+
kind: Some(CompletionItemKind::FUNCTION),
|
|
181
|
+
detail: Some(format!("fun {importPath}.{}", f.name)),
|
|
182
|
+
..Default::default()
|
|
183
|
+
}),
|
|
184
|
+
ast::Item::Const(c) if !c.name.contains(ast::MEMBER_SEP) => items.push(CompletionItem {
|
|
185
|
+
label: c.name.clone(),
|
|
186
|
+
kind: Some(CompletionItemKind::CONSTANT),
|
|
187
|
+
detail: Some(format!("{importPath}.{}", c.name)),
|
|
188
|
+
..Default::default()
|
|
189
|
+
}),
|
|
190
|
+
_ => {}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
items
|
|
194
|
+
}
|
|
195
|
+
|
|
163
196
|
/// Constructor parameters (name, type text) of struct `owner`: its own fields.
|
|
164
197
|
/// Fields set by `init` are not constructor parameters. `own` is the unlowered
|
|
165
198
|
/// parse of the document (header intact); `merged` is the fallback.
|
compiler/lsp/src/main.rs
CHANGED
|
@@ -325,14 +325,24 @@ fn memberCompletionFor(
|
|
|
325
325
|
cursor_line: usize,
|
|
326
326
|
receiver: &str,
|
|
327
327
|
) -> Vec<CompletionItem> {
|
|
328
|
+
let tree = Backend::parseTree(text);
|
|
329
|
+
let own_source = plum_core::AstParser::new(text).parseSourceRaw(tree.root_node());
|
|
330
|
+
// `module.`: an imported module's functions and constants, unless a local has that name.
|
|
331
|
+
// Before the load: the placeholder member (`zzzzzz`) is not a function of the module.
|
|
332
|
+
let isModuleName = receiver.chars().next().is_some_and(|c| c.is_ascii_lowercase());
|
|
333
|
+
if let Some(imp) = own_source.imports.iter().filter(|_| isModuleName).find(|i| i.path.rsplit('/').next() == Some(receiver)) {
|
|
334
|
+
let isLocal = completion::enclosingFn(&own_source, cursor_line)
|
|
335
|
+
.is_some_and(|f| plum_core::modules::fnLocals(f).contains(receiver));
|
|
336
|
+
if !isLocal {
|
|
337
|
+
return completion::moduleItems(libPath, &imp.path);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
328
340
|
let canon = std::fs::canonicalize(path).unwrap_or(path.to_path_buf());
|
|
329
341
|
let mut overrides = HashMap::new();
|
|
330
342
|
overrides.insert(canon, text.to_string());
|
|
331
343
|
let Ok(merged) = plum_core::loadAndMergeWithOverridesDiag(path, libPath, &overrides) else {
|
|
332
344
|
return Vec::new();
|
|
333
345
|
};
|
|
334
|
-
let tree = Backend::parseTree(text);
|
|
335
|
-
let own_source = plum_core::AstParser::new(text).parseSourceRaw(tree.root_node());
|
|
336
346
|
// `Type.`: its associated consts and static functions.
|
|
337
347
|
if receiver
|
|
338
348
|
.chars()
|
|
@@ -690,6 +700,31 @@ mod tests {
|
|
|
690
700
|
let _ = std::fs::remove_dir_all(&dir);
|
|
691
701
|
}
|
|
692
702
|
|
|
703
|
+
/// `module.` offers the functions and constants of the imported module (source names), and a
|
|
704
|
+
/// local named like the module gets its own members instead.
|
|
705
|
+
#[test]
|
|
706
|
+
fn lsp_completes_a_module_and_shows_source_names() {
|
|
707
|
+
let dir = std::env::temp_dir().join(format!("plum-lsp-module-{}", std::process::id()));
|
|
708
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
709
|
+
let path = dir.join("t.plum");
|
|
710
|
+
let text = "import std/Int\nimport tests/modules/mathx\nimport tests/modules/shapes\n\nfun main() =\n x := mathx.zzzzzz\n";
|
|
711
|
+
std::fs::write(&path, text).unwrap();
|
|
712
|
+
let labels: Vec<String> = memberCompletionFor(&path, &repoRoot(), text, 6, "mathx")
|
|
713
|
+
.into_iter()
|
|
714
|
+
.map(|i| i.label)
|
|
715
|
+
.collect();
|
|
716
|
+
for want in ["clamp", "twice", "onlyHere", "Limit", "Double"] {
|
|
717
|
+
assert!(labels.iter().any(|l| l == want), "missing {want} in {labels:?}");
|
|
718
|
+
}
|
|
719
|
+
assert!(!labels.iter().any(|l| l.contains('@')), "{labels:?}");
|
|
720
|
+
let ok = "import std/Int\nimport tests/modules/mathx\nimport tests/modules/shapes\n\nfun clamp(x: Int) -> Int = x\n\nfun main() =\n _ := mathx.clamp(1)\n";
|
|
721
|
+
std::fs::write(&path, ok).unwrap();
|
|
722
|
+
let merged = plum_core::loadAndMergeDiag(&path, &repoRoot()).expect("loads");
|
|
723
|
+
let top: Vec<String> = completion::topLevelItems(&merged).into_iter().map(|i| i.label).collect();
|
|
724
|
+
assert!(top.iter().any(|l| l == "clamp") && !top.iter().any(|l| l.contains('@')), "{top:?}");
|
|
725
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
726
|
+
}
|
|
727
|
+
|
|
693
728
|
#[test]
|
|
694
729
|
fn keywordsCurrent() {
|
|
695
730
|
let labels: Vec<String> = completion::keywordItems()
|
compiler/tooling/tree-sitter-plum/grammar.js
CHANGED
|
@@ -77,7 +77,7 @@ module.exports = grammar({
|
|
|
77
77
|
seq(
|
|
78
78
|
optional($.module),
|
|
79
79
|
repeat($.import),
|
|
80
|
-
repeat(choice($.enum, $.struct, $.primitive, $.fn, $.test)),
|
|
80
|
+
repeat(choice($.enum, $.struct, $.primitive, $.fn, $.const, $.test)),
|
|
81
81
|
),
|
|
82
82
|
|
|
83
83
|
module: ($) => seq("module", $.mod_identifier),
|
compiler/tooling/tree-sitter-plum/queries/plum/format.scm
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
(source
|
|
13
13
|
(fn) @allow_blank_line_before)
|
|
14
14
|
|
|
15
|
+
(source
|
|
16
|
+
(const) @allow_blank_line_before)
|
|
17
|
+
|
|
15
18
|
(source
|
|
16
19
|
(enum) @allow_blank_line_before)
|
|
17
20
|
|
compiler/tooling/tree-sitter-plum/queries/plum/highlights.scm
CHANGED
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
object: (primary_expression (type_identifier))
|
|
13
13
|
member: (type_identifier) @constant)
|
|
14
14
|
|
|
15
|
+
; `module.Name`: a constant of an imported module.
|
|
16
|
+
(attribute
|
|
17
|
+
object: (primary_expression (var_identifier))
|
|
18
|
+
member: (type_identifier) @constant)
|
|
19
|
+
|
|
15
20
|
; (mod_identifier) @constant
|
|
16
21
|
(type_identifier) @type
|
|
17
22
|
(fn_identifier) @function
|
compiler/tooling/tree-sitter-plum/src/grammar.json
CHANGED
|
Binary file
|
compiler/tooling/tree-sitter-plum/src/node-types.json
CHANGED
|
Binary file
|
compiler/tooling/tree-sitter-plum/src/parser.c
CHANGED
|
Binary file
|
compiler/tooling/tree-sitter-plum/test/corpus/const.txt
CHANGED
|
@@ -185,3 +185,29 @@ fun main() =
|
|
|
185
185
|
(type_identifier)))
|
|
186
186
|
(primary_expression
|
|
187
187
|
(float)))))))))
|
|
188
|
+
|
|
189
|
+
================================================================================
|
|
190
|
+
module-level const
|
|
191
|
+
================================================================================
|
|
192
|
+
|
|
193
|
+
ArenaW = 800.0
|
|
194
|
+
|
|
195
|
+
fun f() -> Float =
|
|
196
|
+
ArenaW
|
|
197
|
+
|
|
198
|
+
--------------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
(source
|
|
201
|
+
(const
|
|
202
|
+
(type_identifier)
|
|
203
|
+
(expression
|
|
204
|
+
(primary_expression
|
|
205
|
+
(float))))
|
|
206
|
+
(fn
|
|
207
|
+
(fn_identifier)
|
|
208
|
+
(type
|
|
209
|
+
(type_identifier))
|
|
210
|
+
(body
|
|
211
|
+
(expression
|
|
212
|
+
(primary_expression
|
|
213
|
+
(type_identifier))))))
|
compiler/tooling/vscode-plum/syntaxes/plum.tmLanguage.json
CHANGED
|
@@ -102,8 +102,20 @@
|
|
|
102
102
|
}
|
|
103
103
|
},
|
|
104
104
|
{
|
|
105
|
+
"comment": "module constant access: module.Name",
|
|
106
|
+
"match": "\\b([a-z_][A-Za-z0-9_]*)\\.([A-Z][A-Za-z0-9]+)\\b",
|
|
107
|
+
"captures": {
|
|
108
|
+
"1": {
|
|
109
|
+
"name": "variable.other.plum"
|
|
110
|
+
},
|
|
111
|
+
"2": {
|
|
112
|
+
"name": "constant.other.plum"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
{
|
|
105
|
-
"comment": "constant declarations: Name = expr",
|
|
117
|
+
"comment": "constant declarations: Name = expr (module level or in a type body)",
|
|
106
|
-
"match": "^\\s
|
|
118
|
+
"match": "^\\s*([A-Z][A-Za-z0-9]+)(?=\\s*=(?!=))",
|
|
107
119
|
"captures": {
|
|
108
120
|
"1": {
|
|
109
121
|
"name": "constant.other.plum"
|
compiler/wasm-codegen/src/driver.rs
CHANGED
|
@@ -391,7 +391,8 @@ impl<'a> Setup<'a> {
|
|
|
391
391
|
}
|
|
392
392
|
// An extern METHOD (of the primitive `Io`) takes its receiver as the first
|
|
393
393
|
// wrapper param; the host import has no receiver.
|
|
394
|
+
// A module-level extern's `io: Io` is the capability too: not passed to the host.
|
|
394
|
-
let skip = usize::from(f.type_param.is_some() && f.has_self);
|
|
395
|
+
let skip = usize::from((f.type_param.is_some() && f.has_self) || (f.type_param.is_none() && ast::takesIoFirst(f)));
|
|
395
396
|
let key = if f.type_param.is_some() { fnKey(f) } else { f.name.clone() };
|
|
396
397
|
if f.type_param.is_none() {
|
|
397
398
|
known_fns.insert(f.name.clone());
|
tests/modules/constants.plum
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import std/Int
|
|
2
|
+
import std/Float
|
|
3
|
+
import std/Str
|
|
4
|
+
import std/List
|
|
5
|
+
|
|
6
|
+
# Module-level constants: any pure expression, read in dependency order.
|
|
7
|
+
|
|
8
|
+
Third = Second * 3
|
|
9
|
+
Second = First + 1
|
|
10
|
+
First = 4
|
|
11
|
+
ArenaW = 800.0
|
|
12
|
+
ArenaArea = ArenaW * ArenaH
|
|
13
|
+
ArenaH = 600.0
|
|
14
|
+
Greeting = "hello " + "there"
|
|
15
|
+
Boxes = [Second, Third]
|
|
16
|
+
|
|
17
|
+
test "constants read each other in dependency order"
|
|
18
|
+
assert First == 4
|
|
19
|
+
assert Second == 5
|
|
20
|
+
assert Third == 15
|
|
21
|
+
|
|
22
|
+
test "a float constant"
|
|
23
|
+
assert ArenaW == 800.0
|
|
24
|
+
assert ArenaArea == 480000.0
|
|
25
|
+
|
|
26
|
+
test "a string constant"
|
|
27
|
+
assert Greeting == "hello there"
|
|
28
|
+
|
|
29
|
+
test "a constant holding a list"
|
|
30
|
+
assert Boxes.length() == 2
|
tests/modules/host_extern.plum
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import std/Io
|
|
2
|
+
import std/Float
|
|
3
|
+
|
|
4
|
+
# A module-level `extern fun` takes `io: Io` first and binds to the host by name,
|
|
5
|
+
# the same route the `extern fun` methods of `Io` use.
|
|
6
|
+
extern fun rawNowMillis(io: Io) -> Float
|
|
7
|
+
|
|
8
|
+
fun nowIsLater(io: Io) -> Bool = rawNowMillis(io) > 1000.0
|
|
9
|
+
|
|
10
|
+
test "a module-level extern calls the host"
|
|
11
|
+
assert nowIsLater(io)
|
tests/modules/mathx.plum
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import std/Int
|
|
2
|
+
|
|
3
|
+
# A module other files import as `mathx`. Its `clamp` and `Limit` are also defined by `shapes`.
|
|
4
|
+
|
|
5
|
+
Limit = 10
|
|
6
|
+
Double = Limit * 2
|
|
7
|
+
|
|
8
|
+
fun clamp(x: Int) -> Int = if x > Limit then Limit else x
|
|
9
|
+
|
|
10
|
+
fun twice(x: Int) -> Int = clamp(x) * 2
|
|
11
|
+
|
|
12
|
+
fun onlyHere(x: Int) -> Int = x + 1
|
tests/modules/shapes.plum
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import std/Int
|
|
2
|
+
|
|
3
|
+
# The second module that defines `clamp` and `Limit`.
|
|
4
|
+
|
|
5
|
+
Limit = 100
|
|
6
|
+
Doubled = Limit + Limit
|
|
7
|
+
|
|
8
|
+
fun clamp(x: Int) -> Int = if x > Limit then Limit else x
|
|
9
|
+
|
|
10
|
+
fun twice(x: Int) -> Int = clamp(x) * 2
|
tests/modules/two_modules.plum
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import std/Int
|
|
2
|
+
import tests/modules/mathx
|
|
3
|
+
import tests/modules/shapes
|
|
4
|
+
|
|
5
|
+
# Two modules define `clamp`, `twice` and `Limit`: each is reached by its module name.
|
|
6
|
+
|
|
7
|
+
fun viaFunctionValue(f: fn(Int) -> Int, x: Int) -> Int = f(x)
|
|
8
|
+
|
|
9
|
+
test "the same function name in two modules"
|
|
10
|
+
assert mathx.clamp(50) == 10
|
|
11
|
+
assert shapes.clamp(50) == 50
|
|
12
|
+
assert shapes.clamp(500) == 100
|
|
13
|
+
|
|
14
|
+
test "a function calls its own module's colliding helper"
|
|
15
|
+
assert mathx.twice(50) == 20
|
|
16
|
+
assert shapes.twice(50) == 100
|
|
17
|
+
|
|
18
|
+
test "the same constant name in two modules"
|
|
19
|
+
assert mathx.Limit == 10
|
|
20
|
+
assert shapes.Limit == 100
|
|
21
|
+
|
|
22
|
+
test "a constant may read another constant of its module"
|
|
23
|
+
assert mathx.Double == 20
|
|
24
|
+
assert shapes.Doubled == 200
|
|
25
|
+
|
|
26
|
+
test "a name only one module defines is reached through its module"
|
|
27
|
+
assert mathx.onlyHere(1) == 2
|
|
28
|
+
|
|
29
|
+
test "a qualified function can be passed as a value after a call"
|
|
30
|
+
assert viaFunctionValue(|x: Int| mathx.clamp(x), 99) == 10
|
|
31
|
+
|
|
32
|
+
test "a local named like a module wins over the module"
|
|
33
|
+
mathx := |x: Int| x + 1000
|
|
34
|
+
assert mathx(1) == 1001
|