plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
287a0ad
— Peter John
2026-11-01T16:23:21+05:30
fix: optional reassign/field-store coercion, reject optional==value, keep literal-overflow errors through the loader merge
- plum-checker/src/lib.rs +15 -4
- plum-checker/tests/checker_tests.rs +9 -0
- plum-core/src/loader.rs +9 -1
- plum-core/tests/loader_test.rs +11 -0
- plum-examples/optionals.plum +19 -0
- plum-std/Str.plum +1 -1
- plum-wasm-codegen/src/lib.rs +4 -2
plum-checker/src/lib.rs
CHANGED
|
@@ -2192,10 +2192,10 @@ fn checkStmt(
|
|
|
2192
2192
|
// point of `:=` existing at all (catches `x := 5`
|
|
2193
2193
|
// then later `x = 3.14` at compile time instead of
|
|
2194
2194
|
// silently miscompiling or silently changing type).
|
|
2195
|
+
let existing = lookup(env, name)
|
|
2196
|
+
.expect("already_declared just confirmed this succeeds");
|
|
2195
|
-
match
|
|
2197
|
+
match inferExprWith(value, env, ctx, Some(&existing)) {
|
|
2196
2198
|
Ok(t) => {
|
|
2197
|
-
let existing = lookup(env, name)
|
|
2198
|
-
.expect("already_declared just confirmed this succeeds");
|
|
2199
2199
|
if let Err(msg) = unifyArg(&existing, &t, value, ctx) {
|
|
2200
2200
|
errors.push(CheckError::at(
|
|
2201
2201
|
fn_pos.0,
|
|
@@ -2256,7 +2256,10 @@ fn checkStmt(
|
|
|
2256
2256
|
}
|
|
2257
2257
|
ast::AssignTarget::Field(object, field_name) => {
|
|
2258
2258
|
let label = format!("{}.{}", describeTargetObject(object), field_name);
|
|
2259
|
+
let field_expected = inferExpr(object, env, ctx)
|
|
2260
|
+
.ok()
|
|
2261
|
+
.and_then(|o| fieldTypeOf(&o, field_name, ctx).ok());
|
|
2259
|
-
match (inferExpr(object, env, ctx),
|
|
2262
|
+
match (inferExpr(object, env, ctx), inferExprWith(value, env, ctx, field_expected.as_ref())) {
|
|
2260
2263
|
(Ok(obj_ty @ (PlumType::TNamed(_) | PlumType::TApp(..))), Ok(value_ty)) => {
|
|
2261
2264
|
match fieldTypeOf(&obj_ty, field_name, ctx) {
|
|
2262
2265
|
Ok(field_ty) => {
|
|
@@ -3894,6 +3897,14 @@ fn inferExprInner(
|
|
|
3894
3897
|
if let (PlumType::TErrName(_), PlumType::TErrName(_)) = (<, &rt) {
|
|
3895
3898
|
return Ok(PlumType::TBool);
|
|
3896
3899
|
}
|
|
3900
|
+
for side in [<, &rt] {
|
|
3901
|
+
if matches!(side, PlumType::TErr(..)) {
|
|
3902
|
+
return Err(format!(
|
|
3903
|
+
"cannot compare {}: unwrap it first with `?:`, `!`, `catch` or `try` (only `== null` / `!= null` applies to an optional)",
|
|
3904
|
+
side
|
|
3905
|
+
));
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3897
3908
|
let opn = match c.op {
|
|
3898
3909
|
ast::CmpOp::Eq => Some(("==", "eq")),
|
|
3899
3910
|
ast::CmpOp::Neq | ast::CmpOp::NotEq2 => Some(("!=", "eq")),
|
plum-checker/tests/checker_tests.rs
CHANGED
|
@@ -2105,3 +2105,12 @@ fn pattern_if_needs_no_exhaustiveness() {
|
|
|
2105
2105
|
let src = format!("{}fun f(s: Shape) -> Int =\n if Dot := s\n return 1\n 0\n", PATTERN_IF_ENUM);
|
|
2106
2106
|
assert!(checkSource(&parseClean(&src)).is_ok());
|
|
2107
2107
|
}
|
|
2108
|
+
|
|
2109
|
+
#[test]
|
|
2110
|
+
fn comparing_an_optional_or_error_union_with_a_value_is_rejected() {
|
|
2111
|
+
let msg = errText(checkSource(&parseClean("fun f(a: Int?) -> Bool =\n a == 3\n")));
|
|
2112
|
+
assert!(msg.contains("cannot compare"), "{msg}");
|
|
2113
|
+
let msg = errText(checkSource(&parseClean("fun f(a: error{Bad}!Int) -> Bool =\n a == 3\n")));
|
|
2114
|
+
assert!(msg.contains("cannot compare"), "{msg}");
|
|
2115
|
+
assert!(checkSource(&parseClean("fun f(a: Int?) -> Bool =\n a == null\n")).is_ok());
|
|
2116
|
+
}
|
plum-core/src/loader.rs
CHANGED
|
@@ -45,6 +45,7 @@ pub fn loadAndMergeWithOverrides(
|
|
|
45
45
|
let mut names: HashMap<String, PathBuf> = HashMap::new();
|
|
46
46
|
let mut prims: Vec<PrimitiveDecl> = Vec::new();
|
|
47
47
|
|
|
48
|
+
MERGED_LIT_ERRORS.with(|e| e.borrow_mut().clear());
|
|
48
49
|
visited.insert(entry_canon.clone());
|
|
49
50
|
mergeItems(&entry_canon, &entry_source, &mut items, &mut names, &mut prims)?;
|
|
50
51
|
for import in &entry_source.imports {
|
|
@@ -65,7 +66,7 @@ pub fn loadAndMergeWithOverrides(
|
|
|
65
66
|
imports: entry_source.imports,
|
|
66
67
|
items,
|
|
67
68
|
topLevelOrder: Vec::new(),
|
|
68
|
-
formErrors:
|
|
69
|
+
formErrors: MERGED_LIT_ERRORS.with(|e| std::mem::take(&mut *e.borrow_mut())),
|
|
69
70
|
formUses: Vec::new(),
|
|
70
71
|
stars: Vec::new(),
|
|
71
72
|
formLoops: false,
|
|
@@ -236,6 +237,11 @@ fn firstErrorNode(node: tree_sitter::Node) -> Option<tree_sitter::Node> {
|
|
|
236
237
|
None
|
|
237
238
|
}
|
|
238
239
|
|
|
240
|
+
thread_local! {
|
|
241
|
+
static MERGED_LIT_ERRORS: std::cell::RefCell<Vec<(usize, usize, String)>> =
|
|
242
|
+
std::cell::RefCell::new(Vec::new());
|
|
243
|
+
}
|
|
244
|
+
|
|
239
245
|
fn mergeItems(
|
|
240
246
|
file: &Path,
|
|
241
247
|
source: &Source,
|
|
@@ -243,6 +249,8 @@ fn mergeItems(
|
|
|
243
249
|
names: &mut HashMap<String, PathBuf>,
|
|
244
250
|
prims: &mut Vec<PrimitiveDecl>,
|
|
245
251
|
) -> Result<(), String> {
|
|
252
|
+
// Parse-time literal errors (out-of-range integers) of every merged file.
|
|
253
|
+
MERGED_LIT_ERRORS.with(|e| e.borrow_mut().extend(source.formErrors.iter().cloned()));
|
|
246
254
|
// Namespaces (a `primitive` whose name is no built-in) travel with the
|
|
247
255
|
// merged program so the checker can enforce their rules.
|
|
248
256
|
prims.extend(
|
plum-core/tests/loader_test.rs
CHANGED
|
@@ -369,3 +369,14 @@ fn source_and_fns_carry_their_file_path() {
|
|
|
369
369
|
assert!(fileOf("mainFn").ends_with("main.plum"), "{}", fileOf("mainFn"));
|
|
370
370
|
assert!(fileOf("helperFn").ends_with("Util.plum"), "{}", fileOf("helperFn"));
|
|
371
371
|
}
|
|
372
|
+
|
|
373
|
+
#[test]
|
|
374
|
+
fn outOfRangeLiteralInEntryOrImportSurvivesTheMerge() {
|
|
375
|
+
let dir = TempDir::new().unwrap();
|
|
376
|
+
let lib_path = dir.path().join("libs");
|
|
377
|
+
writeLibFile(&lib_path, "plum-std/Int", "module fixtures\n\nprimitive Int =\n ZERO = 0\n");
|
|
378
|
+
writeLibFile(&lib_path, "helper", "module fixtures\n\nimport std/Int\n\nfun big(u: Unit) -> Unit =\n x := 0x8000000000000000\n u\n");
|
|
379
|
+
let entry = writeLibFile(&lib_path, "main", "module fixtures\n\nimport helper\n\nfun main(u: Unit) -> Unit =\n big(u)\n");
|
|
380
|
+
let merged = loadAndMerge(&entry, &lib_path).expect("loadAndMerge failed");
|
|
381
|
+
assert!(merged.formErrors.iter().any(|e| e.2.contains("out of range")), "{:?}", merged.formErrors);
|
|
382
|
+
}
|
plum-examples/optionals.plum
CHANGED
|
@@ -143,3 +143,22 @@ test "error_union_of_optional"
|
|
|
143
143
|
test "int_min_literal"
|
|
144
144
|
assert Int.MIN == -0x8000_0000_0000_0000
|
|
145
145
|
assert sumBig() == -1
|
|
146
|
+
|
|
147
|
+
test "reassigning an optional local or field to null or a value"
|
|
148
|
+
o := maybeInt(true)
|
|
149
|
+
o = null
|
|
150
|
+
assert o == null
|
|
151
|
+
o = 4
|
|
152
|
+
assert o! == 4
|
|
153
|
+
b := maybeBox(true)
|
|
154
|
+
b = null
|
|
155
|
+
assert b == null
|
|
156
|
+
h := Holder(item: Box(label: "x"), count: 2)
|
|
157
|
+
h.count = null
|
|
158
|
+
h.item = null
|
|
159
|
+
assert h.count == null
|
|
160
|
+
assert h.item == null
|
|
161
|
+
h.count = 5
|
|
162
|
+
h.item = Box(label: "y")
|
|
163
|
+
assert h.count! == 5
|
|
164
|
+
assert h.item!.label == "y"
|
plum-std/Str.plum
CHANGED
|
@@ -44,7 +44,7 @@ enum Str =
|
|
|
44
44
|
# (ordinary 64-bit multiplication), which is fine — only the bit pattern
|
|
45
45
|
# matters, not the numeric value itself.
|
|
46
46
|
fun hash(self) -> Int =
|
|
47
|
-
h := 0xcbf29ce484222325
|
|
47
|
+
h := -0x340d631b7bdddcdb # FNV offset basis 0xcbf29ce484222325 as a signed Int
|
|
48
48
|
i := 0
|
|
49
49
|
while i < self.length()
|
|
50
50
|
h = {h ^ self.byteAt(i)} * 0x100000001b3
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -4682,7 +4682,9 @@ fn compileStmt(
|
|
|
4682
4682
|
for (target, value) in a.targets.iter().zip(a.values.iter()) {
|
|
4683
4683
|
match target {
|
|
4684
4684
|
ast::AssignTarget::Var(name) => {
|
|
4685
|
+
// The coerced slot type: `o = null` / `o = 4` into an `Int?`
|
|
4686
|
+
// local stores the whole optional, not the bare value.
|
|
4685
|
-
let vty =
|
|
4687
|
+
let vty = slotType(value, ctx);
|
|
4686
4688
|
compileExpr(value, body, ctx, state)?;
|
|
4687
4689
|
let idx = ctx
|
|
4688
4690
|
.locals
|
|
@@ -4700,7 +4702,7 @@ fn compileStmt(
|
|
|
4700
4702
|
}
|
|
4701
4703
|
ast::AssignTarget::Field(object, field_name) => {
|
|
4702
4704
|
let src = StoreSrc {
|
|
4703
|
-
ty:
|
|
4705
|
+
ty: slotType(value, ctx),
|
|
4704
4706
|
push: &|dest, body, ctx, state| compileExprInto(value, dest, body, ctx, state),
|
|
4705
4707
|
};
|
|
4706
4708
|
compileFieldStore(object, field_name, &src, body, ctx, state)?;
|