plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
3deb646
— Peter John
2026-11-01T22:02:52+05:30
refactor: Bool.parse and Bool.toStr live in primitive Bool; builtin Bool.toStr removed
- README.md +2 -2
- plum-checker/src/lib.rs +0 -54
- plum-lsp/src/main.rs +1 -1
- plum-std/Bool.plum +29 -0
- plum-std/Str.plum +0 -20
- plum-wasm-codegen/src/lib.rs +0 -3
- plum-wasm-codegen/tests/codegen_tests.rs +48 -4
README.md
CHANGED
|
@@ -929,7 +929,7 @@ primitive Int =
|
|
|
929
929
|
fun parse(s: Str) -> error{BadNumStr}!Int
|
|
930
930
|
```
|
|
931
931
|
|
|
932
|
-
The std signatures: `List.get` fails with `OutOfBounds`, `first`/`last`/`pop`/`sample` with `ListWasEmpty`, `findValue` with `NotFound`, `Map.get` with `KeyNotFound`, `Int.parse`/`Float.parse` with `BadNumStr`, `
|
|
932
|
+
The std signatures: `List.get` fails with `OutOfBounds`, `first`/`last`/`pop`/`sample` with `ListWasEmpty`, `findValue` with `NotFound`, `Map.get` with `KeyNotFound`, `Int.parse`/`Float.parse` with `BadNumStr`, `Bool.parse` with `BadBoolStr`, `Json.fromStr` with `InvalidJson` (`Json.parse` returns where and why as an ordinary value), the `Os` file functions with `NoSuchPath`/`IoFailed` (the set `IoError` where a function can hit both), `Http.request` with `HttpFailed`/`BadResponse` (the set `HttpError`), `Uuid.fromStr` with `InvalidUuid`. Where absence is not an error the result is an optional instead: `List.head`, the `find()` terminal and `Map.getOrNull` return `T?`.
|
|
933
933
|
|
|
934
934
|
### Propagating: `try expr`
|
|
935
935
|
|
|
@@ -1036,7 +1036,7 @@ hello, world!
|
|
|
1036
1036
|
|
|
1037
1037
|
## Standard library highlights
|
|
1038
1038
|
|
|
1039
|
-
- **`Bool`** — built in (no import); `
|
|
1039
|
+
- **`Bool`** — `true`/`false`, `if`, `&&`/`||`/`!` and `==` are built in (no import); `toStr` (also used by `"{b}"` interpolation) and `Bool.parse("true")` need `import std/Bool`
|
|
1040
1040
|
- **`Int`** / **`Float`** (`import std/Int`, `import std/Float`) — built-in numbers with their API in `primitive Int` / `primitive Float`: `abs`, `sign`, `min`, `max`, `sqrt`, `pow`, `floor`, `round`, `toStr`, ..., the constants `Int.MAX`, `Float.PI`, ..., and `Int.parse` / `Float.parse`
|
|
1041
1041
|
- **`Math`** (`import std/Math`) — a `primitive` namespace of floating-point functions: `Math.sqrt`, `Math.sin`, `Math.exp`, `Math.ln`, ...
|
|
1042
1042
|
- **`Str`** (`import std/Str`) — a byte array with a codepoint-aware layer for Unicode correctness: `runeLength`, `runeAt`, `codePointAt`, `codePointToStr`
|
plum-checker/src/lib.rs
CHANGED
|
@@ -1102,63 +1102,9 @@ pub fn buildGlobalTables(source: &ast::Source) -> (TypeEnv, MethodEnv, EnumVaria
|
|
|
1102
1102
|
}
|
|
1103
1103
|
}
|
|
1104
1104
|
|
|
1105
|
-
// `Bool.toStr` is a compiler builtin (see `builtinBoolToStr`); it is visible as a
|
|
1106
|
-
// method once `Str` is in the program.
|
|
1107
|
-
if hasStr(source) {
|
|
1108
|
-
methods
|
|
1109
|
-
.entry(("Bool".to_string(), "toStr".to_string()))
|
|
1110
|
-
.or_insert_with(|| PlumType::TFun(Vec::new(), Box::new(PlumType::TStr)));
|
|
1111
|
-
}
|
|
1112
1105
|
(global_env, methods, enum_variants, enum_params)
|
|
1113
1106
|
}
|
|
1114
1107
|
|
|
1115
|
-
fn hasStr(source: &ast::Source) -> bool {
|
|
1116
|
-
source
|
|
1117
|
-
.items
|
|
1118
|
-
.iter()
|
|
1119
|
-
.any(|i| matches!(i, ast::Item::Enum(e) if e.name == "Str"))
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
/// True if `source` should get the builtin `Bool.toStr` (it returns `Str`, and
|
|
1123
|
-
/// the user has not defined their own).
|
|
1124
|
-
pub fn needsBuiltinBoolToStr(source: &ast::Source) -> bool {
|
|
1125
|
-
hasStr(source)
|
|
1126
|
-
&& !source.items.iter().any(|i| {
|
|
1127
|
-
matches!(i, ast::Item::Fn(f) if f.name == "toStr" && f.type_param.as_deref() == Some("Bool"))
|
|
1128
|
-
})
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
/// The builtin `Bool.toStr` as an ordinary method AST (`if self then "true" else "false"`),
|
|
1132
|
-
/// for codegen to compile as one wasm function. Nothing is added to the source.
|
|
1133
|
-
pub fn builtinBoolToStr() -> ast::Fn {
|
|
1134
|
-
let lit = |t: &str| {
|
|
1135
|
-
ast::Expr::String(ast::StringExpr {
|
|
1136
|
-
parts: vec![ast::StringPart::Text(t.to_string())],
|
|
1137
|
-
})
|
|
1138
|
-
};
|
|
1139
|
-
ast::Fn {
|
|
1140
|
-
file: String::new(),
|
|
1141
|
-
name: "toStr".to_string(),
|
|
1142
|
-
line: 0,
|
|
1143
|
-
column: 0,
|
|
1144
|
-
type_param: Some("Bool".to_string()),
|
|
1145
|
-
is_extern: false,
|
|
1146
|
-
is_star: false,
|
|
1147
|
-
has_self: true,
|
|
1148
|
-
yields: None,
|
|
1149
|
-
params: Vec::new(),
|
|
1150
|
-
returns: Some(ast::Type {
|
|
1151
|
-
name: "Str".to_string(),
|
|
1152
|
-
generics: Vec::new(),
|
|
1153
|
-
}),
|
|
1154
|
-
body: ast::FnBody::Expr(ast::Expr::InlineIf(Box::new(ast::InlineIf {
|
|
1155
|
-
condition: ast::Expr::Self_,
|
|
1156
|
-
then: lit("true"),
|
|
1157
|
-
else_: lit("false"),
|
|
1158
|
-
}))),
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
1108
|
pub fn checkSource(source: &ast::Source) -> CheckResult<()> {
|
|
1163
1109
|
checkSourceFull(source).0
|
|
1164
1110
|
}
|
plum-lsp/src/main.rs
CHANGED
|
@@ -448,7 +448,7 @@ mod tests {
|
|
|
448
448
|
std::fs::write(&path, text).unwrap();
|
|
449
449
|
let cases: [(usize, &str, &[&str]); 3] = [
|
|
450
450
|
(9, "f", &["sqrt", "toStr"]),
|
|
451
|
-
(11, "s", &["length", "split"
|
|
451
|
+
(11, "s", &["length", "split"]),
|
|
452
452
|
(13, "m", &["get", "keys", "values", "entries"]),
|
|
453
453
|
];
|
|
454
454
|
for (line, recv, wants) in cases {
|
plum-std/Bool.plum
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module std
|
|
2
|
+
import std/Str
|
|
3
|
+
|
|
4
|
+
# The API of the built-in `Bool` type: `Bool.parse` and `toStr` (also used by
|
|
5
|
+
# `"{b}"` interpolation). `true`/`false` and the operators are built in; the
|
|
6
|
+
# methods need `import std/Bool`.
|
|
7
|
+
primitive Bool =
|
|
8
|
+
fun toStr(self) -> Str =
|
|
9
|
+
if self then "true" else "false"
|
|
10
|
+
|
|
11
|
+
# Parses exactly `"true"` or `"false"`.
|
|
12
|
+
fun parse(s: Str) -> error{BadBoolStr}!Bool =
|
|
13
|
+
if s == "true"
|
|
14
|
+
true
|
|
15
|
+
else if s == "false"
|
|
16
|
+
false
|
|
17
|
+
else
|
|
18
|
+
return error.BadBoolStr
|
|
19
|
+
|
|
20
|
+
test "Bool.parse accepts true/false and rejects everything else"
|
|
21
|
+
assert Bool.parse("true") catch false
|
|
22
|
+
assert !{Bool.parse("false") catch true}
|
|
23
|
+
assert !{Bool.parse("yes") catch false}
|
|
24
|
+
|
|
25
|
+
test "Bool.toStr is true/false, also through interpolation"
|
|
26
|
+
t := true
|
|
27
|
+
assert t.toStr() == "true"
|
|
28
|
+
assert false.toStr() == "false"
|
|
29
|
+
assert "{t}-{!t}" == "true-false"
|
plum-std/Str.plum
CHANGED
|
@@ -140,15 +140,6 @@ record Str(bytes: []Byte) =
|
|
|
140
140
|
fun toStr(self) -> Str =
|
|
141
141
|
self
|
|
142
142
|
|
|
143
|
-
# Parses exactly `"true"` or `"false"`.
|
|
144
|
-
fun parseBool(self) -> error{BadBoolStr}!Bool =
|
|
145
|
-
if self == "true"
|
|
146
|
-
true
|
|
147
|
-
else if self == "false"
|
|
148
|
-
false
|
|
149
|
-
else
|
|
150
|
-
return error.BadBoolStr
|
|
151
|
-
|
|
152
143
|
# Pads the start of `self` with repeated copies of `sub` until its length is
|
|
153
144
|
# at least `count`. If `sub` doesn't evenly divide the needed amount, the
|
|
154
145
|
# final length may overshoot `count` by up to `sub.length() - 1`.
|
|
@@ -757,17 +748,6 @@ test "escape/unescape round-trip HTML-sensitive characters"
|
|
|
757
748
|
assert "<a href=\"x\">&'</a>".escape() == "<a href="x">&'</a>"
|
|
758
749
|
assert "<a href="x">&'</a>".unescape() == "<a href=\"x\">&'</a>"
|
|
759
750
|
|
|
760
|
-
test "Bool.toStr is a compiler intrinsic: true/false, also through interpolation"
|
|
761
|
-
t := true
|
|
762
|
-
assert t.toStr() == "true"
|
|
763
|
-
assert false.toStr() == "false"
|
|
764
|
-
assert "{t}-{!t}" == "true-false"
|
|
765
|
-
|
|
766
|
-
test "Str.parseBool accepts true/false and rejects everything else"
|
|
767
|
-
assert "true".parseBool() catch false
|
|
768
|
-
assert !{"false".parseBool() catch true}
|
|
769
|
-
assert !{"yes".parseBool() catch false}
|
|
770
|
-
|
|
771
751
|
# ---- large-input (linear-time, no stack growth) regression tests ----
|
|
772
752
|
|
|
773
753
|
test "sub of a 200000-byte string is a single copy"
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -1859,8 +1859,6 @@ fn compileSourceInner(
|
|
|
1859
1859
|
for ((id, ptr), t) in inst_expr_types {
|
|
1860
1860
|
inst_types[id].insert(ptr, t);
|
|
1861
1861
|
}
|
|
1862
|
-
// `Bool.toStr` is a checker/codegen builtin: compiled as one extra method, never added to the source.
|
|
1863
|
-
let bool_to_str_fn = plum_checker::needsBuiltinBoolToStr(source).then(plum_checker::builtinBoolToStr);
|
|
1864
1862
|
let (global_env, methods, enum_variants, enum_params) = plum_checker::buildGlobalTables(source);
|
|
1865
1863
|
let min_required = plum_checker::buildMinRequiredArgs(source);
|
|
1866
1864
|
let primitive_generics = plum_checker::buildPrimitiveGenerics(source);
|
|
@@ -1884,7 +1882,6 @@ fn compileSourceInner(
|
|
|
1884
1882
|
ast::Item::Fn(f) if !f.is_extern && !f.is_star => Some(f),
|
|
1885
1883
|
_ => None,
|
|
1886
1884
|
})
|
|
1887
|
-
.chain(bool_to_str_fn.iter())
|
|
1888
1885
|
.collect();
|
|
1889
1886
|
let mut units: Vec<UnitDef> = Vec::new();
|
|
1890
1887
|
let mut known_fns: HashSet<String> = HashSet::new();
|
plum-wasm-codegen/tests/codegen_tests.rs
CHANGED
|
@@ -1459,6 +1459,7 @@ fun main() -> Int =
|
|
|
1459
1459
|
fn boxed_bool_to_str_and_eq() {
|
|
1460
1460
|
let src = "\
|
|
1461
1461
|
import std/Str
|
|
1462
|
+
import std/Bool
|
|
1462
1463
|
import std/Int
|
|
1463
1464
|
import std/Float
|
|
1464
1465
|
|
|
@@ -1480,6 +1481,7 @@ fun main() -> Int =
|
|
|
1480
1481
|
fn builtin_bool_in_error_union_with_question_and_catch() {
|
|
1481
1482
|
let src = "\
|
|
1482
1483
|
import std/Str
|
|
1484
|
+
import std/Bool
|
|
1483
1485
|
import std/Int
|
|
1484
1486
|
import std/Float
|
|
1485
1487
|
|
|
@@ -1496,7 +1498,7 @@ fun chain(ok: Bool) -> error{Nope}!Bool =
|
|
|
1496
1498
|
fun main() -> Int =
|
|
1497
1499
|
a := chain(true) catch true
|
|
1498
1500
|
b := chain(false) catch true
|
|
1499
|
-
p := \"true\"
|
|
1501
|
+
p := Bool.parse(\"true\") catch false
|
|
1500
1502
|
if !a && b && p
|
|
1501
1503
|
return 1
|
|
1502
1504
|
return 0
|
|
@@ -1561,6 +1563,7 @@ fn bool_tostr_intrinsic() {
|
|
|
1561
1563
|
import std/Int
|
|
1562
1564
|
import std/Float
|
|
1563
1565
|
import std/Str
|
|
1566
|
+
import std/Bool
|
|
1564
1567
|
|
|
1565
1568
|
fun main() -> Int =
|
|
1566
1569
|
t := true
|
|
@@ -1577,6 +1580,7 @@ fn bool_tostr_lowercase() {
|
|
|
1577
1580
|
import std/Int
|
|
1578
1581
|
import std/Float
|
|
1579
1582
|
import std/Str
|
|
1583
|
+
import std/Bool
|
|
1580
1584
|
|
|
1581
1585
|
fun main() -> Int =
|
|
1582
1586
|
t := true
|
|
@@ -1588,17 +1592,57 @@ fun main() -> Int =
|
|
|
1588
1592
|
assert_eq!(runMerged::<i64>("bool_tostr_lowercase", src), 1);
|
|
1589
1593
|
}
|
|
1590
1594
|
|
|
1595
|
+
#[test]
|
|
1596
|
+
fn bool_interpolation_with_import() {
|
|
1597
|
+
let src = "\
|
|
1598
|
+
import std/Int
|
|
1599
|
+
import std/Float
|
|
1600
|
+
import std/Str
|
|
1601
|
+
import std/Bool
|
|
1602
|
+
|
|
1603
|
+
fun main() -> Int =
|
|
1604
|
+
if \"{true}\" == \"true\" && \"{1 > 2}\" == \"false\"
|
|
1605
|
+
return 1
|
|
1606
|
+
return 0
|
|
1607
|
+
";
|
|
1608
|
+
assert_eq!(runMerged::<i64>("bool_interp_import", src), 1);
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
#[test]
|
|
1612
|
+
fn bool_tostr_without_import_is_rejected() {
|
|
1613
|
+
let src = "\
|
|
1614
|
+
import std/Int
|
|
1615
|
+
import std/Float
|
|
1616
|
+
import std/Str
|
|
1617
|
+
|
|
1618
|
+
fun main() -> Int =
|
|
1619
|
+
t := true
|
|
1620
|
+
if t.toStr() == \"true\"
|
|
1621
|
+
return 1
|
|
1622
|
+
return 0
|
|
1623
|
+
";
|
|
1624
|
+
let dir = std::env::temp_dir().join(format!("plum_t3_bool_noimport_{}", std::process::id()));
|
|
1625
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
1626
|
+
let path = dir.join("main.plum");
|
|
1627
|
+
std::fs::write(&path, src).unwrap();
|
|
1628
|
+
let source = plum_core::loadAndMerge(&path, &repoRoot()).expect("load");
|
|
1629
|
+
// Same as any other method of a std type that was not imported: rejected by the checker or by codegen.
|
|
1630
|
+
let rejected = plum_checker::checkSource(&source).is_err() || compileSource(&source).is_err();
|
|
1631
|
+
assert!(rejected, "Bool.toStr without import std/Bool must be rejected");
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1591
1634
|
#[test]
|
|
1592
1635
|
fn parse_bool() {
|
|
1593
1636
|
let src = "\
|
|
1594
1637
|
import std/Int
|
|
1595
1638
|
import std/Float
|
|
1596
1639
|
import std/Str
|
|
1640
|
+
import std/Bool
|
|
1597
1641
|
|
|
1598
1642
|
fun main() -> Int =
|
|
1599
|
-
a := \"true\"
|
|
1643
|
+
a := Bool.parse(\"true\") catch false
|
|
1600
|
-
b := \"false\"
|
|
1644
|
+
b := Bool.parse(\"false\") catch true
|
|
1601
|
-
c := \"nope\"
|
|
1645
|
+
c := Bool.parse(\"nope\") catch false
|
|
1602
1646
|
if a && !b && !c
|
|
1603
1647
|
return 1
|
|
1604
1648
|
return 0
|