plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
a261b90
— Peter John
2026-10-26T11:15:58+05:30
feat(checker): duck-typed generics checked per instantiation
- plum-checker/src/lib.rs +388 -18
- plum-checker/src/types.rs +1 -1
- plum-checker/tests/checker_tests.rs +86 -7
- plum-core/src/loader.rs +0 -28
- plum-wasm-codegen/src/lib.rs +13 -2
plum-checker/src/lib.rs
CHANGED
|
@@ -15,13 +15,78 @@ thread_local! {
|
|
|
15
15
|
/// When set, `inferExprWith` records every expression's type here (see `typedSource`).
|
|
16
16
|
static RECORD_BINDS: RefCell<Option<Binds>> = const { RefCell::new(None) };
|
|
17
17
|
static RECORD: RefCell<Option<HashMap<usize, PlumType>>> = const { RefCell::new(None) };
|
|
18
|
+
/// Generic-call sites seen while checking one body (see `CallSite`); `None` outside `checkSourceInner`.
|
|
19
|
+
static CALLS: RefCell<Option<Vec<CallSite>>> = const { RefCell::new(None) };
|
|
20
|
+
/// While checking a generic body for one instantiation: its generic parameter -> concrete type.
|
|
21
|
+
static TSUBST: RefCell<Option<BTreeMap<String, PlumType>>> = const { RefCell::new(None) };
|
|
22
|
+
/// True while checking an instantiated generic body (method lookups on declared types are strict).
|
|
23
|
+
static INST_MODE: Cell<bool> = const { Cell::new(false) };
|
|
24
|
+
/// Line of the declaration currently being checked (the "called at" line of recorded calls).
|
|
25
|
+
static CUR_LINE: Cell<usize> = const { Cell::new(0) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// A call to a generic fn/method seen in some body: callee identity plus the
|
|
29
|
+
/// (possibly still unresolved) type bound to each of its generic parameters.
|
|
30
|
+
#[derive(Debug, Clone)]
|
|
31
|
+
struct CallSite {
|
|
32
|
+
recv: Option<String>,
|
|
33
|
+
name: String,
|
|
34
|
+
args: BTreeMap<String, PlumType>,
|
|
35
|
+
line: usize,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn recordCall(recv: Option<&str>, name: &str, args: BTreeMap<String, PlumType>) {
|
|
39
|
+
if args.is_empty() {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
CALLS.with(|c| {
|
|
43
|
+
if let Some(v) = c.borrow_mut().as_mut() {
|
|
44
|
+
v.push(CallSite {
|
|
45
|
+
recv: recv.map(|r| r.to_string()),
|
|
46
|
+
name: name.to_string(),
|
|
47
|
+
args,
|
|
48
|
+
line: CUR_LINE.with(|l| l.get()),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
18
52
|
}
|
|
19
53
|
|
|
20
54
|
/// Per-expression instantiated types of a checked source, for codegen.
|
|
55
|
+
///
|
|
56
|
+
/// Generic bodies are checked per concrete instantiation (Zig-style duck
|
|
57
|
+
/// typing). The data for codegen is:
|
|
58
|
+
/// * `expr_types`: types from the generic-erased (TParam) pass, as before;
|
|
59
|
+
/// * `instantiations`: every `(item, type args)` pair reached from a root
|
|
60
|
+
/// (non-generic fns, tests), in discovery order; `Instantiation::id` is the
|
|
61
|
+
/// index into this vector (so one wasm fn per entry can be emitted);
|
|
62
|
+
/// * `inst_expr_types`: `(instantiation id, expr address)` -> that expression's
|
|
63
|
+
/// type inside the instantiated body, generics substituted;
|
|
64
|
+
/// * `inst_index`: `(item path, type args)` -> instantiation id.
|
|
21
65
|
#[derive(Debug, Clone, Default)]
|
|
22
66
|
pub struct TypedInfo {
|
|
23
67
|
/// Expression address (`&ast::Expr as *const _ as usize`) -> its type.
|
|
24
68
|
pub expr_types: HashMap<usize, PlumType>,
|
|
69
|
+
pub instantiations: Vec<Instantiation>,
|
|
70
|
+
pub inst_expr_types: HashMap<(usize, usize), PlumType>,
|
|
71
|
+
pub inst_index: HashMap<(String, Vec<PlumType>), usize>,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// One reached instantiation of a generic fn or method of a generic type.
|
|
75
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
76
|
+
pub struct Instantiation {
|
|
77
|
+
pub id: usize,
|
|
78
|
+
/// Item path: `"max"` for a free fn, `"List.map"` for a method.
|
|
79
|
+
pub item: String,
|
|
80
|
+
pub receiver: Option<String>,
|
|
81
|
+
pub name: String,
|
|
82
|
+
/// Generic parameter names: the owner type's generics first, then the fn's own.
|
|
83
|
+
pub generics: Vec<String>,
|
|
84
|
+
/// Concrete type per entry of `generics`.
|
|
85
|
+
pub type_args: Vec<PlumType>,
|
|
86
|
+
/// Instantiation whose body first called this one (`None` = a root).
|
|
87
|
+
pub parent: Option<usize>,
|
|
88
|
+
/// Declaration line of the calling root/instantiation.
|
|
89
|
+
pub called_at_line: usize,
|
|
25
90
|
}
|
|
26
91
|
|
|
27
92
|
/// Both operands of a binary/compare operator have the same type, so an
|
|
@@ -120,6 +185,14 @@ fn freeTParams(ty: &PlumType, out: &mut Vec<String>) {
|
|
|
120
185
|
/// Instantiates every free `TParam` in `sig`: those named in `owner` take the
|
|
121
186
|
/// owner's (receiver's) type, all others get a fresh inference variable.
|
|
122
187
|
fn instSig(sig: &PlumType, owner: &BTreeMap<String, PlumType>) -> PlumType {
|
|
188
|
+
instSigMap(sig, owner).0
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// `instSig`, also returning the generic-parameter -> type map it used.
|
|
192
|
+
fn instSigMap(
|
|
193
|
+
sig: &PlumType,
|
|
194
|
+
owner: &BTreeMap<String, PlumType>,
|
|
195
|
+
) -> (PlumType, BTreeMap<String, PlumType>) {
|
|
123
196
|
let mut names = Vec::new();
|
|
124
197
|
freeTParams(sig, &mut names);
|
|
125
198
|
let map: BTreeMap<String, PlumType> = names
|
|
@@ -129,7 +202,7 @@ fn instSig(sig: &PlumType, owner: &BTreeMap<String, PlumType>) -> PlumType {
|
|
|
129
202
|
(n, t)
|
|
130
203
|
})
|
|
131
204
|
.collect();
|
|
132
|
-
types::substitute(sig, &map)
|
|
205
|
+
(types::substitute(sig, &map), map)
|
|
133
206
|
}
|
|
134
207
|
|
|
135
208
|
/// One-way matching: binds the inference variables of `pattern` from `actual`.
|
|
@@ -221,6 +294,9 @@ pub fn plumTypeFromAstG(ty: &ast::Type) -> PlumType {
|
|
|
221
294
|
);
|
|
222
295
|
}
|
|
223
296
|
if isGenericName(&ty.name) {
|
|
297
|
+
if let Some(t) = TSUBST.with(|s| s.borrow().as_ref().and_then(|m| m.get(&ty.name).cloned())) {
|
|
298
|
+
return t;
|
|
299
|
+
}
|
|
224
300
|
return PlumType::TParam(ty.name.clone());
|
|
225
301
|
}
|
|
226
302
|
plumTypeFromName(&ty.name)
|
|
@@ -907,9 +983,59 @@ pub fn buildGlobalTables(source: &ast::Source) -> (TypeEnv, MethodEnv, EnumVaria
|
|
|
907
983
|
}
|
|
908
984
|
}
|
|
909
985
|
|
|
986
|
+
// `Bool.toStr` is a compiler builtin (see `builtinBoolToStr`); it is visible as a
|
|
987
|
+
// method (trait conformance, dyn dispatch) once `Str` is in the program.
|
|
988
|
+
if hasStr(source) {
|
|
989
|
+
methods
|
|
990
|
+
.entry(("Bool".to_string(), "toStr".to_string()))
|
|
991
|
+
.or_insert_with(|| PlumType::TFun(Vec::new(), Box::new(PlumType::TStr)));
|
|
992
|
+
}
|
|
910
993
|
(global_env, methods, enum_variants, enum_params)
|
|
911
994
|
}
|
|
912
995
|
|
|
996
|
+
fn hasStr(source: &ast::Source) -> bool {
|
|
997
|
+
source
|
|
998
|
+
.items
|
|
999
|
+
.iter()
|
|
1000
|
+
.any(|i| matches!(i, ast::Item::Enum(e) if e.name == "Str"))
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/// True if `source` should get the builtin `Bool.toStr` (it returns `Str`, and
|
|
1004
|
+
/// the user has not defined their own).
|
|
1005
|
+
pub fn needsBuiltinBoolToStr(source: &ast::Source) -> bool {
|
|
1006
|
+
hasStr(source)
|
|
1007
|
+
&& !source.items.iter().any(|i| {
|
|
1008
|
+
matches!(i, ast::Item::Fn(f) if f.name == "toStr" && f.type_param.as_deref() == Some("Bool"))
|
|
1009
|
+
})
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/// The builtin `Bool.toStr` as an ordinary method AST (`self ? "True" : "False"`),
|
|
1013
|
+
/// for codegen to compile as one wasm function. Nothing is added to the source.
|
|
1014
|
+
pub fn builtinBoolToStr() -> ast::Fn {
|
|
1015
|
+
let lit = |t: &str| {
|
|
1016
|
+
ast::Expr::String(ast::StringExpr {
|
|
1017
|
+
parts: vec![ast::StringPart::Text(t.to_string())],
|
|
1018
|
+
})
|
|
1019
|
+
};
|
|
1020
|
+
ast::Fn {
|
|
1021
|
+
name: "toStr".to_string(),
|
|
1022
|
+
line: 0,
|
|
1023
|
+
column: 0,
|
|
1024
|
+
type_param: Some("Bool".to_string()),
|
|
1025
|
+
is_extern: false,
|
|
1026
|
+
params: Vec::new(),
|
|
1027
|
+
returns: Some(ast::Type {
|
|
1028
|
+
name: "Str".to_string(),
|
|
1029
|
+
generics: Vec::new(),
|
|
1030
|
+
}),
|
|
1031
|
+
body: ast::FnBody::Expr(ast::Expr::Ternary(Box::new(ast::TernaryExpr {
|
|
1032
|
+
condition: ast::Expr::Self_,
|
|
1033
|
+
then: lit("True"),
|
|
1034
|
+
else_: lit("False"),
|
|
1035
|
+
}))),
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
913
1039
|
/// A `Type`'s name for comparison purposes only — deliberately ignores `.generics`
|
|
914
1040
|
/// entirely (on both sides), so a trait's bare return/param type (`cause() ->
|
|
915
1041
|
/// Link`, unable to name a concrete payload since a trait has no instance to
|
|
@@ -1019,26 +1145,35 @@ fn checkTraitConformance(source: &ast::Source) -> Vec<CheckError> {
|
|
|
1019
1145
|
}
|
|
1020
1146
|
|
|
1021
1147
|
pub fn checkSource(source: &ast::Source) -> CheckResult<()> {
|
|
1022
|
-
|
|
1148
|
+
checkSourceFull(source).0
|
|
1023
1149
|
}
|
|
1024
1150
|
|
|
1025
1151
|
/// Type-checks the original source like `checkSource` and also returns the
|
|
1026
|
-
/// instantiated type of every expression (keyed by expression address)
|
|
1152
|
+
/// instantiated type of every expression (keyed by expression address) and the
|
|
1153
|
+
/// set of generic instantiations (see `TypedInfo`) for codegen.
|
|
1027
1154
|
pub fn typedSource(source: &ast::Source) -> CheckResult<TypedInfo> {
|
|
1155
|
+
let (result, info) = checkSourceFull(source);
|
|
1156
|
+
result.map(|()| info)
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
fn checkSourceFull(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
1028
1160
|
RECORD.with(|r| *r.borrow_mut() = Some(HashMap::new()));
|
|
1029
1161
|
RECORD_BINDS.with(|r| *r.borrow_mut() = Some(Binds::new()));
|
|
1162
|
+
CALLS.with(|r| *r.borrow_mut() = Some(Vec::new()));
|
|
1030
|
-
let result = checkSourceInner(source);
|
|
1163
|
+
let (result, mut info) = checkSourceInner(source);
|
|
1031
1164
|
let mut recorded = RECORD.with(|r| r.borrow_mut().take()).unwrap_or_default();
|
|
1032
1165
|
let binds = RECORD_BINDS.with(|r| r.borrow_mut().take()).unwrap_or_default();
|
|
1166
|
+
CALLS.with(|r| r.borrow_mut().take());
|
|
1167
|
+
TSUBST.with(|s| *s.borrow_mut() = None);
|
|
1168
|
+
INST_MODE.with(|m| m.set(false));
|
|
1033
1169
|
// Zonk: apply the final substitution; a variable nothing ever constrained
|
|
1034
1170
|
// (an unused `None`, an empty literal, ...) is defaulted to `Unit`, since
|
|
1035
1171
|
// no value of it can ever be observed.
|
|
1036
|
-
for t in recorded.values_mut() {
|
|
1172
|
+
for t in recorded.values_mut().chain(info.inst_expr_types.values_mut()) {
|
|
1037
1173
|
*t = defaultVars(&substVars(t, &binds));
|
|
1038
1174
|
}
|
|
1039
|
-
result.map(|()| TypedInfo {
|
|
1040
|
-
|
|
1175
|
+
info.expr_types = recorded;
|
|
1041
|
-
|
|
1176
|
+
(result, info)
|
|
1042
1177
|
}
|
|
1043
1178
|
|
|
1044
1179
|
/// A generic type named without type arguments (`p: Parcel` for `enum
|
|
@@ -1118,7 +1253,7 @@ fn checkBareGenericTypes(source: &ast::Source) -> Vec<CheckError> {
|
|
|
1118
1253
|
errors
|
|
1119
1254
|
}
|
|
1120
1255
|
|
|
1121
|
-
fn checkSourceInner(source: &ast::Source) -> CheckResult<()> {
|
|
1256
|
+
fn checkSourceInner(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
1122
1257
|
// Generic items are checked once, structurally (`TApp`/`TParam`), on the
|
|
1123
1258
|
// ORIGINAL source.
|
|
1124
1259
|
let mut errors: Vec<CheckError> = checkTraitConformance(source);
|
|
@@ -1196,15 +1331,46 @@ fn checkSourceInner(source: &ast::Source) -> CheckResult<()> {
|
|
|
1196
1331
|
_ => None,
|
|
1197
1332
|
})
|
|
1198
1333
|
.collect();
|
|
1334
|
+
let file = source
|
|
1335
|
+
.module
|
|
1336
|
+
.as_ref()
|
|
1337
|
+
.map(|m| m.name.clone())
|
|
1338
|
+
.unwrap_or_else(|| "<source>".to_string());
|
|
1339
|
+
let fn_by_key: HashMap<(Option<String>, String), &ast::Fn> = source
|
|
1340
|
+
.items
|
|
1341
|
+
.iter()
|
|
1342
|
+
.filter_map(|i| match i {
|
|
1343
|
+
ast::Item::Fn(f) => Some(((f.type_param.clone(), f.name.clone()), f)),
|
|
1344
|
+
_ => None,
|
|
1345
|
+
})
|
|
1346
|
+
.collect();
|
|
1347
|
+
let mut inst = InstState {
|
|
1348
|
+
info: TypedInfo::default(),
|
|
1349
|
+
chains: Vec::new(),
|
|
1350
|
+
queue: std::collections::VecDeque::new(),
|
|
1351
|
+
file,
|
|
1352
|
+
};
|
|
1353
|
+
drainCalls();
|
|
1354
|
+
|
|
1355
|
+
// Roots: every non-generic fn and every test. A generic fn is only
|
|
1356
|
+
// name-resolved here (its body is type-checked per instantiation below).
|
|
1199
1357
|
for item in &source.items {
|
|
1200
1358
|
if let ast::Item::Fn(f) = item {
|
|
1359
|
+
CUR_LINE.with(|l| l.set(f.line));
|
|
1201
1360
|
let mut local_errors = checkFn(f, &global_env, &ctx, &enums_by_name);
|
|
1361
|
+
if !fnGenericNames(f, &ctx).is_empty() {
|
|
1362
|
+
local_errors.retain(|e| isResolutionError(&e.message));
|
|
1363
|
+
drainCalls();
|
|
1364
|
+
} else {
|
|
1365
|
+
registerCalls(&mut inst, &fn_by_key, &ctx, None);
|
|
1366
|
+
}
|
|
1202
1367
|
errors.append(&mut local_errors);
|
|
1203
1368
|
}
|
|
1204
1369
|
}
|
|
1205
1370
|
|
|
1206
1371
|
for item in &source.items {
|
|
1207
1372
|
if let ast::Item::Test(t) = item {
|
|
1373
|
+
CUR_LINE.with(|l| l.set(t.line));
|
|
1208
1374
|
let mut env = global_env.clone();
|
|
1209
1375
|
let mut block_errors = checkBlock(
|
|
1210
1376
|
&t.body,
|
|
@@ -1215,13 +1381,171 @@ fn checkSourceInner(source: &ast::Source) -> CheckResult<()> {
|
|
|
1215
1381
|
&ctx,
|
|
1216
1382
|
);
|
|
1217
1383
|
errors.append(&mut block_errors);
|
|
1384
|
+
registerCalls(&mut inst, &fn_by_key, &ctx, None);
|
|
1218
1385
|
}
|
|
1219
1386
|
}
|
|
1220
1387
|
|
|
1388
|
+
// Check each reached instantiation (BFS); its own calls enqueue more.
|
|
1389
|
+
let mut seen_msgs: std::collections::HashSet<String> = std::collections::HashSet::new();
|
|
1390
|
+
while let Some(id) = inst.queue.pop_front() {
|
|
1391
|
+
let i = inst.info.instantiations[id].clone();
|
|
1392
|
+
let Some(f) = fn_by_key.get(&(i.receiver.clone(), i.name.clone())).copied() else {
|
|
1393
|
+
continue;
|
|
1394
|
+
};
|
|
1395
|
+
let chain = inst.chains[id].clone();
|
|
1396
|
+
let subst: BTreeMap<String, PlumType> = i
|
|
1397
|
+
.generics
|
|
1398
|
+
.iter()
|
|
1399
|
+
.cloned()
|
|
1400
|
+
.zip(i.type_args.iter().cloned())
|
|
1401
|
+
.collect();
|
|
1402
|
+
TSUBST.with(|s| *s.borrow_mut() = Some(subst));
|
|
1403
|
+
INST_MODE.with(|m| m.set(true));
|
|
1404
|
+
CUR_LINE.with(|l| l.set(f.line));
|
|
1405
|
+
let saved = RECORD.with(|r| r.borrow_mut().replace(HashMap::new()));
|
|
1406
|
+
let errs = checkFn(f, &global_env, &ctx, &enums_by_name);
|
|
1407
|
+
let own = RECORD.with(|r| std::mem::replace(&mut *r.borrow_mut(), saved));
|
|
1408
|
+
TSUBST.with(|s| *s.borrow_mut() = None);
|
|
1409
|
+
INST_MODE.with(|m| m.set(false));
|
|
1410
|
+
for (ptr, t) in own.unwrap_or_default() {
|
|
1411
|
+
inst.info.inst_expr_types.insert((id, ptr), t);
|
|
1412
|
+
}
|
|
1413
|
+
for e in errs {
|
|
1414
|
+
let msg = match (e.message.strip_prefix("fn '"), e.message.find("': ")) {
|
|
1415
|
+
(Some(_), Some(k)) => e.message[k + 3..].to_string(),
|
|
1416
|
+
_ => e.message.clone(),
|
|
1417
|
+
};
|
|
1418
|
+
let full = format!("{}{}", chain, msg);
|
|
1419
|
+
if seen_msgs.insert(full.clone()) {
|
|
1420
|
+
errors.push(CheckError::at(e.line, e.column, full));
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
registerCalls(&mut inst, &fn_by_key, &ctx, Some(id));
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
let result = if errors.is_empty() { Ok(()) } else { Err(errors) };
|
|
1427
|
+
(result, inst.info)
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
struct InstState {
|
|
1431
|
+
info: TypedInfo,
|
|
1432
|
+
/// Per instantiation id: the "in X[T] (called at f:l): " chain prefix.
|
|
1433
|
+
chains: Vec<String>,
|
|
1434
|
+
queue: std::collections::VecDeque<usize>,
|
|
1435
|
+
file: String,
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
/// Errors a generic body keeps even when uninstantiated (name resolution and
|
|
1439
|
+
/// declaration structure); everything else is a type error found per instantiation.
|
|
1440
|
+
fn isResolutionError(msg: &str) -> bool {
|
|
1441
|
+
msg.contains("undefined name")
|
|
1442
|
+
|| msg.contains("extern")
|
|
1443
|
+
|| msg.contains("variadic")
|
|
1444
|
+
|| msg.contains("missing a body")
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
fn drainCalls() -> Vec<CallSite> {
|
|
1448
|
+
CALLS.with(|c| {
|
|
1449
|
+
c.borrow_mut()
|
|
1450
|
+
.as_mut()
|
|
1451
|
+
.map(std::mem::take)
|
|
1452
|
+
.unwrap_or_default()
|
|
1453
|
+
})
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
/// Generic parameter names of `f`: its owner type's generics, then the free
|
|
1457
|
+
/// type parameters of its own signature. Empty = not generic.
|
|
1458
|
+
fn fnGenericNames(f: &ast::Fn, ctx: &CheckCtx) -> Vec<String> {
|
|
1459
|
+
let mut names: Vec<String> = match &f.type_param {
|
|
1460
|
+
Some(r) => enumGenericsOf(r, ctx).to_vec(),
|
|
1461
|
+
None => Vec::new(),
|
|
1462
|
+
};
|
|
1463
|
+
let mut sig = Vec::new();
|
|
1464
|
+
for p in &f.params {
|
|
1465
|
+
freeTParams(¶mTypeG(&p.ty), &mut sig);
|
|
1466
|
+
}
|
|
1467
|
+
if let Some(r) = &f.returns {
|
|
1468
|
+
freeTParams(&plumTypeFromAstG(r), &mut sig);
|
|
1469
|
+
}
|
|
1470
|
+
for n in sig {
|
|
1221
|
-
|
|
1471
|
+
if !names.contains(&n) {
|
|
1472
|
+
names.push(n);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1222
|
-
|
|
1475
|
+
names
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
fn hasFreeVars(t: &PlumType) -> bool {
|
|
1479
|
+
use PlumType::*;
|
|
1223
|
-
|
|
1480
|
+
match t {
|
|
1481
|
+
TVar(_) | TParam(_) => true,
|
|
1482
|
+
TApp(_, a) => a.iter().any(hasFreeVars),
|
|
1483
|
+
TFun(ps, r) => ps.iter().any(hasFreeVars) || hasFreeVars(r),
|
|
1484
|
+
TVariadic(i) => hasFreeVars(i),
|
|
1485
|
+
TTags(m) => m.values().flatten().any(|t| hasFreeVars(t)),
|
|
1486
|
+
_ => false,
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
/// Turns the generic calls recorded while checking one body into new
|
|
1491
|
+
/// instantiations (memoized by item + concrete type args).
|
|
1224
|
-
|
|
1492
|
+
fn registerCalls(
|
|
1493
|
+
st: &mut InstState,
|
|
1494
|
+
fns: &HashMap<(Option<String>, String), &ast::Fn>,
|
|
1495
|
+
ctx: &CheckCtx,
|
|
1496
|
+
parent: Option<usize>,
|
|
1497
|
+
) {
|
|
1498
|
+
let binds = RECORD_BINDS.with(|r| r.borrow().clone()).unwrap_or_default();
|
|
1499
|
+
let parent_chain = parent.map(|p| st.chains[p].clone()).unwrap_or_default();
|
|
1500
|
+
let depth = parent_chain.matches("(called at").count();
|
|
1501
|
+
for cs in drainCalls() {
|
|
1502
|
+
let Some(f) = fns.get(&(cs.recv.clone(), cs.name.clone())) else {
|
|
1503
|
+
continue;
|
|
1504
|
+
};
|
|
1505
|
+
let gens = fnGenericNames(f, ctx);
|
|
1506
|
+
if gens.is_empty() || depth >= 24 || st.info.instantiations.len() >= 20000 {
|
|
1507
|
+
continue;
|
|
1508
|
+
}
|
|
1509
|
+
let mut args = Vec::new();
|
|
1510
|
+
for g in &gens {
|
|
1511
|
+
match cs.args.get(g).map(|t| substVars(t, &binds)) {
|
|
1512
|
+
Some(t) if !hasFreeVars(&t) => args.push(t),
|
|
1513
|
+
_ => break,
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
if args.len() != gens.len() {
|
|
1517
|
+
continue;
|
|
1518
|
+
}
|
|
1519
|
+
let item = match &cs.recv {
|
|
1520
|
+
Some(r) => format!("{}.{}", r, cs.name),
|
|
1521
|
+
None => cs.name.clone(),
|
|
1522
|
+
};
|
|
1523
|
+
let key = (item.clone(), args.clone());
|
|
1524
|
+
if st.info.inst_index.contains_key(&key) {
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
let id = st.info.instantiations.len();
|
|
1528
|
+
let arg_str: Vec<String> = args.iter().map(|a| a.to_string()).collect();
|
|
1529
|
+
st.chains.push(format!(
|
|
1530
|
+
"{}in {}[{}] (called at {}:{}): ",
|
|
1531
|
+
parent_chain,
|
|
1532
|
+
item,
|
|
1533
|
+
arg_str.join(", "),
|
|
1534
|
+
st.file,
|
|
1535
|
+
cs.line
|
|
1536
|
+
));
|
|
1537
|
+
st.info.instantiations.push(Instantiation {
|
|
1538
|
+
id,
|
|
1539
|
+
item,
|
|
1540
|
+
receiver: cs.recv.clone(),
|
|
1541
|
+
name: cs.name.clone(),
|
|
1542
|
+
generics: gens,
|
|
1543
|
+
type_args: args,
|
|
1544
|
+
parent,
|
|
1545
|
+
called_at_line: cs.line,
|
|
1546
|
+
});
|
|
1547
|
+
st.info.inst_index.insert(key, id);
|
|
1548
|
+
st.queue.push_back(id);
|
|
1225
1549
|
}
|
|
1226
1550
|
}
|
|
1227
1551
|
|
|
@@ -1306,6 +1630,10 @@ fn checkFn(
|
|
|
1306
1630
|
generics.iter().map(|g| PlumType::TParam(g.clone())).collect(),
|
|
1307
1631
|
)
|
|
1308
1632
|
};
|
|
1633
|
+
let self_ty = match TSUBST.with(|s| s.borrow().clone()) {
|
|
1634
|
+
Some(m) => types::substitute(&self_ty, &m),
|
|
1635
|
+
None => self_ty,
|
|
1636
|
+
};
|
|
1309
1637
|
env.insert("self".to_string(), TypeScheme::mono(self_ty));
|
|
1310
1638
|
// The owner's declared trait bounds (`K: Hashable`) apply in its methods.
|
|
1311
1639
|
if let Some(e) = enums.get(recv.as_str()) {
|
|
@@ -2055,13 +2383,29 @@ fn inferClassCallRaw(
|
|
|
2055
2383
|
} else {
|
|
2056
2384
|
BTreeMap::new()
|
|
2057
2385
|
};
|
|
2386
|
+
let (inst, mut init_map) = instSigMap(sig, &owner);
|
|
2058
|
-
let PlumType::TFun(_, ret) =
|
|
2387
|
+
let PlumType::TFun(_, ret) = inst else {
|
|
2059
2388
|
unreachable!("instSig preserves the TFun shape")
|
|
2060
2389
|
};
|
|
2061
2390
|
let mut binds = Binds::new();
|
|
2062
2391
|
if let Some(exp) = expected {
|
|
2063
2392
|
bindMatch(&ret, exp, &mut binds);
|
|
2064
2393
|
}
|
|
2394
|
+
for g in gens {
|
|
2395
|
+
if let Some(t) = owner.get(g) {
|
|
2396
|
+
init_map.insert(g.clone(), t.clone());
|
|
2397
|
+
} else if !init_map.contains_key(g) {
|
|
2398
|
+
// Owner generic not in init's signature: pinned by the result type.
|
|
2399
|
+
if let PlumType::TApp(_, rargs) = &*ret {
|
|
2400
|
+
if let Some(i) = gens.iter().position(|x| x == g) {
|
|
2401
|
+
if let Some(a) = rargs.get(i) {
|
|
2402
|
+
init_map.insert(g.clone(), a.clone());
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
recordCall(Some(&call.type_name), "init", init_map);
|
|
2065
2409
|
return Ok(substVars(&ret, &binds));
|
|
2066
2410
|
}
|
|
2067
2411
|
}
|
|
@@ -2545,7 +2889,13 @@ fn inferMethodCall(
|
|
|
2545
2889
|
ctx: &CheckCtx,
|
|
2546
2890
|
expected: Option<&PlumType>,
|
|
2547
2891
|
) -> Result<PlumType, String> {
|
|
2892
|
+
// `Bool.toStr` is a compiler intrinsic ("True"/"False"), not a source method.
|
|
2893
|
+
if matches!(obj_ty, PlumType::TBool) && name == "toStr" && args.is_empty() {
|
|
2894
|
+
return Ok(PlumType::TStr);
|
|
2895
|
+
}
|
|
2548
|
-
// A method on a generic parameter
|
|
2896
|
+
// A method on a generic parameter: a declared bound's signature if there is
|
|
2897
|
+
// one; otherwise duck-typed — checked when the enclosing generic body is
|
|
2898
|
+
// checked per concrete instantiation.
|
|
2549
2899
|
if let PlumType::TParam(p) = obj_ty {
|
|
2550
2900
|
let prefix = format!("{}{}:", BOUND_KEY, p);
|
|
2551
2901
|
for key in env.keys() {
|
|
@@ -2562,8 +2912,10 @@ fn inferMethodCall(
|
|
|
2562
2912
|
return Ok(substVars(ret, &binds));
|
|
2563
2913
|
}
|
|
2564
2914
|
}
|
|
2915
|
+
// Not a type error: the erased pass of a generic body just cannot type
|
|
2916
|
+
// this call (its errors are discarded; see `isResolutionError`).
|
|
2565
2917
|
return Err(format!(
|
|
2566
|
-
"'{}
|
|
2918
|
+
"'{}.{}' is resolved per instantiation",
|
|
2567
2919
|
p, name
|
|
2568
2920
|
));
|
|
2569
2921
|
}
|
|
@@ -2592,6 +2944,13 @@ fn inferMethodCall(
|
|
|
2592
2944
|
if isTraitName(&class_name, ctx) {
|
|
2593
2945
|
return Err(format!("trait '{}' has no method '{}'", class_name, name));
|
|
2594
2946
|
}
|
|
2947
|
+
// Inside an instantiated generic body the receiver is a concrete type:
|
|
2948
|
+
// a declared type lacking the method is a real (duck-typing) error.
|
|
2949
|
+
if INST_MODE.with(|m| m.get())
|
|
2950
|
+
&& ctx.enum_variants.values().any(|i| i.enum_name == class_name)
|
|
2951
|
+
{
|
|
2952
|
+
return Err(format!("{} has no method '{}'", obj_ty, name));
|
|
2953
|
+
}
|
|
2595
2954
|
// Unmodeled method (e.g. builtin/std): allow, codegen will catch.
|
|
2596
2955
|
return Ok(PlumType::TVar("_".to_string()));
|
|
2597
2956
|
};
|
|
@@ -2604,7 +2963,11 @@ fn inferMethodCall(
|
|
|
2604
2963
|
}
|
|
2605
2964
|
_ => BTreeMap::new(),
|
|
2606
2965
|
};
|
|
2966
|
+
let (inst, mut call_map) = instSigMap(sig, &owner);
|
|
2967
|
+
for (k, v) in &owner {
|
|
2968
|
+
call_map.insert(k.clone(), v.clone());
|
|
2969
|
+
}
|
|
2607
|
-
let PlumType::TFun(params, ret) =
|
|
2970
|
+
let PlumType::TFun(params, ret) = inst else {
|
|
2608
2971
|
unreachable!("instSig preserves the TFun shape")
|
|
2609
2972
|
};
|
|
2610
2973
|
let mut binds = Binds::new();
|
|
@@ -2618,6 +2981,7 @@ fn inferMethodCall(
|
|
|
2618
2981
|
.unwrap_or(params.len());
|
|
2619
2982
|
let label = format!("method '{}.{}'", class_name, name);
|
|
2620
2983
|
checkCallArgs(&label, ¶ms, args, required, env, ctx, &mut binds)?;
|
|
2984
|
+
recordCall(Some(&class_name), name, call_map);
|
|
2621
2985
|
Ok(substVars(&ret, &binds))
|
|
2622
2986
|
}
|
|
2623
2987
|
|
|
@@ -3013,8 +3377,13 @@ fn inferExprInner(
|
|
|
3013
3377
|
}
|
|
3014
3378
|
return Ok(substVars(&result, &binds));
|
|
3015
3379
|
}
|
|
3380
|
+
let mut call_map = BTreeMap::new();
|
|
3016
3381
|
let callee = match env.get(&call.name) {
|
|
3382
|
+
Some(s) if !s.vars.is_empty() => {
|
|
3017
|
-
|
|
3383
|
+
let (t, m) = instSigMap(&s.body, &BTreeMap::new());
|
|
3384
|
+
call_map = m;
|
|
3385
|
+
Ok(t)
|
|
3386
|
+
}
|
|
3018
3387
|
_ => lookup(env, &call.name),
|
|
3019
3388
|
};
|
|
3020
3389
|
match callee {
|
|
@@ -3030,6 +3399,7 @@ fn inferExprInner(
|
|
|
3030
3399
|
.unwrap_or(param_types.len());
|
|
3031
3400
|
let label = format!("call '{}'", call.name);
|
|
3032
3401
|
checkCallArgs(&label, ¶m_types, &call.args, required, env, ctx, &mut binds)?;
|
|
3402
|
+
recordCall(None, &call.name, call_map);
|
|
3033
3403
|
Ok(substVars(&ret, &binds))
|
|
3034
3404
|
}
|
|
3035
3405
|
Ok(_) => Err(format!("'{}' is not a function", call.name)),
|
plum-checker/src/types.rs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
use std::collections::BTreeMap;
|
|
2
2
|
|
|
3
|
-
#[derive(Debug, Clone, PartialEq)]
|
|
3
|
+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
4
4
|
pub enum PlumType {
|
|
5
5
|
TInt,
|
|
6
6
|
TFloat,
|
plum-checker/tests/checker_tests.rs
CHANGED
|
@@ -1983,12 +1983,87 @@ fn nested_generic_field() {
|
|
|
1983
1983
|
}
|
|
1984
1984
|
|
|
1985
1985
|
#[test]
|
|
1986
|
-
fn
|
|
1986
|
+
fn unbounded_tparam_method_now_ok() {
|
|
1987
|
+
// Bounds are ignored: a method on a bare `T` is checked per instantiation.
|
|
1987
1988
|
let r = checkSource(&parseClean("fun f(x: T) -> Int =\n x.hash()\n"));
|
|
1989
|
+
assert!(r.is_ok(), "uninstantiated unbounded T must not error: {:?}", r.err());
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
const DUCK_DEFS: &str = "\
|
|
1993
|
+
enum Num =
|
|
1994
|
+
| Num(v: Int)
|
|
1995
|
+
|
|
1996
|
+
fun lt(self, o: Num) -> Bool =
|
|
1997
|
+
self.v < o.v
|
|
1998
|
+
|
|
1999
|
+
enum Plain =
|
|
2000
|
+
| Plain(v: Int)
|
|
2001
|
+
|
|
2002
|
+
fun maxOf(a: T, b: T) -> T =
|
|
2003
|
+
a.lt(b) ? b : a
|
|
2004
|
+
|
|
2005
|
+
";
|
|
2006
|
+
|
|
1988
|
-
|
|
2007
|
+
#[test]
|
|
2008
|
+
fn duck_ok() {
|
|
1989
|
-
|
|
2009
|
+
let src = format!(
|
|
1990
|
-
"
|
|
2010
|
+
"{}fun main() -> Num =\n maxOf(Num(v: 1), Num(v: 2))\n",
|
|
2011
|
+
DUCK_DEFS
|
|
2012
|
+
);
|
|
2013
|
+
let r = checkSource(&parseClean(&src));
|
|
2014
|
+
assert!(r.is_ok(), "{:?}", r.err());
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
#[test]
|
|
2018
|
+
fn duck_missing_method_chain() {
|
|
2019
|
+
let src = format!(
|
|
2020
|
+
"{}fun main() -> Plain =\n maxOf(Plain(v: 1), Plain(v: 2))\n",
|
|
2021
|
+
DUCK_DEFS
|
|
1991
2022
|
);
|
|
2023
|
+
let msg = errText(checkSource(&parseClean(&src)));
|
|
2024
|
+
assert!(msg.contains("called at"), "{}", msg);
|
|
2025
|
+
assert!(msg.contains("in maxOf[Plain]"), "{}", msg);
|
|
2026
|
+
assert!(msg.contains("Plain has no method 'lt'"), "{}", msg);
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
#[test]
|
|
2030
|
+
fn uninstantiated_generic_not_type_errored() {
|
|
2031
|
+
let src = format!("{}fun unused(a: T) -> Int =\n a.nothing() + \"s\"\n", DUCK_DEFS);
|
|
2032
|
+
let r = checkSource(&parseClean(&src));
|
|
2033
|
+
assert!(r.is_ok(), "{:?}", r.err());
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
#[test]
|
|
2037
|
+
fn nested_instantiation_chain() {
|
|
2038
|
+
let src = format!(
|
|
2039
|
+
"{}fun outer(a: T) -> T =\n maxOf(a, a)\n\nfun main() -> Plain =\n outer(Plain(v: 1))\n",
|
|
2040
|
+
DUCK_DEFS
|
|
2041
|
+
);
|
|
2042
|
+
let msg = errText(checkSource(&parseClean(&src)));
|
|
2043
|
+
let first = msg.find("in outer[Plain]").expect(&msg);
|
|
2044
|
+
let second = msg.find("in maxOf[Plain]").expect(&msg);
|
|
2045
|
+
assert!(first < second, "{}", msg);
|
|
2046
|
+
assert!(msg.matches("called at").count() >= 2, "{}", msg);
|
|
2047
|
+
assert!(msg.contains("Plain has no method 'lt'"), "{}", msg);
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
#[test]
|
|
2051
|
+
fn instantiation_set_listed() {
|
|
2052
|
+
let src = format!(
|
|
2053
|
+
"{}fun main() -> Int =\n a := List(items: 1).map(|n| n * 2)\n b := List(items: \"x\").map(|s| s)\n 0\n",
|
|
2054
|
+
GENERIC_DEFS
|
|
2055
|
+
);
|
|
2056
|
+
let info = plum_checker::typedSource(&parseClean(&src)).unwrap_or_else(|e| panic!("{:?}", e));
|
|
2057
|
+
let has = |t: PlumType| {
|
|
2058
|
+
info.instantiations
|
|
2059
|
+
.iter()
|
|
2060
|
+
.any(|i| i.item == "List.map" && i.type_args.first() == Some(&t))
|
|
2061
|
+
};
|
|
2062
|
+
assert!(has(PlumType::TInt) && has(PlumType::TStr), "{:?}", info.instantiations);
|
|
2063
|
+
for i in &info.instantiations {
|
|
2064
|
+
assert_eq!(info.inst_index[&(i.item.clone(), i.type_args.clone())], i.id);
|
|
2065
|
+
}
|
|
2066
|
+
assert!(!info.inst_expr_types.is_empty());
|
|
1992
2067
|
}
|
|
1993
2068
|
|
|
1994
2069
|
#[test]
|
|
@@ -2077,9 +2152,13 @@ fun b() -> Str =
|
|
|
2077
2152
|
|
|
2078
2153
|
#[test]
|
|
2079
2154
|
fn generic_return_of_param_rejected_for_concrete_declared_return() {
|
|
2080
|
-
//
|
|
2155
|
+
// Duck typing: only an instantiation where `T` is not `Int` is rejected.
|
|
2081
|
-
assert!(checkSource(&parseClean("fun f(x: T) -> Int =\n x\n")).
|
|
2156
|
+
assert!(checkSource(&parseClean("fun f(x: T) -> Int =\n x\n")).is_ok());
|
|
2157
|
+
assert!(checkSource(&parseClean(
|
|
2158
|
+
"fun f(x: T) -> Int =\n x\n\nfun main() -> Int =\n f(\"s\")\n"
|
|
2159
|
+
))
|
|
2160
|
+
.is_err());
|
|
2082
|
-
let method = "enum Box[T] =\n | Box(value: T)\n\n fun get() -> Int =\n self.value\n";
|
|
2161
|
+
let method = "enum Box[T] =\n | Box(value: T)\n\n fun get() -> Int =\n self.value\n\nfun main() -> Int =\n Box(value: \"s\").get()\n";
|
|
2083
2162
|
assert!(checkSource(&parseClean(method)).is_err());
|
|
2084
2163
|
assert!(checkSource(&parseClean("fun f(x: T) -> T =\n x\n")).is_ok());
|
|
2085
2164
|
}
|
plum-core/src/loader.rs
CHANGED
|
@@ -57,8 +57,6 @@ pub fn loadAndMergeWithOverrides(
|
|
|
57
57
|
)?;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
injectBoolIntrinsics(&mut items);
|
|
61
|
-
|
|
62
60
|
Ok(Source {
|
|
63
61
|
module: entry_source.module,
|
|
64
62
|
imports: entry_source.imports,
|
|
@@ -67,32 +65,6 @@ pub fn loadAndMergeWithOverrides(
|
|
|
67
65
|
})
|
|
68
66
|
}
|
|
69
67
|
|
|
70
|
-
/// `Bool` is a built-in i32 with no source file; its one method, `toStr`
|
|
71
|
-
/// ("True"/"False" — the same text string interpolation emits), is a compiler
|
|
72
|
-
/// intrinsic. It is supplied as a synthetic method item so the checker, `ToStr`
|
|
73
|
-
/// trait conformance, dyn dispatch and codegen all see an ordinary method. Only
|
|
74
|
-
/// injected once `Str` is in the program (the method returns `Str`).
|
|
75
|
-
fn injectBoolIntrinsics(items: &mut Vec<Item>) {
|
|
76
|
-
let has_str = items
|
|
77
|
-
.iter()
|
|
78
|
-
.any(|i| matches!(i, Item::Enum(e) if e.name == "Str"));
|
|
79
|
-
if !has_str {
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
const SNIPPET: &str = "enum Bool =\n | Bool\n\n fun toStr(self) -> Str =\n self ? \"True\" : \"False\"\n";
|
|
83
|
-
let mut parser = tree_sitter::Parser::new();
|
|
84
|
-
parser
|
|
85
|
-
.set_language(&tree_sitter_plum::LANGUAGE.into())
|
|
86
|
-
.expect("plum grammar loads");
|
|
87
|
-
let tree = parser.parse(SNIPPET, None).expect("intrinsic snippet parses");
|
|
88
|
-
let source = AstParser::new(SNIPPET).parseSource(tree.root_node());
|
|
89
|
-
for item in source.items {
|
|
90
|
-
if let Item::Fn(f) = item {
|
|
91
|
-
items.push(Item::Fn(f));
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
68
|
fn canonicalizeMaybeMissing(path: &Path) -> Result<PathBuf, String> {
|
|
97
69
|
if let Ok(canon) = std::fs::canonicalize(path) {
|
|
98
70
|
return Ok(canon);
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -1760,6 +1760,8 @@ fn compileSourceInner(
|
|
|
1760
1760
|
// The source is compiled as written (generics erased, not specialized): the
|
|
1761
1761
|
// checker's per-expression instantiated types drive every box/unbox decision.
|
|
1762
1762
|
let types = typed.expr_types;
|
|
1763
|
+
// `Bool.toStr` is a checker/codegen builtin: compiled as one extra method, never added to the source.
|
|
1764
|
+
let bool_to_str_fn = plum_checker::needsBuiltinBoolToStr(source).then(plum_checker::builtinBoolToStr);
|
|
1763
1765
|
let (global_env, methods, enum_variants, enum_params) = plum_checker::buildGlobalTables(source);
|
|
1764
1766
|
let min_required = plum_checker::buildMinRequiredArgs(source);
|
|
1765
1767
|
|
|
@@ -2018,8 +2020,16 @@ fn compileSourceInner(
|
|
|
2018
2020
|
|
|
2019
2021
|
// Register every function AND method signature up front (methods get an implicit
|
|
2020
2022
|
// leading `self: pointer` param and are keyed as "Receiver::method").
|
|
2021
|
-
for
|
|
2023
|
+
for f in source
|
|
2024
|
+
.items
|
|
2025
|
+
.iter()
|
|
2026
|
+
.filter_map(|item| match item {
|
|
2022
|
-
|
|
2027
|
+
ast::Item::Fn(f) => Some(f),
|
|
2028
|
+
_ => None,
|
|
2029
|
+
})
|
|
2030
|
+
.chain(bool_to_str_fn.iter())
|
|
2031
|
+
{
|
|
2032
|
+
{
|
|
2023
2033
|
if f.is_extern {
|
|
2024
2034
|
continue;
|
|
2025
2035
|
}
|
|
@@ -2073,6 +2083,7 @@ fn compileSourceInner(
|
|
|
2073
2083
|
ast::Item::Fn(f) if !f.is_extern => Some(f),
|
|
2074
2084
|
_ => None,
|
|
2075
2085
|
})
|
|
2086
|
+
.chain(bool_to_str_fn.iter())
|
|
2076
2087
|
.collect();
|
|
2077
2088
|
|
|
2078
2089
|
// ---- Discovery pre-pass: find every closure literal in every function body. ----
|