Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
d3b4d6b
Add plans/ to .gitignore
disconcision Feb 12, 2026
761667d
Sort-aware mold selection during insertion
disconcision Jan 21, 2026
fa6e95d
Sort-specific expansion for delimiters
disconcision Jan 21, 2026
f0f19e6
unwind now unnecessary special get in mold getter around multi-delimi…
disconcision Jan 21, 2026
b0735de
Add module system foundation: Mod sort, forms, and parsing
disconcision Feb 5, 2026
943660d
Add Mod→Exp expansion fallback
disconcision Feb 12, 2026
410e856
Add module semantics and Menhir parser support
disconcision Feb 5, 2026
dee063b
Add module system tests
disconcision Feb 5, 2026
e643fb9
Module system Phase 1: bug fixes and documentation
disconcision Feb 5, 2026
dbb32c8
Module cursor inspector: ID preservation and semicolon absorption
disconcision Feb 5, 2026
cc4079b
Module semicolon ID handling: partial fix
disconcision Feb 12, 2026
68e2d68
Make ModSeq semicolons chainable in Skel for flat n-ary structure
disconcision Feb 6, 2026
90b5540
Complete semicolon ID collection for modules
disconcision Feb 12, 2026
6ffc8c8
Phase 1.5A: Sig sort concrete syntax, testing, and bug fixes
disconcision Feb 7, 2026
c9616cd
fmt
disconcision Feb 7, 2026
44f6f62
Fix module width checking and empty sig parsing
disconcision Feb 7, 2026
7d1454a
Add empty sig Menhir round-trip test
disconcision Feb 7, 2026
74d3771
fmt
disconcision Feb 7, 2026
9a0e32d
Sort-specific grout precedence for module editing stability
disconcision Feb 10, 2026
ed527dc
Add InfoMod/InfoSig variants and ExplainThis entries for modules
disconcision Feb 12, 2026
df2632f
Untrack plans/modules.md (covered by .gitignore)
disconcision Feb 12, 2026
61d6b91
Exclude {} from operator characters for module delimiters
disconcision Feb 12, 2026
cebb36b
fmt and remove unused open in Form.re
disconcision Feb 12, 2026
142c3c1
Fix sort-inconsistency on module semicolons and minor tweaks
disconcision Feb 12, 2026
f828a83
Fix doubled module error count, Sig semicolon decoration, docs cleanup
disconcision Feb 12, 2026
3f9f51c
Fix Menhir parser for multi-item modules and unskip all module tests
disconcision Feb 12, 2026
131f508
Module keyword + pre-merge polish: MPat sort, capitalized names, edit…
disconcision Feb 13, 2026
973e251
Add dot-label completion in TyDi and restrict suggestions in label po…
disconcision Feb 13, 2026
6ef0b93
Add TyDi test suite and document dot-label completion in modules docs
disconcision Feb 13, 2026
1739b3f
TyDi qualified completions, base type fixes, module keyword renaming
disconcision Feb 13, 2026
82a3432
fix: merge
disconcision Feb 13, 2026
18af0c2
Fix module review issues: remold Sig semicolons, flatten_sig data los…
disconcision Feb 13, 2026
ea02b51
fmt
disconcision Feb 13, 2026
2f25df6
Add qualified type access (M.T) for module type exports
disconcision Feb 14, 2026
78732a8
TVarEntry propagation, TyDi type completions, error messages, tests
disconcision Feb 14, 2026
bbace8a
fmt
disconcision Feb 14, 2026
60b1fea
TyDi qualified completions, base type fixes, module keyword renaming
disconcision Feb 14, 2026
cc3dba5
Update modules documentation and in-editor doc slide
disconcision Feb 14, 2026
cd247e8
Merge branch 'dev' into modules-clean
disconcision Feb 18, 2026
8598c52
Module review fixes: auto-probe scope, hole CI, type alias shadowing,…
disconcision Feb 19, 2026
342e132
Merge branch 'modules-clean' of github.com:hazelgrove/hazel into modu…
disconcision Feb 19, 2026
6315189
Merge branch 'dev' of github.com:hazelgrove/hazel into modules-clean
disconcision Feb 19, 2026
0f66327
fmt
disconcision Feb 19, 2026
54342fd
Merge branch 'dev' into modules-clean
cyrus- Feb 20, 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,6 @@ node_modules

