Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ef9d099
added basic duplicate catching
joshqwang Jun 26, 2025
0f79f72
added duplicate label detection in nested patterns
joshqwang Jun 26, 2025
40cf5a7
added detecting duplicates in labels
joshqwang Jun 27, 2025
6da16da
added duplicatevar error message
joshqwang Jul 2, 2025
15c96e2
added comments, tests, renamed Duplicate to DuplicateLabel in Self.re
joshqwang Jul 10, 2025
1c000d2
Merge remote-tracking branch 'origin/dev' into duplicate-variable-che…
joshqwang Jul 10, 2025
dbd690f
Removed redundant get_bindings function
joshqwang Jul 10, 2025
de757c6
fixed duplicate variables incorrectly having label self values
joshqwang Jul 22, 2025
eea8212
added unit test for let expression
joshqwang Jul 22, 2025
dda3388
separated duplicate_bindings and duplicate_variables
joshqwang Jul 30, 2025
a9943cc
merged with dev
joshqwang Aug 28, 2025
fa1657f
fixed some merging stinkers
joshqwang Aug 28, 2025
5dbf32c
Merge branch 'dev' into duplicate-variable-checking-in-patterns
joshqwang Aug 28, 2025
d8e459c
Fix capitalization in Duplicate variable error message
cyrus- Sep 10, 2025
0d90d7a
Merge branch 'dev' into duplicate-variable-checking-in-patterns
joshqwang Sep 26, 2025
e25865d
added tests and fixed a bug
joshqwang Sep 26, 2025
ffd4029
Merge remote-tracking branch 'origin/dev' into duplicate-variable-che…
dm0n3y Dec 17, 2025
9ba0b30
Merge branch 'dev' into duplicate-variable-checking-in-patterns
dm0n3y Feb 13, 2026
fb0edcf
refmt
dm0n3y Feb 13, 2026
0dd89fc
rm comment + fix benign bug mixing expression/pattern duplicates
dm0n3y Feb 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/haz3lcore/TyDi/ErrorPrint.re
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ let common_error: Info.error_common => string =
| NoType(BadLabel(_)) => "Invalid label"
| NoType(InvalidLabel(_)) => "Invalid label"
| DuplicateLabel(_, _) => "Duplicate label"
| DuplicateVar(_, _) => "Duplicate variable"
| TupleLabelError(_) => "Invalid tuple label"
| NoType(UnexpectedLabelSort(_)) => "Unexpected label sort"
| NoType(BadToken(token)) => prn("\"%s\" isn't a valid token", token)
Expand Down
3 changes: 1 addition & 2 deletions src/language/statics/Elaborator.re
Original file line number Diff line number Diff line change
Expand Up @@ -311,8 +311,7 @@ let rec elaborate = (m: Statics.Map.t, uexp: Exp.t): (DHExp.t, Typ.t) => {
};
let is_recursive =
Statics.is_recursive(ctx, p, def, ty1)
&& Pat.get_bindings(p)
|> Option.get
&& Pat.bound_vars(p)
|> List.exists(f => VarMap.lookup(co_ctx, f) != None);
if (!is_recursive) {
let (def, _) = elaborate(m, def);
Expand Down
13 changes: 10 additions & 3 deletions src/language/statics/Info.re
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ type error_common =
| Inconsistent(error_inconsistent)
/* The error on a specific duplicate label */
| DuplicateLabel(LabeledTuple.label, Typ.t)
| DuplicateVar(string, Typ.t)
/* Tuple/TupLabel contains malformed labels, duplicate labels, and/or invalid labels */
| TupleLabelError({
malformed_labels: list(Any.t),
Expand Down Expand Up @@ -492,9 +493,12 @@ let rec status_common =
typ,
}),
)
| (Duplicate(lab, Just(ty)), _) => InHole(DuplicateLabel(lab, ty))
| (Duplicate(lab, _), _) =>
| (DuplicateLabel(lab, Just(ty)), _) => InHole(DuplicateLabel(lab, ty))
| (DuplicateVar(lab, Just(ty)), _) => InHole(DuplicateVar(lab, ty))
| (DuplicateLabel(lab, _), _) =>
InHole(DuplicateLabel(lab, Unknown(Internal) |> Typ.temp))
| (DuplicateVar(lab, _), _) =>
InHole(DuplicateVar(lab, Unknown(Internal) |> Typ.temp))
| (IsMulti, _) => NotInHole(Syn(Unknown(Internal) |> Typ.temp))
| (NoMeet(PolyEq, tys), _)
| (NoMeet(_, tys), {term: Unknown(SynSwitch), _}) =>
Expand Down Expand Up @@ -546,6 +550,7 @@ let rec status_pat = (ctx: Ctx.t, ty_ana: Typ.t, self: Self.pat): status_pat =>
Inconsistent(Internal(_) | Expectation(_) | CompareFun(_)) |
NoType(_) |
DuplicateLabel(_) |
DuplicateVar(_) |
TupleLabelError(_),
) as err,
) =>
Expand Down Expand Up @@ -583,6 +588,7 @@ let rec status_exp = (ctx: Ctx.t, ty_ana, self: Self.exp): status_exp =>
| InHole(Common(NoType(_)))
| InHole(Common(TupleLabelError(_)))
| InHole(Common(DuplicateLabel(_)))
| InHole(Common(DuplicateVar(_)))
| InHole(TupleExtensionRequiresTuples)
| InHole(
FreeVariable(_) | InexhaustiveMatch(_) | UnusedDeferral |
Expand Down Expand Up @@ -829,7 +835,8 @@ let fixed_typ_err_common: (error_common, Typ.t) => Typ.t =
| NoType(InvalidLabel(_))
| NoType(UnexpectedLabelSort(_)) => Unknown(Internal) |> Typ.temp
| TupleLabelError({typ, _})
| DuplicateLabel(_, typ) => typ_or_ana(typ)
| DuplicateLabel(_, typ)
| DuplicateVar(_, typ) => typ_or_ana(typ)
| Inconsistent(Expectation({ana, _})) => ana
| Inconsistent(Internal(_)) => Unknown(Internal) |> Typ.temp // Should this be some sort of meet?
| Inconsistent(CompareFun(_)) => typ_or_ana(Atom(Bool) |> Typ.temp)
Expand Down
9 changes: 6 additions & 3 deletions src/language/statics/Self.re
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ type meet_type =
type t =
| Just(Typ.t) /* Just a regular type */
| NoMeet(meet_type, list(Typ.source)) /* Inconsistent types for e.g match, listlits */
| Duplicate(LabeledTuple.label, t) /* Duplicate label, marked as duplicate */
| DuplicateLabel(LabeledTuple.label, t) /* Duplicate label, marked as duplicate */
| CompareFun(Typ.t) /* Type equality failed because of arrow type inside */
| DuplicateVar(string, t)
| BadToken(string) /* Invalid expression token, continues with undefined behavior */
| BadLabel(Any.t) /* TupLabel label component is not a valid Label*/
| InvalidLabel(LabeledTuple.label, list(LabeledTuple.label)) /* Invalid label in a labeled tuple where these labels are expected */
Expand Down Expand Up @@ -108,7 +109,8 @@ let meet_of = (j: meet_type, ty: Typ.t): Typ.t =>
let typ_of: t => option(Typ.t) =
fun
| Just(typ)
| Duplicate(_, Just(typ))
| DuplicateLabel(_, Just(typ))
| DuplicateVar(_, Just(typ))
| TupleLabelError({typ, _}) => Some(typ)
| CompareFun(_) => Some(Atom(Bool) |> Typ.fresh)
| FreeConstructor(name) =>
Expand All @@ -125,7 +127,8 @@ let typ_of: t => option(Typ.t) =
)
| BadToken(_)
| IsMulti
| Duplicate(_)
| DuplicateLabel(_)
| DuplicateVar(_)
| BadLabel(_)
| InvalidLabel(_)
| ExplicitNonlabel
Expand Down
81 changes: 59 additions & 22 deletions src/language/statics/Statics.re
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ let rec any_to_info_map =
~is_synswitch=false,
~co_ctx=CoCtx.empty,
~ancestors,
~duplicates=[],
~duplicate_bindings=[],
~duplicate_labels=[],
~ctx,
p,
m,
Expand Down Expand Up @@ -210,7 +211,7 @@ and uexp_to_info_map =
go(~ana, ~duplicates, e, m) |> (((e, m)) => (es @ [e], m)),
([], m),
);
let go_pat = upat_to_info_map(~ctx, ~ancestors, ~duplicates);
let go_pat = upat_to_info_map(~ctx, ~ancestors);
let go_typ = utyp_to_info_map(~ctx, ~ancestors);
let label_to_info_map =
(expected_labels, labmode, label: Exp.t, m: Map.t)
Expand Down Expand Up @@ -681,7 +682,7 @@ and uexp_to_info_map =
| Label(name) when label_sort =>
let self = Self.Just(Label(name) |> Typ.temp);
List.exists(l => name == l, duplicates)
? atomic(Duplicate(name, self)) : atomic(self);
? atomic(DuplicateLabel(name, self)) : atomic(self);
| Label(name) =>
let self = Self.UnexpectedLabelSort(name);
atomic(self);
Expand Down Expand Up @@ -1394,7 +1395,8 @@ and upat_to_info_map =
~ctx,
~co_ctx,
~ancestors: Info.ancestors,
~duplicates: list(string),
~duplicate_bindings: list(string)=[],
~duplicate_labels: list(string)=[],
~expected_labels=?,
~ana: Typ.t=Unknown(Internal) |> Typ.temp,
~under_ascription: bool=false,
Expand Down Expand Up @@ -1460,7 +1462,8 @@ and upat_to_info_map =
~ctx,
~co_ctx,
~ancestors,
~duplicates=[],
~duplicate_bindings=[],
~duplicate_labels=[],
~expected_labels=?,
~ana,
~under_ascription=false,
Expand All @@ -1475,7 +1478,8 @@ and upat_to_info_map =
~ctx,
~co_ctx,
~ancestors,
~duplicates,
~duplicate_bindings,
~duplicate_labels,
~ana,
~under_ascription,
~override_self?,
Expand All @@ -1491,10 +1495,18 @@ and upat_to_info_map =
let go = (~under_ascription=false) =>
upat_to_info_map(~under_ascription, ~is_synswitch, ~ancestors, ~co_ctx);
let unknown = Unknown(is_synswitch ? SynSwitch : Internal) |> Typ.temp;
let ctx_fold = (ctx: Ctx.t, m, ~duplicates=[]) =>
let ctx_fold = (ctx: Ctx.t, m, ~duplicate_bindings=[], ~duplicate_labels=[]) =>
List.fold_left2(
((ctx, tys, cons, m, info_all), e, ana) =>
go(~ctx, ~ana, ~duplicates, ~inferred_label?, e, m)
go(
~ctx,
~ana,
~duplicate_bindings,
~duplicate_labels,
~inferred_label?,
e,
m,
)
|> (
((info, m)) => (
info.ctx,
Expand Down Expand Up @@ -1645,12 +1657,21 @@ and upat_to_info_map =
typ: ctx_typ,
custom_statics: None,
});
add(
~self=Just(unknown),
~ctx=Ctx.extend(ctx, entry),
~constraint_=Coverage.Constraint.Truth,
m,
);

List.exists(l => name == l, duplicate_bindings)
? add(
~self=DuplicateVar(name, Just(unknown)),
~ctx=Ctx.extend(ctx, entry),
~constraint_=Coverage.Constraint.Truth,
m,
)
: add(
~self=Just(unknown),
~ctx=Ctx.extend(ctx, entry),
~constraint_=Coverage.Constraint.Truth,
m,
);

| TupLabel({term: ExplicitNonlabel, _} as label, p) =>
let (p, m) = go(~ana, ~ctx, p, m);
let (_, m) = go(~label_sort=true, ~ctx, ~ana=syn, label, m);
Expand All @@ -1672,12 +1693,21 @@ and upat_to_info_map =
~ctx,
~ana=labmode,
~override_self=?label_self,
~duplicates,
~duplicate_bindings,
~duplicate_labels,
~label_sort=true,
label,
m,
);
let (p, m) = go(~ctx, ~ana=val_mode, ~inferred_label?, p, m);
let (p, m) =
go(
~ctx,
~ana=val_mode,
~inferred_label?,
~duplicate_bindings,
p,
m,
);
(lab, p, m);
| _ =>
let (lab, m) =
Expand All @@ -1694,7 +1724,8 @@ and upat_to_info_map =
| (EmptyHole, _) => None
| _ => Some(BadLabel(Pat(label)))
},
~duplicates,
~duplicate_bindings,
~duplicate_labels,
label,
m,
);
Expand Down Expand Up @@ -1782,7 +1813,9 @@ and upat_to_info_map =

