plum

#treesitter#compiler#wasm

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

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


f47709e — Peter John 2026-11-01T22:25:35+05:30
refactor: remove PlumType::TStr; Str is an ordinary named type
plum-checker/src/lib.rs CHANGED
@@ -593,7 +593,7 @@ pub fn nullableErrPayload(ty: &PlumType) -> Option<&PlumType> {
593
593
  return None;
594
594
  }
595
595
  let is_ref = match &**t {
596
- PlumType::TStr | PlumType::TByteSlice | PlumType::TFun(..) => true,
596
+ PlumType::TByteSlice | PlumType::TFun(..) => true,
597
597
  PlumType::TNamed(_) => true,
598
598
  PlumType::TApp(..) => true,
599
599
  _ => false,
@@ -625,7 +625,6 @@ pub fn plumTypeFromName(name: &str) -> PlumType {
625
625
  "Int" => PlumType::TInt,
626
626
  "Float" => PlumType::TFloat,
627
627
  "Bool" => PlumType::TBool,
628
- "Str" => PlumType::TStr,
629
628
  "Byte" => PlumType::TByte,
630
629
  "[]Byte" => PlumType::TByteSlice,
631
630
  "Unit" => PlumType::TUnit,
@@ -662,7 +661,6 @@ pub fn methodReceiverName(ty: &PlumType) -> Option<String> {
662
661
  PlumType::TInt => Some("Int".to_string()),
663
662
  PlumType::TFloat => Some("Float".to_string()),
664
663
  PlumType::TBool => Some("Bool".to_string()),
665
- PlumType::TStr => Some("Str".to_string()),
666
664
  PlumType::TByte => Some("Byte".to_string()),
667
665
  PlumType::TByteSlice => Some("ByteSlice".to_string()),
668
666
  _ => None,
@@ -767,19 +765,6 @@ pub fn unify(t1: &PlumType, t2: &PlumType, ctx: &CheckCtx) -> Result<(), String>
767
765
  (PlumType::TInt, PlumType::TInt) => Ok(()),
768
766
  (PlumType::TFloat, PlumType::TFloat) => Ok(()),
769
767
  (PlumType::TBool, PlumType::TBool) => Ok(()),
770
- (PlumType::TStr, PlumType::TStr) => Ok(()),
771
- // `Str` is an ordinary class (`type Str(...) = bytes: []Byte ...` in
772
- // `str.plum`) — a bare `Str(...)` construction (e.g. `bytesToStr`'s own
773
- // body) infers as `TNamed("Str")` via `inferClassCallRaw`, while a `Str`
774
- // type ANNOTATION (a param/return type, a string literal) resolves via
775
- // `plumTypeFromName` to the dedicated `TStr` variant instead — these are
776
- // the same real type spelled two different ways depending on how the
777
- // checker reached it, so they must unify with each other too.
778
- (PlumType::TStr, PlumType::TNamed(n)) | (PlumType::TNamed(n), PlumType::TStr)
779
- if n == "Str" =>
780
- {
781
- Ok(())
782
- }
783
768
  (PlumType::TByte, PlumType::TByte) => Ok(()),
784
769
  (PlumType::TByteSlice, PlumType::TByteSlice) => Ok(()),
785
770
  (PlumType::TUnit, PlumType::TUnit) => Ok(()),
@@ -1093,7 +1078,7 @@ pub fn buildGlobalTables(source: &ast::Source) -> (TypeEnv, MethodEnv, EnumVaria
1093
1078
  let ty = match &c.value {
1094
1079
  ast::Expr::Int(_) => PlumType::TInt,
1095
1080
  ast::Expr::Float(_) => PlumType::TFloat,
1096
- ast::Expr::String(_) => PlumType::TStr,
1081
+ ast::Expr::String(_) => types::strType(),
1097
1082
  _ => PlumType::TVar("_".to_string()),
1098
1083
  };
1099
1084
  global_env.insert(c.name.clone(), TypeScheme::mono(ty));
@@ -2483,7 +2468,7 @@ fn checkStmtInner(
2483
2468
  ast::Stmt::Break | ast::Stmt::Continue | ast::Stmt::Todo => {}
2484
2469
  ast::Stmt::Crash(e) => match inferExpr(e, env, ctx) {
2485
2470
  Ok(t) => {
2486
- if let Err(msg) = unify(&PlumType::TStr, &t, ctx) {
2471
+ if let Err(msg) = unify(&types::strType(), &t, ctx) {
2487
2472
  errors.push(CheckError::at(
2488
2473
  fn_pos.0,
2489
2474
  fn_pos.1,
@@ -3014,7 +2999,7 @@ pub(crate) fn checkPattern(
3014
2999
  ast::CasePattern::Wildcard => Ok(()),
3015
3000
  ast::CasePattern::Int(_) => unify(subject_ty, &PlumType::TInt, ctx),
3016
3001
  ast::CasePattern::Float(_) => unify(subject_ty, &PlumType::TFloat, ctx),
3017
- ast::CasePattern::String(_) => unify(subject_ty, &PlumType::TStr, ctx),
3002
+ ast::CasePattern::String(_) => unify(subject_ty, &types::strType(), ctx),
3018
3003
  ast::CasePattern::Bool(_) => unify(subject_ty, &PlumType::TBool, ctx),
3019
3004
  ast::CasePattern::Name(n) => {
3020
3005
  let is_known_variant =n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
@@ -3304,7 +3289,6 @@ fn checkCallArgs(
3304
3289
  fn fieldTypeOf(obj_ty: &PlumType, field: &str, ctx: &CheckCtx) -> Result<PlumType, String> {
3305
3290
  match obj_ty {
3306
3291
  PlumType::TNamed(n) => lookupFieldType(n, field, ctx),
3307
- PlumType::TStr => lookupFieldType("Str", field, ctx),
3308
3292
  PlumType::TApp(n, args) => {
3309
3293
  let raw = lookupFieldType(n, field, ctx)?;
3310
3294
  let gens = enumGenericsOf(n, ctx);
@@ -3332,7 +3316,7 @@ fn inferMethodCall(
3332
3316
  let call_addr = CUR_EXPR.with(|c| c.get());
3333
3317
  // `e.name()` on an error value (`catch e`) is its text.
3334
3318
  if matches!(obj_ty, PlumType::TErrName(_)) && name == "name" && args.is_empty() {
3335
- return Ok(PlumType::TStr);
3319
+ return Ok(types::strType());
3336
3320
  }
3337
3321
  // An error union has no methods of its own except the `?.` desugaring
3338
3322
  // (`a?.b` = `a.andThen(|x| x.b)`), which keeps the set and flattens `!U`.
@@ -3498,8 +3482,7 @@ fn startVariant(
3498
3482
  /// True for an operand whose operators are user methods (`add`, `eq`, ...): a
3499
3483
  /// declared class/enum, not a primitive, `Str`, `Bool` or an error name.
3500
3484
  pub fn isOperatorOperand(t: &PlumType) -> bool {
3501
- matches!(t, PlumType::TStr)
3502
- || matches!(t, PlumType::TNamed(n) | PlumType::TApp(n, _) if n != "Int" && n != "Float" && n != "Bool" && n != "Byte")
3485
+ matches!(t, PlumType::TNamed(n) | PlumType::TApp(n, _) if n != "Int" && n != "Float" && n != "Bool" && n != "Byte")
3503
3486
  }
3504
3487
 
3505
3488
  /// Resolves operator `sym` on a non-primitive left operand to `left.method(right)`,
@@ -3792,7 +3775,7 @@ fn inferExprInner(
3792
3775
  }
3793
3776
  }
3794
3777
  }
3795
- Ok(PlumType::TStr)
3778
+ Ok(types::strType())
3796
3779
  }
3797
3780
  ast::Expr::Var(name) => match env.get(name) {
3798
3781
  Some(s) if !s.vars.is_empty() => Ok(instSig(&s.body, &BTreeMap::new())),
@@ -4182,7 +4165,7 @@ fn inferExprInner(
4182
4165
  ast::AttrKind::Field(field_name) => match &obj_ty {
4183
4166
  // `Str` is an ordinary class with a real field; a generic
4184
4167
  // class substitutes its receiver's type arguments.
4185
- PlumType::TNamed(_) | PlumType::TStr | PlumType::TApp(..) => {
4168
+ PlumType::TNamed(_) | PlumType::TApp(..) => {
4186
4169
  fieldTypeOf(&obj_ty, field_name, ctx)
4187
4170
  }
4188
4171
  // An unresolved generic return reaches here as a plain `TVar`;
plum-checker/src/types.rs CHANGED
@@ -5,7 +5,6 @@ pub enum PlumType {
5
5
  TInt,
6
6
  TFloat,
7
7
  TBool,
8
- TStr,
9
8
  TByte,
10
9
  /// `[]Byte` — a fixed builtin, not a generic. The checker
11
10
  /// only ever produces this for the exact source spelling `[]Byte`
@@ -73,7 +72,6 @@ impl std::fmt::Display for PlumType {
73
72
  PlumType::TInt => write!(f, "Int"),
74
73
  PlumType::TFloat => write!(f, "Float"),
75
74
  PlumType::TBool => write!(f, "Bool"),
76
- PlumType::TStr => write!(f, "Str"),
77
75
  PlumType::TByte => write!(f, "Byte"),
78
76
  PlumType::TByteSlice => write!(f, "[]Byte"),
79
77
  PlumType::TUnit => write!(f, "Unit"),
@@ -213,3 +211,8 @@ impl std::fmt::Display for CheckError {
213
211
  }
214
212
 
215
213
  pub type CheckResult<T> = Result<T, Vec<CheckError>>;
214
+
215
+ /// The type of a string literal: the ordinary named record `Str`.
216
+ pub fn strType() -> PlumType {
217
+ PlumType::TNamed(plum_core::ast::STR_TYPE.to_string())
218
+ }
plum-checker/tests/checker_tests.rs CHANGED
@@ -1272,7 +1272,7 @@ fn method_own_generic_instantiated_per_call() {
1272
1272
  );
1273
1273
  assert_eq!(
1274
1274
  lastFnBodyType("fun f(xs: List[Int]) -> List[Str] =\n xs.map(|n| \"{n}\")\n"),
1275
- tapp("List", vec![PlumType::TStr])
1275
+ tapp("List", vec![plum_checker::types::strType()])
1276
1276
  );
1277
1277
  // The two instantiations are independent: Str result where Int is declared is an error.
1278
1278
  let bad = checkSource(&parseClean(&format!(
@@ -1371,7 +1371,7 @@ fn instantiation_set_listed() {
1371
1371
  .iter()
1372
1372
  .any(|i| i.item == "List.map" && i.type_args.first() == Some(&t))
1373
1373
  };
1374
- assert!(has(PlumType::TInt) && has(PlumType::TStr), "{:?}", info.instantiations);
1374
+ assert!(has(PlumType::TInt) && has(plum_checker::types::strType()), "{:?}", info.instantiations);
1375
1375
  for i in &info.instantiations {
1376
1376
  assert_eq!(info.inst_index[&(i.item.clone(), i.type_args.clone())], i.id);
1377
1377
  }
plum-checker/tests/types_tests.rs CHANGED
@@ -44,7 +44,7 @@ fn display_round_trips() {
44
44
  assert_eq!(t.to_string(), "error{A}!Int");
45
45
  assert_eq!(TParam("T".into()).to_string(), "T");
46
46
  assert_eq!(
47
- TApp("Map".into(), vec![TStr, TApp("List".into(), vec![TInt])]).to_string(),
47
+ TApp("Map".into(), vec![plum_checker::types::strType(), TApp("List".into(), vec![TInt])]).to_string(),
48
48
  "Map[Str, List[Int]]"
49
49
  );
50
50
  }
plum-core/src/ast.rs CHANGED
@@ -269,6 +269,9 @@ pub const FN_TYPE_NAME: &str = "fn";
269
269
  /// inline `error{A, B}` (`name` = `ERROR_SET_TYPE_NAME`, `generics` = the
270
270
  /// names). No parts means an inferred set. `!` cannot be a real type name.
271
271
  pub const ERROR_UNION_TYPE_NAME: &str = "!";
272
+ /// Name of the std `Str` record (`record Str(bytes: []Byte)`). String literals,
273
+ /// string match patterns and host boundaries are the only places that know its shape.
274
+ pub const STR_TYPE: &str = "Str";
272
275
 
273
276
  /// The reserved error name of an absent optional: `T?` is `error{null}!T` inside
274
277
  /// the compiler, and `null` is the error value `error.null`. No source error
plum-wasm-codegen/src/lib.rs CHANGED
@@ -883,6 +883,10 @@ fn boxIfValue(ty: &PlumType, body: &mut Vec<u8>) {
883
883
  }
884
884
  }
885
885
 
886
+ fn strType() -> PlumType {
887
+ plum_checker::types::strType()
888
+ }
889
+
886
890
  fn plumTypeToValtype(t: &PlumType) -> ValType {
887
891
  withGcTypes(|r| plumTypeToGcValtype(t, r))
888
892
  }
@@ -998,7 +1002,6 @@ fn plumTypeToGcValtype(t: &PlumType, registry: &GcTypeRegistry) -> ValType {
998
1002
  PlumType::TInt => ValType::I64,
999
1003
  PlumType::TFloat => ValType::F64,
1000
1004
  PlumType::TBool => ValType::I32,
1001
- PlumType::TStr => strGcValtypeOrFallback(registry),
1002
1005
  PlumType::TByte => ValType::I32,
1003
1006
  PlumType::TByteSlice => gcRef(registry.byte_array_type_idx),
1004
1007
  PlumType::TNamed(name) => match registry
@@ -1525,7 +1528,7 @@ fn desugarTestsToFns(source: &ast::Source) -> (ast::Source, Vec<(String, String)
1525
1528
  yields: None,
1526
1529
  params: vec![],
1527
1530
  returns: Some(ast::Type {
1528
- name: "Str".to_string(),
1531
+ name: ast::STR_TYPE.to_string(),
1529
1532
  generics: vec![],
1530
1533
  }),
1531
1534
  body: ast::FnBody::Block(ast::Block { stmts }),
@@ -2105,7 +2108,7 @@ fn compileSourceInner(
2105
2108
  {
2106
2109
  let uses_crash = sourceUsesCrash(&format!("{:?}", source.items));
2107
2110
  if uses_crash {
2108
- let str_vt = plumTypeToValtype(&PlumType::TStr);
2111
+ let str_vt = plumTypeToValtype(&strType());
2109
2112
  let type_idx = module.addType(&[str_vt], &[]);
2110
2113
  let func_idx = module.addImport("plum", "crash", type_idx);
2111
2114
  func_ids.insert(CRASH_FN_KEY.to_string(), func_idx);
@@ -2269,7 +2272,7 @@ fn compileSourceInner(
2269
2272
  module.nameFunction(start_func_idx, "start");
2270
2273
  module.setStartFunction(start_func_idx);
2271
2274
 
2272
- if let Some(&str_idx) = gc_types.class_type_idx.get("Str") {
2275
+ if let Some(&str_idx) = gc_types.class_type_idx.get(ast::STR_TYPE) {
2273
2276
  let mut b = vec![0u8];
2274
2277
  Instruction::LocalGet(0).encode(&mut b);
2275
2278
  Instruction::StructNew(str_idx).encode(&mut b);
@@ -2760,27 +2763,12 @@ fn compileIntrinsicFnBody(f: &ast::Fn, subst: &Subst) -> Option<Vec<u8>> {
2760
2763
  }
2761
2764
  }
2762
2765
 
2763
- /// `Str`'s wasm-gc valtype, exactly like `plumTypeToGcValtype(&PlumType::TStr,
2764
- /// r)` — EXCEPT it tolerates a program compiled against a `lib_path` that
2765
- /// never loaded `str.plum` at all (see `plum-core::loader::loadAndMerge`'s
2766
- /// best-effort `std/str` prelude), falling back to the raw `array<i8>` type
2767
- /// instead of panicking. Safe specifically because this is only used by the
2768
- /// unconditionally registered string runtime helpers, which every compiled
2769
- /// program gets regardless of whether it uses `Str`.
2770
- fn strGcValtypeOrFallback(r: &GcTypeRegistry) -> ValType {
2771
- match r.class_type_idx.get("Str") {
2772
- Some(idx) => gcRef(*idx),
2773
- None => gcRef(r.byte_array_type_idx),
2774
- }
2775
- }
2776
-
2777
2766
  /// Wraps a just-pushed raw `array<i8>` (top of the value stack, whose length
2778
2767
  /// IS the string's length) into `Str`'s `{bytes: []Byte}` representation via
2779
2768
  /// `mk_str_fn`. Every codegen site that builds a `Str` from raw bytes goes
2780
2769
  /// through this.
2781
2770
  fn wrapBytesAsStr(body: &mut Vec<u8>) {
2782
- // No real `Str` registered at all (see `strGcValtypeOrFallback`'s doc
2783
- // comment) — a raw `array<i8>` already IS this program's "Str".
2771
+ // No real `Str` registered at all — a raw `array<i8>` already IS this program's "Str".
2784
2772
  let Some(mk) = withGcTypes(|r| r.mk_str_fn) else {
2785
2773
  return;
2786
2774
  };
@@ -6085,7 +6073,7 @@ fn compileTagName(
6085
6073
  .ok_or_else(|| "internal codegen error: missing name() scratch".to_string())?;
6086
6074
  compileExpr(obj, body, ctx, state)?;
6087
6075
  Instruction::LocalSet(tmp).encode(body);
6088
- let svt = plumTypeToValtype(&PlumType::TStr);
6076
+ let svt = plumTypeToValtype(&strType());
6089
6077
  // An open set (a `!T` parameter's) can be any error of the program.
6090
6078
  let all: std::collections::BTreeSet<String>;
6091
6079
  let names = if names.is_empty() {
@@ -6756,12 +6744,8 @@ fn compileFieldRead(
6756
6744
  ctx: &LocalCtx,
6757
6745
  state: &mut ModuleState,
6758
6746
  ) -> Result<(), String> {
6759
- // `Str` is an ordinary record (`bytes: []Byte`) with no name of its own carried
6760
- // in `PlumType::TStr` — resolve it via the literal name "Str", same as a
6761
- // `TNamed`/`TApp` does via its own name.
6762
6747
  let class_name = match (plum_checker::nominalName(obj_ty), obj_ty) {
6763
6748
  (Some(n), _) => n.to_string(),
6764
- (None, PlumType::TStr) => "Str".to_string(),
6765
6749
  _ => {
6766
6750
  return Err(format!(
6767
6751
  "codegen: cannot access field '{}' on non-class type {}",
@@ -6781,10 +6765,7 @@ fn compileFieldRead(
6781
6765
  }
6782
6766
  return Ok(());
6783
6767
  }
6784
- let ety = match obj_ty {
6785
- PlumType::TStr => PlumType::TNamed("Str".to_string()),
6786
- t => t.clone(),
6768
+ let ety = obj_ty.clone();
6787
- };
6788
6769
  // A discriminant enum's shared params are declared directly on the enum's
6789
6770
  // SUPERTYPE (see `buildGcTypeRegistry`'s `EnumSuper` arm) — no `ref.cast` to
6790
6771
  // any one variant needed.
@@ -7139,7 +7120,7 @@ fn compileInterpolatedString(
7139
7120
  ast::StringPart::Interp(expr) => {
7140
7121
  let ty = inferLocalType(expr, ctx);
7141
7122
  match ty {
7142
- PlumType::TStr => {
7123
+ PlumType::TNamed(ref n) if n == ast::STR_TYPE => {
7143
7124
  compileExpr(expr, body, ctx, state)?;
7144
7125
  }
7145
7126
  PlumType::TInt => {
@@ -7150,7 +7131,7 @@ fn compileInterpolatedString(
7150
7131
  Instruction::Call(func_idx).encode(body);
7151
7132
  }
7152
7133
  PlumType::TBool => {
7153
- let str_ref = withGcTypes(|r| plumTypeToGcValtype(&PlumType::TStr, r));
7134
+ let str_ref = withGcTypes(|r| plumTypeToGcValtype(&strType(), r));
7154
7135
  compileBoolConditionAsI32(expr, body, ctx, state)?;
7155
7136
  Instruction::If(BlockType::Result(str_ref)).encode(body);
7156
7137
  compileStaticString("true", body, state);