# Claude Code
.claude/

# Project plans (local only)
plans/
427 changes: 427 additions & 0 deletions docs/modules.md

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions src/haz3lcore/ProbePerform.re
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ let rec target_subterm_ids = (id: Id.t, info_map: Statics.Map.t) =>
IdTagged.rep_id(body),
IdTagged.rep_id(pat),
]
| Some(InfoExp({term: {term: Let(_pat, def, _), _}, _})) =>
/* If trying to probe a let, probe the definition instead.
| Some(InfoExp({term: {term: Let(_, def, _), _}, _}))
| Some(InfoExp({term: {term: ModuleExp(_, def, _), _}, _})) =>
/* If trying to probe a let/module, probe the definition instead.
Recurse so that if def is a fun literal, the above case will get it */
target_subterm_ids(IdTagged.rep_id(def), info_map)

Expand Down
3 changes: 3 additions & 0 deletions src/haz3lcore/TyDi/ErrorPrint.re
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,9 @@ let term_string_of: Info.t => string =
| InfoPat({term, _}) => Print.term(Pat(term))
| InfoTyp({term, _}) => Print.term(Typ(term))
| InfoTPat({term, _}) => Print.term(TPat(term))
| InfoMod({term, _}) => Print.term(Mod(term))
| InfoSig({term, _}) => Print.term(Sig(term))
| InfoMPat({term, _}) => Print.term(MPat(term))
| Secondary(_) => failwith("ChatLSP: term_string_of: Secondary");