let new_labels =
List.map(p => Pat.match_tup_label(p) |> Option.map(fst), ps);
let duplicate_labels =
let new_duplicate_bindings =
Pat.get_duplicate_bindings(Pat.fresh(term));
let new_duplicate_labels =
LabeledTuple.get_duplicate_labels(Pat.match_tup_label, ps);
let (ctx, tys, cons, m, info_pats) =
List.fold_left2(
Expand All @@ -1791,7 +1824,10 @@ and upat_to_info_map =
~ctx,
~ana,
~inferred_label?,
~duplicates=duplicate_labels,
// Perhaps multiple copies of something in duplicates, but probably not an issue.
// Needed so that nested tuples can have duplicate bindings saved.
~duplicate_bindings=duplicate_bindings @ new_duplicate_bindings,
~duplicate_labels=new_duplicate_labels,
~expected_labels?,
e,
m,
Expand Down Expand Up @@ -1862,12 +1898,13 @@ and upat_to_info_map =
);
| Label(name) =>
let self = Self.Just(Label(name) |> Typ.temp);
List.exists(l => name == l, duplicates)
? atomic(Duplicate(name, self), Coverage.Constraint.Truth)
List.exists(l => name == l, duplicate_labels)
? atomic(DuplicateLabel(name, self), Coverage.Constraint.Truth)
: atomic(self, Coverage.Constraint.Truth);
| Parens(p)
| Projector(_, p) =>
let (p, m) = go(~ctx, ~ana, p, m);
let (p, m) =
go(~ctx, ~ana, p, ~duplicate_bindings, ~duplicate_labels, m);
add'(~self=p.self, ~ctx=p.ctx, ~constraint_=p.constraint_, m);
| Constructor(ctr, ty) =>
let self = Self.of_ctr(ctx, ctr, ana, ty);
Expand Down
8 changes: 8 additions & 0 deletions src/language/term/Pat.re
Original file line number Diff line number Diff line change
Expand Up @@ -302,3 +302,11 @@ let bound_var_ids = (ctx, pat): list(Binding.t) =>
}
}
);

