Skip to content

Commit 5ebdba2

Browse files
feat(parser): accept lower_ident head in qualified_type_name (closes #448 item 1) (#523)
## Summary Item 1 of the qualified-path adjacent gaps surfaced by #448: the `qualified_type_name` production in `lib/parser.mly:154-161` required `upper_ident` as the head segment, so qualified type paths whose first segment was a lowercase stdlib module name (`json`, `option`, `prelude`, `dict`, `alib`, `collections`, `io`, `string`, `result`, `testing`, `effects`, `math`) parse-errored at the separator (`.` or `::`). Reported symptom: `parse error at .` on `x: json.Value` even with `use json;` in scope. ## Fix Add a parallel production with `lower_ident` head: ```ocaml qualified_type_name: | head = upper_ident qsep rest = qualified_type_name_rest { head ^ "::" ^ rest } | head = lower_ident qsep rest = qualified_type_name_rest { head ^ "::" ^ rest } ``` `qualified_type_name_rest` stays `upper_ident`-only — the tail segment is the type/effect name proper (a TyCon), so it must remain uppercase. Only the *qualifier* segment was wrongly constrained. Option (b) from #448 (relax the parser) rather than (a) (rename stdlib modules), per the original triage. ## LR(1) safety The disambiguation strategy is symmetric to the existing `upper_ident` path: a bare `lower_ident` in type position reduces to TyVar (existing rule at line 504); the parser only shifts past the `lower_ident` if the next token is a `qsep` (`.` or `::`). Menhir reports the **same conflict counts** as before the change (21 / 1 / 68 / 7), confirming no new conflicts introduced. ## Tests Three new cases in `test/test_qualified_paths.ml`: - `#448(1) lowercase qualifier head parses` — `x: json.Value` no longer emits a parse error - `#448(1) lowercase qualifier head + ::` parses — same with the `::` separator - `#448(1) fully-lowercase qualified path still rejected` — `x: json.value` MUST still parse-error because the tail segment is now reaching `qualified_type_name_rest` which still requires `upper_ident` (regression guard) Test results: `dune runtest` → **366 tests passed** (was 363); no regressions on existing `test_qualified_paths` cases. ## Scope Closes **#448 item 1** only. Does NOT address: - Item 2 (`use A.B;` leaf-only qualifier registration) - Item 3 (`use A::{Item}` multi-segment brace-list parse) - Item 4 (bare unknown TyCon permissive lookup) Those remain open under the parent issue. ## Downstream Unblocks `Pkg.Type` syntax with lowercase stdlib heads — directly helps hyperpolymath/echidna#117 (Client.res AffineScript-TEA port) which had `json.Value` / `dict.Dict` references that were stranded behind this parse error. Refs #228, refs #447, refs #241.
1 parent fffae8b commit 5ebdba2

2 files changed

Lines changed: 50 additions & 0 deletions

File tree

lib/parser.mly

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,14 @@ module_path:
154154
qualified_type_name:
155155
| head = upper_ident qsep rest = qualified_type_name_rest
156156
{ head ^ "::" ^ rest }
157+
/* #448 item 1: stdlib has lowercase module names (json, option,
158+
prelude, dict, …) that must be representable as qualifier heads
159+
at type/effect position. Same right-recursive shape, lookahead
160+
at qsep (`.`/`::`) disambiguates against the bare lower_ident →
161+
TyVar reduce; `qualified_type_name_rest` stays upper_ident-only
162+
so a tail segment like `.value` still parse-errors. */
163+
| head = lower_ident qsep rest = qualified_type_name_rest
164+
{ head ^ "::" ^ rest }
157165

158166
qualified_type_name_rest:
159167
| name = upper_ident { name }

test/test_qualified_paths.ml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,42 @@ let qualified_reserved_effect_with_use_passes () =
103103
| Ok () -> ()
104104
| Error m -> Alcotest.failf "expected Ok, got: %s" m
105105

106+
(* #448 item 1: `qualified_type_name` head now accepts `lower_ident`
107+
so stdlib's lowercase module names (json, option, prelude, dict,
108+
alib, collections, io, string, result, testing, effects, math)
109+
are representable at type position. Parse-side regression guard. *)
110+
let lowercase_qualified_type_parses () =
111+
let src = "use json;\npub fn f(x: json.Value) -> () { () }\n" in
112+
match frontend src with
113+
| Ok () -> ()
114+
| Error m ->
115+
(* `json` may not be a known module in this isolated frontend run,
116+
so accept any non-parse error — what we explicitly forbid is the
117+
pre-fix `parse error at .` from the lowercase head. *)
118+
Alcotest.(check bool) "no parse error on lowercase qualifier head"
119+
false (contains ~needle:"Parse error" m);
120+
Alcotest.(check bool) "no `parse error at .`"
121+
false (contains ~needle:"parse error at" m);
122+
ignore m
123+
124+
(* Same shape with the `::` separator. *)
125+
let lowercase_qualified_type_double_colon_parses () =
126+
let src = "use json;\npub fn f(x: json::Value) -> () { () }\n" in
127+
match frontend src with
128+
| Ok () -> ()
129+
| Error m ->
130+
Alcotest.(check bool) "no parse error on lowercase qualifier head (`::`)"
131+
false (contains ~needle:"Parse error" m);
132+
ignore m
133+
134+
(* Tail segment still requires UpperCase (TyCon name), so a fully-
135+
lowercase qualified path (`json.value`) must STILL parse-error. *)
136+
let fully_lowercase_qualified_type_still_rejected () =
137+
let src = "use json;\npub fn f(x: json.value) -> () { () }\n" in
138+
match frontend src with
139+
| Ok () -> Alcotest.fail "expected parse error on lowercase tail segment"
140+
| Error _ -> ()
141+
106142
let tests = [
107143
Alcotest.test_case "qualified type + use → passes" `Quick
108144
qualified_type_with_use_passes;
@@ -116,4 +152,10 @@ let tests = [
116152
bare_typecon_unaffected;
117153
Alcotest.test_case "qualified reserved effect + use → passes" `Quick
118154
qualified_reserved_effect_with_use_passes;
155+
Alcotest.test_case "#448(1) lowercase qualifier head parses" `Quick
156+
lowercase_qualified_type_parses;
157+
Alcotest.test_case "#448(1) lowercase qualifier head + `::` parses" `Quick
158+
lowercase_qualified_type_double_colon_parses;
159+
Alcotest.test_case "#448(1) fully-lowercase qualified path still rejected" `Quick
160+
fully_lowercase_qualified_type_still_rejected;
119161
]

0 commit comments

Comments
 (0)