plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
29313cb
— Peter John
2026-09-05T13:28:55+05:30
refactor(stdlib): move libs/std to top-level plum-std, gate Int/Float/Str imports
- examples/basics.plum +3 -0
- examples/closures.plum +2 -0
- examples/control_flow.plum +1 -0
- examples/dop_visitor.plum +3 -1
- examples/functions.plum +1 -0
- examples/match.plum +2 -0
- examples/methods.plum +1 -0
- examples/oop_visitor.plum +3 -1
- examples/strings.plum +2 -0
- examples/testing.plum +1 -0
- examples/types.plum +3 -0
- plum-checker/tests/examples_test.rs +2 -2
- plum-cli/src/main.rs +5 -5
- plum-cli/tests/compile_tests.rs +6 -3
- plum-core/src/builtin_usage.rs +263 -0
- plum-core/src/lib.rs +1 -0
- plum-core/src/loader.rs +47 -2
- plum-core/tests/loader_test.rs +27 -20
- {libs/std → plum-std}/Array.plum +1 -0
- {libs/std/encoding → plum-std}/Base64.plum +1 -0
- {libs/std → plum-std}/Bool.plum +0 -0
- {libs/std → plum-std}/Buffer.plum +1 -0
- {libs/std → plum-std}/Byte.plum +1 -0
- {libs/std → plum-std}/ByteSlice.plum +1 -0
- {libs/std → plum-std}/Err.plum +2 -0
- {libs/std → plum-std}/Float.plum +0 -0
- libs/std/http.plum → plum-std/Http.plum +1 -0
- {libs/std → plum-std}/Int.plum +0 -0
- {libs/std → plum-std}/Json.plum +0 -0
- {libs/std → plum-std}/List.plum +1 -0
- {libs/std → plum-std}/Map.plum +2 -0
- {libs/std → plum-std}/Option.plum +1 -0
- libs/std/os.plum → plum-std/Os.plum +1 -0
- libs/std/regex.mi → plum-std/Regex.mi +0 -0
- {libs/std → plum-std}/Result.plum +1 -0
- {libs/std → plum-std}/Str.plum +1 -0
- libs/std/testing.plum → plum-std/Testing.plum +2 -0
- {libs/std → plum-std}/Time.plum +1 -0
- {libs/std → plum-std}/Uuid.plum +0 -0
- plum-wasm-codegen/src/lib.rs +1 -1
- plum-wasm-codegen/tests/examples_test.rs +5 -6
- scripts/test-plum.sh +8 -7
examples/basics.plum
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
module basics
|
|
2
2
|
import std/Bool
|
|
3
|
+
import std/Int
|
|
4
|
+
import std/Float
|
|
5
|
+
import std/Str
|
|
3
6
|
|
|
4
7
|
MAX_RETRIES = 3
|
|
5
8
|
GOLDEN_RATIO = 1.61803
|
examples/closures.plum
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import std/Str
|
|
2
2
|
import std/Bool
|
|
3
|
+
import std/Int
|
|
4
|
+
import std/Float
|
|
3
5
|
|
|
4
6
|
fun each(cb: fn(Int) -> Int) -> Int =
|
|
5
7
|
cb(5)
|
examples/control_flow.plum
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import std/Str
|
|
2
2
|
import std/Bool
|
|
3
|
+
import std/Int
|
|
3
4
|
|
|
4
5
|
fun loopSum(limit: Int) -> Int =
|
|
5
6
|
total := 0
|
examples/dop_visitor.plum
CHANGED
|
@@ -36,8 +36,10 @@
|
|
|
36
36
|
# `oop_visitor.plum`'s header), which tilts things further toward DOP
|
|
37
37
|
# for anything that needs to treat the shapes polymorphically.
|
|
38
38
|
|
|
39
|
-
import std/
|
|
39
|
+
import std/Os
|
|
40
40
|
import std/Bool
|
|
41
|
+
import std/Int
|
|
42
|
+
import std/Str
|
|
41
43
|
|
|
42
44
|
enum Rating =
|
|
43
45
|
| Good(name: Str)
|
examples/functions.plum
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import std/Str
|
|
2
2
|
import std/Bool
|
|
3
|
+
import std/Int
|
|
3
4
|
|
|
4
5
|
fun addInts(a: Int, b: Int) -> Int =
|
|
5
6
|
a + b
|
examples/match.plum
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import std/Option
|
|
2
2
|
import std/Bool
|
|
3
|
+
import std/Int
|
|
4
|
+
import std/Str
|
|
3
5
|
|
|
4
6
|
enum Color =
|
|
5
7
|
| Red
|
examples/methods.plum
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import std/Str
|
|
2
2
|
import std/Bool
|
|
3
|
+
import std/Int
|
|
3
4
|
|
|
4
5
|
type Cat =
|
|
5
6
|
name: Str
|
examples/oop_visitor.plum
CHANGED
|
@@ -29,8 +29,10 @@
|
|
|
29
29
|
# removes OOP's usual "call through one interface" benefit entirely,
|
|
30
30
|
# which is a real cost specific to this language.
|
|
31
31
|
|
|
32
|
-
import std/
|
|
32
|
+
import std/Os
|
|
33
33
|
import std/Bool
|
|
34
|
+
import std/Int
|
|
35
|
+
import std/Str
|
|
34
36
|
|
|
35
37
|
enum Rating =
|
|
36
38
|
| Good(name: Str)
|
examples/strings.plum
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import std/Bool
|
|
2
|
+
import std/Int
|
|
3
|
+
import std/Str
|
|
2
4
|
|
|
3
5
|
fun greet(name: Str) -> Str =
|
|
4
6
|
"Hello, {name}!"
|
examples/testing.plum
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import std/Str
|
|
2
2
|
import std/Bool
|
|
3
|
+
import std/Int
|
|
3
4
|
|
|
4
5
|
fun add(a: Int, b: Int) -> Int =
|
|
5
6
|
a + b
|
examples/types.plum
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import std/Option
|
|
2
2
|
import std/Bool
|
|
3
|
+
import std/Int
|
|
4
|
+
import std/Float
|
|
5
|
+
import std/Str
|
|
3
6
|
|
|
4
7
|
type Point =
|
|
5
8
|
x: Int
|
plum-checker/tests/examples_test.rs
CHANGED
|
@@ -6,7 +6,7 @@ fn examplesDir() -> std::path::PathBuf {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
fn libPath() -> std::path::PathBuf {
|
|
9
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..
|
|
9
|
+
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
fn exampleFiles() -> Vec<std::path::PathBuf> {
|
|
@@ -21,7 +21,7 @@ fn exampleFiles() -> Vec<std::path::PathBuf> {
|
|
|
21
21
|
files
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/// Every example must load (resolving its own `import`s against
|
|
24
|
+
/// Every example must load (resolving its own `import`s against the repo root,
|
|
25
25
|
/// the real compilation path — several examples now genuinely `import` real
|
|
26
26
|
/// stdlib types like `std/Bool`/`std/Str`, so a bare single-file parse would
|
|
27
27
|
/// leave those names undeclared) and pass `checkSource`. These files exist
|
plum-cli/src/main.rs
CHANGED
|
@@ -42,7 +42,7 @@ enum Command {
|
|
|
42
42
|
#[arg(short, long)]
|
|
43
43
|
output: Option<std::path::PathBuf>,
|
|
44
44
|
/// Root directory `import <path>` is resolved against (`import std/x` -> `<lib-path>/std/x.plum`)
|
|
45
|
-
#[arg(long, default_value = "
|
|
45
|
+
#[arg(long, default_value = ".")]
|
|
46
46
|
lib_path: std::path::PathBuf,
|
|
47
47
|
},
|
|
48
48
|
/// Compile a Plum source file to WASM and immediately run it under wasmtime
|
|
@@ -50,7 +50,7 @@ enum Command {
|
|
|
50
50
|
/// Source file to compile and run
|
|
51
51
|
file: std::path::PathBuf,
|
|
52
52
|
/// Root directory `import <path>` is resolved against (`import std/x` -> `<lib-path>/std/x.plum`)
|
|
53
|
-
#[arg(long, default_value = "
|
|
53
|
+
#[arg(long, default_value = ".")]
|
|
54
54
|
lib_path: std::path::PathBuf,
|
|
55
55
|
},
|
|
56
56
|
/// Compile a Plum source file to WASM and embed it into a wasmtime-backed
|
|
@@ -62,7 +62,7 @@ enum Command {
|
|
|
62
62
|
#[arg(short, long)]
|
|
63
63
|
output: Option<std::path::PathBuf>,
|
|
64
64
|
/// Root directory `import <path>` is resolved against (`import std/x` -> `<lib-path>/std/x.plum`)
|
|
65
|
-
#[arg(long, default_value = "
|
|
65
|
+
#[arg(long, default_value = ".")]
|
|
66
66
|
lib_path: std::path::PathBuf,
|
|
67
67
|
},
|
|
68
68
|
/// Compile a Plum source file's `test` blocks and run them under wasmtime
|
|
@@ -70,7 +70,7 @@ enum Command {
|
|
|
70
70
|
/// Source file to compile and test
|
|
71
71
|
file: std::path::PathBuf,
|
|
72
72
|
/// Root directory `import <path>` is resolved against (`import std/x` -> `<lib-path>/std/x.plum`)
|
|
73
|
-
#[arg(long, default_value = "
|
|
73
|
+
#[arg(long, default_value = ".")]
|
|
74
74
|
lib_path: std::path::PathBuf,
|
|
75
75
|
},
|
|
76
76
|
/// Install Plum syntax highlighting into an editor's configuration
|
|
@@ -408,7 +408,7 @@ fn hostImports(store: &mut wasmtime::Store<()>, module: &wasmtime::Module) -> Re
|
|
|
408
408
|
}
|
|
409
409
|
|
|
410
410
|
/// Performs one blocking HTTP round trip for `plum::rawHttpRequest` and packs
|
|
411
|
-
/// the outcome into the wire format `
|
|
411
|
+
/// the outcome into the wire format `plum-std/Http.plum`'s `request` expects
|
|
412
412
|
/// (see that extern's doc comment): `"1\x01<status>\x01<headers>\x00<body>"`
|
|
413
413
|
/// on success, `"0\x01\x01<message>\x00"` for a transport-level failure. A
|
|
414
414
|
/// non-2xx HTTP status (404, 500, ...) is NOT a failure here — it's still a
|
plum-cli/tests/compile_tests.rs
CHANGED
|
@@ -25,7 +25,10 @@ fn freshTempDir(name: &str) -> std::path::PathBuf {
|
|
|
25
25
|
fn compileSimpleAddProducesWasm() {
|
|
26
26
|
let dir = freshTempDir("simple_add");
|
|
27
27
|
let src_path = dir.join("simple_add.plum");
|
|
28
|
+
// `Unit` (unlike `Int`/`Float`/`Str`) needs no `import std/...` — see
|
|
29
|
+
// `plum_core::loader::checkBuiltinImports` — which keeps this fixture
|
|
30
|
+
// free of the repo's real stdlib.
|
|
28
|
-
std::fs::write(&src_path, "fun add(a:
|
|
31
|
+
std::fs::write(&src_path, "fun add(a: Unit, b: Unit) -> Unit =\n a\n").unwrap();
|
|
29
32
|
let out_path = dir.join("simple_add.wasm");
|
|
30
33
|
let status = Command::new(plumBin())
|
|
31
34
|
.args(["compile", src_path.to_str().unwrap(), "-o", out_path.to_str().unwrap()])
|
|
@@ -39,9 +42,9 @@ fn compileSimpleAddProducesWasm() {
|
|
|
39
42
|
#[test]
|
|
40
43
|
fn compileWithImportResolvesViaLibPath() {
|
|
41
44
|
let dir = freshTempDir("import_fixtures");
|
|
42
|
-
std::fs::write(dir.join("helper.plum"), "fun helperValue() ->
|
|
45
|
+
std::fs::write(dir.join("helper.plum"), "fun helperValue(u: Unit) -> Unit =\n u\n").unwrap();
|
|
43
46
|
let src_path = dir.join("main.plum");
|
|
44
|
-
std::fs::write(&src_path, "module fixtures\n\nimport helper\n\nfun main() ->
|
|
47
|
+
std::fs::write(&src_path, "module fixtures\n\nimport helper\n\nfun main(u: Unit) -> Unit =\n helperValue(u)\n").unwrap();
|
|
45
48
|
let out_path = dir.join("main.wasm");
|
|
46
49
|
let status = Command::new(plumBin())
|
|
47
50
|
.args([
|
plum-core/src/builtin_usage.rs
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
use std::collections::HashSet;
|
|
2
|
+
|
|
3
|
+
use crate::ast::{
|
|
4
|
+
Arg, AssignTarget, AttrKind, CasePattern, Expr, FnBody, Item, ParamType, Source, Stmt,
|
|
5
|
+
StringPart, Type,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/// The builtin type names this walk tracks: primitive value types with no
|
|
9
|
+
/// enum-variant/name-resolution step that naturally fails when unimported
|
|
10
|
+
/// (unlike `Bool`, whose `True`/`False` variants simply don't resolve without
|
|
11
|
+
/// `import std/Bool`) — so nothing else catches a missing `import std/Int` /
|
|
12
|
+
/// `import std/Float` / `import std/Str`.
|
|
13
|
+
const TRACKED_TYPES: [&str; 3] = ["Int", "Float", "Str"];
|
|
14
|
+
|
|
15
|
+
fn isTracked(name: &str) -> bool {
|
|
16
|
+
TRACKED_TYPES.contains(&name)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// Every builtin type name (`Int`, `Float`, `Str` — literal or type-annotation)
|
|
20
|
+
/// mentioned anywhere in `source` — used by `loader::loadAndMerge` to enforce
|
|
21
|
+
/// that a file using one of them explicitly `import`s it, exactly like every
|
|
22
|
+
/// other stdlib type (`Bool`, ...) already must be.
|
|
23
|
+
pub fn usedBuiltinTypeNames(source: &Source) -> HashSet<String> {
|
|
24
|
+
let mut names = HashSet::new();
|
|
25
|
+
for item in &source.items {
|
|
26
|
+
match item {
|
|
27
|
+
Item::Class(c) => {
|
|
28
|
+
for f in &c.fields {
|
|
29
|
+
walkType(&f.ty, &mut names);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
Item::Trait(t) => {
|
|
33
|
+
for m in &t.methods {
|
|
34
|
+
for p in &m.params {
|
|
35
|
+
walkParamType(&p.ty, &mut names);
|
|
36
|
+
}
|
|
37
|
+
if let Some(r) = &m.returns {
|
|
38
|
+
walkType(r, &mut names);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
Item::Enum(e) => {
|
|
43
|
+
for p in &e.params {
|
|
44
|
+
walkType(&p.ty, &mut names);
|
|
45
|
+
}
|
|
46
|
+
for v in &e.variants {
|
|
47
|
+
for f in &v.fields {
|
|
48
|
+
walkType(f, &mut names);
|
|
49
|
+
}
|
|
50
|
+
for val in &v.values {
|
|
51
|
+
walkExpr(val, &mut names);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
Item::Fn(f) => {
|
|
56
|
+
for p in &f.params {
|
|
57
|
+
walkParamType(&p.ty, &mut names);
|
|
58
|
+
if let Some(d) = &p.default {
|
|
59
|
+
walkExpr(d, &mut names);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if let Some(r) = &f.returns {
|
|
63
|
+
walkType(r, &mut names);
|
|
64
|
+
}
|
|
65
|
+
match &f.body {
|
|
66
|
+
FnBody::Expr(e) => walkExpr(e, &mut names),
|
|
67
|
+
FnBody::Block(b) => walkStmts(&b.stmts, &mut names),
|
|
68
|
+
FnBody::Extern => {}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
Item::Const(c) => walkExpr(&c.value, &mut names),
|
|
72
|
+
Item::Test(t) => walkStmts(&t.body.stmts, &mut names),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
names
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fn walkType(ty: &Type, names: &mut HashSet<String>) {
|
|
79
|
+
if isTracked(&ty.name) {
|
|
80
|
+
names.insert(ty.name.clone());
|
|
81
|
+
}
|
|
82
|
+
for g in &ty.generics {
|
|
83
|
+
walkType(g, names);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fn walkParamType(pt: &ParamType, names: &mut HashSet<String>) {
|
|
88
|
+
match pt {
|
|
89
|
+
ParamType::Type(t) => walkType(t, names),
|
|
90
|
+
ParamType::Variadic(t) => walkType(t, names),
|
|
91
|
+
ParamType::Fn(params, ret) => {
|
|
92
|
+
for p in params {
|
|
93
|
+
walkType(p, names);
|
|
94
|
+
}
|
|
95
|
+
if let Some(r) = ret {
|
|
96
|
+
walkType(r, names);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fn walkStmts(stmts: &[Stmt], names: &mut HashSet<String>) {
|
|
103
|
+
for s in stmts {
|
|
104
|
+
walkStmt(s, names);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
fn walkStmt(stmt: &Stmt, names: &mut HashSet<String>) {
|
|
109
|
+
match stmt {
|
|
110
|
+
Stmt::Assign(a) => {
|
|
111
|
+
for t in &a.targets {
|
|
112
|
+
if let AssignTarget::Field(obj, _) = t {
|
|
113
|
+
walkExpr(obj, names);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for v in &a.values {
|
|
117
|
+
walkExpr(v, names);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
Stmt::Break | Stmt::Continue | Stmt::Todo => {}
|
|
121
|
+
Stmt::Assert(check) => walkExpr(&check.cond, names),
|
|
122
|
+
Stmt::For(f) => {
|
|
123
|
+
walkExpr(&f.iter, names);
|
|
124
|
+
walkStmts(&f.body.stmts, names);
|
|
125
|
+
}
|
|
126
|
+
Stmt::While(w) => {
|
|
127
|
+
walkExpr(&w.condition, names);
|
|
128
|
+
walkStmts(&w.body.stmts, names);
|
|
129
|
+
}
|
|
130
|
+
Stmt::If(i) => {
|
|
131
|
+
walkExpr(&i.condition, names);
|
|
132
|
+
walkStmts(&i.body.stmts, names);
|
|
133
|
+
for ei in &i.else_ifs {
|
|
134
|
+
walkExpr(&ei.condition, names);
|
|
135
|
+
walkStmts(&ei.body.stmts, names);
|
|
136
|
+
}
|
|
137
|
+
if let Some(e) = &i.else_ {
|
|
138
|
+
walkStmts(&e.stmts, names);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
Stmt::Match(m) => {
|
|
142
|
+
for s in &m.subjects {
|
|
143
|
+
walkExpr(s, names);
|
|
144
|
+
}
|
|
145
|
+
for c in &m.cases {
|
|
146
|
+
for p in &c.patterns {
|
|
147
|
+
walkCasePattern(p, names);
|
|
148
|
+
}
|
|
149
|
+
if let Some(g) = &c.guard {
|
|
150
|
+
walkExpr(g, names);
|
|
151
|
+
}
|
|
152
|
+
walkStmts(&c.body.stmts, names);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
Stmt::Return(e) => {
|
|
156
|
+
if let Some(e) = e {
|
|
157
|
+
walkExpr(e, names);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
Stmt::Expr(e) => walkExpr(e, names),
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
fn walkCasePattern(pat: &CasePattern, names: &mut HashSet<String>) {
|
|
165
|
+
match pat {
|
|
166
|
+
CasePattern::Class { fields, .. } => {
|
|
167
|
+
for f in fields {
|
|
168
|
+
walkCasePattern(f, names);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
CasePattern::Int(_) => {
|
|
172
|
+
names.insert("Int".to_string());
|
|
173
|
+
}
|
|
174
|
+
CasePattern::Float(_) => {
|
|
175
|
+
names.insert("Float".to_string());
|
|
176
|
+
}
|
|
177
|
+
CasePattern::String(_) => {
|
|
178
|
+
names.insert("Str".to_string());
|
|
179
|
+
}
|
|
180
|
+
CasePattern::Name(_) | CasePattern::Wildcard => {}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
fn walkExpr(expr: &Expr, names: &mut HashSet<String>) {
|
|
185
|
+
match expr {
|
|
186
|
+
Expr::Binary(b) => {
|
|
187
|
+
walkExpr(&b.left, names);
|
|
188
|
+
walkExpr(&b.right, names);
|
|
189
|
+
}
|
|
190
|
+
Expr::Unary(u) => walkExpr(&u.operand, names),
|
|
191
|
+
Expr::Bool(b) => {
|
|
192
|
+
walkExpr(&b.left, names);
|
|
193
|
+
walkExpr(&b.right, names);
|
|
194
|
+
}
|
|
195
|
+
Expr::Not(e) => walkExpr(e, names),
|
|
196
|
+
Expr::Compare(c) => {
|
|
197
|
+
walkExpr(&c.left, names);
|
|
198
|
+
walkExpr(&c.right, names);
|
|
199
|
+
}
|
|
200
|
+
Expr::Ternary(t) => {
|
|
201
|
+
walkExpr(&t.condition, names);
|
|
202
|
+
walkExpr(&t.then, names);
|
|
203
|
+
walkExpr(&t.else_, names);
|
|
204
|
+
}
|
|
205
|
+
Expr::FnCall(call) => {
|
|
206
|
+
if isTracked(&call.name) {
|
|
207
|
+
names.insert(call.name.clone());
|
|
208
|
+
}
|
|
209
|
+
for a in &call.args {
|
|
210
|
+
walkArg(a, names);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
Expr::ClassCall(c) => {
|
|
214
|
+
if isTracked(&c.type_name) {
|
|
215
|
+
names.insert(c.type_name.clone());
|
|
216
|
+
}
|
|
217
|
+
for g in &c.generics {
|
|
218
|
+
walkType(g, names);
|
|
219
|
+
}
|
|
220
|
+
for f in &c.fields {
|
|
221
|
+
walkExpr(&f.value, names);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
Expr::Attribute(a) => {
|
|
225
|
+
walkExpr(&a.object, names);
|
|
226
|
+
if let AttrKind::Method(call) = &a.attr {
|
|
227
|
+
for arg in &call.args {
|
|
228
|
+
walkArg(arg, names);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
Expr::Paren(e) => walkExpr(e, names),
|
|
233
|
+
Expr::String(s) => {
|
|
234
|
+
names.insert("Str".to_string());
|
|
235
|
+
for part in &s.parts {
|
|
236
|
+
if let StringPart::Interp(e) = part {
|
|
237
|
+
walkExpr(e, names);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
Expr::Int(_) => {
|
|
242
|
+
names.insert("Int".to_string());
|
|
243
|
+
}
|
|
244
|
+
Expr::Float(_) => {
|
|
245
|
+
names.insert("Float".to_string());
|
|
246
|
+
}
|
|
247
|
+
Expr::Self_ | Expr::Var(_) => {}
|
|
248
|
+
Expr::TypeName(name) => {
|
|
249
|
+
if isTracked(name) {
|
|
250
|
+
names.insert(name.clone());
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
Expr::Closure(c) => walkStmts(&c.body.stmts, names),
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
fn walkArg(arg: &Arg, names: &mut HashSet<String>) {
|
|
258
|
+
match arg {
|
|
259
|
+
Arg::Positional(e) => walkExpr(e, names),
|
|
260
|
+
Arg::Keyword { value, .. } => walkExpr(value, names),
|
|
261
|
+
Arg::Pair { value, .. } => walkExpr(value, names),
|
|
262
|
+
}
|
|
263
|
+
}
|
plum-core/src/lib.rs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
#![allow(non_snake_case)]
|
|
4
4
|
|
|
5
5
|
pub mod ast;
|
|
6
|
+
pub mod builtin_usage;
|
|
6
7
|
pub mod parser;
|
|
7
8
|
pub mod formatter;
|
|
8
9
|
pub mod loader;
|
plum-core/src/loader.rs
CHANGED
|
@@ -2,6 +2,7 @@ use std::collections::{HashMap, HashSet};
|
|
|
2
2
|
use std::path::{Path, PathBuf};
|
|
3
3
|
|
|
4
4
|
use crate::ast::{Import, Item, Source};
|
|
5
|
+
use crate::builtin_usage::usedBuiltinTypeNames;
|
|
5
6
|
use crate::parser::AstParser;
|
|
6
7
|
|
|
7
8
|
/// Resolves `entry`'s `import` statements (transitively, and tolerating
|
|
@@ -11,11 +12,14 @@ use crate::parser::AstParser;
|
|
|
11
12
|
/// fields are not otherwise used once their `items` have been merged in) —
|
|
12
13
|
/// only `items` accumulates across files.
|
|
13
14
|
///
|
|
14
|
-
/// `import "std/option"` resolves to `<lib_path>/std/option.plum`
|
|
15
|
+
/// `import "std/option"` resolves to `<lib_path>/plum-std/option.plum` — the
|
|
16
|
+
/// on-disk stdlib directory is `plum-std`, but source files keep writing
|
|
17
|
+
/// `std/...` imports, so the `std/` prefix is remapped here.
|
|
15
18
|
pub fn loadAndMerge(entry: &Path, lib_path: &Path) -> Result<Source, String> {
|
|
16
19
|
let entry_canon = std::fs::canonicalize(entry)
|
|
17
20
|
.map_err(|e| format!("cannot read entry file '{}': {}", entry.display(), e))?;
|
|
18
21
|
let entry_source = parseFile(&entry_canon)?;
|
|
22
|
+
checkBuiltinImports(&entry_canon, &entry_source)?;
|
|
19
23
|
|
|
20
24
|
let mut visited: HashSet<PathBuf> = HashSet::new();
|
|
21
25
|
let mut items: Vec<Item> = Vec::new();
|
|
@@ -42,7 +46,11 @@ fn loadImport(
|
|
|
42
46
|
items: &mut Vec<Item>,
|
|
43
47
|
names: &mut HashMap<String, PathBuf>,
|
|
44
48
|
) -> Result<(), String> {
|
|
49
|
+
let resolved_path = match import.path.strip_prefix("std/") {
|
|
50
|
+
Some(rest) => format!("plum-std/{}", rest),
|
|
51
|
+
None => import.path.clone(),
|
|
52
|
+
};
|
|
45
|
-
let target = lib_path.join(format!("{}.plum",
|
|
53
|
+
let target = lib_path.join(format!("{}.plum", resolved_path));
|
|
46
54
|
let target_canon = std::fs::canonicalize(&target)
|
|
47
55
|
.map_err(|_| format!("import '{}': no such file '{}'", import.path, target.display()))?;
|
|
48
56
|
|
|
@@ -53,6 +61,7 @@ fn loadImport(
|
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
let source = parseFile(&target_canon)?;
|
|
64
|
+
checkBuiltinImports(&target_canon, &source)?;
|
|
56
65
|
mergeItems(&target_canon, &source, items, names)?;
|
|
57
66
|
for nested in &source.imports {
|
|
58
67
|
loadImport(nested, lib_path, visited, items, names)?;
|
|
@@ -60,6 +69,42 @@ fn loadImport(
|
|
|
60
69
|
Ok(())
|
|
61
70
|
}
|
|
62
71
|
|
|
72
|
+
/// `Int`/`Float`/`Str` are primitive value types baked directly into the type
|
|
73
|
+
/// system (unlike `Bool`, which is an ordinary enum whose `True`/`False`
|
|
74
|
+
/// variants simply fail to resolve when `std/Bool` isn't imported), so
|
|
75
|
+
/// nothing naturally stops a file from using an `Int`/`Float` literal, a
|
|
76
|
+
/// bare string literal, or a matching type annotation without ever importing
|
|
77
|
+
/// them. This walks `source` for any such use and requires the matching
|
|
78
|
+
/// `import std/Int` / `import std/Float` / `import std/Str` — except in
|
|
79
|
+
/// `Int.plum`/`Float.plum`/`Str.plum` themselves, which declare the type
|
|
80
|
+
/// rather than import it.
|
|
81
|
+
fn checkBuiltinImports(path: &Path, source: &Source) -> Result<(), String> {
|
|
82
|
+
let used = usedBuiltinTypeNames(source);
|
|
83
|
+
for name in ["Int", "Float", "Str"] {
|
|
84
|
+
if !used.contains(name) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
let declares_self = source
|
|
88
|
+
.items
|
|
89
|
+
.iter()
|
|
90
|
+
.any(|i| matches!(i, Item::Class(c) if c.name == name));
|
|
91
|
+
if declares_self {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
let import_path = format!("std/{}", name);
|
|
95
|
+
let already_imported = source.imports.iter().any(|imp| imp.path == import_path);
|
|
96
|
+
if !already_imported {
|
|
97
|
+
return Err(format!(
|
|
98
|
+
"'{}' uses type '{}' but doesn't import it: add `import {}`",
|
|
99
|
+
path.display(),
|
|
100
|
+
name,
|
|
101
|
+
import_path
|
|
102
|
+
));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
Ok(())
|
|
106
|
+
}
|
|
107
|
+
|
|
63
108
|
fn parseFile(path: &Path) -> Result<Source, String> {
|
|
64
109
|
let text = std::fs::read_to_string(path)
|
|
65
110
|
.map_err(|e| format!("cannot read '{}': {}", path.display(), e))?;
|
plum-core/tests/loader_test.rs
CHANGED
|
@@ -6,6 +6,13 @@ use tempfile::TempDir;
|
|
|
6
6
|
/// Writes `name` (without a directory prefix — always placed directly under
|
|
7
7
|
/// the temp dir's `lib_path` subdirectory) with `contents`, creating parent
|
|
8
8
|
/// directories as needed (so `"std/option"` works).
|
|
9
|
+
///
|
|
10
|
+
/// Fixture bodies deliberately use `Unit` (not `Int`/`Float`/`Str`) params and
|
|
11
|
+
/// return types — `plum_core::loader::checkBuiltinImports` requires any real
|
|
12
|
+
/// `Int`/`Float`/`Str` use to `import std/Int`/`import std/Float`/`import
|
|
13
|
+
/// std/Str`, which these synthetic, stdlib-free temp dirs don't have; `Unit`
|
|
14
|
+
/// has no such requirement, so it keeps these fixtures focused on
|
|
15
|
+
/// loader/import-graph mechanics rather than the builtin-import policy.
|
|
9
16
|
fn writeLibFile(lib_path: &std::path::Path, name: &str, contents: &str) -> std::path::PathBuf {
|
|
10
17
|
let path = lib_path.join(format!("{}.plum", name));
|
|
11
18
|
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
|
@@ -17,8 +24,8 @@ fn writeLibFile(lib_path: &std::path::Path, name: &str, contents: &str) -> std::
|
|
|
17
24
|
fn importSeesImportedDeclarations() {
|
|
18
25
|
let dir = TempDir::new().unwrap();
|
|
19
26
|
let lib_path = dir.path().join("libs");
|
|
20
|
-
writeLibFile(&lib_path, "helper", "module fixtures\n\nfun helperFn() ->
|
|
27
|
+
writeLibFile(&lib_path, "helper", "module fixtures\n\nfun helperFn(u: Unit) -> Unit =\n u\n");
|
|
21
|
-
let entry = writeLibFile(&lib_path, "main", "module fixtures\n\nimport helper\n\nfun main() ->
|
|
28
|
+
let entry = writeLibFile(&lib_path, "main", "module fixtures\n\nimport helper\n\nfun main(u: Unit) -> Unit =\n helperFn(u)\n");
|
|
22
29
|
|
|
23
30
|
let merged = loadAndMerge(&entry, &lib_path).expect("loadAndMerge failed");
|
|
24
31
|
let names: Vec<&str> = merged.items.iter().filter_map(|i| match i {
|
|
@@ -33,9 +40,9 @@ fn importSeesImportedDeclarations() {
|
|
|
33
40
|
fn transitiveImportSurfacesGrandchildDeclarations() {
|
|
34
41
|
let dir = TempDir::new().unwrap();
|
|
35
42
|
let lib_path = dir.path().join("libs");
|
|
36
|
-
writeLibFile(&lib_path, "c", "module fixtures\n\nfun cFn() ->
|
|
43
|
+
writeLibFile(&lib_path, "c", "module fixtures\n\nfun cFn(u: Unit) -> Unit =\n u\n");
|
|
37
|
-
writeLibFile(&lib_path, "b", "module fixtures\n\nimport c\n\nfun bFn() ->
|
|
44
|
+
writeLibFile(&lib_path, "b", "module fixtures\n\nimport c\n\nfun bFn(u: Unit) -> Unit =\n cFn(u)\n");
|
|
38
|
-
let entry = writeLibFile(&lib_path, "a", "module fixtures\n\nimport b\n\nfun main() ->
|
|
45
|
+
let entry = writeLibFile(&lib_path, "a", "module fixtures\n\nimport b\n\nfun main(u: Unit) -> Unit =\n bFn(u)\n");
|
|
39
46
|
|
|
40
47
|
let merged = loadAndMerge(&entry, &lib_path).expect("loadAndMerge failed");
|
|
41
48
|
let names: Vec<&str> = merged.items.iter().filter_map(|i| match i {
|
|
@@ -49,10 +56,10 @@ fn transitiveImportSurfacesGrandchildDeclarations() {
|
|
|
49
56
|
fn diamondImportIncludesSharedDependencyOnce() {
|
|
50
57
|
let dir = TempDir::new().unwrap();
|
|
51
58
|
let lib_path = dir.path().join("libs");
|
|
52
|
-
writeLibFile(&lib_path, "d", "module fixtures\n\nfun dFn() ->
|
|
59
|
+
writeLibFile(&lib_path, "d", "module fixtures\n\nfun dFn(u: Unit) -> Unit =\n u\n");
|
|
53
|
-
writeLibFile(&lib_path, "b", "module fixtures\n\nimport d\n\nfun bFn() ->
|
|
60
|
+
writeLibFile(&lib_path, "b", "module fixtures\n\nimport d\n\nfun bFn(u: Unit) -> Unit =\n dFn(u)\n");
|
|
54
|
-
writeLibFile(&lib_path, "c", "module fixtures\n\nimport d\n\nfun cFn() ->
|
|
61
|
+
writeLibFile(&lib_path, "c", "module fixtures\n\nimport d\n\nfun cFn(u: Unit) -> Unit =\n dFn(u)\n");
|
|
55
|
-
let entry = writeLibFile(&lib_path, "a", "module fixtures\n\nimport b\nimport c\n\nfun main() ->
|
|
62
|
+
let entry = writeLibFile(&lib_path, "a", "module fixtures\n\nimport b\nimport c\n\nfun main(u: Unit) -> Unit =\n bFn(cFn(u))\n");
|
|
56
63
|
|
|
57
64
|
let merged = loadAndMerge(&entry, &lib_path).expect("loadAndMerge failed");
|
|
58
65
|
let d_count = merged.items.iter().filter(|i| matches!(i, plum_core::ast::Item::Fn(f) if f.name == "dFn")).count();
|
|
@@ -63,8 +70,8 @@ fn diamondImportIncludesSharedDependencyOnce() {
|
|
|
63
70
|
fn importCycleResolvesWithoutHangingOrErroring() {
|
|
64
71
|
let dir = TempDir::new().unwrap();
|
|
65
72
|
let lib_path = dir.path().join("libs");
|
|
66
|
-
writeLibFile(&lib_path, "b", "module fixtures\n\nimport a\n\nfun bFn() ->
|
|
73
|
+
writeLibFile(&lib_path, "b", "module fixtures\n\nimport a\n\nfun bFn(u: Unit) -> Unit =\n u\n");
|
|
67
|
-
let entry = writeLibFile(&lib_path, "a", "module fixtures\n\nimport b\n\nfun main() ->
|
|
74
|
+
let entry = writeLibFile(&lib_path, "a", "module fixtures\n\nimport b\n\nfun main(u: Unit) -> Unit =\n bFn(u)\n");
|
|
68
75
|
|
|
69
76
|
let merged = loadAndMerge(&entry, &lib_path).expect("import cycle should resolve, not error");
|
|
70
77
|
let names: Vec<&str> = merged.items.iter().filter_map(|i| match i {
|
|
@@ -79,7 +86,7 @@ fn importCycleResolvesWithoutHangingOrErroring() {
|
|
|
79
86
|
fn unresolvableImportIsAClearError() {
|
|
80
87
|
let dir = TempDir::new().unwrap();
|
|
81
88
|
let lib_path = dir.path().join("libs");
|
|
82
|
-
let entry = writeLibFile(&lib_path, "main", "module fixtures\n\nimport does_not_exist\n\nfun main() ->
|
|
89
|
+
let entry = writeLibFile(&lib_path, "main", "module fixtures\n\nimport does_not_exist\n\nfun main(u: Unit) -> Unit =\n u\n");
|
|
83
90
|
|
|
84
91
|
let result = loadAndMerge(&entry, &lib_path);
|
|
85
92
|
assert!(result.is_err());
|
|
@@ -91,8 +98,8 @@ fn unresolvableImportIsAClearError() {
|
|
|
91
98
|
fn duplicateTopLevelNameAcrossFilesIsAClearError() {
|
|
92
99
|
let dir = TempDir::new().unwrap();
|
|
93
100
|
let lib_path = dir.path().join("libs");
|
|
94
|
-
writeLibFile(&lib_path, "helper", "module fixtures\n\nfun sharedFn() ->
|
|
101
|
+
writeLibFile(&lib_path, "helper", "module fixtures\n\nfun sharedFn(u: Unit) -> Unit =\n u\n");
|
|
95
|
-
let entry = writeLibFile(&lib_path, "main", "module fixtures\n\nimport helper\n\nfun sharedFn() ->
|
|
102
|
+
let entry = writeLibFile(&lib_path, "main", "module fixtures\n\nimport helper\n\nfun sharedFn(u: Unit) -> Unit =\n u\n");
|
|
96
103
|
|
|
97
104
|
let result = loadAndMerge(&entry, &lib_path);
|
|
98
105
|
assert!(result.is_err());
|
|
@@ -108,9 +115,9 @@ fn sameMethodNameOnDifferentReceiversAcrossFilesIsNotACollision() {
|
|
|
108
115
|
module fixtures
|
|
109
116
|
|
|
110
117
|
type Cat =
|
|
111
|
-
age:
|
|
118
|
+
age: Unit
|
|
112
119
|
|
|
113
|
-
fun length(self) ->
|
|
120
|
+
fun length(self) -> Unit =
|
|
114
121
|
self.age
|
|
115
122
|
");
|
|
116
123
|
let entry = writeLibFile(&lib_path, "main", "\
|
|
@@ -119,13 +126,13 @@ module fixtures
|
|
|
119
126
|
import cat
|
|
120
127
|
|
|
121
128
|
type Box =
|
|
122
|
-
items:
|
|
129
|
+
items: Unit
|
|
123
130
|
|
|
124
|
-
fun length(self) ->
|
|
131
|
+
fun length(self) -> Unit =
|
|
125
132
|
self.items
|
|
126
133
|
|
|
127
|
-
fun main() ->
|
|
134
|
+
fun main(u: Unit) -> Unit =
|
|
128
|
-
|
|
135
|
+
u
|
|
129
136
|
");
|
|
130
137
|
|
|
131
138
|
let merged = loadAndMerge(&entry, &lib_path).expect("same method name on different receivers should not collide");
|
{libs/std → plum-std}/Array.plum
RENAMED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
module std
|
|
2
2
|
import std/Bool
|
|
3
|
+
import std/Int
|
|
3
4
|
|
|
4
5
|
# A fixed-length, O(1)-indexable array of `T`, backed directly by a wasm-gc
|
|
5
6
|
# `array<anyref>` — every `Array[T]` specialization (`Array$Int`, `Array$Str`,
|
{libs/std/encoding → plum-std}/Base64.plum
RENAMED
|
@@ -3,6 +3,7 @@ import std/Str
|
|
|
3
3
|
import std/Buffer
|
|
4
4
|
import std/Option
|
|
5
5
|
import std/Bool
|
|
6
|
+
import std/Int
|
|
6
7
|
|
|
7
8
|
# The Base64 package contains support for doing Base64 binary-to-text encodings.
|
|
8
9
|
#
|
{libs/std → plum-std}/Bool.plum
RENAMED
|
File without changes
|
{libs/std → plum-std}/Buffer.plum
RENAMED
|
@@ -2,6 +2,7 @@ module std
|
|
|
2
2
|
import std/ByteSlice
|
|
3
3
|
import std/Str
|
|
4
4
|
import std/Bool
|
|
5
|
+
import std/Int
|
|
5
6
|
|
|
6
7
|
# A Buffer is a growable, mutable sequence of bytes for efficiently building
|
|
7
8
|
# up a `Str` piece by piece — modeled on Go's `bytes.Buffer`. Backed by a
|
{libs/std → plum-std}/Byte.plum
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module std
|
|
2
2
|
import std/Int
|
|
3
3
|
import std/Bool
|
|
4
|
+
import std/Str
|
|
4
5
|
|
|
5
6
|
# Byte is an unsigned 8-bit value (0-255) — Plum's counterpart to Go's
|
|
6
7
|
# `byte`/`uint8`. `Byte(x)` converts an `Int` (wrapping, not trapping, if `x`
|
{libs/std → plum-std}/ByteSlice.plum
RENAMED
|
@@ -2,6 +2,7 @@ module std
|
|
|
2
2
|
import std/Byte
|
|
3
3
|
import std/Str
|
|
4
4
|
import std/Bool
|
|
5
|
+
import std/Int
|
|
5
6
|
|
|
6
7
|
# ByteSlice is `[]Byte` — Plum's counterpart to Go's byte slice: a
|
|
7
8
|
# fixed-length, mutable sequence of raw bytes backed directly by a wasm-gc
|
{libs/std → plum-std}/Err.plum
RENAMED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
module std
|
|
2
2
|
import std/Bool
|
|
3
|
+
import std/Int
|
|
4
|
+
import std/Str
|
|
3
5
|
|
|
4
6
|
# This is used to represent an error value across the language
|
|
5
7
|
trait Err =
|
{libs/std → plum-std}/Float.plum
RENAMED
|
File without changes
|
libs/std/http.plum → plum-std/Http.plum
RENAMED
|
@@ -8,6 +8,7 @@ import std/Option
|
|
|
8
8
|
import std/Result
|
|
9
9
|
import std/Str
|
|
10
10
|
import std/Bool
|
|
11
|
+
import std/Int
|
|
11
12
|
|
|
12
13
|
# An HTTP response: status code, response headers, and the raw response
|
|
13
14
|
# body as a `Str` (a byte array, so binary bodies round-trip intact).
|
{libs/std → plum-std}/Int.plum
RENAMED
|
File without changes
|
{libs/std → plum-std}/Json.plum
RENAMED
|
File without changes
|
{libs/std → plum-std}/List.plum
RENAMED
|
@@ -4,6 +4,7 @@ import std/Option
|
|
|
4
4
|
import std/Buffer
|
|
5
5
|
import std/Int
|
|
6
6
|
import std/Bool
|
|
7
|
+
import std/Str
|
|
7
8
|
|
|
8
9
|
# A node stores the data in a list and contains pointers to the previous and next sibling nodes
|
|
9
10
|
type Node[T] =
|
{libs/std → plum-std}/Map.plum
RENAMED
|
@@ -3,6 +3,8 @@ import std/List
|
|
|
3
3
|
import std/Option
|
|
4
4
|
import std/Array
|
|
5
5
|
import std/Bool
|
|
6
|
+
import std/Int
|
|
7
|
+
import std/Str
|
|
6
8
|
|
|
7
9
|
# Any type usable as a `Map` key needs a `hash`, so keys landing in different
|
|
8
10
|
# buckets (`hash(k) % BUCKET_COUNT`) can be told apart in O(1) without a full
|
{libs/std → plum-std}/Option.plum
RENAMED
|
@@ -3,6 +3,7 @@ module std
|
|
|
3
3
|
import std/Result
|
|
4
4
|
import std/Str
|
|
5
5
|
import std/Bool
|
|
6
|
+
import std/Int
|
|
6
7
|
|
|
7
8
|
# Option[T] represents a value that may or may not be present — Plum's
|
|
8
9
|
# counterpart to Rust's `Option`/Go's "zero value or ok bool" idiom.
|
libs/std/os.plum → plum-std/Os.plum
RENAMED
|
@@ -4,6 +4,7 @@ import std/Result
|
|
|
4
4
|
import std/Str
|
|
5
5
|
import std/Uuid
|
|
6
6
|
import std/Bool
|
|
7
|
+
import std/Int
|
|
7
8
|
|
|
8
9
|
# NOTE: the original file modeled `stdin`/`stdout`/`stderr` as bare top-level
|
|
9
10
|
# variable bindings and a `File(...)` constructor, but this language has no
|
libs/std/regex.mi → plum-std/Regex.mi
RENAMED
|
File without changes
|
{libs/std → plum-std}/Result.plum
RENAMED
|
@@ -2,6 +2,7 @@ module std
|
|
|
2
2
|
import std/Option
|
|
3
3
|
import std/Str
|
|
4
4
|
import std/Bool
|
|
5
|
+
import std/Int
|
|
5
6
|
|
|
6
7
|
# Result[T, E] represents either success (`Ok`, carrying a `T`) or failure
|
|
7
8
|
# (`Err`, carrying an `E`) — used throughout std for fallible operations
|
{libs/std → plum-std}/Str.plum
RENAMED
|
@@ -3,6 +3,7 @@ module std
|
|
|
3
3
|
import std/List
|
|
4
4
|
import std/Buffer
|
|
5
5
|
import std/Bool
|
|
6
|
+
import std/Int
|
|
6
7
|
|
|
7
8
|
# Any type that can be converted to a str needs to implement this trait
|
|
8
9
|
trait ToStr =
|
libs/std/testing.plum → plum-std/Testing.plum
RENAMED
|
@@ -4,6 +4,8 @@ import std/Option
|
|
|
4
4
|
import std/Result
|
|
5
5
|
import std/List
|
|
6
6
|
import std/Bool
|
|
7
|
+
import std/Int
|
|
8
|
+
import std/Str
|
|
7
9
|
|
|
8
10
|
# Small jest/rspec-style assertion helpers for use inside `test` blocks, e.g.
|
|
9
11
|
#
|
{libs/std → plum-std}/Time.plum
RENAMED
|
@@ -2,6 +2,7 @@ module std
|
|
|
2
2
|
import std/Int
|
|
3
3
|
import std/Str
|
|
4
4
|
import std/Bool
|
|
5
|
+
import std/Float
|
|
5
6
|
|
|
6
7
|
# A raw, host-provided wall-clock reading: milliseconds since the Unix epoch.
|
|
7
8
|
extern fun rawNowMillis() -> Int
|
{libs/std → plum-std}/Uuid.plum
RENAMED
|
File without changes
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -1156,7 +1156,7 @@ fn compileSourceInner(source: &ast::Source, extra_exports: &[(String, String)])
|
|
|
1156
1156
|
CURRENT_GC_TYPES.with(|c| *c.borrow_mut() = Some(gc_types.clone()));
|
|
1157
1157
|
}
|
|
1158
1158
|
|
|
1159
|
-
// Register every `extern fun` (e.g. `
|
|
1159
|
+
// Register every `extern fun` (e.g. `plum-std/Os.plum`'s `printLn`) as a
|
|
1160
1160
|
// genuine wasm import BEFORE any function (including the `start` function
|
|
1161
1161
|
// set up right below) — imports must occupy the low end of the function
|
|
1162
1162
|
// index space for every later `addFunction`'s index arithmetic to stay
|
plum-wasm-codegen/tests/examples_test.rs
CHANGED
|
@@ -6,15 +6,14 @@ fn examplesDir() -> std::path::PathBuf {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
fn libPath() -> std::path::PathBuf {
|
|
9
|
-
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..
|
|
9
|
+
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/// Loads `name` via the REAL compilation path (`loadAndMerge`, resolving its
|
|
13
|
-
/// own `import`s against `../libs` and merging in `Str`'s always-implicit
|
|
14
|
-
/// `
|
|
13
|
+
/// own `import`s against the repo root — see `plum-core::loader::loadAndMerge`'s
|
|
15
|
-
/// rather than parsing it in isolation — several example files
|
|
14
|
+
/// doc comment) rather than parsing it in isolation — several example files
|
|
16
|
-
/// `import` real stdlib types (e.g. `std/option`), so a bare
|
|
15
|
+
/// now genuinely `import` real stdlib types (e.g. `std/option`), so a bare
|
|
17
|
-
/// parse would leave those names undeclared.
|
|
16
|
+
/// single-file parse would leave those names undeclared.
|
|
18
17
|
fn parseFile(name: &str) -> plum_core::ast::Source {
|
|
19
18
|
let path = examplesDir().join(name);
|
|
20
19
|
plum_core::loadAndMerge(&path, &libPath())
|
scripts/test-plum.sh
CHANGED
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
# unit-test shortcut. Most of what used to be Rust `#[test]` functions in
|
|
6
6
|
# plum-wasm-codegen/tests/codegen_tests.rs (compile a snippet, run it,
|
|
7
7
|
# assert on the result) now live as `test` blocks co-located with the Plum
|
|
8
|
-
# source they exercise — stdlib-shaped regression tests inside
|
|
8
|
+
# source they exercise — stdlib-shaped regression tests inside
|
|
9
|
-
# itself, and core-language-feature tests inside the
|
|
9
|
+
# plum-std/*.plum itself, and core-language-feature tests inside the
|
|
10
|
-
# (closures.plum, match.plum, functions.plum,
|
|
10
|
+
# matching examples/*.plum (closures.plum, match.plum, functions.plum,
|
|
11
|
+
# types.plum, control_flow.plum) — a Plum-language behavior is verified in
|
|
11
|
-
#
|
|
12
|
+
# Plum itself, not re-described in Rust.
|
|
12
13
|
#
|
|
13
14
|
# `plum test` follows a file's `import`s transitively (same as `plum run`), so
|
|
14
|
-
# running it on one
|
|
15
|
+
# running it on one plum-std file also re-runs every test in whatever it
|
|
15
16
|
# imports — harmless duplication, not a correctness issue.
|
|
16
17
|
#
|
|
17
18
|
# Files with no `test` block just report "no tests found" and are skipped
|
|
@@ -38,7 +39,7 @@ PLUM_BIN="target/debug/plum"
|
|
|
38
39
|
# unrelated to `test`/`assert` regressions.
|
|
39
40
|
is_known_incomplete() {
|
|
40
41
|
case "$(basename "$1")" in
|
|
41
|
-
|
|
42
|
+
Http.plum) return 0 ;;
|
|
42
43
|
*) return 1 ;;
|
|
43
44
|
esac
|
|
44
45
|
}
|
|
@@ -46,7 +47,7 @@ is_known_incomplete() {
|
|
|
46
47
|
failed=0
|
|
47
48
|
files_run=0
|
|
48
49
|
|
|
49
|
-
for src in
|
|
50
|
+
for src in plum-std/*.plum examples/*.plum; do
|
|
50
51
|
[ -f "$src" ] || continue
|
|
51
52
|
if is_known_incomplete "$src"; then
|
|
52
53
|
echo "=== $src === (skipped: known incomplete, see README's Known gaps)"
|