let get_duplicate_bindings = (pat: t) => {
let bindings = bound_vars(pat);
List.filter(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be a fn in listutil that does this

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think you mean the dedup fn? that returns entire list deduplicated, this returns just the duplicates.

binding => {List.length(List.filter(x => x == binding, bindings)) > 1},
bindings,
);
};
1 change: 1 addition & 0 deletions src/web/app/inspector/CursorInspector.re
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ let common_err_view =
? []
: [text("Invalid labels: "), ...List.map(code, invalid_labels)]
)
| DuplicateVar(name, _) => [text("Duplicate Variable:"), code(name)]
| DuplicateLabel(name, _) => [
text("Duplicate Label:"),
label_view(name),
Expand Down
58 changes: 58 additions & 0 deletions test/statics/Test_Statics_Sums.re
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,64 @@ end
let _ : +Yo = Yo("lol") in ?
|} |> parse_exp,
),
inconsistent_typecheck(
"duplicate variables in patterns",
// #err: type incons#
{|
case (1,2,3) | (x, y, x) => 0 end
|} |> parse_exp,
),
inconsistent_typecheck(
"duplicate variables in patterns with nested tuples",
// #err: type incons#
{|
case (1,(2,3),4) | (x, (x,y), z) => 0 end
|} |> parse_exp,
),
inconsistent_typecheck(
"duplicate variables in patterns with labels",
// #err: type incons#
{|
case (1,(2,3),4) | (x=1, (x,y), z) => 0 end
|} |> parse_exp,
),
inconsistent_typecheck(
"duplicate variables in patterns with let expressions",
// #err: type incons#
{|
let (x,x) = 1,2 in ?
|} |> parse_exp,
),
inconsistent_typecheck(
"duplicate variables in patterns in functions",
// #err: type incons#
{|
fun x,x -> = 1 in ?
|} |> parse_exp,
),
inconsistent_typecheck(
"duplicate variables in patterns in labeled tuples",
// #err: type incons#
{|
let (x=a, x=(b, c)) = ? in
|} |> parse_exp,
),
inconsistent_typecheck(
"duplicate variables in patterns in labeled tuples #2",
// #err: type incons#
{|
let (x, x=(x, y)) = ? in
|} |> parse_exp,
),
fully_consistent_typecheck(
"duplicate variable tests: happy",
{|
let (x=(x, y)) = ? in
let (x, x=?) = ? in
let (x, x=(z, y=(x=a))) = ? in
|},
Some(unknown(Internal)),
),
Alcotest.test_case(
"Sum type duplicate constructor",
`Quick,
Expand Down