plum

#treesitter#compiler#wasm

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 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`, `Str.parseBool` 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?`.
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); `true`/`false`, `toStr` is a compiler intrinsic, `"true".parseBool()` (in `std/Str`) parses it
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", "parseBool"]),
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() == "&lt;a href=&quot;x&quot;&gt;&amp;&#39;&lt;/a&gt;"
758
749
  assert "&lt;a href=&quot;x&quot;&gt;&amp;&#39;&lt;/a&gt;".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\".parseBool() catch false
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\".parseBool() catch false
1643
+ a := Bool.parse(\"true\") catch false
1600
- b := \"false\".parseBool() catch true
1644
+ b := Bool.parse(\"false\") catch true
1601
- c := \"nope\".parseBool() catch false
1645
+ c := Bool.parse(\"nope\") catch false
1602
1646
  if a && !b && !c
1603
1647
  return 1
1604
1648
  return 0