plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


73b5e55Peter John 2026-09-04T20:39:27+05:30
feat(plum): make Bool an ordinary import-gated enum, no hardcoded prelude
examples/basics.plum CHANGED
@@ -1,4 +1,5 @@
1
1
  module basics
2
+ import std/Bool
2
3
 
3
4
  MAX_RETRIES = 3
4
5
  GOLDEN_RATIO = 1.61803
examples/closures.plum CHANGED
@@ -1,4 +1,5 @@
1
1
  import std/Str
2
+ import std/Bool
2
3
 
3
4
  fun each(cb: fn(Int) -> Int) -> Int =
4
5
  cb(5)
examples/control_flow.plum CHANGED
@@ -1,4 +1,5 @@
1
1
  import std/Str
2
+ import std/Bool
2
3
 
3
4
  fun loopSum(limit: Int) -> Int =
4
5
  total := 0
examples/dop_visitor.plum CHANGED
@@ -37,6 +37,7 @@
37
37
  # for anything that needs to treat the shapes polymorphically.
38
38
 
39
39
  import std/os
40
+ import std/Bool
40
41
 
41
42
  enum Rating =
42
43
  | Good(name: Str)
examples/functions.plum CHANGED
@@ -1,4 +1,5 @@
1
1
  import std/Str
2
+ import std/Bool
2
3
 
3
4
  fun addInts(a: Int, b: Int) -> Int =
4
5
  a + b
examples/io.plum CHANGED
@@ -1,4 +1,5 @@
1
1
  import std/Str
2
+ import std/Bool
2
3
 
3
4
  # `printLn` mirrors `libs/std/os.plum`'s declaration; redeclared locally (rather
4
5
  # than `import std/os`) so this stays a self-contained example for the
examples/match.plum CHANGED
@@ -1,4 +1,5 @@
1
1
  import std/Option
2
+ import std/Bool
2
3
 
3
4
  enum Color =
4
5
  | Red
examples/methods.plum CHANGED
@@ -1,4 +1,5 @@
1
1
  import std/Str
2
+ import std/Bool
2
3
 
3
4
  type Cat =
4
5
  name: Str
examples/oop_visitor.plum CHANGED
@@ -30,6 +30,7 @@
30
30
  # which is a real cost specific to this language.
31
31
 
32
32
  import std/os
33
+ import std/Bool
33
34
 
34
35
  enum Rating =
35
36
  | Good(name: Str)
examples/strings.plum CHANGED
@@ -1,3 +1,5 @@
1
+ import std/Bool
2
+
1
3
  fun greet(name: Str) -> Str =
2
4
  "Hello, {name}!"
3
5
 
examples/testing.plum CHANGED
@@ -1,4 +1,5 @@
1
1
  import std/Str
2
+ import std/Bool
2
3
 
3
4
  fun add(a: Int, b: Int) -> Int =
4
5
  a + b
examples/types.plum CHANGED
@@ -1,4 +1,5 @@
1
1
  import std/Option
2
+ import std/Bool
2
3
 
3
4
  type Point =
4
5
  x: Int
libs/std/Array.plum CHANGED
@@ -1,4 +1,5 @@
1
1
  module std
2
+ import std/Bool
2
3
 
3
4
  # A fixed-length, O(1)-indexable array of `T`, backed directly by a wasm-gc
4
5
  # `array<anyref>` — every `Array[T]` specialization (`Array$Int`, `Array$Str`,
libs/std/Buffer.plum CHANGED
@@ -1,6 +1,7 @@
1
1
  module std
2
2
  import std/ByteSlice
3
3
  import std/Str
4
+ import std/Bool
4
5
 
5
6
  # A Buffer is a growable, mutable sequence of bytes for efficiently building
6
7
  # up a `Str` piece by piece — modeled on Go's `bytes.Buffer`. Backed by a
libs/std/Byte.plum CHANGED
@@ -1,5 +1,6 @@
1
1
  module std
2
2
  import std/Int
3
+ import std/Bool
3
4
 
4
5
  # Byte is an unsigned 8-bit value (0-255) — Plum's counterpart to Go's
5
6
  # `byte`/`uint8`. `Byte(x)` converts an `Int` (wrapping, not trapping, if `x`
