diff --git a/bootstrap/exec_corpus/literal_or_patterns/expected.txt b/bootstrap/exec_corpus/literal_or_patterns/expected.txt index 6554681..ee8582f 100644 --- a/bootstrap/exec_corpus/literal_or_patterns/expected.txt +++ b/bootstrap/exec_corpus/literal_or_patterns/expected.txt @@ -2,3 +2,9 @@ closed small small other +string +float +bool +nested +mixed +tags diff --git a/bootstrap/exec_corpus/literal_or_patterns/main.plum b/bootstrap/exec_corpus/literal_or_patterns/main.plum index cee5bc5..2ba2f57 100644 --- a/bootstrap/exec_corpus/literal_or_patterns/main.plum +++ b/bootstrap/exec_corpus/literal_or_patterns/main.plum @@ -8,5 +8,11 @@ let main (): Unit = { println(classify(-1)); println(classify(0)); println(classify(3)); - println(classify(4)) + println(classify(4)); + println(match "ok" { "yes" | "ok" => "string", _ => "miss" }); + println(match 2.5 { 1.5 | 2.5 => "float", _ => "miss" }); + println(match false { true | false => "bool" }); + println(match Some("ok") { Some("yes" | "ok") => "nested", _ => "miss" }); + println(match 7 { 1 | _ => "mixed" }); + println(match None { None | Some(_) => "tags" }) } diff --git a/bootstrap/self_host/typecheck/infer.plum b/bootstrap/self_host/typecheck/infer.plum index 8b5eb42..a0b59ed 100644 --- a/bootstrap/self_host/typecheck/infer.plum +++ b/bootstrap/self_host/typecheck/infer.plum @@ -3438,15 +3438,83 @@ let bind_pattern_ty (p2: Program2) (state: FreshState) (acc: Subst) (env: TyEnv) PTuple(elems) => bind_tuple_pattern(p2, state, acc, env, elems, scrutinee_ty), PVariant(path, arg_pats) => bind_variant_pattern(p2, state, acc, env, path, arg_pats, scrutinee_ty), PStruct(path, field_pats, rest) => bind_struct_pattern(p2, state, acc, env, path, field_pats, rest, scrutinee_ty), - POr(alts) => { - let first = bind_pattern_ty(p2, state, acc, env, alts[0], scrutinee_ty); - check_or_unique_bindings(first.env.bindings, env.bindings.len()); - bind_or_alts(p2, first.state, first.subst, env, first.env, alts, scrutinee_ty, 1, [first.pattern]) - }, + POr(alts) => bind_or_pattern(p2, state, acc, env, alts, scrutinee_ty), + } + +// Category is local to this or-pattern, including nested occurrences. +// Constructor syntax is tag-shaped until name resolution: `Pair(x, y)` +// may resolve to a positional struct rather than an enum variant. +// Mixed alternatives and nested or-patterns use the structural rules. +enum OrPatternCategory { OrLiteral, OrTagShaped, OrStructural } + +let or_pattern_category (alts: Array[parser.PPattern]): OrPatternCategory = + if Array.is_empty(alts) { OrStructural } + else { or_pattern_category_rest(alts, 1, or_alternative_category(alts[0])) } + +let or_alternative_category (pattern: parser.PPattern): OrPatternCategory = + match pattern { + PInt(_) => OrLiteral, + PFloat(_) => OrLiteral, + PStr(_) => OrLiteral, + PBool(_) => OrLiteral, + PVariant(_, _) => OrTagShaped, + _ => OrStructural, + } + +let or_pattern_category_rest (alts: Array[parser.PPattern]) (i: Int) (category: OrPatternCategory): OrPatternCategory = + if i >= alts.len() { category } + else if or_alternative_category(alts[i]) == category { or_pattern_category_rest(alts, i + 1, category) } + else { OrStructural } + +// Classification tests include syntax that only name resolution can +// disambiguate, plus mixed and empty lists that cannot take the literal +// fast path. +pub let test_or_pattern_categories (): Unit = { + assert(match or_pattern_category([PInt(1), PFloat(2.0), PStr("yes"), PBool(true)]) { OrLiteral => true, _ => false }); + assert(match or_pattern_category([PVariant(["None"], []), PVariant(["Some"], [PIdent("x", 0)])]) { OrTagShaped => true, _ => false }); + assert(match or_pattern_category([PVariant(["Pair"], [PWildcard, PWildcard])]) { OrTagShaped => true, _ => false }); + assert(match or_pattern_category([PTuple([PWildcard]), PTuple([PWildcard])]) { OrStructural => true, _ => false }); + assert(match or_pattern_category([PStruct(["Pair"], [], true)]) { OrStructural => true, _ => false }); + assert(match or_pattern_category([PInt(1), PWildcard]) { OrStructural => true, _ => false }); + assert(match or_pattern_category([PVariant(["None"], []), POr([PVariant(["None"], [])])]) { OrStructural => true, _ => false }); + assert(match or_pattern_category([]) { OrStructural => true, _ => false }); +} + +let bind_or_pattern (p2: Program2) (state: FreshState) (acc: Subst) (env: TyEnv) (alts: Array[parser.PPattern]) (scrutinee_ty: ITy): BindResult = + match or_pattern_category(alts) { + // Literals introduce no bindings. Check EVERY alternative + // against the scrutinee, including mixed literal types. + OrLiteral => bind_literal_or_alts(p2, state, acc, env, alts, scrutinee_ty, 0, []), + // Tag-shaped alternatives resolve their heads through the + // ordinary binder, then enforce the shared binding contract. + OrTagShaped => bind_binding_or_pattern(p2, state, acc, env, alts, scrutinee_ty), + // Structural alternatives are supported too (issue #75). + // Their recursively introduced names obey the same contract. + OrStructural => bind_binding_or_pattern(p2, state, acc, env, alts, scrutinee_ty), + } + +let bind_literal_or_alts (p2: Program2) (state: FreshState) (acc: Subst) (env: TyEnv) (alts: Array[parser.PPattern]) (scrutinee_ty: ITy) (i: Int) (out: Array[parser.PPattern]): BindResult = + if i >= alts.len() { BindResult { env: env, subst: acc, state: state, pattern: POr(out) } } + else { + let r = bind_pattern_ty(p2, state, acc, env, alts[i], scrutinee_ty); + bind_literal_or_alts(p2, r.state, r.subst, env, alts, scrutinee_ty, i + 1, out.push(r.pattern)) + } + +let bind_binding_or_pattern (p2: Program2) (state: FreshState) (acc: Subst) (env: TyEnv) (alts: Array[parser.PPattern]) (scrutinee_ty: ITy): BindResult = + if Array.is_empty(alts) { + fail_tc("or-pattern must have at least one alternative"); + BindResult { env: env, subst: acc, state: state, pattern: POr(alts) } + } else { + let first = bind_pattern_ty(p2, state, acc, env, alts[0], scrutinee_ty); + check_or_unique_bindings(first.env.bindings, env.bindings.len()); + bind_or_alts(p2, first.state, first.subst, env, first.env, alts, scrutinee_ty, 1, [first.pattern]) } -// Alternatives start in the outer environment and share the first -// binding environment. Every alternative contributes type constraints. +// Each alternative starts in the OUTER environment, so shadowed outer +// names do not count toward the contract. Alternatives must introduce +// the same set of names, once each, with unifiable types; order may +// differ. Only the first environment escapes to the guard and body. +// Every alternative contributes constraints to the shared substitution. let bind_or_alts (p2: Program2) (state: FreshState) (acc: Subst) (outer: TyEnv) (shared: TyEnv) (alts: Array[parser.PPattern]) (scrutinee_ty: ITy) (i: Int) (out: Array[parser.PPattern]): BindResult = if i >= alts.len() { BindResult { env: shared, subst: acc, state: state, pattern: POr(out) } } else { diff --git a/bootstrap/typecheck_corpus/or_pattern_structural_binding_types/expected-error.txt b/bootstrap/typecheck_corpus/or_pattern_structural_binding_types/expected-error.txt new file mode 100644 index 0000000..bee31fd --- /dev/null +++ b/bootstrap/typecheck_corpus/or_pattern_structural_binding_types/expected-error.txt @@ -0,0 +1,2 @@ +error: or-pattern binding types: Int != String +main.plum:1:43 diff --git a/bootstrap/typecheck_corpus/or_pattern_structural_binding_types/main.plum b/bootstrap/typecheck_corpus/or_pattern_structural_binding_types/main.plum new file mode 100644 index 0000000..ca2bfcd --- /dev/null +++ b/bootstrap/typecheck_corpus/or_pattern_structural_binding_types/main.plum @@ -0,0 +1,2 @@ +let f (v: (Int, String)): Int = match v { (x, _) | (_, x) => x } +let main (): Unit = () diff --git a/sh.real b/sh.real index ed31cc9..40930dd 100755 Binary files a/sh.real and b/sh.real differ