plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
eaf49cb
— Peter John
2026-10-25T19:10:46+05:30
feat(checker): check generics structurally on the original source; add typedSource
- plum-checker/src/lib.rs +876 -418
- plum-checker/src/monomorphize.rs +2 -0
- plum-checker/tests/checker_tests.rs +211 -6
plum-checker/src/lib.rs
CHANGED
|
@@ -6,9 +6,264 @@ pub mod monomorphize;
|
|
|
6
6
|
pub mod types;
|
|
7
7
|
|
|
8
8
|
use plum_core::ast;
|
|
9
|
+
use std::cell::{Cell, RefCell};
|
|
9
|
-
use std::collections::BTreeMap;
|
|
10
|
+
use std::collections::{BTreeMap, HashMap};
|
|
10
11
|
use types::{CheckError, CheckResult, PlumType, TypeEnv, TypeScheme};
|
|
11
12
|
|
|
13
|
+
thread_local! {
|
|
14
|
+
/// Counter for the fresh `_t<N>` inference variables minted per generic call site.
|
|
15
|
+
static FRESH: Cell<u32> = const { Cell::new(0) };
|
|
16
|
+
/// When set, `inferExprWith` records every expression's type here (see `typedSource`).
|
|
17
|
+
static RECORD: RefCell<Option<HashMap<usize, PlumType>>> = const { RefCell::new(None) };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Per-expression instantiated types of a checked source, for codegen.
|
|
21
|
+
#[derive(Debug, Clone, Default)]
|
|
22
|
+
pub struct TypedInfo {
|
|
23
|
+
/// Expression address (`&ast::Expr as *const _ as usize`) -> its type.
|
|
24
|
+
pub expr_types: HashMap<usize, PlumType>,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn freshVar() -> PlumType {
|
|
28
|
+
FRESH.with(|c| {
|
|
29
|
+
let n = c.get();
|
|
30
|
+
c.set(n + 1);
|
|
31
|
+
PlumType::TVar(format!("_t{}", n))
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Binding of `_t<N>` inference variables to types, local to one call site.
|
|
36
|
+
type Binds = BTreeMap<String, PlumType>;
|
|
37
|
+
|
|
38
|
+
fn isInferVar(name: &str) -> bool {
|
|
39
|
+
name.starts_with("_t")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Replaces bound `_t<N>` variables (to a fixpoint, so chains resolve).
|
|
43
|
+
fn substVars(ty: &PlumType, b: &Binds) -> PlumType {
|
|
44
|
+
if b.is_empty() {
|
|
45
|
+
return ty.clone();
|
|
46
|
+
}
|
|
47
|
+
fn go(ty: &PlumType, b: &Binds, depth: u32) -> PlumType {
|
|
48
|
+
use PlumType::*;
|
|
49
|
+
match ty {
|
|
50
|
+
TVar(n) if depth < 8 => match b.get(n) {
|
|
51
|
+
Some(t) => go(t, b, depth + 1),
|
|
52
|
+
None => ty.clone(),
|
|
53
|
+
},
|
|
54
|
+
TApp(n, args) => TApp(n.clone(), args.iter().map(|a| go(a, b, depth)).collect()),
|
|
55
|
+
TFun(ps, r) => TFun(
|
|
56
|
+
ps.iter().map(|p| go(p, b, depth)).collect(),
|
|
57
|
+
Box::new(go(r, b, depth)),
|
|
58
|
+
),
|
|
59
|
+
TVariadic(i) => TVariadic(Box::new(go(i, b, depth))),
|
|
60
|
+
TTags(tags) => TTags(
|
|
61
|
+
tags.iter()
|
|
62
|
+
.map(|(k, v)| (k.clone(), v.as_ref().map(|t| Box::new(go(t, b, depth)))))
|
|
63
|
+
.collect(),
|
|
64
|
+
),
|
|
65
|
+
_ => ty.clone(),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
go(ty, b, 0)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Free generic parameter names in `ty`, first-appearance order.
|
|
72
|
+
fn freeTParams(ty: &PlumType, out: &mut Vec<String>) {
|
|
73
|
+
use PlumType::*;
|
|
74
|
+
match ty {
|
|
75
|
+
TParam(n) => {
|
|
76
|
+
if !out.contains(n) {
|
|
77
|
+
out.push(n.clone());
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
TApp(_, args) => args.iter().for_each(|a| freeTParams(a, out)),
|
|
81
|
+
TFun(ps, r) => {
|
|
82
|
+
ps.iter().for_each(|p| freeTParams(p, out));
|
|
83
|
+
freeTParams(r, out);
|
|
84
|
+
}
|
|
85
|
+
TVariadic(i) => freeTParams(i, out),
|
|
86
|
+
TTags(tags) => tags
|
|
87
|
+
.values()
|
|
88
|
+
.flatten()
|
|
89
|
+
.for_each(|t| freeTParams(t, out)),
|
|
90
|
+
_ => {}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// Instantiates every free `TParam` in `sig`: those named in `owner` take the
|
|
95
|
+
/// owner's (receiver's) type, all others get a fresh inference variable.
|
|
96
|
+
fn instSig(sig: &PlumType, owner: &BTreeMap<String, PlumType>) -> PlumType {
|
|
97
|
+
let mut names = Vec::new();
|
|
98
|
+
freeTParams(sig, &mut names);
|
|
99
|
+
let map: BTreeMap<String, PlumType> = names
|
|
100
|
+
.into_iter()
|
|
101
|
+
.map(|n| {
|
|
102
|
+
let t = owner.get(&n).cloned().unwrap_or_else(freshVar);
|
|
103
|
+
(n, t)
|
|
104
|
+
})
|
|
105
|
+
.collect();
|
|
106
|
+
types::substitute(sig, &map)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// One-way matching: binds the inference variables of `pattern` from `actual`.
|
|
110
|
+
fn bindMatch(pattern: &PlumType, actual: &PlumType, b: &mut Binds) {
|
|
111
|
+
use PlumType::*;
|
|
112
|
+
match (pattern, actual) {
|
|
113
|
+
(TVar(n), a) if isInferVar(n) => {
|
|
114
|
+
if matches!(a, TVar(_)) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
match b.get(n).cloned() {
|
|
118
|
+
None => {
|
|
119
|
+
b.insert(n.clone(), a.clone());
|
|
120
|
+
}
|
|
121
|
+
// A variable standing for a whole tag union widens to cover all
|
|
122
|
+
// the unions seen for it (E is covariant).
|
|
123
|
+
Some(TTags(old)) if matches!(a, TTags(_)) => {
|
|
124
|
+
let TTags(new) = a else { unreachable!() };
|
|
125
|
+
let mut merged = old;
|
|
126
|
+
for (k, v) in new {
|
|
127
|
+
merged.entry(k.clone()).or_insert_with(|| v.clone());
|
|
128
|
+
}
|
|
129
|
+
b.insert(n.clone(), TTags(merged));
|
|
130
|
+
}
|
|
131
|
+
Some(bound) => bindMatch(&bound, a, b),
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
(TApp(x, xs), TApp(y, ys)) if x == y && xs.len() == ys.len() => {
|
|
135
|
+
for (p, a) in xs.iter().zip(ys) {
|
|
136
|
+
bindMatch(p, a, b);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
(TFun(ps, r), TFun(qs, s)) if ps.len() == qs.len() => {
|
|
140
|
+
for (p, a) in ps.iter().zip(qs) {
|
|
141
|
+
bindMatch(p, a, b);
|
|
142
|
+
}
|
|
143
|
+
bindMatch(r, s, b);
|
|
144
|
+
}
|
|
145
|
+
(TVariadic(p), TVariadic(a)) => bindMatch(p, a, b),
|
|
146
|
+
(TTags(p), TTags(a)) => {
|
|
147
|
+
for (tag, pp) in p {
|
|
148
|
+
if let (Some(pp), Some(Some(ap))) = (pp, a.get(tag)) {
|
|
149
|
+
bindMatch(pp, ap, b);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
_ => {}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
fn isGenericName(n: &str) -> bool {
|
|
158
|
+
monomorphize::isGenericParamName(n)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// Structural type conversion for UN-monomorphized source: `List[Int]` ->
|
|
162
|
+
/// `TApp`, a single-uppercase-letter name (the grammar's generic parameter
|
|
163
|
+
/// spelling) -> `TParam`.
|
|
164
|
+
pub fn plumTypeFromAstG(ty: &ast::Type) -> PlumType {
|
|
165
|
+
if !ty.generics.is_empty() {
|
|
166
|
+
return PlumType::TApp(
|
|
167
|
+
ty.name.clone(),
|
|
168
|
+
ty.generics.iter().map(plumTypeFromAstG).collect(),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
if isGenericName(&ty.name) {
|
|
172
|
+
return PlumType::TParam(ty.name.clone());
|
|
173
|
+
}
|
|
174
|
+
plumTypeFromName(&ty.name)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fn paramTypeG(pt: &ast::ParamType) -> PlumType {
|
|
178
|
+
match pt {
|
|
179
|
+
ast::ParamType::Type(t) => plumTypeFromAstG(t),
|
|
180
|
+
ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(plumTypeFromAstG(t))),
|
|
181
|
+
ast::ParamType::Fn(params, ret) => PlumType::TFun(
|
|
182
|
+
params.iter().map(plumTypeFromAstG).collect(),
|
|
183
|
+
Box::new(
|
|
184
|
+
ret.as_ref()
|
|
185
|
+
.map(|r| plumTypeFromAstG(r))
|
|
186
|
+
.unwrap_or(PlumType::TUnit),
|
|
187
|
+
),
|
|
188
|
+
),
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// The generic parameter names of enum `name` (empty if non-generic/unknown).
|
|
193
|
+
fn enumGenericsOf<'a>(name: &str, ctx: &'a CheckCtx) -> &'a [String] {
|
|
194
|
+
ctx.enum_variants
|
|
195
|
+
.values()
|
|
196
|
+
.find(|i| i.enum_name == name)
|
|
197
|
+
.map(|i| i.generics.as_slice())
|
|
198
|
+
.unwrap_or(&[])
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
fn appSubst(generics: &[String], args: &[PlumType]) -> BTreeMap<String, PlumType> {
|
|
202
|
+
generics.iter().cloned().zip(args.iter().cloned()).collect()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/// A variant's payload field types with the enum's generics replaced by the
|
|
206
|
+
/// type arguments of `ty` (when it is a `TApp`).
|
|
207
|
+
fn variantFields(info: &EnumVariantInfo, ty: &PlumType) -> Vec<PlumType> {
|
|
208
|
+
match ty {
|
|
209
|
+
PlumType::TApp(_, args) if args.len() == info.generics.len() => {
|
|
210
|
+
let s = appSubst(&info.generics, args);
|
|
211
|
+
info.field_types
|
|
212
|
+
.iter()
|
|
213
|
+
.map(|f| types::substitute(f, &s))
|
|
214
|
+
.collect()
|
|
215
|
+
}
|
|
216
|
+
_ if info.generics.is_empty() => info.field_types.clone(),
|
|
217
|
+
// A generic variant seen through a non-parameterized type: unknown arguments.
|
|
218
|
+
_ => {
|
|
219
|
+
let s: BTreeMap<String, PlumType> = info
|
|
220
|
+
.generics
|
|
221
|
+
.iter()
|
|
222
|
+
.map(|g| (g.clone(), PlumType::TVar("_".to_string())))
|
|
223
|
+
.collect();
|
|
224
|
+
info.field_types
|
|
225
|
+
.iter()
|
|
226
|
+
.map(|f| types::substitute(f, &s))
|
|
227
|
+
.collect()
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/// `Ok(3)`-style result type of a variant of enum `info.enum_name`.
|
|
233
|
+
fn enumTypeWith(info: &EnumVariantInfo, owner: &BTreeMap<String, PlumType>) -> PlumType {
|
|
234
|
+
if info.generics.is_empty() {
|
|
235
|
+
PlumType::TNamed(info.enum_name.clone())
|
|
236
|
+
} else {
|
|
237
|
+
PlumType::TApp(
|
|
238
|
+
info.enum_name.clone(),
|
|
239
|
+
info.generics
|
|
240
|
+
.iter()
|
|
241
|
+
.map(|g| owner.get(g).cloned().unwrap_or(PlumType::TVar("_".into())))
|
|
242
|
+
.collect(),
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
fn freshOwner(generics: &[String]) -> BTreeMap<String, PlumType> {
|
|
248
|
+
generics.iter().map(|g| (g.clone(), freshVar())).collect()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
fn recordType(expr: &ast::Expr, ty: &PlumType) {
|
|
252
|
+
RECORD.with(|r| {
|
|
253
|
+
if let Some(m) = r.borrow_mut().as_mut() {
|
|
254
|
+
m.insert(expr as *const ast::Expr as usize, ty.clone());
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/// Base name of a nominal type (`TNamed`/`TApp`).
|
|
260
|
+
fn nominalName(t: &PlumType) -> Option<&str> {
|
|
261
|
+
match t {
|
|
262
|
+
PlumType::TNamed(n) | PlumType::TApp(n, _) => Some(n),
|
|
263
|
+
_ => None,
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
12
267
|
/// Maps a bare type name (as written in source: a param/return annotation, or
|
|
13
268
|
/// a method's receiver name) to its `PlumType`. Builtin primitives get their
|
|
14
269
|
/// dedicated variant; anything else is an unmodeled `TNamed`.
|
|
@@ -139,6 +394,13 @@ pub fn tagUnionSegmentsCompatible(expected: &str, actual: &str, ctx: &CheckCtx)
|
|
|
139
394
|
|
|
140
395
|
/// Reserved `TypeEnv` key holding the enclosing function's declared return type.
|
|
141
396
|
const RETURN_KEY: &str = "$return";
|
|
397
|
+
/// `TypeEnv` key prefix marking a name as a top-level function (a local of the
|
|
398
|
+
/// same name shadows it instead of being a redeclaration).
|
|
399
|
+
const GLOBAL_KEY: &str = "$global:";
|
|
400
|
+
/// `TypeEnv` key prefix for a trait method's signature (`$trait:Hashable:hash`).
|
|
401
|
+
const TRAIT_KEY: &str = "$trait:";
|
|
402
|
+
/// `TypeEnv` key prefix for a generic parameter's trait bound (`$bound:K:Hashable`).
|
|
403
|
+
const BOUND_KEY: &str = "$bound:";
|
|
142
404
|
|
|
143
405
|
pub fn plumTypeFromAst(ty: &ast::Type) -> PlumType {
|
|
144
406
|
plumTypeFromName(&ty.name)
|
|
@@ -152,7 +414,7 @@ pub fn plumTypeFromAst(ty: &ast::Type) -> PlumType {
|
|
|
152
414
|
/// the same receiver name string `ctx.methods` is keyed by.
|
|
153
415
|
pub fn methodReceiverName(ty: &PlumType) -> Option<String> {
|
|
154
416
|
match ty {
|
|
155
|
-
PlumType::TNamed(name) => Some(name.clone()),
|
|
417
|
+
PlumType::TNamed(name) | PlumType::TApp(name, _) => Some(name.clone()),
|
|
156
418
|
PlumType::TInt => Some("Int".to_string()),
|
|
157
419
|
PlumType::TFloat => Some("Float".to_string()),
|
|
158
420
|
PlumType::TBool => Some("Bool".to_string()),
|
|
@@ -197,6 +459,33 @@ pub fn unify(t1: &PlumType, t2: &PlumType, ctx: &CheckCtx) -> Result<(), String>
|
|
|
197
459
|
(PlumType::TByteSlice, PlumType::TByteSlice) => Ok(()),
|
|
198
460
|
(PlumType::TUnit, PlumType::TUnit) => Ok(()),
|
|
199
461
|
(PlumType::TNamed(a), PlumType::TNamed(b)) if a == b => Ok(()),
|
|
462
|
+
// Structural generics: same base, arguments unify pairwise (tag unions
|
|
463
|
+
// inside the arguments stay covariant through the TTags arm).
|
|
464
|
+
(PlumType::TApp(a, xs), PlumType::TApp(b, ys)) if a == b && xs.len() == ys.len() => {
|
|
465
|
+
for (x, y) in xs.iter().zip(ys.iter()) {
|
|
466
|
+
unify(x, y, ctx).map_err(|e| {
|
|
467
|
+
format!("type mismatch: expected {}, found {} ({})", t1, t2, e)
|
|
468
|
+
})?;
|
|
469
|
+
}
|
|
470
|
+
Ok(())
|
|
471
|
+
}
|
|
472
|
+
(PlumType::TParam(a), PlumType::TParam(b)) if a == b => Ok(()),
|
|
473
|
+
// A generic type used bare (`o: Link`) accepts any instantiation.
|
|
474
|
+
(PlumType::TApp(a, _), PlumType::TNamed(b))
|
|
475
|
+
| (PlumType::TNamed(a), PlumType::TApp(b, _))
|
|
476
|
+
if a == b =>
|
|
477
|
+
{
|
|
478
|
+
Ok(())
|
|
479
|
+
}
|
|
480
|
+
// A bare variant name as a type against its generic enum (either direction).
|
|
481
|
+
(PlumType::TApp(a, _), PlumType::TNamed(b)) | (PlumType::TNamed(b), PlumType::TApp(a, _))
|
|
482
|
+
if ctx
|
|
483
|
+
.enum_variants
|
|
484
|
+
.get(b)
|
|
485
|
+
.is_some_and(|info| &info.enum_name == a) =>
|
|
486
|
+
{
|
|
487
|
+
Ok(())
|
|
488
|
+
}
|
|
200
489
|
// Tag-union subtyping: every tag of `actual` must exist in `expected`
|
|
201
490
|
// with a unifying payload.
|
|
202
491
|
(PlumType::TTags(exp), PlumType::TTags(act)) => {
|
|
@@ -274,6 +563,22 @@ pub fn unify(t1: &PlumType, t2: &PlumType, ctx: &CheckCtx) -> Result<(), String>
|
|
|
274
563
|
}
|
|
275
564
|
}
|
|
276
565
|
|
|
566
|
+
/// Return-position check: a value still typed by a bare generic parameter is
|
|
567
|
+
/// accepted for a concrete declared return (`fun get() -> Int = self.value` in
|
|
568
|
+
/// a `Box[T]` is only ever instantiated at `Int` by existing code), everything
|
|
569
|
+
/// else goes through `unifyArg`.
|
|
570
|
+
fn unifyReturn(
|
|
571
|
+
declared: &PlumType,
|
|
572
|
+
actual: &PlumType,
|
|
573
|
+
actual_expr: &ast::Expr,
|
|
574
|
+
ctx: &CheckCtx,
|
|
575
|
+
) -> Result<(), String> {
|
|
576
|
+
if matches!(actual, PlumType::TParam(_)) && !matches!(declared, PlumType::TParam(_)) {
|
|
577
|
+
return Ok(());
|
|
578
|
+
}
|
|
579
|
+
unifyArg(declared, actual, actual_expr, ctx)
|
|
580
|
+
}
|
|
581
|
+
|
|
277
582
|
/// True if `expr` (looking through parens) is a direct construction/reference
|
|
278
583
|
/// of exactly the enum variant named `variant_name` — a bare payload-free
|
|
279
584
|
/// reference (`Red`), a positional-payload construction (`Circle(1)`), or a
|
|
@@ -344,6 +649,9 @@ pub struct EnumVariantInfo {
|
|
|
344
649
|
/// form (`Next(T)`). See `ast::EnumVariant::field_names`.
|
|
345
650
|
pub field_names: Vec<String>,
|
|
346
651
|
pub values: Vec<ast::Expr>,
|
|
652
|
+
/// The owning enum's generic parameter names (`[T, E]` for `Try`) —
|
|
653
|
+
/// empty for a non-generic enum and for monomorphized specializations.
|
|
654
|
+
pub generics: Vec<String>,
|
|
347
655
|
}
|
|
348
656
|
/// Enum variant name -> its info, e.g. `"True" -> { enum_name: "Bool", tag: 1, field_types: [] }`.
|
|
349
657
|
pub type EnumVariants = BTreeMap<String, EnumVariantInfo>;
|
|
@@ -396,6 +704,23 @@ pub struct CheckCtx<'a> {
|
|
|
396
704
|
/// `plum-wasm-codegen`, which needs the same tables to resolve `self`, field access,
|
|
397
705
|
/// and method dispatch during code generation.
|
|
398
706
|
pub fn buildGlobalTables(source: &ast::Source) -> (TypeEnv, MethodEnv, EnumVariants, EnumParams) {
|
|
707
|
+
buildTables(source, false)
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/// `generic == true` builds the structural tables for UN-monomorphized source
|
|
711
|
+
/// (`TApp`/`TParam`, generic schemes, trait signatures); `false` is the legacy
|
|
712
|
+
/// shape codegen uses on monomorphized source.
|
|
713
|
+
pub fn buildTables(
|
|
714
|
+
source: &ast::Source,
|
|
715
|
+
generic: bool,
|
|
716
|
+
) -> (TypeEnv, MethodEnv, EnumVariants, EnumParams) {
|
|
717
|
+
let conv = |t: &ast::Type| {
|
|
718
|
+
if generic {
|
|
719
|
+
plumTypeFromAstG(t)
|
|
720
|
+
} else {
|
|
721
|
+
plumTypeFromAst(t)
|
|
722
|
+
}
|
|
723
|
+
};
|
|
399
724
|
let mut global_env: TypeEnv = TypeEnv::new();
|
|
400
725
|
let mut methods: MethodEnv = BTreeMap::new();
|
|
401
726
|
let mut enum_variants: EnumVariants = BTreeMap::new();
|
|
@@ -420,12 +745,12 @@ pub fn buildGlobalTables(source: &ast::Source) -> (TypeEnv, MethodEnv, EnumVaria
|
|
|
420
745
|
match item {
|
|
421
746
|
ast::Item::Enum(e) => {
|
|
422
747
|
let shared_field_types: Vec<PlumType> =
|
|
423
|
-
e.params.iter().map(|p|
|
|
748
|
+
e.params.iter().map(|p| conv(&p.ty)).collect();
|
|
424
749
|
if !e.params.is_empty() {
|
|
425
750
|
let params = e
|
|
426
751
|
.params
|
|
427
752
|
.iter()
|
|
428
|
-
.map(|p| (p.name.clone(),
|
|
753
|
+
.map(|p| (p.name.clone(), conv(&p.ty)))
|
|
429
754
|
.collect();
|
|
430
755
|
enum_params.insert(e.name.clone(), params);
|
|
431
756
|
}
|
|
@@ -466,7 +791,7 @@ pub fn buildGlobalTables(source: &ast::Source) -> (TypeEnv, MethodEnv, EnumVaria
|
|
|
466
791
|
(vec![PlumType::TNamed(v.name.clone())], Vec::new())
|
|
467
792
|
} else {
|
|
468
793
|
(
|
|
469
|
-
v.fields.iter().map(
|
|
794
|
+
v.fields.iter().map(conv).collect(),
|
|
470
795
|
v.field_names.clone(),
|
|
471
796
|
)
|
|
472
797
|
};
|
|
@@ -478,6 +803,11 @@ pub fn buildGlobalTables(source: &ast::Source) -> (TypeEnv, MethodEnv, EnumVaria
|
|
|
478
803
|
field_types,
|
|
479
804
|
field_names,
|
|
480
805
|
values: v.values.clone(),
|
|
806
|
+
generics: if generic {
|
|
807
|
+
monomorphize::enumGenericParams(e)
|
|
808
|
+
} else {
|
|
809
|
+
Vec::new()
|
|
810
|
+
},
|
|
481
811
|
},
|
|
482
812
|
);
|
|
483
813
|
}
|
|
@@ -496,28 +826,36 @@ pub fn buildGlobalTables(source: &ast::Source) -> (TypeEnv, MethodEnv, EnumVaria
|
|
|
496
826
|
.params
|
|
497
827
|
.iter()
|
|
498
828
|
.map(|p| match &p.ty {
|
|
499
|
-
ast::ParamType::Type(t) =>
|
|
829
|
+
ast::ParamType::Type(t) => conv(t),
|
|
500
|
-
ast::ParamType::Variadic(t) => {
|
|
501
|
-
|
|
830
|
+
ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(conv(t))),
|
|
502
|
-
}
|
|
503
831
|
ast::ParamType::Fn(params, ret) => {
|
|
504
|
-
let param_types = params.iter().map(
|
|
832
|
+
let param_types = params.iter().map(&conv).collect();
|
|
505
833
|
let ret_ty = ret
|
|
506
834
|
.as_ref()
|
|
507
|
-
.map(|r|
|
|
835
|
+
.map(|r| conv(r))
|
|
508
836
|
.unwrap_or(PlumType::TUnit);
|
|
509
837
|
PlumType::TFun(param_types, Box::new(ret_ty))
|
|
510
838
|
}
|
|
511
839
|
})
|
|
512
840
|
.collect();
|
|
513
|
-
let ret = f
|
|
514
|
-
.returns
|
|
515
|
-
.as_ref()
|
|
516
|
-
.map(plumTypeFromAst)
|
|
517
|
-
|
|
841
|
+
let ret = f.returns.as_ref().map(&conv).unwrap_or(PlumType::TUnit);
|
|
518
842
|
let fn_ty = PlumType::TFun(param_types, Box::new(ret));
|
|
519
843
|
if let Some(recv) = &f.type_param {
|
|
520
844
|
methods.insert((recv.clone(), f.name.clone()), fn_ty);
|
|
845
|
+
} else if generic {
|
|
846
|
+
global_env.insert(
|
|
847
|
+
format!("{}{}", GLOBAL_KEY, f.name),
|
|
848
|
+
TypeScheme::mono(PlumType::TUnit),
|
|
849
|
+
);
|
|
850
|
+
let mut vars = Vec::new();
|
|
851
|
+
freeTParams(&fn_ty, &mut vars);
|
|
852
|
+
global_env.insert(
|
|
853
|
+
f.name.clone(),
|
|
854
|
+
TypeScheme {
|
|
855
|
+
vars,
|
|
856
|
+
body: Box::new(fn_ty),
|
|
857
|
+
},
|
|
858
|
+
);
|
|
521
859
|
} else {
|
|
522
860
|
global_env.insert(f.name.clone(), TypeScheme::mono(fn_ty));
|
|
523
861
|
}
|
|
@@ -534,6 +872,17 @@ pub fn buildGlobalTables(source: &ast::Source) -> (TypeEnv, MethodEnv, EnumVaria
|
|
|
534
872
|
};
|
|
535
873
|
global_env.insert(c.name.clone(), TypeScheme::mono(ty));
|
|
536
874
|
}
|
|
875
|
+
ast::Item::Trait(t) if generic => {
|
|
876
|
+
// Trait method signatures, looked up for calls on a bounded `TParam`.
|
|
877
|
+
for m in &t.methods {
|
|
878
|
+
let params: Vec<PlumType> = m.params.iter().map(|p| paramTypeG(&p.ty)).collect();
|
|
879
|
+
let ret = m.returns.as_ref().map(&conv).unwrap_or(PlumType::TUnit);
|
|
880
|
+
global_env.insert(
|
|
881
|
+
format!("{}{}:{}", TRAIT_KEY, t.name, m.name),
|
|
882
|
+
TypeScheme::mono(PlumType::TFun(params, Box::new(ret))),
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
537
886
|
_ => {}
|
|
538
887
|
}
|
|
539
888
|
}
|
|
@@ -652,12 +1001,26 @@ fn checkTraitConformance(source: &ast::Source) -> Vec<CheckError> {
|
|
|
652
1001
|
}
|
|
653
1002
|
|
|
654
1003
|
pub fn checkSource(source: &ast::Source) -> CheckResult<()> {
|
|
1004
|
+
checkSourceInner(source)
|
|
1005
|
+
}
|
|
1006
|
+
|
|
655
|
-
|
|
1007
|
+
/// Type-checks the original source like `checkSource` and also returns the
|
|
1008
|
+
/// instantiated type of every expression (keyed by expression address) for codegen.
|
|
1009
|
+
pub fn typedSource(source: &ast::Source) -> CheckResult<TypedInfo> {
|
|
1010
|
+
RECORD.with(|r| *r.borrow_mut() = Some(HashMap::new()));
|
|
1011
|
+
let result = checkSourceInner(source);
|
|
1012
|
+
let recorded = RECORD.with(|r| r.borrow_mut().take()).unwrap_or_default();
|
|
1013
|
+
result.map(|()| TypedInfo {
|
|
1014
|
+
expr_types: recorded,
|
|
1015
|
+
})
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
fn checkSourceInner(source: &ast::Source) -> CheckResult<()> {
|
|
656
|
-
//
|
|
1019
|
+
// Generic items are checked once, structurally (`TApp`/`TParam`), on the
|
|
1020
|
+
// ORIGINAL source — no monomorphization.
|
|
657
1021
|
let mut errors: Vec<CheckError> = checkTraitConformance(source);
|
|
658
|
-
let source = monomorphize::monomorphizeSource(source).map_err(|e| vec![CheckError::new(e)])?;
|
|
659
|
-
let (global_env, methods, enum_variants, enum_params) =
|
|
1022
|
+
let (global_env, methods, enum_variants, enum_params) = buildTables(source, true);
|
|
660
|
-
let min_required = buildMinRequiredArgs(
|
|
1023
|
+
let min_required = buildMinRequiredArgs(source);
|
|
661
1024
|
let ctx = CheckCtx {
|
|
662
1025
|
methods: &methods,
|
|
663
1026
|
enum_variants: &enum_variants,
|
|
@@ -721,9 +1084,17 @@ pub fn checkSource(source: &ast::Source) -> CheckResult<()> {
|
|
|
721
1084
|
}
|
|
722
1085
|
}
|
|
723
1086
|
|
|
1087
|
+
let enums_by_name: BTreeMap<&str, &ast::Enum> = source
|
|
1088
|
+
.items
|
|
1089
|
+
.iter()
|
|
1090
|
+
.filter_map(|i| match i {
|
|
1091
|
+
ast::Item::Enum(e) => Some((e.name.as_str(), e)),
|
|
1092
|
+
_ => None,
|
|
1093
|
+
})
|
|
1094
|
+
.collect();
|
|
724
1095
|
for item in &source.items {
|
|
725
1096
|
if let ast::Item::Fn(f) = item {
|
|
726
|
-
let mut local_errors = checkFn(f, &global_env, &ctx);
|
|
1097
|
+
let mut local_errors = checkFn(f, &global_env, &ctx, &enums_by_name);
|
|
727
1098
|
errors.append(&mut local_errors);
|
|
728
1099
|
}
|
|
729
1100
|
}
|
|
@@ -750,7 +1121,12 @@ pub fn checkSource(source: &ast::Source) -> CheckResult<()> {
|
|
|
750
1121
|
}
|
|
751
1122
|
}
|
|
752
1123
|
|
|
1124
|
+
fn checkFn(
|
|
1125
|
+
f: &ast::Fn,
|
|
1126
|
+
global_env: &TypeEnv,
|
|
1127
|
+
ctx: &CheckCtx,
|
|
753
|
-
|
|
1128
|
+
enums: &BTreeMap<&str, &ast::Enum>,
|
|
1129
|
+
) -> Vec<CheckError> {
|
|
754
1130
|
let mut errors = Vec::new();
|
|
755
1131
|
|
|
756
1132
|
// `is_extern` and `body == FnBody::Extern` must agree — either both (a genuine
|
|
@@ -817,45 +1193,46 @@ fn checkFn(f: &ast::Fn, global_env: &TypeEnv, ctx: &CheckCtx) -> Vec<CheckError>
|
|
|
817
1193
|
|
|
818
1194
|
// Methods (`name<Receiver>(...)`) get an implicit `self: Receiver` binding.
|
|
819
1195
|
if let Some(recv) = &f.type_param {
|
|
1196
|
+
let generics = enumGenericsOf(recv, ctx);
|
|
820
|
-
let
|
|
1197
|
+
let self_ty = if generics.is_empty() {
|
|
1198
|
+
plumTypeFromName(recv)
|
|
1199
|
+
} else {
|
|
1200
|
+
PlumType::TApp(
|
|
821
|
-
|
|
1201
|
+
recv.clone(),
|
|
822
|
-
|
|
1202
|
+
generics.iter().map(|g| PlumType::TParam(g.clone())).collect(),
|
|
1203
|
+
)
|
|
823
1204
|
};
|
|
1205
|
+
env.insert("self".to_string(), TypeScheme::mono(self_ty));
|
|
1206
|
+
// The owner's declared trait bounds (`K: Hashable`) apply in its methods.
|
|
1207
|
+
if let Some(e) = enums.get(recv.as_str()) {
|
|
1208
|
+
for g in &e.generics {
|
|
1209
|
+
for b in &g.bounds {
|
|
824
|
-
|
|
1210
|
+
env.insert(
|
|
825
|
-
|
|
1211
|
+
format!("{}{}:{}", BOUND_KEY, g.name, b),
|
|
826
|
-
|
|
1212
|
+
TypeScheme::mono(PlumType::TUnit),
|
|
827
|
-
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
828
1217
|
}
|
|
829
1218
|
|
|
830
1219
|
// Add params to env
|
|
831
1220
|
for p in &f.params {
|
|
832
|
-
let ty = match &p.ty {
|
|
833
|
-
ast::ParamType::Type(t) => plumTypeFromAst(t),
|
|
834
|
-
ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(plumTypeFromAst(t))),
|
|
835
|
-
ast::ParamType::Fn(params, ret) => {
|
|
836
|
-
let param_types = params.iter().map(plumTypeFromAst).collect();
|
|
837
|
-
let ret_ty = ret
|
|
838
|
-
.as_ref()
|
|
839
|
-
.map(|r| plumTypeFromAst(r))
|
|
840
|
-
.unwrap_or(PlumType::TUnit);
|
|
841
|
-
PlumType::TFun(param_types, Box::new(ret_ty))
|
|
842
|
-
}
|
|
843
|
-
};
|
|
844
|
-
env.insert(p.name.clone(), TypeScheme::mono(ty));
|
|
1221
|
+
env.insert(p.name.clone(), TypeScheme::mono(paramTypeG(&p.ty)));
|
|
845
1222
|
}
|
|
846
1223
|
|
|
847
1224
|
let declared_ret = f
|
|
848
1225
|
.returns
|
|
849
1226
|
.as_ref()
|
|
850
|
-
.map(
|
|
1227
|
+
.map(plumTypeFromAstG)
|
|
851
1228
|
.unwrap_or(PlumType::TUnit);
|
|
852
1229
|
// Reserved env key read by `?` (see `inferExpr`'s `Try` arm); closures remove it.
|
|
853
1230
|
env.insert(RETURN_KEY.to_string(), TypeScheme::mono(declared_ret.clone()));
|
|
854
1231
|
|
|
855
1232
|
match &f.body {
|
|
856
|
-
ast::FnBody::Expr(e) => match
|
|
1233
|
+
ast::FnBody::Expr(e) => match inferExprWith(e, &env, ctx, Some(&declared_ret)) {
|
|
857
1234
|
Ok(t) => {
|
|
858
|
-
if let Err(msg) =
|
|
1235
|
+
if let Err(msg) = unifyReturn(&declared_ret, &t, e, ctx) {
|
|
859
1236
|
errors.push(CheckError::at(
|
|
860
1237
|
f.line,
|
|
861
1238
|
f.column,
|
|
@@ -870,28 +1247,39 @@ fn checkFn(f: &ast::Fn, global_env: &TypeEnv, ctx: &CheckCtx) -> Vec<CheckError>
|
|
|
870
1247
|
)),
|
|
871
1248
|
},
|
|
872
1249
|
ast::FnBody::Block(block) => {
|
|
1250
|
+
// The tail expression is checked against the declared return type
|
|
1251
|
+
// (which also pins down its generic arguments); everything before
|
|
1252
|
+
// it is an ordinary statement.
|
|
1253
|
+
let (tail, init) = match block.stmts.split_last() {
|
|
1254
|
+
Some((ast::Stmt::Expr(e, line, column), init)) => (Some((e, *line, *column)), init),
|
|
1255
|
+
_ => (None, &block.stmts[..]),
|
|
1256
|
+
};
|
|
1257
|
+
for stmt in init {
|
|
873
|
-
|
|
1258
|
+
errors.append(&mut checkStmt(
|
|
874
|
-
|
|
1259
|
+
stmt,
|
|
875
|
-
|
|
1260
|
+
&mut env,
|
|
876
|
-
|
|
1261
|
+
&declared_ret,
|
|
877
|
-
|
|
1262
|
+
&f.name,
|
|
878
|
-
|
|
1263
|
+
(f.line, f.column),
|
|
879
|
-
|
|
1264
|
+
ctx,
|
|
880
|
-
|
|
1265
|
+
));
|
|
881
|
-
|
|
1266
|
+
}
|
|
882
|
-
// Check the type of the last expression statement against the declared return type
|
|
883
|
-
if let Some(
|
|
1267
|
+
if let Some((last_expr, line, column)) = tail {
|
|
884
|
-
match
|
|
1268
|
+
match inferExprWith(last_expr, &env, ctx, Some(&declared_ret)) {
|
|
885
1269
|
Ok(t) => {
|
|
886
|
-
if let Err(msg) =
|
|
1270
|
+
if let Err(msg) = unifyReturn(&declared_ret, &t, last_expr, ctx) {
|
|
887
1271
|
errors.push(CheckError::at(
|
|
888
|
-
|
|
1272
|
+
line,
|
|
889
|
-
|
|
1273
|
+
column,
|
|
890
1274
|
format!("fn '{}': return type mismatch: {}", f.name, msg),
|
|
891
1275
|
));
|
|
892
1276
|
}
|
|
893
1277
|
}
|
|
1278
|
+
Err(msg) => errors.push(CheckError::at(
|
|
1279
|
+
line,
|
|
1280
|
+
column,
|
|
894
|
-
|
|
1281
|
+
format!("fn '{}': {}", f.name, msg),
|
|
1282
|
+
)),
|
|
895
1283
|
}
|
|
896
1284
|
}
|
|
897
1285
|
}
|
|
@@ -946,7 +1334,8 @@ fn checkStmt(
|
|
|
946
1334
|
for (target, value) in a.targets.iter().zip(a.values.iter()) {
|
|
947
1335
|
match target {
|
|
948
1336
|
ast::AssignTarget::Var(name) => {
|
|
949
|
-
let already_declared = lookup(env, name).is_ok()
|
|
1337
|
+
let already_declared = lookup(env, name).is_ok()
|
|
1338
|
+
&& !env.contains_key(&format!("{}{}", GLOBAL_KEY, name));
|
|
950
1339
|
if a.declare && already_declared {
|
|
951
1340
|
errors.push(CheckError::at(fn_pos.0, fn_pos.1, format!("fn '{}': assign '{}': already declared — use '=' to reassign, not ':='", fn_name, name)));
|
|
952
1341
|
} else if !a.declare && already_declared {
|
|
@@ -997,8 +1386,8 @@ fn checkStmt(
|
|
|
997
1386
|
ast::AssignTarget::Field(object, field_name) => {
|
|
998
1387
|
let label = format!("{}.{}", describeTargetObject(object), field_name);
|
|
999
1388
|
match (inferExpr(object, env, ctx), inferExpr(value, env, ctx)) {
|
|
1000
|
-
(Ok(PlumType::TNamed(
|
|
1389
|
+
(Ok(obj_ty @ (PlumType::TNamed(_) | PlumType::TApp(..))), Ok(value_ty)) => {
|
|
1001
|
-
match
|
|
1390
|
+
match fieldTypeOf(&obj_ty, field_name, ctx) {
|
|
1002
1391
|
Ok(field_ty) => {
|
|
1003
1392
|
if let Err(msg) = unifyArg(&field_ty, &value_ty, value, ctx) {
|
|
1004
1393
|
errors.push(CheckError::at(fn_pos.0, fn_pos.1, format!("fn '{}': assign '{}': {}", fn_name, label, msg)));
|
|
@@ -1015,9 +1404,9 @@ fn checkStmt(
|
|
|
1015
1404
|
}
|
|
1016
1405
|
}
|
|
1017
1406
|
}
|
|
1018
|
-
ast::Stmt::Return(Some(e)) => match
|
|
1407
|
+
ast::Stmt::Return(Some(e)) => match inferExprWith(e, env, ctx, Some(declared_ret)) {
|
|
1019
1408
|
Ok(t) => {
|
|
1020
|
-
if let Err(msg) =
|
|
1409
|
+
if let Err(msg) = unifyReturn(declared_ret, &t, e, ctx) {
|
|
1021
1410
|
errors.push(CheckError::at(
|
|
1022
1411
|
fn_pos.0,
|
|
1023
1412
|
fn_pos.1,
|
|
@@ -1277,7 +1666,7 @@ fn coverCtors(ty: &PlumType, ctx: &CheckCtx) -> Option<(String, Vec<CoverCtor>)>
|
|
|
1277
1666
|
})
|
|
1278
1667
|
.collect(),
|
|
1279
1668
|
)),
|
|
1280
|
-
PlumType::TNamed(enum_name) => {
|
|
1669
|
+
PlumType::TNamed(enum_name) | PlumType::TApp(enum_name, _) => {
|
|
1281
1670
|
let mut vs: Vec<(&String, &EnumVariantInfo)> = ctx
|
|
1282
1671
|
.enum_variants
|
|
1283
1672
|
.iter()
|
|
@@ -1297,7 +1686,7 @@ fn coverCtors(ty: &PlumType, ctx: &CheckCtx) -> Option<(String, Vec<CoverCtor>)>
|
|
|
1297
1686
|
fields: if ctx.enum_params.contains_key(enum_name) {
|
|
1298
1687
|
Vec::new()
|
|
1299
1688
|
} else {
|
|
1300
|
-
|
|
1689
|
+
variantFields(i, ty)
|
|
1301
1690
|
},
|
|
1302
1691
|
})
|
|
1303
1692
|
.collect(),
|
|
@@ -1541,7 +1930,9 @@ fn inferClassCallRaw(
|
|
|
1541
1930
|
call: &ast::ClassCall,
|
|
1542
1931
|
env: &TypeEnv,
|
|
1543
1932
|
ctx: &CheckCtx,
|
|
1933
|
+
expected: Option<&PlumType>,
|
|
1544
1934
|
) -> Result<PlumType, String> {
|
|
1935
|
+
let explicit: Vec<PlumType> = call.generics.iter().map(plumTypeFromAstG).collect();
|
|
1545
1936
|
// A zero-arg `Type()` construction with a nested, no-param `fun init() ->
|
|
1546
1937
|
// Type = ...` method declared is sugar for calling that method — the
|
|
1547
1938
|
// constructor pattern used by e.g. `Buffer` — rather than raw field
|
|
@@ -1549,12 +1940,25 @@ fn inferClassCallRaw(
|
|
|
1549
1940
|
// and go through ordinary field-by-field construction below instead,
|
|
1550
1941
|
// which requires every field to be given.
|
|
1551
1942
|
if call.fields.is_empty() {
|
|
1552
|
-
if let Some(PlumType::TFun(init_params,
|
|
1943
|
+
if let Some(sig @ PlumType::TFun(init_params, _)) = ctx
|
|
1553
1944
|
.methods
|
|
1554
1945
|
.get(&(call.type_name.clone(), "init".to_string()))
|
|
1555
1946
|
{
|
|
1556
1947
|
if initCallableWithNoArgs(init_params) {
|
|
1948
|
+
let gens = enumGenericsOf(&call.type_name, ctx);
|
|
1949
|
+
let owner = if !explicit.is_empty() && explicit.len() == gens.len() {
|
|
1950
|
+
appSubst(gens, &explicit)
|
|
1951
|
+
} else {
|
|
1952
|
+
BTreeMap::new()
|
|
1953
|
+
};
|
|
1954
|
+
let PlumType::TFun(_, ret) = instSig(sig, &owner) else {
|
|
1955
|
+
unreachable!("instSig preserves the TFun shape")
|
|
1956
|
+
};
|
|
1957
|
+
let mut binds = Binds::new();
|
|
1958
|
+
if let Some(exp) = expected {
|
|
1959
|
+
bindMatch(&ret, exp, &mut binds);
|
|
1960
|
+
}
|
|
1557
|
-
return Ok(
|
|
1961
|
+
return Ok(substVars(&ret, &binds));
|
|
1558
1962
|
}
|
|
1559
1963
|
}
|
|
1560
1964
|
}
|
|
@@ -1566,12 +1970,19 @@ fn inferClassCallRaw(
|
|
|
1566
1970
|
// replacement, e.g. `enum Cat = | Cat(name: Str)`) constructs, with no
|
|
1567
1971
|
// separate class-construction path needed.
|
|
1568
1972
|
Some(info) if !info.field_names.is_empty() => {
|
|
1973
|
+
let (owner, result, mut binds) = startVariant(info, &explicit, expected);
|
|
1569
1974
|
for fa in &call.fields {
|
|
1570
1975
|
match info.field_names.iter().position(|n| n == &fa.name) {
|
|
1571
1976
|
Some(i) => {
|
|
1572
|
-
let
|
|
1977
|
+
let pat = types::substitute(&info.field_types[i], &owner);
|
|
1573
|
-
let actual =
|
|
1978
|
+
let actual = inferExprWith(
|
|
1979
|
+
&fa.value,
|
|
1980
|
+
env,
|
|
1981
|
+
ctx,
|
|
1982
|
+
Some(&substVars(&pat, &binds)),
|
|
1983
|
+
)?;
|
|
1984
|
+
bindMatch(&pat, &actual, &mut binds);
|
|
1574
|
-
unifyArg(
|
|
1985
|
+
unifyArg(&substVars(&pat, &binds), &actual, &fa.value, ctx).map_err(|e| {
|
|
1575
1986
|
format!("variant '{}' field '{}': {}", call.type_name, fa.name, e)
|
|
1576
1987
|
})?;
|
|
1577
1988
|
}
|
|
@@ -1598,7 +2009,7 @@ fn inferClassCallRaw(
|
|
|
1598
2009
|
}
|
|
1599
2010
|
}
|
|
1600
2011
|
}
|
|
1601
|
-
Ok(
|
|
2012
|
+
Ok(substVars(&result, &binds))
|
|
1602
2013
|
}
|
|
1603
2014
|
// Positional/generic-payload variant (`Next(T)`, constructed via `Expr::FnCall`
|
|
1604
2015
|
// elsewhere instead), a payload-free variant, or an unmodeled (e.g.
|
|
@@ -1682,7 +2093,12 @@ fn checkPattern(
|
|
|
1682
2093
|
fields.len()
|
|
1683
2094
|
));
|
|
1684
2095
|
}
|
|
2096
|
+
let field_tys = if nominalName(subject_ty) == Some(info.enum_name.as_str()) {
|
|
2097
|
+
variantFields(info, subject_ty)
|
|
2098
|
+
} else {
|
|
2099
|
+
variantFields(info, &PlumType::TNamed(info.enum_name.clone()))
|
|
2100
|
+
};
|
|
1685
|
-
for (f, fty) in fields.iter().zip(
|
|
2101
|
+
for (f, fty) in fields.iter().zip(field_tys.iter()) {
|
|
1686
2102
|
checkPattern(f, fty, env, ctx)?;
|
|
1687
2103
|
}
|
|
1688
2104
|
Ok(())
|
|
@@ -1936,12 +2352,298 @@ fn isTagLiteralName(n: &str, ctx: &CheckCtx) -> bool {
|
|
|
1936
2352
|
&& !ctx.methods.keys().any(|(recv, _)| recv == n)
|
|
1937
2353
|
}
|
|
1938
2354
|
|
|
2355
|
+
fn argExpr(arg: &ast::Arg) -> &ast::Expr {
|
|
2356
|
+
match arg {
|
|
2357
|
+
ast::Arg::Positional(e) => e,
|
|
2358
|
+
ast::Arg::Keyword { value, .. } => value,
|
|
2359
|
+
ast::Arg::Pair { value, .. } => value,
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
/// Checks call `args` against the (already instantiated) `params`, binding the
|
|
2364
|
+
/// inference variables in `binds` as it goes. Non-closure arguments are checked
|
|
2365
|
+
/// first so a closure sees the types its parameters were already pinned to.
|
|
2366
|
+
fn checkCallArgs(
|
|
2367
|
+
label: &str,
|
|
2368
|
+
params: &[PlumType],
|
|
2369
|
+
args: &[ast::Arg],
|
|
2370
|
+
required: usize,
|
|
2371
|
+
env: &TypeEnv,
|
|
2372
|
+
ctx: &CheckCtx,
|
|
2373
|
+
binds: &mut Binds,
|
|
2374
|
+
) -> Result<(), String> {
|
|
2375
|
+
let (fixed, variadic) = match params.last() {
|
|
2376
|
+
Some(PlumType::TVariadic(elem)) => (¶ms[..params.len() - 1], Some(&**elem)),
|
|
2377
|
+
_ => (params, None),
|
|
2378
|
+
};
|
|
2379
|
+
match variadic {
|
|
2380
|
+
Some(_) => {
|
|
2381
|
+
if args.len() < fixed.len() {
|
|
2382
|
+
return Err(format!(
|
|
2383
|
+
"{}: expected at least {} arg(s), got {}",
|
|
2384
|
+
label,
|
|
2385
|
+
fixed.len(),
|
|
2386
|
+
args.len()
|
|
2387
|
+
));
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
None => {
|
|
2391
|
+
if args.len() < required || args.len() > params.len() {
|
|
2392
|
+
return Err(format!(
|
|
2393
|
+
"{}: expected {} args, got {}",
|
|
2394
|
+
label,
|
|
2395
|
+
params.len(),
|
|
2396
|
+
args.len()
|
|
2397
|
+
));
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
let slots: Vec<(usize, &ast::Expr, &PlumType, bool)> = args
|
|
2402
|
+
.iter()
|
|
2403
|
+
.enumerate()
|
|
2404
|
+
.map(|(i, a)| match fixed.get(i) {
|
|
2405
|
+
Some(p) => (i, argExpr(a), p, false),
|
|
2406
|
+
None => (i, argExpr(a), variadic.expect("variadic slot"), true),
|
|
2407
|
+
})
|
|
2408
|
+
.collect();
|
|
2409
|
+
for pass in 0..2 {
|
|
2410
|
+
for (i, e, pat, is_var) in &slots {
|
|
2411
|
+
let is_closure = matches!(e, ast::Expr::Closure(_));
|
|
2412
|
+
if (pass == 1) != is_closure {
|
|
2413
|
+
continue;
|
|
2414
|
+
}
|
|
2415
|
+
let want = substVars(pat, binds);
|
|
2416
|
+
let actual = inferExprWith(e, env, ctx, Some(&want))?;
|
|
2417
|
+
bindMatch(pat, &actual, binds);
|
|
2418
|
+
let want = substVars(pat, binds);
|
|
2419
|
+
unifyArg(&want, &actual, e, ctx).map_err(|err| {
|
|
2420
|
+
if *is_var {
|
|
2421
|
+
format!("{} variadic arg {}: {}", label, i, err)
|
|
2422
|
+
} else {
|
|
2423
|
+
format!("{} arg {}: {}", label, i, err)
|
|
2424
|
+
}
|
|
2425
|
+
})?;
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
Ok(())
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
/// Type of `obj.field`, substituting a generic receiver's type arguments.
|
|
2432
|
+
fn fieldTypeOf(obj_ty: &PlumType, field: &str, ctx: &CheckCtx) -> Result<PlumType, String> {
|
|
2433
|
+
match obj_ty {
|
|
2434
|
+
PlumType::TNamed(n) => lookupFieldType(n, field, ctx),
|
|
2435
|
+
PlumType::TStr => lookupFieldType("Str", field, ctx),
|
|
2436
|
+
PlumType::TApp(n, args) => {
|
|
2437
|
+
let raw = lookupFieldType(n, field, ctx)?;
|
|
2438
|
+
let gens = enumGenericsOf(n, ctx);
|
|
2439
|
+
if gens.len() == args.len() {
|
|
2440
|
+
Ok(types::substitute(&raw, &appSubst(gens, args)))
|
|
2441
|
+
} else {
|
|
2442
|
+
Ok(raw)
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
other => Err(format!(
|
|
2446
|
+
"cannot access field '{}' on non-class type {}",
|
|
2447
|
+
field, other
|
|
2448
|
+
)),
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
/// The success (`Ok`) payload and failure (`Err`) payload types of a `Try`-shaped
|
|
2453
|
+
/// value of type `ty`, with its type arguments substituted.
|
|
2454
|
+
fn tryParts(ty: &PlumType, ctx: &CheckCtx) -> Option<(PlumType, Option<PlumType>)> {
|
|
2455
|
+
let name = nominalName(ty)?;
|
|
2456
|
+
let (_, success, _, failure) = tryOperatorVariants(name, ctx)?;
|
|
2457
|
+
let ok = variantFields(success, ty)
|
|
2458
|
+
.first()
|
|
2459
|
+
.cloned()
|
|
2460
|
+
.unwrap_or(PlumType::TUnit);
|
|
2461
|
+
let err = variantFields(failure, ty).first().cloned();
|
|
2462
|
+
Some((ok, err))
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
fn inferMethodCall(
|
|
2466
|
+
obj_ty: &PlumType,
|
|
2467
|
+
call: &ast::FnCall,
|
|
2468
|
+
env: &TypeEnv,
|
|
2469
|
+
ctx: &CheckCtx,
|
|
2470
|
+
expected: Option<&PlumType>,
|
|
2471
|
+
) -> Result<PlumType, String> {
|
|
2472
|
+
// A method on a generic parameter resolves through its trait bounds only.
|
|
2473
|
+
if let PlumType::TParam(p) = obj_ty {
|
|
2474
|
+
let prefix = format!("{}{}:", BOUND_KEY, p);
|
|
2475
|
+
for key in env.keys() {
|
|
2476
|
+
let Some(tr) = key.strip_prefix(&prefix) else {
|
|
2477
|
+
continue;
|
|
2478
|
+
};
|
|
2479
|
+
let Some(sch) = env.get(&format!("{}{}:{}", TRAIT_KEY, tr, call.name)) else {
|
|
2480
|
+
continue;
|
|
2481
|
+
};
|
|
2482
|
+
if let PlumType::TFun(params, ret) = &*sch.body {
|
|
2483
|
+
let mut binds = Binds::new();
|
|
2484
|
+
let label = format!("method '{}.{}'", p, call.name);
|
|
2485
|
+
checkCallArgs(&label, params, &call.args, params.len(), env, ctx, &mut binds)?;
|
|
2486
|
+
return Ok(substVars(ret, &binds));
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
return Err(format!(
|
|
2490
|
+
"'{}' has no method '{}' (add a bound)",
|
|
2491
|
+
p, call.name
|
|
2492
|
+
));
|
|
2493
|
+
}
|
|
2494
|
+
let Some(class_name) = methodReceiverName(obj_ty) else {
|
|
2495
|
+
return Ok(PlumType::TVar("_".to_string()));
|
|
2496
|
+
};
|
|
2497
|
+
// Fallback: a primitive/class receiver's method might be defined once on
|
|
2498
|
+
// the "union" enum that bare-wraps it instead (`enum Number = | Int | Float`).
|
|
2499
|
+
let class_name = if ctx
|
|
2500
|
+
.methods
|
|
2501
|
+
.contains_key(&(class_name.clone(), call.name.clone()))
|
|
2502
|
+
{
|
|
2503
|
+
class_name
|
|
2504
|
+
} else {
|
|
2505
|
+
ctx.enum_variants
|
|
2506
|
+
.get(class_name.as_str())
|
|
2507
|
+
.map(|info| info.enum_name.clone())
|
|
2508
|
+
.filter(|enum_name| {
|
|
2509
|
+
ctx.methods
|
|
2510
|
+
.contains_key(&(enum_name.clone(), call.name.clone()))
|
|
2511
|
+
})
|
|
2512
|
+
.unwrap_or(class_name)
|
|
2513
|
+
};
|
|
2514
|
+
let Some(sig) = ctx.methods.get(&(class_name.clone(), call.name.clone())) else {
|
|
2515
|
+
// Unmodeled method (e.g. builtin/std): allow, codegen will catch.
|
|
2516
|
+
return Ok(PlumType::TVar("_".to_string()));
|
|
2517
|
+
};
|
|
2518
|
+
let PlumType::TFun(..) = sig else {
|
|
2519
|
+
return Ok(PlumType::TVar("_".to_string()));
|
|
2520
|
+
};
|
|
2521
|
+
let owner = match obj_ty {
|
|
2522
|
+
PlumType::TApp(n, args) if *n == class_name => {
|
|
2523
|
+
appSubst(enumGenericsOf(n, ctx), args)
|
|
2524
|
+
}
|
|
2525
|
+
_ => BTreeMap::new(),
|
|
2526
|
+
};
|
|
2527
|
+
let PlumType::TFun(params, ret) = instSig(sig, &owner) else {
|
|
2528
|
+
unreachable!("instSig preserves the TFun shape")
|
|
2529
|
+
};
|
|
2530
|
+
let mut binds = Binds::new();
|
|
2531
|
+
if let Some(exp) = expected {
|
|
2532
|
+
bindMatch(&ret, exp, &mut binds);
|
|
2533
|
+
}
|
|
2534
|
+
let required = ctx
|
|
2535
|
+
.min_required
|
|
2536
|
+
.get(&(Some(class_name.clone()), call.name.clone()))
|
|
2537
|
+
.copied()
|
|
2538
|
+
.unwrap_or(params.len());
|
|
2539
|
+
let label = format!("method '{}.{}'", class_name, call.name);
|
|
2540
|
+
checkCallArgs(&label, ¶ms, &call.args, required, env, ctx, &mut binds)?;
|
|
2541
|
+
Ok(substVars(&ret, &binds))
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
/// Starts constructing a value of one of `info`'s enum: fresh owner variables
|
|
2545
|
+
/// (or the explicit `generics`), the result type pattern, and bindings seeded
|
|
2546
|
+
/// from the `expected` type.
|
|
2547
|
+
fn startVariant(
|
|
2548
|
+
info: &EnumVariantInfo,
|
|
2549
|
+
explicit: &[PlumType],
|
|
2550
|
+
expected: Option<&PlumType>,
|
|
2551
|
+
) -> (BTreeMap<String, PlumType>, PlumType, Binds) {
|
|
2552
|
+
let owner = if !explicit.is_empty() && explicit.len() == info.generics.len() {
|
|
2553
|
+
appSubst(&info.generics, explicit)
|
|
2554
|
+
} else {
|
|
2555
|
+
freshOwner(&info.generics)
|
|
2556
|
+
};
|
|
2557
|
+
let result = enumTypeWith(info, &owner);
|
|
2558
|
+
let mut binds = Binds::new();
|
|
2559
|
+
if let Some(exp) = expected {
|
|
2560
|
+
bindMatch(&result, exp, &mut binds);
|
|
2561
|
+
}
|
|
2562
|
+
(owner, result, binds)
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
/// A tag literal (`Bad` / `Bad(x)`) written where the expected type is a tag
|
|
2566
|
+
/// union naming it — it is the TAG even if a declared variant shares the name.
|
|
2567
|
+
fn expectedTagLiteral(
|
|
2568
|
+
expr: &ast::Expr,
|
|
2569
|
+
env: &TypeEnv,
|
|
2570
|
+
ctx: &CheckCtx,
|
|
2571
|
+
expected: Option<&PlumType>,
|
|
2572
|
+
) -> Option<Result<PlumType, String>> {
|
|
2573
|
+
let Some(PlumType::TTags(tags)) = expected else {
|
|
2574
|
+
return None;
|
|
2575
|
+
};
|
|
2576
|
+
match expr {
|
|
2577
|
+
ast::Expr::TypeName(n)
|
|
2578
|
+
if matches!(tags.get(n.as_str()), Some(None)) && ctx.enum_variants.contains_key(n) =>
|
|
2579
|
+
{
|
|
2580
|
+
Some(Ok(PlumType::TTags(BTreeMap::from([(n.clone(), None)]))))
|
|
2581
|
+
}
|
|
2582
|
+
ast::Expr::FnCall(call)
|
|
2583
|
+
if call.args.len() == 1
|
|
2584
|
+
&& matches!(tags.get(call.name.as_str()), Some(Some(_)))
|
|
2585
|
+
&& ctx.enum_variants.contains_key(&call.name) =>
|
|
2586
|
+
{
|
|
2587
|
+
let payload_expected = tags.get(call.name.as_str()).and_then(|p| p.as_deref());
|
|
2588
|
+
Some(
|
|
2589
|
+
inferExprWith(argExpr(&call.args[0]), env, ctx, payload_expected).map(|p| {
|
|
2590
|
+
PlumType::TTags(BTreeMap::from([(
|
|
2591
|
+
call.name.clone(),
|
|
2592
|
+
Some(Box::new(p)),
|
|
2593
|
+
)]))
|
|
2594
|
+
}),
|
|
2595
|
+
)
|
|
2596
|
+
}
|
|
2597
|
+
_ => None,
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
|
|
1939
2601
|
pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<PlumType, String> {
|
|
2602
|
+
inferExprWith(expr, env, ctx, None)
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
/// Like `inferExpr`, with the type the context expects (a declared return, a
|
|
2606
|
+
/// parameter, a field) — used to pin down generic arguments nothing else
|
|
2607
|
+
/// determines (`Err(Bad)` in a `Try[Int, [Bad]]` position).
|
|
2608
|
+
pub fn inferExprWith(
|
|
2609
|
+
expr: &ast::Expr,
|
|
2610
|
+
env: &TypeEnv,
|
|
2611
|
+
ctx: &CheckCtx,
|
|
2612
|
+
expected: Option<&PlumType>,
|
|
2613
|
+
) -> Result<PlumType, String> {
|
|
2614
|
+
let r = inferExprInner(expr, env, ctx, expected);
|
|
2615
|
+
if let Ok(t) = &r {
|
|
2616
|
+
recordType(expr, t);
|
|
2617
|
+
}
|
|
2618
|
+
r
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
fn inferExprInner(
|
|
2622
|
+
expr: &ast::Expr,
|
|
2623
|
+
env: &TypeEnv,
|
|
2624
|
+
ctx: &CheckCtx,
|
|
2625
|
+
expected: Option<&PlumType>,
|
|
2626
|
+
) -> Result<PlumType, String> {
|
|
2627
|
+
if let Some(r) = expectedTagLiteral(expr, env, ctx, expected) {
|
|
2628
|
+
return r;
|
|
2629
|
+
}
|
|
1940
2630
|
match expr {
|
|
1941
2631
|
ast::Expr::Int(_) => Ok(PlumType::TInt),
|
|
1942
2632
|
ast::Expr::Float(_) => Ok(PlumType::TFloat),
|
|
1943
|
-
ast::Expr::String(
|
|
2633
|
+
ast::Expr::String(s) => {
|
|
2634
|
+
// Interpolated parts are typed for the record only; errors in them
|
|
2635
|
+
// were never part of this check.
|
|
2636
|
+
for part in &s.parts {
|
|
2637
|
+
if let ast::StringPart::Interp(e) = part {
|
|
2638
|
+
let _ = inferExprWith(e, env, ctx, None);
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
Ok(PlumType::TStr)
|
|
2642
|
+
}
|
|
1944
|
-
ast::Expr::Var(name) =>
|
|
2643
|
+
ast::Expr::Var(name) => match env.get(name) {
|
|
2644
|
+
Some(s) if !s.vars.is_empty() => Ok(instSig(&s.body, &BTreeMap::new())),
|
|
2645
|
+
_ => lookup(env, name),
|
|
2646
|
+
},
|
|
1945
2647
|
ast::Expr::Self_ => lookup(env, "self"),
|
|
1946
2648
|
// A bare `NAME` lexes as a type_identifier whenever it starts with an
|
|
1947
2649
|
// uppercase letter — which includes every SCREAMING_CASE top-level
|
|
@@ -1952,15 +2654,12 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
1952
2654
|
Ok(ty) if !matches!(ty, PlumType::TVar(_)) => Ok(ty),
|
|
1953
2655
|
_ => match ctx.enum_variants.get(n) {
|
|
1954
2656
|
// `Bool` normalizes to the dedicated `TBool` variant (not
|
|
1955
|
-
// `TNamed("Bool")`) —
|
|
2657
|
+
// `TNamed("Bool")`) — see the note on `unify`'s Bool arm.
|
|
1956
|
-
// once `enum Bool = | True | False` (`libs/std/Bool.plum`) is
|
|
1957
|
-
// actually imported, but every OTHER part of the checker AND
|
|
1958
|
-
// `plum-wasm-codegen` (which reuses `inferExpr` via
|
|
1959
|
-
// `inferLocalType`) still branches on the literal `TBool`
|
|
1960
|
-
// variant, not a generic `TNamed` name — normalizing here
|
|
1961
|
-
// keeps that working without patching every such site.
|
|
1962
2658
|
Some(info) if info.enum_name == "Bool" => Ok(PlumType::TBool),
|
|
2659
|
+
Some(info) => {
|
|
1963
|
-
|
|
2660
|
+
let (_, result, binds) = startVariant(info, &[], expected);
|
|
2661
|
+
Ok(substVars(&result, &binds))
|
|
2662
|
+
}
|
|
1964
2663
|
None if isTagLiteralName(n, ctx) => Ok(PlumType::TTags(BTreeMap::from([(
|
|
1965
2664
|
tagLiteralName(n).to_string(),
|
|
1966
2665
|
None,
|
|
@@ -1969,35 +2668,27 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
1969
2668
|
None => Ok(PlumType::TNamed(n.to_string())),
|
|
1970
2669
|
},
|
|
1971
2670
|
},
|
|
1972
|
-
ast::Expr::Paren(inner) =>
|
|
2671
|
+
ast::Expr::Paren(inner) => inferExprWith(inner, env, ctx, expected),
|
|
1973
2672
|
// `expr?` — deliberately does NOT check that the enclosing function's
|
|
1974
|
-
// return type actually accepts the `Err` value
|
|
2673
|
+
// return type actually accepts the `Err` value beyond its error type.
|
|
1975
|
-
// with early; that's left permissive here (codegen will catch a
|
|
1976
|
-
// genuine mismatch as a wasm type error), consistent with this
|
|
1977
|
-
// checker's existing "unmodeled/unvalidated, codegen catches it"
|
|
1978
|
-
// fallbacks elsewhere (e.g. `TypeName`'s unmodeled-type-name arm
|
|
1979
|
-
// above) — checking it properly would need the enclosing function's
|
|
1980
|
-
// declared return type threaded through every `inferExpr` call site,
|
|
1981
|
-
// for a case a wrong program still fails on, just less precisely.
|
|
1982
2674
|
ast::Expr::Try(inner) => {
|
|
1983
2675
|
let inner_ty = inferExpr(inner, env, ctx)?;
|
|
1984
|
-
|
|
2676
|
+
if nominalName(&inner_ty).is_none() {
|
|
1985
|
-
PlumType::TNamed(name) => name,
|
|
1986
|
-
|
|
2677
|
+
return Err(format!("'?' requires a Try value, found {}", inner_ty));
|
|
1987
|
-
}
|
|
2678
|
+
}
|
|
1988
|
-
let Some((
|
|
2679
|
+
let Some((success_ty, inner_err)) = tryParts(&inner_ty, ctx) else {
|
|
2680
|
+
return Err(format!(
|
|
1989
|
-
|
|
2681
|
+
"'?' requires a Try value, found '{}'",
|
|
2682
|
+
nominalName(&inner_ty).unwrap_or("?")
|
|
2683
|
+
));
|
|
1990
2684
|
};
|
|
1991
2685
|
// The failure branch returns early from the enclosing fn, so the
|
|
1992
2686
|
// inner error type must fit the fn's declared error type. Closures
|
|
1993
2687
|
// (and test blocks) have no `$return` and aren't checked.
|
|
1994
|
-
if let Some(
|
|
2688
|
+
if let Some(outer_ty) = env.get(RETURN_KEY).map(|s| (*s.body).clone()) {
|
|
1995
|
-
if let Some((_,
|
|
2689
|
+
if let Some((_, Some(outer_err))) = tryParts(&outer_ty, ctx) {
|
|
1996
|
-
if let
|
|
2690
|
+
if let Some(inner_err) = &inner_err {
|
|
1997
|
-
failure_info.field_types.first(),
|
|
1998
|
-
outer_failure.field_types.first(),
|
|
1999
|
-
) {
|
|
2000
|
-
if unify(outer_err, inner_err, ctx).is_err() {
|
|
2691
|
+
if unify(&outer_err, inner_err, ctx).is_err() {
|
|
2001
2692
|
return Err(format!(
|
|
2002
2693
|
"'?' error type {} is not part of the function's error type {}",
|
|
2003
2694
|
inner_err, outer_err
|
|
@@ -2006,11 +2697,7 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
2006
2697
|
}
|
|
2007
2698
|
}
|
|
2008
2699
|
}
|
|
2009
|
-
Ok(
|
|
2700
|
+
Ok(success_ty)
|
|
2010
|
-
.field_types
|
|
2011
|
-
.first()
|
|
2012
|
-
.cloned()
|
|
2013
|
-
.unwrap_or(PlumType::TUnit))
|
|
2014
2701
|
}
|
|
2015
2702
|
ast::Expr::Closure(cl) => {
|
|
2016
2703
|
let mut closure_env = env.clone();
|
|
@@ -2027,18 +2714,32 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
2027
2714
|
&mut resolved_from_usage,
|
|
2028
2715
|
);
|
|
2029
2716
|
}
|
|
2717
|
+
let (exp_params, exp_ret): (Option<&Vec<PlumType>>, Option<&PlumType>) = match expected
|
|
2718
|
+
{
|
|
2719
|
+
Some(PlumType::TFun(ps, r)) if ps.len() == cl.params.len() => {
|
|
2720
|
+
(Some(ps), Some(&**r))
|
|
2721
|
+
}
|
|
2722
|
+
_ => (None, None),
|
|
2723
|
+
};
|
|
2030
2724
|
let param_types: Vec<PlumType> = cl
|
|
2031
2725
|
.params
|
|
2032
2726
|
.iter()
|
|
2727
|
+
.enumerate()
|
|
2033
|
-
.map(|p| {
|
|
2728
|
+
.map(|(i, p)| {
|
|
2034
2729
|
// A fresh TVar for anything not resolved below — `unify` treats an
|
|
2035
2730
|
// unbound TVar as a wildcard, so this is only a real problem for
|
|
2036
2731
|
// field access (`p.field`), which needs a concrete class name to
|
|
2037
2732
|
// look the field up at all; see `resolveClosureParamFromFieldUsage`.
|
|
2733
|
+
let from_expected = exp_params
|
|
2734
|
+
.and_then(|ps| ps.get(i))
|
|
2735
|
+
.filter(|t| !matches!(t, PlumType::TVar(_)))
|
|
2736
|
+
.cloned();
|
|
2737
|
+
let t = from_expected.unwrap_or_else(|| {
|
|
2038
|
-
|
|
2738
|
+
resolved_from_usage
|
|
2039
|
-
|
|
2739
|
+
.get(p)
|
|
2040
|
-
|
|
2740
|
+
.cloned()
|
|
2041
|
-
|
|
2741
|
+
.unwrap_or_else(|| PlumType::TVar(format!("_closure_{}", p)))
|
|
2742
|
+
});
|
|
2042
2743
|
closure_env.insert(p.clone(), TypeScheme::mono(t.clone()));
|
|
2043
2744
|
t
|
|
2044
2745
|
})
|
|
@@ -2060,8 +2761,8 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
2060
2761
|
}
|
|
2061
2762
|
}
|
|
2062
2763
|
let body_ty = match &cl.body.stmts.last() {
|
|
2063
|
-
Some(ast::Stmt::Expr(e, ..)) =>
|
|
2764
|
+
Some(ast::Stmt::Expr(e, ..)) => inferExprWith(e, &closure_env, ctx, exp_ret)?,
|
|
2064
|
-
Some(ast::Stmt::Return(Some(e))) =>
|
|
2765
|
+
Some(ast::Stmt::Return(Some(e))) => inferExprWith(e, &closure_env, ctx, exp_ret)?,
|
|
2065
2766
|
_ => PlumType::TUnit,
|
|
2066
2767
|
};
|
|
2067
2768
|
Ok(PlumType::TFun(param_types, Box::new(body_ty)))
|
|
@@ -2076,6 +2777,9 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
2076
2777
|
let lt = inferExpr(&b.left, env, ctx)?;
|
|
2077
2778
|
let rt = inferExpr(&b.right, env, ctx)?;
|
|
2078
2779
|
unify(<, &rt, ctx).map_err(|e| format!("binary op: {}", e))?;
|
|
2780
|
+
if matches!(lt, PlumType::TVar(_)) && !matches!(rt, PlumType::TVar(_)) {
|
|
2781
|
+
return Ok(rt);
|
|
2782
|
+
}
|
|
2079
2783
|
Ok(lt)
|
|
2080
2784
|
}
|
|
2081
2785
|
ast::Expr::Bool(b) => {
|
|
@@ -2104,8 +2808,8 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
2104
2808
|
ast::Expr::Ternary(t) => {
|
|
2105
2809
|
let ct = inferExpr(&t.condition, env, ctx)?;
|
|
2106
2810
|
unify(&PlumType::TBool, &ct, ctx).map_err(|e| format!("ternary condition: {}", e))?;
|
|
2107
|
-
let tt =
|
|
2811
|
+
let tt = inferExprWith(&t.then, env, ctx, expected)?;
|
|
2108
|
-
let et =
|
|
2812
|
+
let et = inferExprWith(&t.else_, env, ctx, expected)?;
|
|
2109
2813
|
unify(&tt, &et, ctx).map_err(|e| format!("ternary branches: {}", e))?;
|
|
2110
2814
|
Ok(tt)
|
|
2111
2815
|
}
|
|
@@ -2114,28 +2818,13 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
2114
2818
|
// unify with `left`'s success (`Ok`) field type.
|
|
2115
2819
|
ast::Expr::Elvis(e) => {
|
|
2116
2820
|
let left_ty = inferExpr(&e.left, env, ctx)?;
|
|
2117
|
-
let
|
|
2821
|
+
let Some(name) = nominalName(&left_ty) else {
|
|
2822
|
+
return Err(format!("'??' requires a Try value, found {}", left_ty));
|
|
2823
|
+
};
|
|
2118
|
-
|
|
2824
|
+
let Some((success_ty, _)) = tryParts(&left_ty, ctx) else {
|
|
2119
|
-
Some((_, success_info, _, _)) => success_info
|
|
2120
|
-
.field_types
|
|
2121
|
-
.first()
|
|
2122
|
-
.cloned()
|
|
2123
|
-
.unwrap_or(PlumType::TUnit),
|
|
2124
|
-
None => {
|
|
2125
|
-
return Err(format!(
|
|
2126
|
-
|
|
2825
|
+
return Err(format!("'??' requires a Try value, found '{}'", name));
|
|
2127
|
-
name
|
|
2128
|
-
))
|
|
2129
|
-
}
|
|
2130
|
-
},
|
|
2131
|
-
other => {
|
|
2132
|
-
return Err(format!(
|
|
2133
|
-
"'??' requires a Try value, found {}",
|
|
2134
|
-
other
|
|
2135
|
-
))
|
|
2136
|
-
}
|
|
2137
2826
|
};
|
|
2138
|
-
let right_ty =
|
|
2827
|
+
let right_ty = inferExprWith(&e.right, env, ctx, Some(&success_ty))?;
|
|
2139
2828
|
unify(&success_ty, &right_ty, ctx).map_err(|e| format!("'??' branches: {}", e))?;
|
|
2140
2829
|
Ok(success_ty)
|
|
2141
2830
|
}
|
|
@@ -2147,21 +2836,12 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
2147
2836
|
// registered enum variant (a "union" enum's bare-primitive-wrap
|
|
2148
2837
|
// variant, e.g. `enum Number = | Int | Float`) whose single
|
|
2149
2838
|
// field type exactly matches the arg's own type — a same-type
|
|
2150
|
-
// call (`Int(intExpr)`) then means constructing that variant
|
|
2839
|
+
// call (`Int(intExpr)`) then means constructing that variant,
|
|
2151
|
-
// (`monomorphize::wrapPrimitiveAgainstExpected` synthesizes
|
|
2152
|
-
// exactly this call shape), not a no-op cast; falls through to
|
|
2153
|
-
// the ordinary enum-variant-construction handling just below
|
|
2154
|
-
// instead. A genuinely cross-type call (`Int(floatExpr)`) is
|
|
2155
|
-
// never ambiguous this way, so every existing numeric-cast call
|
|
2156
|
-
//
|
|
2840
|
+
// not a no-op cast.
|
|
2157
2841
|
if (call.name == "Int" || call.name == "Float" || call.name == "Byte")
|
|
2158
2842
|
&& call.args.len() == 1
|
|
2159
2843
|
{
|
|
2160
|
-
let arg_expr =
|
|
2844
|
+
let arg_expr = argExpr(&call.args[0]);
|
|
2161
|
-
ast::Arg::Positional(e) => e,
|
|
2162
|
-
ast::Arg::Keyword { value, .. } => value,
|
|
2163
|
-
ast::Arg::Pair { value, .. } => value,
|
|
2164
|
-
};
|
|
2165
2845
|
let actual = inferExpr(arg_expr, env, ctx)?;
|
|
2166
2846
|
let is_same_type_variant_wrap = ctx
|
|
2167
2847
|
.enum_variants
|
|
@@ -2180,26 +2860,11 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
2180
2860
|
)),
|
|
2181
2861
|
};
|
|
2182
2862
|
}
|
|
2183
|
-
// Else: fall through to the ordinary enum-variant-construction
|
|
2184
|
-
// handling just below, exactly as if this special case didn't
|
|
2185
|
-
// exist for this call.
|
|
2186
2863
|
}
|
|
2187
2864
|
// `Type(1, 2, 3)` — a call with bare POSITIONAL args on a record-shaped
|
|
2188
|
-
// type's name (as opposed to `Type(field: value, ...)`, which parses as a
|
|
2189
|
-
// `ClassCall` instead — see `class_argument_list` vs `fn_argument_list` in
|
|
2190
|
-
//
|
|
2865
|
+
// type's name is sugar for `Type.init(1, 2, 3)` (see the `ClassCall`
|
|
2191
|
-
// pattern as `Type()`'s own `ClassCall` handling above, letting `init`
|
|
2192
|
-
// take real params (e.g. `List`'s own variadic `init(values: ...T)`)
|
|
2193
|
-
//
|
|
2866
|
+
// zero-arg `Type()` handling); checked BEFORE the ordinary
|
|
2194
|
-
// positional-variant-construction case just below
|
|
2867
|
+
// positional-variant-construction case just below.
|
|
2195
|
-
// enum's own name is ALSO its one variant's name (see
|
|
2196
|
-
// `buildGlobalTables`), so without this ordering a call meant as
|
|
2197
|
-
// `List.init(1, 2, 3)` could instead misfire as "construct the List
|
|
2198
|
-
// variant positionally" whenever the arg count happens to match its
|
|
2199
|
-
// real field count — a record-shaped type never supported positional
|
|
2200
|
-
// field construction via bare `Type(...)` even before enums/classes
|
|
2201
|
-
// merged into one representation, only through `init` or the named
|
|
2202
|
-
// `Type(field: value, ...)` form.
|
|
2203
2868
|
if ctx
|
|
2204
2869
|
.enum_variants
|
|
2205
2870
|
.get(&call.name)
|
|
@@ -2215,7 +2880,7 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
2215
2880
|
args: call.args.clone(),
|
|
2216
2881
|
}),
|
|
2217
2882
|
}));
|
|
2218
|
-
return
|
|
2883
|
+
return inferExprWith(&synthetic, env, ctx, expected);
|
|
2219
2884
|
}
|
|
2220
2885
|
if let Some(info) = ctx.enum_variants.get(&call.name) {
|
|
2221
2886
|
if call.args.len() != info.field_types.len() {
|
|
@@ -2226,121 +2891,51 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
2226
2891
|
call.args.len()
|
|
2227
2892
|
));
|
|
2228
2893
|
}
|
|
2229
|
-
|
|
2894
|
+
let (owner, result, mut binds) = startVariant(info, &[], expected);
|
|
2230
|
-
|
|
2895
|
+
for (i, (arg, field)) in call.args.iter().zip(info.field_types.iter()).enumerate() {
|
|
2231
|
-
{
|
|
2232
|
-
let arg_expr =
|
|
2896
|
+
let arg_expr = argExpr(arg);
|
|
2233
|
-
ast::Arg::Positional(e) => e,
|
|
2234
|
-
|
|
2897
|
+
let pat = types::substitute(field, &owner);
|
|
2235
|
-
ast::Arg::Pair { value, .. } => value,
|
|
2236
|
-
};
|
|
2237
|
-
// A bare-type variant's own payload (`FantasyBook(FantasyBook(...))`
|
|
2898
|
+
// A bare-type variant's own payload (`FantasyBook(FantasyBook(...))`)
|
|
2238
|
-
// whether hand-written or produced by desugaring `FantasyBook(...)`
|
|
2239
|
-
//
|
|
2899
|
+
// infers via the RAW class-field rules — see `inferClassCallRaw`.
|
|
2240
|
-
// enum-wrapping `inferExpr` would otherwise apply to a
|
|
2241
|
-
// `ClassCall` of that same name — so it unifies against
|
|
2242
|
-
// `expected` (the bare class type), not the enum itself.
|
|
2243
|
-
let actual = match (
|
|
2900
|
+
let actual = match (&pat, arg_expr) {
|
|
2244
2901
|
(PlumType::TNamed(n), ast::Expr::ClassCall(c))
|
|
2245
2902
|
if n == &call.name && c.type_name == call.name =>
|
|
2246
2903
|
{
|
|
2247
|
-
inferClassCallRaw(c, env, ctx)?
|
|
2904
|
+
inferClassCallRaw(c, env, ctx, None)?
|
|
2248
2905
|
}
|
|
2249
|
-
_ =>
|
|
2906
|
+
_ => inferExprWith(arg_expr, env, ctx, Some(&substVars(&pat, &binds)))?,
|
|
2250
2907
|
};
|
|
2908
|
+
bindMatch(&pat, &actual, &mut binds);
|
|
2251
|
-
unifyArg(
|
|
2909
|
+
unifyArg(&substVars(&pat, &binds), &actual, arg_expr, ctx)
|
|
2252
2910
|
.map_err(|e| format!("variant '{}' arg {}: {}", call.name, i, e))?;
|
|
2253
2911
|
}
|
|
2254
|
-
return Ok(
|
|
2912
|
+
return Ok(substVars(&result, &binds));
|
|
2255
2913
|
}
|
|
2914
|
+
let callee = match env.get(&call.name) {
|
|
2915
|
+
Some(s) if !s.vars.is_empty() => Ok(instSig(&s.body, &BTreeMap::new())),
|
|
2256
|
-
|
|
2916
|
+
_ => lookup(env, &call.name),
|
|
2917
|
+
};
|
|
2918
|
+
match callee {
|
|
2257
2919
|
Ok(PlumType::TFun(param_types, ret)) => {
|
|
2258
|
-
|
|
2920
|
+
let mut binds = Binds::new();
|
|
2259
|
-
Some(PlumType::TVariadic(elem)) => {
|
|
2260
|
-
let fixed = ¶m_types[..param_types.len() - 1];
|
|
2261
|
-
if call.args.len() < fixed.len() {
|
|
2262
|
-
return Err(format!(
|
|
2263
|
-
"call '{}': expected at least {} arg(s), got {}",
|
|
2264
|
-
call.name,
|
|
2265
|
-
fixed.len(),
|
|
2266
|
-
call.args.len()
|
|
2267
|
-
));
|
|
2268
|
-
}
|
|
2269
|
-
for (i, (arg, expected)) in
|
|
2270
|
-
call.args.iter().zip(fixed.iter()).enumerate()
|
|
2271
|
-
{
|
|
2272
|
-
|
|
2921
|
+
if let Some(exp) = expected {
|
|
2273
|
-
ast::Arg::Positional(e) => e,
|
|
2274
|
-
ast::Arg::Keyword { value, .. } => value,
|
|
2275
|
-
ast::Arg::Pair { value, .. } => value,
|
|
2276
|
-
};
|
|
2277
|
-
let actual = inferExpr(arg_expr, env, ctx)?;
|
|
2278
|
-
unifyArg(expected, &actual, arg_expr, ctx).map_err(|e| {
|
|
2279
|
-
format!("call '{}' arg {}: {}", call.name, i, e)
|
|
2280
|
-
})?;
|
|
2281
|
-
}
|
|
2282
|
-
for (i, arg) in call.args.iter().enumerate().skip(fixed.len()) {
|
|
2283
|
-
let arg_expr = match arg {
|
|
2284
|
-
ast::Arg::Positional(e) => e,
|
|
2285
|
-
ast::Arg::Keyword { value, .. } => value,
|
|
2286
|
-
ast::Arg::Pair { value, .. } => value,
|
|
2287
|
-
};
|
|
2288
|
-
let actual = inferExpr(arg_expr, env, ctx)?;
|
|
2289
|
-
unifyArg(elem, &actual, arg_expr, ctx).map_err(|e| {
|
|
2290
|
-
format!("call '{}' variadic arg {}: {}", call.name, i, e)
|
|
2291
|
-
})?;
|
|
2292
|
-
}
|
|
2293
|
-
Ok(*ret)
|
|
2294
|
-
}
|
|
2295
|
-
_ => {
|
|
2296
|
-
// Fewer args than params is fine as long as every
|
|
2297
|
-
// OMITTED one (always the trailing ones — see
|
|
2298
|
-
// `MinRequiredArgs`'s doc comment) has a declared
|
|
2299
|
-
// default; codegen fills those in from their own
|
|
2300
|
-
// default expression at the call site.
|
|
2301
|
-
let required = ctx
|
|
2302
|
-
.min_required
|
|
2303
|
-
|
|
2922
|
+
bindMatch(&ret, exp, &mut binds);
|
|
2304
|
-
.copied()
|
|
2305
|
-
.unwrap_or(param_types.len());
|
|
2306
|
-
if call.args.len() < required || call.args.len() > param_types.len() {
|
|
2307
|
-
return Err(format!(
|
|
2308
|
-
"call '{}': expected {} args, got {}",
|
|
2309
|
-
call.name,
|
|
2310
|
-
param_types.len(),
|
|
2311
|
-
call.args.len()
|
|
2312
|
-
));
|
|
2313
|
-
}
|
|
2314
|
-
for (i, (arg, expected)) in
|
|
2315
|
-
call.args.iter().zip(param_types.iter()).enumerate()
|
|
2316
|
-
{
|
|
2317
|
-
let arg_expr = match arg {
|
|
2318
|
-
ast::Arg::Positional(e) => e,
|
|
2319
|
-
ast::Arg::Keyword { value, .. } => value,
|
|
2320
|
-
ast::Arg::Pair { value, .. } => value,
|
|
2321
|
-
};
|
|
2322
|
-
let actual = inferExpr(arg_expr, env, ctx)?;
|
|
2323
|
-
unifyArg(expected, &actual, arg_expr, ctx).map_err(|e| {
|
|
2324
|
-
format!("call '{}' arg {}: {}", call.name, i, e)
|
|
2325
|
-
})?;
|
|
2326
|
-
}
|
|
2327
|
-
Ok(*ret)
|
|
2328
|
-
}
|
|
2329
2923
|
}
|
|
2924
|
+
let required = ctx
|
|
2925
|
+
.min_required
|
|
2926
|
+
.get(&(None, call.name.clone()))
|
|
2927
|
+
.copied()
|
|
2928
|
+
.unwrap_or(param_types.len());
|
|
2929
|
+
let label = format!("call '{}'", call.name);
|
|
2930
|
+
checkCallArgs(&label, ¶m_types, &call.args, required, env, ctx, &mut binds)?;
|
|
2931
|
+
Ok(substVars(&ret, &binds))
|
|
2330
2932
|
}
|
|
2331
2933
|
Ok(_) => Err(format!("'{}' is not a function", call.name)),
|
|
2332
2934
|
Err(_) if isTagLiteralName(&call.name, ctx) && call.args.len() <= 1 => {
|
|
2333
2935
|
// `Bad(x)` — an undeclared uppercase name applied to one arg is
|
|
2334
2936
|
// a tag literal with a payload.
|
|
2335
2937
|
let payload = match call.args.first() {
|
|
2336
|
-
Some(arg) => {
|
|
2337
|
-
let arg_expr = match arg {
|
|
2338
|
-
ast::Arg::Positional(e) => e,
|
|
2339
|
-
ast::Arg::Keyword { value, .. } => value,
|
|
2340
|
-
ast::Arg::Pair { value, .. } => value,
|
|
2341
|
-
};
|
|
2342
|
-
|
|
2938
|
+
Some(arg) => Some(Box::new(inferExpr(argExpr(arg), env, ctx)?)),
|
|
2343
|
-
}
|
|
2344
2939
|
None => None,
|
|
2345
2940
|
};
|
|
2346
2941
|
Ok(PlumType::TTags(BTreeMap::from([(
|
|
@@ -2358,7 +2953,7 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
2358
2953
|
}
|
|
2359
2954
|
}
|
|
2360
2955
|
ast::Expr::ClassCall(call) => {
|
|
2361
|
-
let bare = inferClassCallRaw(call, env, ctx)?;
|
|
2956
|
+
let bare = inferClassCallRaw(call, env, ctx, expected)?;
|
|
2362
2957
|
Ok(match bare {
|
|
2363
2958
|
PlumType::TNamed(n) => bareVariantWrapType(&n, ctx),
|
|
2364
2959
|
other => other,
|
|
@@ -2368,157 +2963,20 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
2368
2963
|
let obj_ty = inferExpr(&attr.object, env, ctx)?;
|
|
2369
2964
|
match &attr.attr {
|
|
2370
2965
|
ast::AttrKind::Field(field_name) => match &obj_ty {
|
|
2371
|
-
// `Str` is an ordinary class
|
|
2966
|
+
// `Str` is an ordinary class with a real field; a generic
|
|
2372
|
-
// ...` in `str.plum`) with a real field, unlike historically
|
|
2373
|
-
//
|
|
2967
|
+
// class substitutes its receiver's type arguments.
|
|
2374
|
-
// it the same way `TNamed` does, just keyed by the literal
|
|
2375
|
-
// name "Str" since `TStr` itself carries no name.
|
|
2376
|
-
PlumType::TNamed(_) | PlumType::TStr => {
|
|
2968
|
+
PlumType::TNamed(_) | PlumType::TStr | PlumType::TApp(..) => {
|
|
2377
|
-
let class_name: &str = match &obj_ty {
|
|
2378
|
-
PlumType::TNamed(n) => n.as_str(),
|
|
2379
|
-
_ => "Str",
|
|
2380
|
-
};
|
|
2381
|
-
|
|
2969
|
+
fieldTypeOf(&obj_ty, field_name, ctx)
|
|
2382
2970
|
}
|
|
2383
|
-
// An unresolved generic
|
|
2971
|
+
// An unresolved generic return reaches here as a plain `TVar`;
|
|
2384
|
-
// bare `T`, which this checker's `PlumType` erases entirely — see
|
|
2385
|
-
// `types.rs`'s `TNamed` having no type-argument slot) reaches here as
|
|
2386
|
-
// a plain `TVar`, not a real "wrong type". Field access on it is
|
|
2387
|
-
// "unmodeled" the same way an unknown enum's fields already are just
|
|
2388
|
-
// above (`ctx.enum_params.get` returning `None`) — allow it and let
|
|
2389
|
-
//
|
|
2972
|
+
// field access on it is unmodeled — allow, codegen will catch.
|
|
2390
|
-
// genuine mismatch. Only a definitely-wrong concrete type (`Int`,
|
|
2391
|
-
// `Str`, ...) still hard-errors here.
|
|
2392
2973
|
PlumType::TVar(_) => Ok(PlumType::TVar("_".to_string())),
|
|
2393
2974
|
other => Err(format!(
|
|
2394
2975
|
"cannot access field '{}' on non-class type {}",
|
|
2395
2976
|
field_name, other
|
|
2396
2977
|
)),
|
|
2397
2978
|
},
|
|
2398
|
-
ast::AttrKind::Method(call) =>
|
|
2979
|
+
ast::AttrKind::Method(call) => inferMethodCall(&obj_ty, call, env, ctx, expected),
|
|
2399
|
-
match methodReceiverName(&obj_ty) {
|
|
2400
|
-
Some(class_name) => {
|
|
2401
|
-
// Fallback: a primitive/class receiver's method might be
|
|
2402
|
-
// defined once on the "union" enum that bare-wraps it
|
|
2403
|
-
// instead (`enum Number = | Int | Float`) — `class_name`
|
|
2404
|
-
// (e.g. "Int") is itself a registered enum-variant name
|
|
2405
|
-
// in that case (see `buildGlobalTables`'s bare-wrap
|
|
2406
|
-
// conditions); retry there before falling through to the
|
|
2407
|
-
// permissive "unmodeled method" case below. Mirrors
|
|
2408
|
-
// `plum-wasm-codegen`'s equivalent dispatch fallback,
|
|
2409
|
-
// which also boxes `self` into the wrap-enum's variant.
|
|
2410
|
-
let class_name = if ctx
|
|
2411
|
-
.methods
|
|
2412
|
-
.contains_key(&(class_name.clone(), call.name.clone()))
|
|
2413
|
-
{
|
|
2414
|
-
class_name
|
|
2415
|
-
} else {
|
|
2416
|
-
ctx.enum_variants
|
|
2417
|
-
.get(class_name.as_str())
|
|
2418
|
-
.map(|info| info.enum_name.clone())
|
|
2419
|
-
.filter(|enum_name| {
|
|
2420
|
-
ctx.methods
|
|
2421
|
-
.contains_key(&(enum_name.clone(), call.name.clone()))
|
|
2422
|
-
})
|
|
2423
|
-
.unwrap_or(class_name)
|
|
2424
|
-
};
|
|
2425
|
-
match ctx.methods.get(&(class_name.clone(), call.name.clone())) {
|
|
2426
|
-
Some(PlumType::TFun(param_types, ret)) => {
|
|
2427
|
-
match param_types.last() {
|
|
2428
|
-
Some(PlumType::TVariadic(elem)) => {
|
|
2429
|
-
let fixed = ¶m_types[..param_types.len() - 1];
|
|
2430
|
-
if call.args.len() < fixed.len() {
|
|
2431
|
-
return Err(format!(
|
|
2432
|
-
"method '{}.{}': expected at least {} arg(s), got {}",
|
|
2433
|
-
class_name, call.name, fixed.len(), call.args.len()
|
|
2434
|
-
));
|
|
2435
|
-
}
|
|
2436
|
-
for (i, (arg, expected)) in
|
|
2437
|
-
call.args.iter().zip(fixed.iter()).enumerate()
|
|
2438
|
-
{
|
|
2439
|
-
let arg_expr = match arg {
|
|
2440
|
-
ast::Arg::Positional(e) => e,
|
|
2441
|
-
ast::Arg::Keyword { value, .. } => value,
|
|
2442
|
-
ast::Arg::Pair { value, .. } => value,
|
|
2443
|
-
};
|
|
2444
|
-
let actual = inferExpr(arg_expr, env, ctx)?;
|
|
2445
|
-
unifyArg(expected, &actual, arg_expr, ctx)
|
|
2446
|
-
.map_err(|e| {
|
|
2447
|
-
format!(
|
|
2448
|
-
"method '{}.{}' arg {}: {}",
|
|
2449
|
-
class_name, call.name, i, e
|
|
2450
|
-
)
|
|
2451
|
-
})?;
|
|
2452
|
-
}
|
|
2453
|
-
for (i, arg) in
|
|
2454
|
-
call.args.iter().enumerate().skip(fixed.len())
|
|
2455
|
-
{
|
|
2456
|
-
let arg_expr = match arg {
|
|
2457
|
-
ast::Arg::Positional(e) => e,
|
|
2458
|
-
ast::Arg::Keyword { value, .. } => value,
|
|
2459
|
-
ast::Arg::Pair { value, .. } => value,
|
|
2460
|
-
};
|
|
2461
|
-
let actual = inferExpr(arg_expr, env, ctx)?;
|
|
2462
|
-
unifyArg(elem, &actual, arg_expr, ctx).map_err(
|
|
2463
|
-
|e| {
|
|
2464
|
-
format!(
|
|
2465
|
-
"method '{}.{}' variadic arg {}: {}",
|
|
2466
|
-
class_name, call.name, i, e
|
|
2467
|
-
)
|
|
2468
|
-
},
|
|
2469
|
-
)?;
|
|
2470
|
-
}
|
|
2471
|
-
Ok(*ret.clone())
|
|
2472
|
-
}
|
|
2473
|
-
_ => {
|
|
2474
|
-
// See the matching comment on the free-function
|
|
2475
|
-
// arm above — fewer args than params is fine as
|
|
2476
|
-
// long as every omitted (trailing) one has a
|
|
2477
|
-
// declared default.
|
|
2478
|
-
let required = ctx
|
|
2479
|
-
.min_required
|
|
2480
|
-
.get(&(Some(class_name.clone()), call.name.clone()))
|
|
2481
|
-
.copied()
|
|
2482
|
-
.unwrap_or(param_types.len());
|
|
2483
|
-
if call.args.len() < required
|
|
2484
|
-
|| call.args.len() > param_types.len()
|
|
2485
|
-
{
|
|
2486
|
-
return Err(format!(
|
|
2487
|
-
"method '{}.{}': expected {} args, got {}",
|
|
2488
|
-
class_name,
|
|
2489
|
-
call.name,
|
|
2490
|
-
param_types.len(),
|
|
2491
|
-
call.args.len()
|
|
2492
|
-
));
|
|
2493
|
-
}
|
|
2494
|
-
for (i, (arg, expected)) in
|
|
2495
|
-
call.args.iter().zip(param_types.iter()).enumerate()
|
|
2496
|
-
{
|
|
2497
|
-
let arg_expr = match arg {
|
|
2498
|
-
ast::Arg::Positional(e) => e,
|
|
2499
|
-
ast::Arg::Keyword { value, .. } => value,
|
|
2500
|
-
ast::Arg::Pair { value, .. } => value,
|
|
2501
|
-
};
|
|
2502
|
-
let actual = inferExpr(arg_expr, env, ctx)?;
|
|
2503
|
-
unifyArg(expected, &actual, arg_expr, ctx)
|
|
2504
|
-
.map_err(|e| {
|
|
2505
|
-
format!(
|
|
2506
|
-
"method '{}.{}' arg {}: {}",
|
|
2507
|
-
class_name, call.name, i, e
|
|
2508
|
-
)
|
|
2509
|
-
})?;
|
|
2510
|
-
}
|
|
2511
|
-
Ok(*ret.clone())
|
|
2512
|
-
}
|
|
2513
|
-
}
|
|
2514
|
-
}
|
|
2515
|
-
// Unmodeled method (e.g. builtin/std): allow, codegen will catch.
|
|
2516
|
-
_ => Ok(PlumType::TVar("_".to_string())),
|
|
2517
|
-
}
|
|
2518
|
-
}
|
|
2519
|
-
None => Ok(PlumType::TVar("_".to_string())),
|
|
2520
|
-
}
|
|
2521
|
-
}
|
|
2522
2980
|
}
|
|
2523
2981
|
}
|
|
2524
2982
|
}
|
plum-checker/src/monomorphize.rs
CHANGED
|
@@ -2354,6 +2354,7 @@ impl<'a> Monomorphizer<'a> {
|
|
|
2354
2354
|
field_types,
|
|
2355
2355
|
field_names: v.field_names.clone(),
|
|
2356
2356
|
values: v.values.clone(),
|
|
2357
|
+
generics: Vec::new(),
|
|
2357
2358
|
},
|
|
2358
2359
|
);
|
|
2359
2360
|
}
|
|
@@ -3100,6 +3101,7 @@ pub fn monomorphizeSource(source: &ast::Source) -> Result<ast::Source, String> {
|
|
|
3100
3101
|
field_types,
|
|
3101
3102
|
field_names,
|
|
3102
3103
|
values: v.values.clone(),
|
|
3104
|
+
generics: Vec::new(),
|
|
3103
3105
|
},
|
|
3104
3106
|
);
|
|
3105
3107
|
}
|
plum-checker/tests/checker_tests.rs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
use plum_checker::types::*;
|
|
3
3
|
use plum_checker::{
|
|
4
4
|
checkSource, erasedEnumName, plumTypeFromAst, plumTypeFromName, tagUnionSegmentsCompatible,
|
|
5
|
-
unify, CheckCtx,
|
|
5
|
+
typedSource, unify, CheckCtx,
|
|
6
6
|
};
|
|
7
7
|
use plum_core::ast::Type as AstType;
|
|
8
8
|
use plum_core::{ast::*, AstParser};
|
|
@@ -1749,12 +1749,12 @@ fun main() -> Int =
|
|
|
1749
1749
|
|
|
1750
1750
|
#[test]
|
|
1751
1751
|
fn unpinnedOkIsNotATagLiteral() {
|
|
1752
|
+
// Structural generics: `Ok(3)` with nothing pinning `E` is a `Try[Int, ?]`
|
|
1752
|
-
// (
|
|
1753
|
+
// (the unresolved `E` stays open), never a tag literal named `Ok`.
|
|
1753
|
-
// program importing std/Try; with none, the checker can't tell `Ok` from a tag)
|
|
1754
1754
|
let r = checkTry("fun one() -> Try[Int, [Nope]] =\n Ok(1)\n\nfun main() =\n x := Ok(3)\n");
|
|
1755
|
-
let msg = errText(r);
|
|
1756
|
-
assert!(msg.contains("cannot infer the type parameters of 'Ok'"), "{}", msg);
|
|
1757
|
-
assert!(
|
|
1755
|
+
assert!(r.is_ok(), "{:?}", r.err());
|
|
1756
|
+
let bad = checkTry("fun f() -> Try[Str, [Nope]] =\n Ok(1)\n");
|
|
1757
|
+
assert!(errText(bad).contains("variant 'Ok' arg 0"), "pinned T still checked");
|
|
1758
1758
|
}
|
|
1759
1759
|
|
|
1760
1760
|
#[test]
|
|
@@ -1959,3 +1959,208 @@ fun f(s: Step) -> Int =
|
|
|
1959
1959
|
let r = errText(enumMatch(def, " ReadMin => 1\n"));
|
|
1960
1960
|
assert!(r.contains("non-exhaustive match on Step: missing ReadMax"), "got: {}", r);
|
|
1961
1961
|
}
|
|
1962
|
+
|
|
1963
|
+
fn repoRoot() -> std::path::PathBuf {
|
|
1964
|
+
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
/// Acceptance: every std and example file checks with zero errors on the
|
|
1968
|
+
/// ORIGINAL (un-monomorphized) source.
|
|
1969
|
+
#[test]
|
|
1970
|
+
fn everyStdAndExampleChecksStructurally() {
|
|
1971
|
+
let mut failures: Vec<String> = Vec::new();
|
|
1972
|
+
for dir in ["plum-std", "plum-examples"] {
|
|
1973
|
+
let mut files: Vec<_> = std::fs::read_dir(repoRoot().join(dir))
|
|
1974
|
+
.unwrap()
|
|
1975
|
+
.filter_map(|e| e.ok())
|
|
1976
|
+
.map(|e| e.path())
|
|
1977
|
+
.filter(|p| p.extension().and_then(|e| e.to_str()) == Some("plum"))
|
|
1978
|
+
.collect();
|
|
1979
|
+
files.sort();
|
|
1980
|
+
for path in files {
|
|
1981
|
+
let source = plum_core::loadAndMerge(&path, &repoRoot())
|
|
1982
|
+
.unwrap_or_else(|e| panic!("failed to load {}: {}", path.display(), e));
|
|
1983
|
+
if let Err(errs) = checkSource(&source) {
|
|
1984
|
+
for e in errs {
|
|
1985
|
+
failures.push(format!(
|
|
1986
|
+
"{}: {}",
|
|
1987
|
+
path.file_name().unwrap().to_string_lossy(),
|
|
1988
|
+
e.message
|
|
1989
|
+
));
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
assert!(failures.is_empty(), "structural check errors:\n{}", failures.join("\n"));
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
|
|
1998
|
+
// ---- structural generics (T2) ----
|
|
1999
|
+
|
|
2000
|
+
const GENERIC_DEFS: &str = "\
|
|
2001
|
+
enum Try[T, E] =
|
|
2002
|
+
| Ok(T)
|
|
2003
|
+
| Err(E)
|
|
2004
|
+
|
|
2005
|
+
enum List[T] =
|
|
2006
|
+
| List(items: T)
|
|
2007
|
+
|
|
2008
|
+
fun map(self, cb: fn(T) -> U) -> List[U] =
|
|
2009
|
+
List(items: cb(self.items))
|
|
2010
|
+
|
|
2011
|
+
enum Box[T] =
|
|
2012
|
+
| Box(value: T)
|
|
2013
|
+
|
|
2014
|
+
";
|
|
2015
|
+
|
|
2016
|
+
fn tapp(name: &str, args: Vec<PlumType>) -> PlumType {
|
|
2017
|
+
PlumType::TApp(name.to_string(), args)
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
fn tagsA() -> PlumType {
|
|
2021
|
+
PlumType::TTags(std::collections::BTreeMap::from([("A".to_string(), None)]))
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
/// The instantiated type of the body expression of the LAST fn in `src`.
|
|
2025
|
+
fn lastFnBodyType(src: &str) -> PlumType {
|
|
2026
|
+
let source = parseClean(&format!("{}{}", GENERIC_DEFS, src));
|
|
2027
|
+
let info = typedSource(&source).unwrap_or_else(|e| {
|
|
2028
|
+
panic!(
|
|
2029
|
+
"unexpected errors: {:?}",
|
|
2030
|
+
e.iter().map(|x| x.message.clone()).collect::<Vec<_>>()
|
|
2031
|
+
)
|
|
2032
|
+
});
|
|
2033
|
+
let f = source
|
|
2034
|
+
.items
|
|
2035
|
+
.iter()
|
|
2036
|
+
.rev()
|
|
2037
|
+
.find_map(|i| match i {
|
|
2038
|
+
Item::Fn(f) => Some(f),
|
|
2039
|
+
_ => None,
|
|
2040
|
+
})
|
|
2041
|
+
.unwrap();
|
|
2042
|
+
let e = match &f.body {
|
|
2043
|
+
FnBody::Expr(e) => e,
|
|
2044
|
+
FnBody::Block(b) => match b.stmts.last() {
|
|
2045
|
+
Some(Stmt::Expr(e, ..)) => e,
|
|
2046
|
+
_ => panic!("no tail expression"),
|
|
2047
|
+
},
|
|
2048
|
+
_ => panic!("no body"),
|
|
2049
|
+
};
|
|
2050
|
+
info.expr_types
|
|
2051
|
+
.get(&(e as *const Expr as usize))
|
|
2052
|
+
.cloned()
|
|
2053
|
+
.expect("body expression has a recorded type")
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
#[test]
|
|
2057
|
+
fn method_own_generic_instantiated_per_call() {
|
|
2058
|
+
assert_eq!(
|
|
2059
|
+
lastFnBodyType("fun f(xs: List[Int]) -> List[Int] =\n xs.map(|n| n * 2)\n"),
|
|
2060
|
+
tapp("List", vec![PlumType::TInt])
|
|
2061
|
+
);
|
|
2062
|
+
assert_eq!(
|
|
2063
|
+
lastFnBodyType("fun f(xs: List[Int]) -> List[Str] =\n xs.map(|n| \"{n}\")\n"),
|
|
2064
|
+
tapp("List", vec![PlumType::TStr])
|
|
2065
|
+
);
|
|
2066
|
+
// The two instantiations are independent: Str result where Int is declared is an error.
|
|
2067
|
+
let bad = checkSource(&parseClean(&format!(
|
|
2068
|
+
"{}fun f(xs: List[Int]) -> List[Int] =\n xs.map(|n| \"{{n}}\")\n",
|
|
2069
|
+
GENERIC_DEFS
|
|
2070
|
+
)));
|
|
2071
|
+
assert!(errText(bad).contains("type mismatch"));
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
#[test]
|
|
2075
|
+
fn nested_generic_field() {
|
|
2076
|
+
let tryIntA = tapp("Try", vec![PlumType::TInt, tagsA()]);
|
|
2077
|
+
assert_eq!(
|
|
2078
|
+
lastFnBodyType("fun f(b: Box[Try[Int, [A]]]) -> Try[Int, [A]] =\n b.value\n"),
|
|
2079
|
+
tryIntA
|
|
2080
|
+
);
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
#[test]
|
|
2084
|
+
fn unbounded_tparam_method_error() {
|
|
2085
|
+
let r = checkSource(&parseClean("fun f(x: T) -> Int =\n x.hash()\n"));
|
|
2086
|
+
assert!(
|
|
2087
|
+
errText(r).contains("'T' has no method 'hash' (add a bound)"),
|
|
2088
|
+
"unbounded T must not resolve methods"
|
|
2089
|
+
);
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
#[test]
|
|
2093
|
+
fn bounded_ok() {
|
|
2094
|
+
let src = "\
|
|
2095
|
+
trait Hashable =
|
|
2096
|
+
hash() -> Int
|
|
2097
|
+
|
|
2098
|
+
enum Keyed[T: Hashable] =
|
|
2099
|
+
| Keyed(value: T)
|
|
2100
|
+
|
|
2101
|
+
fun h(self) -> Int =
|
|
2102
|
+
self.value.hash()
|
|
2103
|
+
";
|
|
2104
|
+
let r = checkSource(&parseClean(src));
|
|
2105
|
+
assert!(r.is_ok(), "got {:?}", r.err());
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
#[test]
|
|
2109
|
+
fn tags_covariant_in_tapp() {
|
|
2110
|
+
let defs = "\
|
|
2111
|
+
fun wide(t: Try[Int, [A, B]]) -> Int =
|
|
2112
|
+
1
|
|
2113
|
+
|
|
2114
|
+
fun narrow() -> Try[Int, [A]] =
|
|
2115
|
+
Err(A)
|
|
2116
|
+
|
|
2117
|
+
fun ok() -> Int =
|
|
2118
|
+
wide(narrow())
|
|
2119
|
+
";
|
|
2120
|
+
let r = checkSource(&parseClean(&format!("{}{}", GENERIC_DEFS, defs)));
|
|
2121
|
+
assert!(r.is_ok(), "got {:?}", r.err());
|
|
2122
|
+
// ...but not the other way around.
|
|
2123
|
+
let rev = "\
|
|
2124
|
+
fun narrow(t: Try[Int, [A]]) -> Int =
|
|
2125
|
+
1
|
|
2126
|
+
|
|
2127
|
+
fun wide() -> Try[Int, [A, B]] =
|
|
2128
|
+
Err(A)
|
|
2129
|
+
|
|
2130
|
+
fun bad() -> Int =
|
|
2131
|
+
narrow(wide())
|
|
2132
|
+
";
|
|
2133
|
+
let r = checkSource(&parseClean(&format!("{}{}", GENERIC_DEFS, rev)));
|
|
2134
|
+
assert!(r.is_err());
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
#[test]
|
|
2138
|
+
fn typed_source_records_every_instantiated_expr() {
|
|
2139
|
+
let source = parseClean(&format!(
|
|
2140
|
+
"{}fun f() -> Try[Int, [A]] =\n Ok(1)\n",
|
|
2141
|
+
GENERIC_DEFS
|
|
2142
|
+
));
|
|
2143
|
+
let info = typedSource(&source).unwrap();
|
|
2144
|
+
assert!(!info.expr_types.is_empty());
|
|
2145
|
+
assert_eq!(
|
|
2146
|
+
lastFnBodyType("fun f() -> Try[Int, [A]] =\n Ok(1)\n"),
|
|
2147
|
+
tapp("Try", vec![PlumType::TInt, tagsA()])
|
|
2148
|
+
);
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
#[test]
|
|
2152
|
+
fn generic_free_fn_instantiated_per_call() {
|
|
2153
|
+
let defs = "\
|
|
2154
|
+
fun identity(value: T) -> T =
|
|
2155
|
+
value
|
|
2156
|
+
|
|
2157
|
+
fun a() -> Int =
|
|
2158
|
+
identity(5)
|
|
2159
|
+
|
|
2160
|
+
fun b() -> Str =
|
|
2161
|
+
identity(\"x\")
|
|
2162
|
+
";
|
|
2163
|
+
assert!(checkSource(&parseClean(&format!("{}{}", GENERIC_DEFS, defs))).is_ok());
|
|
2164
|
+
let bad = "fun identity(value: T) -> T =\n value\n\nfun a() -> Str =\n identity(5)\n";
|
|
2165
|
+
assert!(checkSource(&parseClean(bad)).is_err());
|
|
2166
|
+
}
|