libs/std/ByteSlice.plum CHANGED
@@ -1,6 +1,7 @@
1
1
  module std
2
2
  import std/Byte
3
3
  import std/Str
4
+ import std/Bool
4
5
 
5
6
  # ByteSlice is `[]Byte` — Plum's counterpart to Go's byte slice: a
6
7
  # fixed-length, mutable sequence of raw bytes backed directly by a wasm-gc
libs/std/Err.plum CHANGED
@@ -1,4 +1,5 @@
1
1
  module std
2
+ import std/Bool
2
3
 
3
4
  # This is used to represent an error value across the language
4
5
  trait Err =
libs/std/Float.plum CHANGED
@@ -2,6 +2,7 @@ module std
2
2
  import std/Int
3
3
  import std/Result
4
4
  import std/Str
5
+ import std/Bool
5
6
 
6
7
  E = 2.718281828459045f # Euler's number, the base of natural logarithms, e, https://oeis.org/A001113
7
8
  LN10 = 2.302585092994046f # The natural logarithm of 10, https://oeis.org/A002392
libs/std/Int.plum CHANGED
@@ -2,6 +2,7 @@ module std
2
2
  import std/Float
3
3
  import std/Result
4
4
  import std/Str
5
+ import std/Bool
5
6
 
6
7
  MIN_VALUE = -0x8000_0000_0000_0000 # Lowest value of Int
7
8
  MAX_VALUE = 0x7FFF_FFFF_FFFF_FFFF # Highest value of Int
libs/std/Json.plum CHANGED
@@ -9,6 +9,7 @@ import std/Float
9
9
  import std/Str
10
10
  import std/Buffer
11
11
  import std/Err
12
+ import std/Bool
12
13
 
13
14
  # A parsed JSON value. Numbers are always stored as `Float` (JSON doesn't
14
15
  # distinguish integers from floats the way Plum does), and `JsonList`/
libs/std/List.plum CHANGED
@@ -3,6 +3,7 @@ module std
3
3
  import std/Option
4
4
  import std/Buffer
5
5
  import std/Int
6
+ import std/Bool
6
7
 
7
8
  # A node stores the data in a list and contains pointers to the previous and next sibling nodes
8
9
  type Node[T] =
libs/std/Map.plum CHANGED
@@ -2,6 +2,7 @@ module std
2
2
  import std/List
3
3
  import std/Option
4
4
  import std/Array
5
+ import std/Bool
5
6
 
6
7
  # Any type usable as a `Map` key needs a `hash`, so keys landing in different
7
8
  # buckets (`hash(k) % BUCKET_COUNT`) can be told apart in O(1) without a full
libs/std/Option.plum CHANGED
@@ -2,6 +2,7 @@ module std
2
2
 
3
3
  import std/Result
4
4
  import std/Str
5
+ import std/Bool
5
6
 
6
7
  # Option[T] represents a value that may or may not be present — Plum's
7
8
  # counterpart to Rust's `Option`/Go's "zero value or ok bool" idiom.
libs/std/Result.plum CHANGED
@@ -1,6 +1,7 @@
1
1
  module std
2
2
  import std/Option
3
3
  import std/Str
4
+ import std/Bool
4
5
 
5
6
  # Result[T, E] represents either success (`Ok`, carrying a `T`) or failure
6
7
  # (`Err`, carrying an `E`) — used throughout std for fallible operations
libs/std/Str.plum CHANGED
@@ -2,6 +2,7 @@ module std
2
2
 
3
3
  import std/List
4
4
  import std/Buffer
5
+ import std/Bool
5
6
 
6
7
  # Any type that can be converted to a str needs to implement this trait
7
8
  trait ToStr =
libs/std/Time.plum CHANGED
@@ -1,6 +1,7 @@
1
1
  module std
2
2
  import std/Int
3
3
  import std/Str
4
+ import std/Bool
4
5
 
5
6
  # A raw, host-provided wall-clock reading: milliseconds since the Unix epoch.
6
7
  extern fun rawNowMillis() -> Int
libs/std/Uuid.plum CHANGED
@@ -2,6 +2,7 @@ module std
2
2
  import std/Int
3
3
  import std/Result
