plum
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 +8 -25
- plum-checker/src/types.rs +5 -2
- plum-checker/tests/checker_tests.rs +2 -2
- plum-checker/tests/types_tests.rs +1 -1
- plum-core/src/ast.rs +3 -0
- plum-wasm-codegen/src/lib.rs +12 -31
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::
|
|
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(_) =>
|
|
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(&
|
|
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, &
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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::
|
|
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![
|
|
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(
|
|
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![
|
|
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:
|
|
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(&
|
|
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(
|
|
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
|
-
//
|
|
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(&
|
|
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
|
-
|
|
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::
|
|
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(&
|
|
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);
|