plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
f0eddab
— Peter John
2026-10-25T16:45:02+05:30
fix(checker): resolve Ok/Err args from the expected type; tags win over same-named variants
- README.md +4 -2
- plum-checker/src/lib.rs +41 -4
- plum-checker/src/monomorphize.rs +128 -0
- plum-checker/tests/checker_tests.rs +82 -1
- plum-examples/dop_visitor.plum +2 -2
- plum-examples/match.plum +2 -5
- plum-examples/oop_visitor.plum +2 -2
- plum-examples/types.plum +2 -5
- plum-wasm-codegen/src/lib.rs +17 -4
README.md
CHANGED
|
@@ -354,6 +354,8 @@ fun parsePositive(s: Str) -> Try[Int, [BadNumStr, NegativeNum]] =
|
|
|
354
354
|
return Ok(n)
|
|
355
355
|
```
|
|
356
356
|
|
|
357
|
+
Where the expected type is a tag union (a typed return, parameter or field), a name it lists is that tag even if a declared enum variant shares the name; elsewhere the variant wins.
|
|
358
|
+
|
|
357
359
|
A "missing value" is just an error tag: `List.get` fails with `[OutOfBounds]`, `first`/`last`/`pop` with `[ListWasEmpty]`, `Map.get` with `[KeyNotFound]`, `parseInt`/`parseFloat` with `[BadNumStr]`, `Json.fromStr` with `[InvalidJson(Str)]`.
|
|
358
360
|
|
|
359
361
|
`expr?` unwraps an `Ok`, or exits the enclosing function early with the `Err` value — Rust's `?`. Tag unions merge automatically across `?`: the function's declared union just has to cover every union it propagates:
|
|
@@ -377,8 +379,8 @@ fun parseOrInvalid(s: Str) -> Try[Int, [InvalidInput]] =
|
|
|
377
379
|
|
|
378
380
|
```plum
|
|
379
381
|
fun sumOrZero(a: Str, b: Str) -> Int =
|
|
380
|
-
x :=
|
|
382
|
+
x := parsePositive(a) ?? 0
|
|
381
|
-
y :=
|
|
383
|
+
y := parsePositive(b) ?? 0
|
|
382
384
|
x + y
|
|
383
385
|
```
|
|
384
386
|
|
plum-checker/src/lib.rs
CHANGED
|
@@ -1761,10 +1761,37 @@ pub fn tryOperatorVariants<'a>(
|
|
|
1761
1761
|
}
|
|
1762
1762
|
}
|
|
1763
1763
|
|
|
1764
|
+
/// Prefix the monomorphizer puts on a tag literal whose name a declared variant
|
|
1765
|
+
/// also uses, at a site whose expected type is a tag union naming it
|
|
1766
|
+
/// (`Err(Bad)` in `-> Try[Int, [Bad]]` next to `enum Rating = | Bad(...)`).
|
|
1767
|
+
/// Not a legal identifier character, so it can't clash with source names.
|
|
1768
|
+
pub const TAG_MARKER: char = '#';
|
|
1769
|
+
|
|
1770
|
+
/// The tag name a tag-literal expression name stands for (drops `TAG_MARKER`).
|
|
1771
|
+
pub fn tagLiteralName(n: &str) -> &str {
|
|
1772
|
+
n.strip_prefix(TAG_MARKER).unwrap_or(n)
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
/// True if `n` is the base name of a generic enum, or of one of its variants,
|
|
1776
|
+
/// that only survives monomorphization under mangled names (`Ok` ->
|
|
1777
|
+
/// `Ok$Int$[X]`, `Try` -> `Try$Int$[X]`).
|
|
1778
|
+
fn isGenericEnumBaseName(n: &str, ctx: &CheckCtx) -> bool {
|
|
1779
|
+
let base = |k: &str| k.split_once('$').map(|(b, _)| b == n).unwrap_or(false);
|
|
1780
|
+
!ctx.enum_variants.contains_key(n)
|
|
1781
|
+
&& ctx
|
|
1782
|
+
.enum_variants
|
|
1783
|
+
.iter()
|
|
1784
|
+
.any(|(k, info)| base(k) || base(&info.enum_name))
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1764
1787
|
/// True if `n` is an uppercase name that is not a declared type, variant, method
|
|
1765
1788
|
/// receiver, builtin type or const — i.e. a structural tag literal.
|
|
1766
1789
|
fn isTagLiteralName(n: &str, ctx: &CheckCtx) -> bool {
|
|
1790
|
+
if n.starts_with(TAG_MARKER) {
|
|
1791
|
+
return true;
|
|
1792
|
+
}
|
|
1767
1793
|
n.chars().next().is_some_and(|c| c.is_uppercase())
|
|
1794
|
+
&& !isGenericEnumBaseName(n, ctx)
|
|
1768
1795
|
&& !matches!(
|
|
1769
1796
|
n,
|
|
1770
1797
|
"Int" | "Float" | "Bool" | "Str" | "Byte" | "Unit" | "Self"
|
|
@@ -1799,9 +1826,10 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
1799
1826
|
// keeps that working without patching every such site.
|
|
1800
1827
|
Some(info) if info.enum_name == "Bool" => Ok(PlumType::TBool),
|
|
1801
1828
|
Some(info) => Ok(PlumType::TNamed(info.enum_name.clone())),
|
|
1802
|
-
None if isTagLiteralName(n, ctx) =>
|
|
1829
|
+
None if isTagLiteralName(n, ctx) => Ok(PlumType::TTags(BTreeMap::from([(
|
|
1803
|
-
|
|
1830
|
+
tagLiteralName(n).to_string(),
|
|
1804
|
-
|
|
1831
|
+
None,
|
|
1832
|
+
)]))),
|
|
1805
1833
|
// Unmodeled/builtin type name: allow, codegen will catch.
|
|
1806
1834
|
None => Ok(PlumType::TNamed(n.to_string())),
|
|
1807
1835
|
},
|
|
@@ -2180,8 +2208,17 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
|
|
|
2180
2208
|
}
|
|
2181
2209
|
None => None,
|
|
2182
2210
|
};
|
|
2183
|
-
Ok(PlumType::TTags(BTreeMap::from([(
|
|
2211
|
+
Ok(PlumType::TTags(BTreeMap::from([(
|
|
2212
|
+
tagLiteralName(&call.name).to_string(),
|
|
2213
|
+
payload,
|
|
2214
|
+
)])))
|
|
2184
2215
|
}
|
|
2216
|
+
// `Ok(3)` that nothing pinned to a concrete specialization (no
|
|
2217
|
+
// typed param/field/return to take `E` from) — it is never a tag.
|
|
2218
|
+
Err(_) if isGenericEnumBaseName(&call.name, ctx) => Err(format!(
|
|
2219
|
+
"cannot infer the type parameters of '{}' here: use it where its full type is known (a typed parameter, field or return)",
|
|
2220
|
+
call.name
|
|
2221
|
+
)),
|
|
2185
2222
|
Err(_) => Ok(PlumType::TVar("_".to_string())), // unknown fn: allow, codegen will catch
|
|
2186
2223
|
}
|
|
2187
2224
|
}
|
plum-checker/src/monomorphize.rs
CHANGED
|
@@ -1259,6 +1259,7 @@ impl<'a> Monomorphizer<'a> {
|
|
|
1259
1259
|
if let Some(idx) = info.field_names.iter().position(|n| n == &fa.name) {
|
|
1260
1260
|
let expected = info.field_types[idx].clone();
|
|
1261
1261
|
self.resolveBareVariantAgainstExpected(&mut fa.value, &expected);
|
|
1262
|
+
self.resolveGenericVariantAgainstExpected(&mut fa.value, &expected);
|
|
1262
1263
|
self.wrapPrimitiveAgainstExpected(&mut fa.value, &expected, env);
|
|
1263
1264
|
}
|
|
1264
1265
|
}
|
|
@@ -1683,6 +1684,7 @@ impl<'a> Monomorphizer<'a> {
|
|
|
1683
1684
|
/// gets there. No-op if `expr` isn't a bare generic-enum variant, or
|
|
1684
1685
|
/// `expected` doesn't name a specialization of the SAME enum.
|
|
1685
1686
|
fn resolveBareVariantAgainstExpected(&mut self, expr: &mut ast::Expr, expected: &PlumType) {
|
|
1687
|
+
self.markTagsAgainstExpected(expr, expected);
|
|
1686
1688
|
let ast::Expr::TypeName(n) = expr else { return };
|
|
1687
1689
|
let Some(e) = self.enums_generic_by_variant.get(n.as_str()).copied() else {
|
|
1688
1690
|
return;
|
|
@@ -1700,6 +1702,77 @@ impl<'a> Monomorphizer<'a> {
|
|
|
1700
1702
|
}
|
|
1701
1703
|
}
|
|
1702
1704
|
|
|
1705
|
+
/// A tag literal (`Bad` / `Bad(x)`) whose expected type is a tag union
|
|
1706
|
+
/// naming it, but whose name a declared variant also uses (`enum Rating =
|
|
1707
|
+
/// | Bad(...)`): prefixes it with `crate::TAG_MARKER` so the checker and
|
|
1708
|
+
/// codegen treat it as the tag, not the variant. Recurses into an
|
|
1709
|
+
/// already-specialized variant construction's payloads (`Err$Int$[Bad](Bad)`)
|
|
1710
|
+
/// against that variant's own field types.
|
|
1711
|
+
fn markTagsAgainstExpected(&self, expr: &mut ast::Expr, expected: &PlumType) {
|
|
1712
|
+
let collides = |n: &str| {
|
|
1713
|
+
self.enum_variants.contains_key(n) || self.enums_generic_by_variant.contains_key(n)
|
|
1714
|
+
};
|
|
1715
|
+
match expr {
|
|
1716
|
+
ast::Expr::Paren(inner) => self.markTagsAgainstExpected(inner, expected),
|
|
1717
|
+
ast::Expr::TypeName(n) => {
|
|
1718
|
+
if let PlumType::TTags(tags) = expected {
|
|
1719
|
+
if matches!(tags.get(n.as_str()), Some(None)) && collides(n) {
|
|
1720
|
+
*n = format!("{}{}", crate::TAG_MARKER, n);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
ast::Expr::FnCall(call) => {
|
|
1725
|
+
if let PlumType::TTags(tags) = expected {
|
|
1726
|
+
if call.args.len() == 1
|
|
1727
|
+
&& matches!(tags.get(call.name.as_str()), Some(Some(_)))
|
|
1728
|
+
&& collides(&call.name)
|
|
1729
|
+
{
|
|
1730
|
+
call.name = format!("{}{}", crate::TAG_MARKER, call.name);
|
|
1731
|
+
}
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
if self.enums_generic_by_variant.contains_key(call.name.as_str()) {
|
|
1735
|
+
return; // not specialized yet: no concrete field types
|
|
1736
|
+
}
|
|
1737
|
+
if let Some(info) = self.enum_variants.get(call.name.as_str()) {
|
|
1738
|
+
for (arg, ft) in call.args.iter_mut().zip(info.field_types.iter()) {
|
|
1739
|
+
let e = match arg {
|
|
1740
|
+
ast::Arg::Positional(e) => e,
|
|
1741
|
+
ast::Arg::Keyword { value, .. } => value,
|
|
1742
|
+
ast::Arg::Pair { value, .. } => value,
|
|
1743
|
+
};
|
|
1744
|
+
self.markTagsAgainstExpected(e, ft);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
_ => {}
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
/// `Ok(3)` passed where a concrete `Try$Int$[X]` is expected (a typed
|
|
1753
|
+
/// param or field): a generic variant whose own payload can't pin every
|
|
1754
|
+
/// enum param takes its specialization from that expected type instead —
|
|
1755
|
+
/// the arg/field analogue of `resolveEnumInstantiation`'s return-type
|
|
1756
|
+
/// fallback. Also marks colliding tag payloads (`Err(Bad)`).
|
|
1757
|
+
fn resolveGenericVariantAgainstExpected(&mut self, expr: &mut ast::Expr, expected: &PlumType) {
|
|
1758
|
+
if let ast::Expr::FnCall(call) = expr {
|
|
1759
|
+
if let Some(e) = self.enums_generic_by_variant.get(call.name.as_str()).copied() {
|
|
1760
|
+
if let PlumType::TNamed(mangled) = expected {
|
|
1761
|
+
if mangled.starts_with(&format!("{}$", e.name)) {
|
|
1762
|
+
if let Some(mv) = self
|
|
1763
|
+
.enum_variant_mangling
|
|
1764
|
+
.get(mangled)
|
|
1765
|
+
.and_then(|table| table.get(&call.name))
|
|
1766
|
+
{
|
|
1767
|
+
call.name = mv.clone();
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
self.markTagsAgainstExpected(expr, expected);
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1703
1776
|
/// If `expr`'s own inferred type is a builtin primitive (`Int`/`Float`)
|
|
1704
1777
|
/// and `expected` names a "union" enum with a bare-primitive-wrap variant
|
|
1705
1778
|
/// matching that exact primitive (`enum Number = | Int | Float` — see
|
|
@@ -2054,6 +2127,32 @@ impl<'a> Monomorphizer<'a> {
|
|
|
2054
2127
|
let Some(variant) = e.variants.iter().find(|v| v.name == call.name) else {
|
|
2055
2128
|
return Ok(());
|
|
2056
2129
|
};
|
|
2130
|
+
// A payload whose param the enclosing return type pins to a tag union
|
|
2131
|
+
// (`return Err(Bad)` in `-> Try[Int, [Bad]]`) is that tag, even when a
|
|
2132
|
+
// declared variant shares its name — mark it before inferring from it.
|
|
2133
|
+
if let Some(rt) = self.current_return_type.clone() {
|
|
2134
|
+
if rt.name == e.name && rt.generics.len() == params.len() {
|
|
2135
|
+
for (field_ty, arg) in variant.fields.iter().zip(call.args.iter_mut()) {
|
|
2136
|
+
let Some(pi) = params.iter().position(|p| p == &field_ty.name) else {
|
|
2137
|
+
continue;
|
|
2138
|
+
};
|
|
2139
|
+
let gt = &rt.generics[pi];
|
|
2140
|
+
if gt.generics.is_empty() && isGenericParamName(>.name) {
|
|
2141
|
+
continue;
|
|
2142
|
+
}
|
|
2143
|
+
let resolved = self.resolveFieldType(gt);
|
|
2144
|
+
let expected = crate::plumTypeFromAst(&resolved);
|
|
2145
|
+
if matches!(expected, PlumType::TTags(_)) {
|
|
2146
|
+
let arg_expr = match arg {
|
|
2147
|
+
ast::Arg::Positional(e) => e,
|
|
2148
|
+
ast::Arg::Keyword { value, .. } => value,
|
|
2149
|
+
ast::Arg::Pair { value, .. } => value,
|
|
2150
|
+
};
|
|
2151
|
+
self.markTagsAgainstExpected(arg_expr, &expected);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2057
2156
|
let mut bindings: BTreeMap<String, PlumType> = BTreeMap::new();
|
|
2058
2157
|
for (field_ty, arg) in variant.fields.iter().zip(call.args.iter()) {
|
|
2059
2158
|
if params.contains(&field_ty.name) {
|
|
@@ -2476,6 +2575,7 @@ impl<'a> Monomorphizer<'a> {
|
|
|
2476
2575
|
});
|
|
2477
2576
|
if let Some(expected) = expected_ty {
|
|
2478
2577
|
self.wrapPrimitiveAgainstExpected(&mut fa.value, &expected, env);
|
|
2578
|
+
self.resolveGenericVariantAgainstExpected(&mut fa.value, &expected);
|
|
2479
2579
|
}
|
|
2480
2580
|
}
|
|
2481
2581
|
// Runs next (before the generic per-field rewrite below): a
|
|
@@ -2539,6 +2639,7 @@ impl<'a> Monomorphizer<'a> {
|
|
|
2539
2639
|
ast::Arg::Pair { value, .. } => value,
|
|
2540
2640
|
};
|
|
2541
2641
|
self.wrapPrimitiveAgainstExpected(e, expected, env);
|
|
2642
|
+
self.resolveGenericVariantAgainstExpected(e, expected);
|
|
2542
2643
|
}
|
|
2543
2644
|
}
|
|
2544
2645
|
for arg in &mut call.args {
|
|
@@ -2619,6 +2720,7 @@ impl<'a> Monomorphizer<'a> {
|
|
|
2619
2720
|
};
|
|
2620
2721
|
if !matches!(e, ast::Expr::Closure(_)) {
|
|
2621
2722
|
self.wrapPrimitiveAgainstExpected(e, expected, env);
|
|
2723
|
+
self.resolveGenericVariantAgainstExpected(e, expected);
|
|
2622
2724
|
}
|
|
2623
2725
|
}
|
|
2624
2726
|
}
|
|
@@ -2918,6 +3020,32 @@ pub fn monomorphizeSource(source: &ast::Source) -> Result<ast::Source, String> {
|
|
|
2918
3020
|
}
|
|
2919
3021
|
}
|
|
2920
3022
|
|
|
3023
|
+
// Same reasoning for a non-generic enum's variant field types: a
|
|
3024
|
+
// construction (`Holder(t: Ok(5))`) in an item EARLIER than the enum's own
|
|
3025
|
+
// declaration must already see `Try$Int$[X]`, not the stale bare `Try`,
|
|
3026
|
+
// to resolve its field values against (the main loop below refreshes
|
|
3027
|
+
// them again, idempotently, when it reaches the enum).
|
|
3028
|
+
for item in &source.items {
|
|
3029
|
+
if let ast::Item::Enum(e) = item {
|
|
3030
|
+
if !enumGenericParams(e).is_empty() {
|
|
3031
|
+
continue;
|
|
3032
|
+
}
|
|
3033
|
+
for v in &e.variants {
|
|
3034
|
+
if v.fields.is_empty() || !m.enum_variants.contains_key(v.name.as_str()) {
|
|
3035
|
+
continue;
|
|
3036
|
+
}
|
|
3037
|
+
let field_types: Vec<PlumType> = v
|
|
3038
|
+
.fields
|
|
3039
|
+
.iter()
|
|
3040
|
+
.map(|f| crate::plumTypeFromAst(&m.resolveFieldType(f)))
|
|
3041
|
+
.collect();
|
|
3042
|
+
if let Some(info) = m.enum_variants.get_mut(v.name.as_str()) {
|
|
3043
|
+
info.field_types = field_types;
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
|
|
2921
3049
|
for item in &source.items {
|
|
2922
3050
|
match item {
|
|
2923
3051
|
ast::Item::Enum(e) if enumGenericParams(e).is_empty() => {
|
plum-checker/tests/checker_tests.rs
CHANGED
|
@@ -1674,11 +1674,23 @@ enum Color =
|
|
|
1674
1674
|
| Red
|
|
1675
1675
|
| Blue
|
|
1676
1676
|
|
|
1677
|
-
fun f() -> Try[Int, [
|
|
1677
|
+
fun f() -> Try[Int, [Green]] =
|
|
1678
1678
|
Err(Red)
|
|
1679
1679
|
",
|
|
1680
1680
|
);
|
|
1681
1681
|
assert!(bad.is_err());
|
|
1682
|
+
// Where the expected type is a tag union naming it, `Red` is the tag.
|
|
1683
|
+
let tag = checkTry(
|
|
1684
|
+
"\
|
|
1685
|
+
enum Color =
|
|
1686
|
+
| Red
|
|
1687
|
+
| Blue
|
|
1688
|
+
|
|
1689
|
+
fun f() -> Try[Int, [Red]] =
|
|
1690
|
+
Err(Red)
|
|
1691
|
+
",
|
|
1692
|
+
);
|
|
1693
|
+
assert!(tag.is_ok(), "got {:?}", tag.err());
|
|
1682
1694
|
}
|
|
1683
1695
|
|
|
1684
1696
|
#[test]
|
|
@@ -1707,3 +1719,72 @@ fun main() =
|
|
|
1707
1719
|
let mono = plum_checker::monomorphize::monomorphizeSource(&source);
|
|
1708
1720
|
assert!(mono.is_ok(), "expected Ok, got {:?}", mono.err());
|
|
1709
1721
|
}
|
|
1722
|
+
|
|
1723
|
+
#[test]
|
|
1724
|
+
fn okErrPassedDirectlyAsArgOrFieldTakeExpectedSpecialization() {
|
|
1725
|
+
// `Ok(3)` only pins `T`; `E` must come from the param/field type, and the
|
|
1726
|
+
// generic variant's base name `Ok` must never be read as a tag literal.
|
|
1727
|
+
// `Holder` is declared AFTER its use on purpose.
|
|
1728
|
+
let src = "\
|
|
1729
|
+
fun take(t: Try[Int, [Nope]]) -> Int =
|
|
1730
|
+
match t
|
|
1731
|
+
Ok(v) => v
|
|
1732
|
+
Err(_) => 0
|
|
1733
|
+
|
|
1734
|
+
fun mk() -> Holder =
|
|
1735
|
+
Holder(t: Ok(4))
|
|
1736
|
+
|
|
1737
|
+
enum Holder =
|
|
1738
|
+
| Holder(t: Try[Int, [Nope]])
|
|
1739
|
+
|
|
1740
|
+
fun put(self, t: Try[Int, [Nope]]) -> Int =
|
|
1741
|
+
take(t)
|
|
1742
|
+
|
|
1743
|
+
fun main() -> Int =
|
|
1744
|
+
take(Ok(3)) + take(Err(Nope)) + mk().put(Ok(5))
|
|
1745
|
+
";
|
|
1746
|
+
let r = checkTry(src);
|
|
1747
|
+
assert!(r.is_ok(), "expected Ok, got {}", errText(r));
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
#[test]
|
|
1751
|
+
fn unpinnedOkIsNotATagLiteral() {
|
|
1752
|
+
// (needs some concrete `Try` specialization to exist, as in any real
|
|
1753
|
+
// program importing std/Try; with none, the checker can't tell `Ok` from a tag)
|
|
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!(!msg.contains("[Ok("), "{}", msg);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
#[test]
|
|
1761
|
+
fn tagNamedLikeAVariantIsTheTagWhereAUnionIsExpected() {
|
|
1762
|
+
let src = "\
|
|
1763
|
+
enum Rating =
|
|
1764
|
+
| Good(name: Str)
|
|
1765
|
+
| Bad(name: Str)
|
|
1766
|
+
|
|
1767
|
+
fun f() -> Try[Int, [Bad]] =
|
|
1768
|
+
return Err(Bad)
|
|
1769
|
+
|
|
1770
|
+
fun g() -> [Bad, Worse] =
|
|
1771
|
+
Bad
|
|
1772
|
+
|
|
1773
|
+
fun code(t: [Bad, Worse]) -> Int =
|
|
1774
|
+
match t
|
|
1775
|
+
Bad => 1
|
|
1776
|
+
Worse => 2
|
|
1777
|
+
|
|
1778
|
+
fun errCode(t: Try[Int, [Bad, Worse]]) -> Int =
|
|
1779
|
+
match t
|
|
1780
|
+
Ok(_) => 0
|
|
1781
|
+
Err(Bad) => 1
|
|
1782
|
+
Err(Worse) => 2
|
|
1783
|
+
|
|
1784
|
+
fun main() -> Int =
|
|
1785
|
+
r := Bad(name: \"x\")
|
|
1786
|
+
code(g()) + code(Bad) + errCode(Err(Bad)) + errCode(f())
|
|
1787
|
+
";
|
|
1788
|
+
let r = checkTry(src);
|
|
1789
|
+
assert!(r.is_ok(), "expected Ok, got {}", errText(r));
|
|
1790
|
+
}
|
plum-examples/dop_visitor.plum
CHANGED
|
@@ -43,7 +43,7 @@ import std/Str
|
|
|
43
43
|
|
|
44
44
|
enum Rating =
|
|
45
45
|
| Good(name: Str)
|
|
46
|
-
|
|
|
46
|
+
| Bad(name: Str)
|
|
47
47
|
|
|
48
48
|
enum Book =
|
|
49
49
|
| FantasyBook(title: Str, pages: Int, hasMythicalCreatures: Bool)
|
|
@@ -107,6 +107,6 @@ test "non fiction book with a bad rating reports mixed reviews"
|
|
|
107
107
|
title: "Shaky Claims",
|
|
108
108
|
pages: 80,
|
|
109
109
|
rating1: Good(name: "Alice"),
|
|
110
|
-
rating2:
|
|
110
|
+
rating2: Bad(name: "Carol"),
|
|
111
111
|
)
|
|
112
112
|
assert collectInterestingInfo(book) == "Non-fiction book \"Shaky Claims\" has mixed reviews"
|
plum-examples/match.plum
CHANGED
|
@@ -125,9 +125,6 @@ fun combine(a: Int, b: Int) -> Int =
|
|
|
125
125
|
x, 0 => x
|
|
126
126
|
x, y => x + y
|
|
127
127
|
|
|
128
|
-
fun okInt(n: Int) -> Try[Int, [Missing]] =
|
|
129
|
-
return Ok(n)
|
|
130
|
-
|
|
131
128
|
fun bothOptions(a: Try[Int, [Missing]], b: Try[Int, [Missing]]) -> Int =
|
|
132
129
|
match a, b
|
|
133
130
|
Ok(x), Ok(y) => x + y
|
|
@@ -194,7 +191,7 @@ test "non bool bare tag pattern runs correctly"
|
|
|
194
191
|
assert colorCode(Green) == 2
|
|
195
192
|
|
|
196
193
|
test "constructor pattern wildcard field runs correctly"
|
|
197
|
-
assert isSome(
|
|
194
|
+
assert isSome(Ok(99)) == 1
|
|
198
195
|
|
|
199
196
|
test "constructor pattern does not misfire on payload free sibling"
|
|
200
197
|
assert describeOption(Absent) == 0
|
|
@@ -212,7 +209,7 @@ test "multi subject match with binding and wildcard runs correctly"
|
|
|
212
209
|
assert combine(3, 4) == 7
|
|
213
210
|
|
|
214
211
|
test "multi subject match with generic enum variant runs correctly"
|
|
215
|
-
assert bothOptions(
|
|
212
|
+
assert bothOptions(Ok(3), Ok(4)) == 7
|
|
216
213
|
|
|
217
214
|
test "match destructures a plain class the same way it destructures an enum variant"
|
|
218
215
|
assert Point(x: 3, y: 4).sum() == 7
|
plum-examples/oop_visitor.plum
CHANGED
|
@@ -36,7 +36,7 @@ import std/Str
|
|
|
36
36
|
|
|
37
37
|
enum Rating =
|
|
38
38
|
| Good(name: Str)
|
|
39
|
-
|
|
|
39
|
+
| Bad(name: Str)
|
|
40
40
|
|
|
41
41
|
trait Book =
|
|
42
42
|
collectInterestingInfo() -> Str
|
|
@@ -119,6 +119,6 @@ test "non fiction book with a bad rating reports mixed reviews"
|
|
|
119
119
|
title: "Shaky Claims",
|
|
120
120
|
pages: 80,
|
|
121
121
|
rating1: Good(name: "Alice"),
|
|
122
|
-
rating2:
|
|
122
|
+
rating2: Bad(name: "Carol"),
|
|
123
123
|
)
|
|
124
124
|
assert book.collectInterestingInfo() == "Non-fiction book \"Shaky Claims\" has mixed reviews"
|
plum-examples/types.plum
CHANGED
|
@@ -88,9 +88,6 @@ fun stepToNumber(s: Step) -> Int =
|
|
|
88
88
|
ReadMin => 1
|
|
89
89
|
ReadMax => 2
|
|
90
90
|
|
|
91
|
-
fun okIntForTypes(n: Int) -> Try[Int, [Missing]] =
|
|
92
|
-
return Ok(n)
|
|
93
|
-
|
|
94
91
|
fun unwrapTryOr(o: Try[Int, [Missing]], default: Int) -> Int =
|
|
95
92
|
match o
|
|
96
93
|
Ok(v) =>
|
|
@@ -155,7 +152,7 @@ test "enum discriminant value matches by variant name correctly"
|
|
|
155
152
|
assert stepToNumber(ReadMin) * 10 + stepToNumber(ReadMax) == 12
|
|
156
153
|
|
|
157
154
|
test "payload variant construction compiles and runs"
|
|
158
|
-
assert unwrapTryOr(
|
|
155
|
+
assert unwrapTryOr(Ok(7), 0) == 7
|
|
159
156
|
|
|
160
157
|
test "multi field variant construction compiles and runs"
|
|
161
158
|
assert area(Rect(3, 4)) == 12
|
|
@@ -197,7 +194,7 @@ test "a bare Int/Float value dispatches a method defined only on Number, via fal
|
|
|
197
194
|
assert {2.5}.kind() == "Float"
|
|
198
195
|
|
|
199
196
|
test "enum class field construct and destructure runs correctly"
|
|
200
|
-
b := TryBox(value:
|
|
197
|
+
b := TryBox(value: Ok(42))
|
|
201
198
|
assert b.unwrap(0) == 42
|
|
202
199
|
|
|
203
200
|
test "class field mutation and spread update run correctly"
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -5843,7 +5843,11 @@ fn compileExpr(
|
|
|
5843
5843
|
compileVariantConstruction(info, call, expr, body, ctx, state)?;
|
|
5844
5844
|
} else if !ctx.func_ids.contains_key(&call.name)
|
|
5845
5845
|
&& call.args.len() <= 1
|
|
5846
|
+
&& call
|
|
5847
|
+
.name
|
|
5848
|
+
.chars()
|
|
5849
|
+
.next()
|
|
5846
|
-
|
|
5850
|
+
.is_some_and(|c| c.is_uppercase() || c == plum_checker::TAG_MARKER)
|
|
5847
5851
|
&& matches!(inferLocalType(expr, ctx), PlumType::TTags(_))
|
|
5848
5852
|
{
|
|
5849
5853
|
// `Bad(x)` / `Bad()` — an undeclared uppercase name is a tag literal.
|
|
@@ -5852,7 +5856,13 @@ fn compileExpr(
|
|
|
5852
5856
|
ast::Arg::Keyword { value, .. } => value,
|
|
5853
5857
|
ast::Arg::Pair { value, .. } => value,
|
|
5854
5858
|
});
|
|
5859
|
+
compileTagLiteral(
|
|
5855
|
-
|
|
5860
|
+
plum_checker::tagLiteralName(&call.name),
|
|
5861
|
+
payload,
|
|
5862
|
+
body,
|
|
5863
|
+
ctx,
|
|
5864
|
+
state,
|
|
5865
|
+
)?;
|
|
5856
5866
|
} else {
|
|
5857
5867
|
fn argExprOf(arg: &ast::Arg) -> &ast::Expr {
|
|
5858
5868
|
match arg {
|
|
@@ -5953,10 +5963,13 @@ fn compileExpr(
|
|
|
5953
5963
|
let const_value = CURRENT_CONSTS.with(|c| c.borrow().get(n).cloned());
|
|
5954
5964
|
match const_value {
|
|
5955
5965
|
Some(value) => compileExpr(&value, body, ctx, state)?,
|
|
5966
|
+
None if n
|
|
5967
|
+
.chars()
|
|
5968
|
+
.next()
|
|
5956
|
-
|
|
5969
|
+
.is_some_and(|c| c.is_uppercase() || c == plum_checker::TAG_MARKER)
|
|
5957
5970
|
&& matches!(inferLocalType(expr, ctx), PlumType::TTags(_)) =>
|
|
5958
5971
|
{
|
|
5959
|
-
compileTagLiteral(n, None, body, ctx, state)?
|
|
5972
|
+
compileTagLiteral(plum_checker::tagLiteralName(n), None, body, ctx, state)?
|
|
5960
5973
|
}
|
|
5961
5974
|
None => {
|
|
5962
5975
|
return Err(format!(
|