4
4
  import std/Str
5
+ import std/Bool
5
6
 
6
7
  # A UUID, stored as its canonical 36-character
7
8
  # `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` text form.
libs/std/encoding/Base64.plum CHANGED
@@ -2,6 +2,7 @@ module std
2
2
  import std/Str
3
3
  import std/Buffer
4
4
  import std/Option
5
+ import std/Bool
5
6
 
6
7
  # The Base64 package contains support for doing Base64 binary-to-text encodings.
7
8
  #
libs/std/http.plum CHANGED
@@ -7,6 +7,7 @@ import std/Map
7
7
  import std/Option
8
8
  import std/Result
9
9
  import std/Str
10
+ import std/Bool
10
11
 
11
12
  # An HTTP response: status code, response headers, and the raw response
12
13
  # body as a `Str` (a byte array, so binary bodies round-trip intact).
libs/std/os.plum CHANGED
@@ -3,6 +3,7 @@ import std/List
3
3
  import std/Result
4
4
  import std/Str
5
5
  import std/Uuid
6
+ import std/Bool
6
7
 
7
8
  # NOTE: the original file modeled `stdin`/`stdout`/`stderr` as bare top-level
8
9
  # variable bindings and a `File(...)` constructor, but this language has no
libs/std/testing.plum CHANGED
@@ -3,6 +3,7 @@ module std
3
3
  import std/Option
4
4
  import std/Result
5
5
  import std/List
6
+ import std/Bool
6
7
 
7
8
  # Small jest/rspec-style assertion helpers for use inside `test` blocks, e.g.
8
9
  #