let all = (info_map: Statics.Map.t): list(string) => {
Expand Down
23 changes: 22 additions & 1 deletion src/haz3lcore/TyDi/TyDi.re
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,33 @@ let suggest = (ci: Info.t, z: Zipper.t): list(t) => {
* recency bias in ctx. Revisit this later. I'm sorting before
* combination because we want backpack candidates to show up first */
switch (ci) {
| InfoExp({dot_labels, _}) when dot_labels != [] =>
List.map(
label =>
TyDiSuggestion.{
content: label,
strategy: Exp(Common(FromCtx(Label(label) |> Typ.fresh))),
},
dot_labels,
)
| InfoTyp({expects: LabelProjectionExpected(Some(labels)), _})
when labels != [] =>
List.map(
label =>
TyDiSuggestion.{
content: label,
strategy: Typ(FromCtx),
},
labels,
)
| InfoExp({label_sort: true, _})
| InfoPat({label_sort: true, _})
| InfoExp({cls: Exp(Label), _})
| InfoPat({cls: Pat(Label), _})
| InfoTyp({cls: Typ(Label), _})
| InfoExp({cls: Exp(TupLabel), _})
| InfoPat({cls: Pat(TupLabel), _})
| InfoTyp({cls: Typ(TupLabel), _}) => [] // TODO: Autocomplete for labels
| InfoTyp({cls: Typ(TupLabel), _}) => []
| _ =>
suggest_backpack(z)
@ (
Expand Down
76 changes: 76 additions & 0 deletions src/haz3lcore/TyDi/TyDiCtx.re
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,71 @@ let bound_constructor_aps =
ctx.entries,
);

/* Suggest qualified module member access: for variables with labeled tuple
* (module) types, suggest Name.label for fields consistent with the expected
* type. E.g., if String has type (empty=String, length=String->Int), and we
* expect String, suggest "String.empty".
*
* TODO: Only goes one level deep. Nested qualified access (A.B.x) would
* require recursive expansion. See also: List(Prod) types could generate
* qualified suggestions where field types are wrapped in List(...). */
let bound_qualified = (ty_expect: Typ.t, ctx: Ctx.t): list(TyDiSuggestion.t) =>
List.concat_map(
fun
| Ctx.VarEntry({typ, name, _}) =>
switch (Typ.normalize(ctx, typ) |> Typ.term_of) {
| Prod(ts) =>
List.filter_map(
label_ty =>
switch (Typ.match_tup_label(label_ty)) {
| Some((label, field_ty))
when Typ.is_consistent(ctx, ty_expect, field_ty) =>
Some({
content: name ++ "." ++ label,
strategy: Exp(Common(FromCtx(field_ty))),
})
| _ => None
},
ts,
)
| _ => []
}
| _ => [],
ctx.entries,
);

/* Like bound_qualified but for arrow-typed fields: suggest Name.label(
* when the field's return type is consistent with the expected type.
* E.g., if String has (length=String->Int) and we expect Int,
* suggest "String.length(". */
let bound_qualified_aps =
(ty_expect: Typ.t, ctx: Ctx.t): list(TyDiSuggestion.t) =>
List.concat_map(
fun
| Ctx.VarEntry({typ, name, _}) =>
switch (Typ.normalize(ctx, typ) |> Typ.term_of) {
| Prod(ts) =>
List.filter_map(
label_ty =>
switch (Typ.match_tup_label(label_ty)) {
| Some((label, {term: Arrow(_, ty_out), _} as field_ty))
when
Typ.is_consistent(ctx, ty_expect, ty_out)
&& !Typ.is_consistent(ctx, ty_expect, field_ty) =>
Some({
content: name ++ "." ++ label ++ "(",
strategy: Exp(Common(FromCtxAp(ty_out))),
})
| _ => None
},
ts,
)
| _ => []
}
| _ => [],
ctx.entries,
);

/* Suggest bound type aliases in type annotations or definitions */
let typ_context_entries = (ctx: Ctx.t): list(TyDiSuggestion.t) =>
List.filter_map(
Expand All @@ -117,6 +182,13 @@ let typ_context_entries = (ctx: Ctx.t): list(TyDiSuggestion.t) =>
ctx.entries,
);

/* NOTE(perf): suggest_variable and suggest_lookahead_variable each iterate
* over ctx.entries multiple times (currently ~7 passes in suggest_variable,
* up to ~33 in lookahead worst case for Bool). At typical context sizes
* (<500 entries) this is negligible. If it becomes a bottleneck, the main
* optimization is a single-pass refactor that classifies entries into
* buckets in one traversal, and/or pre-caching results for the fixed
* builtin context. */
let suggest_variable = (ci: Info.t): list(TyDiSuggestion.t) => {
let ctx = Info.ctx_of(ci);
let ctx = Ctx.filter_shadowed(ctx); /* Remove shadowing */
Expand All @@ -125,6 +197,8 @@ let suggest_variable = (ci: Info.t): list(TyDiSuggestion.t) => {
bound_variables(ana, ctx)
@ bound_livelits(ana, ctx)
@ bound_aps(ana, ctx)
@ bound_qualified(ana, ctx)
@ bound_qualified_aps(ana, ctx)
@ bound_constructors(x => Exp(Common(x)), ana, ctx)
@ bound_constructor_aps(x => Exp(Common(x)), ana, ctx)
| InfoPat({ana, co_ctx, _}) =>
Expand Down Expand Up @@ -169,9 +243,11 @@ let suggest_lookahead_variable = (ci: Info.t): list(TyDiSuggestion.t) => {
| InfoExp({ana, _}) =>
let exp_refs = ty =>
bound_variables(ty, ctx)
@ bound_qualified(ty, ctx)
@ bound_constructors(x => Exp(Common(x)), ty, ctx);
let exp_aps = ty =>
bound_aps(ty, ctx)
@ bound_qualified_aps(ty, ctx)
@ bound_constructor_aps(x => Exp(Common(x)), ty, ctx);
switch (ana |> Typ.term_of) {
| List(ty) =>
Expand Down
43 changes: 28 additions & 15 deletions src/haz3lcore/TyDi/TyDiForms.re
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,16 @@ module Delims = {
let leading = (sort: Sort.t): list(Token.t) =>
Form.delims
|> List.map(token => {
let (lbl, _) = Form.Expansion.get(token);
List.filter_map(
(m: Mold.t) =>
List.length(lbl) > 1 && token == List.hd(lbl) && m.out == sort
? Some(token ++ leading_expander) : None,
Form.Molds.get(lbl),
);
let (lbl, _) = Form.Expansion.get(sort, token);
switch (Form.Molds.try_get(sort, lbl)) {
| None => []
| Some(molds) =>
molds
|> List.filter_map((_: Mold.t) =>
List.length(lbl) > 1 && token == List.hd(lbl)
? Some(token ++ leading_expander) : None
)
};
})
|> List.flatten
|> List.sort_uniq(compare);
Expand Down Expand Up @@ -151,18 +154,28 @@ module Delims = {
let const_mono = (sort: Sort.t): list(Token.t) =>
Token.const_mono_delims
|> List.map(token => {
List.filter_map(
(m: Mold.t) =>
m.out == sort && List.mem(token, Token.const_mono_delims)
? Some(token) : None,
Form.Molds.get([token]),
)
switch (Form.Molds.try_get(sort, [token])) {
| None => []
| Some(molds) =>
molds
|> List.filter_map((_: Mold.t) =>
List.mem(token, Token.const_mono_delims) ? Some(token) : None
)
}
})
|> List.flatten
|> List.sort_uniq(compare);

let const_mono_exp = const_mono(Exp);
let const_mono_pat = const_mono(Pat);
/* base_typs (String, Int, Float, Bool, Nat, SInt) have Exp/Pat-sort
* molds (as constructors) but no type entry in Typ.of_const_mono_delim.
* Without an entry, filter_by assigns Unknown type, making them match
* any expected type. Exclude them from Exp and Pat suggestions;
* constructor suggestions come from TyDiCtx.bound_constructors instead.
* They remain in Typ sort for type-position completion. */
let const_mono_exp =
const_mono(Exp) |> List.filter(t => !List.mem(t, Token.base_typs));
let const_mono_pat =
const_mono(Pat) |> List.filter(t => !List.mem(t, Token.base_typs));
let const_mono_typ = const_mono(Typ);

let const_mono = (sort: Sort.t): list(string) =>
Expand Down
20 changes: 20 additions & 0 deletions src/haz3lcore/derived/AutoProbe.re
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,18 @@ let term_is_let = (term: Language.Any.t): bool =>
| _ => false
};

/* Module declarations (ModLet, ModType, ModuleMod) don't have runtime
* values — they're declarations, not expressions. Auto-probe should
* never probe them; it should probe their definition subexpression
* instead (which appears as a separate candidate on the same line).
* ModExp (bare expression in module body) is excluded — it wraps
* an expression that does have a value. */
let term_is_mod_declaration = (term: Language.Any.t): bool =>
switch (term) {
| Mod({term: ModLet(_, _) | ModType(_, _) | ModuleMod(_, _), _}) => true
| _ => false
};

let let_body_is_hole = (term: Language.Any.t): bool =>
switch (term) {
| Exp({term: Let(_, _, body), _}) => term_is_hole(Exp(body))
Expand Down Expand Up @@ -492,6 +504,13 @@ let candidate_allowed_by_term_sort =
Language.Statics.Map.lookup(candidate_id, env.info_map),
);

let candidate_allowed_by_mod_declaration =
(candidate_id: Id.t, env: selection_env): bool =>
switch (get_term(candidate_id, env.terms)) {
| Some(term) => !term_is_mod_declaration(term)
| None => true
};

let candidate_is_allowed =
(
candidate_id: Id.t,
Expand All @@ -501,6 +520,7 @@ let candidate_is_allowed =
)
: bool =>
candidate_allowed_by_term_sort(candidate_id, env)
&& candidate_allowed_by_mod_declaration(candidate_id, env)
&& candidate_allowed_by_holes(candidate_id, row, env)
&& candidate_allowed_by_function_types(candidate_id, env)
&& candidate_allowed_by_container(candidate_id, env)
Expand Down
Loading