plum-checker/src/lib.rs CHANGED
@@ -63,6 +63,13 @@ pub fn unify(t1: &PlumType, t2: &PlumType) -> Result<(), String> {
63
63
  // the same real type spelled two different ways depending on how the
64
64
  // checker reached it, so they must unify with each other too.
65
65
  (PlumType::TStr, PlumType::TNamed(n)) | (PlumType::TNamed(n), PlumType::TStr) if n == "Str" => Ok(()),
66
+ // Same reasoning as `Str` above — `Bool` is an ordinary enum now
67
+ // (`enum Bool = | True | False` in `libs/std/Bool.plum`), so a bare
68
+ // `True`/`False`/`Ok(...)`-wrapped-variant reference can infer as
69
+ // `TNamed("Bool")` (via `EnumVariantInfo`/`bareVariantWrapType`)
70
+ // while a `Bool` type ANNOTATION resolves via `plumTypeFromName` to
71
+ // the dedicated `TBool` variant instead.
72
+ (PlumType::TBool, PlumType::TNamed(n)) | (PlumType::TNamed(n), PlumType::TBool) if n == "Bool" => Ok(()),
66
73
  (PlumType::TByte, PlumType::TByte) => Ok(()),
67
74
  (PlumType::TByteSlice, PlumType::TByteSlice) => Ok(()),
68
75
  (PlumType::TUnit, PlumType::TUnit) => Ok(()),
@@ -158,10 +165,6 @@ pub fn buildGlobalTables(source: &ast::Source) -> (TypeEnv, ClassEnv, MethodEnv,
158
165
  let mut methods: MethodEnv = BTreeMap::new();
159
166
  let mut enum_variants: EnumVariants = BTreeMap::new();
160
167
  let mut enum_params: EnumParams = BTreeMap::new();
161
- // `Bool`'s variants are built in (see `inferExpr`'s TypeName handling) rather
162
- // than requiring every source file to redeclare `enum Bool = | True | False`.
163
- enum_variants.insert("True".to_string(), EnumVariantInfo { enum_name: "Bool".to_string(), tag: 1, field_types: vec![], field_names: vec![], values: vec![] });
164
- enum_variants.insert("False".to_string(), EnumVariantInfo { enum_name: "Bool".to_string(), tag: 0, field_types: vec![], field_names: vec![], values: vec![] });
165
168
 
166
169
  // First pass: register class fields, THEN enum variants, so a bare variant
167
170
  // (`| FantasyBook`, no `[...]` payload) that names an already-declared
@@ -179,15 +182,6 @@ pub fn buildGlobalTables(source: &ast::Source) -> (TypeEnv, ClassEnv, MethodEnv,
179
182
  }
180
183
  for item in &source.items {
181
184
  match item {
182
- // `Bool` may be re-"declared" (`enum Bool = | True | False`) purely to
183
- // give it a nesting site for methods (`and`/`or`/`parse`/...) — the
184
- // language currently has no other way to attach a method to a builtin
185
- // type (see `libs/std/int.plum`/`float.plum`'s own `type Int =`/`type
186
- // Float =` for the same pattern). Re-registering its variants here
187
- // would silently overwrite the hardcoded tags above with whatever
188
- // order this declaration happens to list them in, flipping every
189
- // `True`/`False` tag used throughout the rest of the codegen. Skip.
190
- ast::Item::Enum(e) if e.name == "Bool" => {}
191
185
  ast::Item::Enum(e) => {
192
186
  let shared_field_types: Vec<PlumType> = e.params.iter()
193
187
  .map(|p| plumTypeFromAst(&p.ty))
@@ -1015,15 +1009,21 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
1015
1009
  // const (`PI`, `MAX_VALUE`, ...), since there's no separate "constant"
1016
1010
  // token. Prefer a matching const's real type over the enum-variant/
1017
1011
  // unmodeled-type-name fallbacks below.
1018
- ast::Expr::TypeName(n) => match n.as_str() {
1012
+ ast::Expr::TypeName(n) => match lookup(env, n) {
1019
- "True" | "False" => Ok(PlumType::TBool),
1020
- _ => match lookup(env, n) {
1021
- Ok(ty) if !matches!(ty, PlumType::TVar(_)) => Ok(ty),
1013
+ Ok(ty) if !matches!(ty, PlumType::TVar(_)) => Ok(ty),
1022
- _ => match ctx.enum_variants.get(n) {
1014
+ _ => match ctx.enum_variants.get(n) {
1015
+ // `Bool` normalizes to the dedicated `TBool` variant (not
1016
+ // `TNamed("Bool")`) — `True`/`False` only resolve here at all
1017
+ // once `enum Bool = | True | False` (`libs/std/Bool.plum`) is
1018
+ // actually imported, but every OTHER part of the checker AND
1019
+ // `plum-wasm-codegen` (which reuses `inferExpr` via
1020
+ // `inferLocalType`) still branches on the literal `TBool`
1021
+ // variant, not a generic `TNamed` name — normalizing here
1022
+ // keeps that working without patching every such site.
1023
+ Some(info) if info.enum_name == "Bool" => Ok(PlumType::TBool),
1023
- Some(info) => Ok(PlumType::TNamed(info.enum_name.clone())),
1024
+ Some(info) => Ok(PlumType::TNamed(info.enum_name.clone())),
1024
- // Unmodeled/builtin type name: allow, codegen will catch.
1025
+ // Unmodeled/builtin type name: allow, codegen will catch.
1025
- None => Ok(PlumType::TNamed(n.to_string())),
1026
+ None => Ok(PlumType::TNamed(n.to_string())),
1026
- },
1027
1027
  },
1028
1028
  },
1029
1029
  ast::Expr::Paren(inner) => inferExpr(inner, env, ctx),
plum-checker/tests/checker_tests.rs CHANGED
@@ -114,7 +114,7 @@ fn typeMismatchInBinaryOpIsError() {
114
114
 
115
115
  #[test]
116
116
  fn boolLiteralTrueFalseAreBool() {
117
- let src = "fun isTrue() -> Bool =\n True\n";
117
+ let src = "enum Bool =\n | True\n | False\n\nfun isTrue() -> Bool =\n True\n";
118
118
  let source = parse(src);
119
119
  assert!(checkSource(&source).is_ok(), "expected Ok, got {:?}", checkSource(&source).err());
120
120
  }
@@ -490,6 +490,10 @@ fun makeStrBox() -> Box =
490
490
  #[test]
491
491
  fn genericFunctionCalledWithDifferentConcreteTypesPerSiteTypeChecks() {
492
492
  let src = "\
493
+ enum Bool =
494
+ | True
495
+ | False
496
+
493
497
  fun wrap(value: T) -> Bool =
494
498
  True
495
499
 
@@ -507,6 +511,10 @@ fun useStr() -> Bool =
507
511
  #[test]
508
512
  fn genericFunctionWithTwoIndependentTypeParamsTypeChecks() {
509
513
  let src = "\
514
+ enum Bool =
515
+ | True
516
+ | False
517
+
510
518
  fun pair(first: T, second: U) -> Bool =
511
519
  True
512
520
 
@@ -730,6 +738,10 @@ fun use() -> Int =
730
738
  #[test]
731
739
  fn closureLiteralInfersAsAFunctionType() {
732
740
  let src = "\
741
+ enum Bool =
742
+ | True
743
+ | False
744
+
733
745
  fun useClosure() -> Bool =
734
746
  cb = |v|
735
747
  True
@@ -798,6 +810,10 @@ type Cat =
798
810
  #[test]
799
811
  fn closurePassedToFnValueTypedParamTypeChecks() {
800
812
  let src = "\
813
+ enum Bool =
814
+ | True
815
+ | False
816
+
801
817
  fun each(cb: fn(Int) -> Bool) -> Bool =
802
818
  cb(5)
803
819
 
plum-checker/tests/examples_test.rs CHANGED
@@ -1,11 +1,14 @@
1
1
  #![allow(non_snake_case)]
2
2
  use plum_checker::checkSource;
3
- use plum_core::AstParser;
4
3
 
5
4
  fn examplesDir() -> std::path::PathBuf {
6
5
  std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples")
7
6
  }
8
7
 
8
+ fn libPath() -> std::path::PathBuf {
9
+ std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../libs")
10
+ }
11
+
9
12
  fn exampleFiles() -> Vec<std::path::PathBuf> {
10
13
  let mut files: Vec<_> = std::fs::read_dir(examplesDir())
11
14
  .expect("examples/ directory should exist")
@@ -18,26 +21,17 @@ fn exampleFiles() -> Vec<std::path::PathBuf> {
18
21
  files
19
22
  }
20
23
 
21
- /// Every example must parse with zero ERROR/MISSING nodes and pass `checkSource`.
24
+ /// Every example must load (resolving its own `import`s against `../libs`,
25
+ /// the real compilation path — several examples now genuinely `import` real
26
+ /// stdlib types like `std/Bool`/`std/Str`, so a bare single-file parse would
27
+ /// leave those names undeclared) and pass `checkSource`. These files exist
22
- /// These files exist specifically to prove each piece of the currently-supported
28
+ /// specifically to prove each piece of the currently-supported grammar
23
- /// grammar surface actually works end to end, not just in isolated unit tests.
29
+ /// surface actually works end to end, not just in isolated unit tests.
24
30
  #[test]
25
31
  fn everyExampleParsesAndTypeChecks() {
26
- let mut parser = tree_sitter::Parser::new();
27
- parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
28
-
29
32
  for path in exampleFiles() {
33
+ let source = plum_core::loadAndMerge(&path, &libPath())
30
- let src = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("failed to read {}: {}", path.display(), e));
34
+ .unwrap_or_else(|e| panic!("failed to load {}: {}", path.display(), e));
31
- let tree = parser.parse(&src, None).unwrap_or_else(|| panic!("failed to parse {}", path.display()));
32
- assert!(
33
- !tree.root_node().has_error(),
34
- "{} has a parse error:\n{}",
35
- path.display(),
36
- tree.root_node().to_sexp()
37
- );
38
-
39
- let ap = AstParser::new(&src);
40
- let source = ap.parseSource(tree.root_node());
41
35
  let result = checkSource(&source);
42
36
  assert!(
43
37
  result.is_ok(),
plum-wasm-codegen/src/lib.rs CHANGED
@@ -742,29 +742,13 @@ fn buildGcTypeRegistry(
742
742
  }
743
743
  }
744
744
 
745
- // Bool is built into `EnumVariants` (True/False) by `buildGlobalTables` with no
745
+ // `Bool` is an ordinary enum now (`type Bool = | True | False` in
746
- // `ast::Item::Enum` of its own (see `docs/superpowers/plans/2026-07-25-wasm-gc-migration.md`'s
747
- // Decision 1: Bool is a full wasm-gc struct, no special-casing) — register it
746
+ // `libs/std/Bool.plum`) registered exactly like any other enum
748
- // exactly like a real enum here, ahead of whatever the source actually declares.
747
+ // declared in `source.items`, present only if actually imported.
749
- let mut enum_decls: Vec<(String, Vec<String>)> =
748
+ let mut enum_decls: Vec<(String, Vec<String>)> = Vec::new();
750
- vec![("Bool".to_string(), vec!["False".to_string(), "True".to_string()])];
751
749
  for item in &source.items {
752
- // A source file may re-"declare" `enum Bool = | True | False` purely to
753
- // give it a nesting site for methods (no other way exists to attach a
754
- // method to a builtin type) — see the identical skip, with the full
755
- // rationale, in `plum-checker`'s `buildGlobalTables`. Registering it
756
- // again here would give Bool a SECOND, orphaned GC struct (the first,
757
- // hardcoded one is still referenced by every OTHER already-registered
758
- // slot/type by index) and — worse — since slot assignment for a
759
- // specialized generic enum with a `Bool` field (e.g. `Result[Bool,
760
- // Str]`) resolves "Bool" by NAME at the point it's compiled, later
761
- // duplicate registrations can leave that field pointing at whichever
762
- // Bool slot was assigned last, an index that isn't guaranteed to
763
- // satisfy wasm-gc's "supertypes before subtypes" ordering rule.
764
750
  if let ast::Item::Enum(e) = item {
765
- if e.name != "Bool" {
766
- enum_decls.push((e.name.clone(), e.variants.iter().map(|v| v.name.clone()).collect()));
751
+ enum_decls.push((e.name.clone(), e.variants.iter().map(|v| v.name.clone()).collect()));
767
- }
768
752
  }
769
753
  }
770
754
  for (enum_name, variant_names) in &enum_decls {
plum-wasm-codegen/tests/codegen_tests.rs CHANGED
@@ -47,6 +47,10 @@ fn outputValidates() {
47
47
  #[test]
48
48
  fn factorialCompiles() {
49
49
  let src = "\
50
+ enum Bool =
51
+ | True
52
+ | False
53
+
50
54
  fun factorial(x: Int) -> Int =
51
55
  if x < 2
52
56
  return 1
@@ -92,6 +96,10 @@ fn readStrResult(store: &mut wasmtime::Store<()>, val: &wasmtime::Val) -> String
92
96
  #[test]
93
97
  fn testBlocksCompileAndRun() {
94
98
  let src = "\
99
+ enum Bool =
100
+ | True
101
+ | False
102
+
95
103
  fun add(a: Int, b: Int) -> Int =
96
104
  a + b
97
105
 
@@ -166,8 +174,9 @@ fn assertValid(src: &str) -> Vec<u8> {
166
174
 
167
175
  #[test]
168
176
  fn boolLiteralsCompile() {
177
+ let boolDecl = "enum Bool =\n | True\n | False\n\n";
169
- assertValid("fun main() -> Bool =\n True\n");
178
+ assertValid(&format!("{}fun main() -> Bool =\n True\n", boolDecl));
170
- assertValid("fun main() -> Bool =\n False\n");
179
+ assertValid(&format!("{}fun main() -> Bool =\n False\n", boolDecl));
171
180
  }
172
181
 
173
182
  #[test]
@@ -252,7 +261,7 @@ fn matchInlineCaseBodyCompiles() {
252
261
 
253
262
  #[test]
254
263
  fn matchBoolVariantPatternCompiles() {
255
- let src = "fun main(a: Bool) -> Int =\n match a\n True =>\n return 1\n False =>\n return 0\n";
264
+ let src = "enum Bool =\n | True\n | False\n\nfun main(a: Bool) -> Int =\n match a\n True =>\n return 1\n False =>\n return 0\n";
256
265
  assertValid(src);
257
266
  }
258
267
 
@@ -272,6 +281,10 @@ fn matchStringPatternIsAClearError() {
272
281
  #[test]
273
282
  fn assertTrapsOnFalse() {
274
283
  let src_trap = "\
284
+ enum Bool =
285
+ | True
286
+ | False
287
+
275
288
  fun check(n: Int) -> Int =
276
289
  assert n > 0
277
290
  n