Skip to content

Commit 9f83930

Browse files
hyperpolymathclaude
andcommitted
feat(parser): builtin-type & lowercase-module qualified paths + TOTAL field name (Refs gitbot-fleet#148)
Three closely-related expr_primary/field_name extensions, all driven by sustainabot hand-port shapes (gitbot-fleet#148): 1. Builtin-type-qualified value path: `Int::to_string(n)`, `String::len(s)`, etc. The built-in type names are reserved keyword tokens (NAT/INT_T/BOOL/FLOAT_T/STRING_T/CHAR_T), not UPPER_IDENT, so the existing `upper_ident COLONCOLON lower_ident` production never fired for them. Six new productions, one per builtin keyword, producing the canonical `ExprField (ExprVar TypeName, fname)` shape that [Resolve] already expects from upper-ident paths. 2. Lowercase-module-qualified value path: `json::encode_string(s)`, `string::join(xs, sep)`, etc. The stdlib already defines lowercase modules (`module json;`, `module string;`) and `use json::{...}` parses, but the expression-position qualified path only covered uppercase modules. The new `lower_ident COLONCOLON lower_ident` rule mirrors the upper-ident form. 3. `total` as a record-field name. Added to `field_name` alongside the existing `handle` contextual-keyword exception. Safe by the same reasoning HANDLE is safe: as a function-decl modifier TOTAL appears between `visibility?` and `FN`, never after DOT/before COLON in a record body; as a field name it appears between COLON / DOT and the field value. The surrounding token disambiguates. Used by sustainabot's `HealthIndex.total`, `Recommendation.total`, etc. Conflict-cost: zero. Parser builds with 21 S/R + 1 R/R, identical to the pre-patch baseline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 44b5341 commit 9f83930

1 file changed

Lines changed: 54 additions & 1 deletion

File tree

lib/parser.mly

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,52 @@ expr_primary:
871871
| modu = upper_ident COLONCOLON fname = lower_ident
872872
{ ExprField (ExprVar (mk_ident modu $startpos(modu) $endpos(modu)),
873873
mk_ident fname $startpos(fname) $endpos(fname)) }
874+
/* Builtin-type-qualified value path: `Int::to_string(n)`, `String::len(s)`,
875+
etc. The built-in type names are reserved keyword tokens (INT_T, STRING_T,
876+
...) rather than UPPER_IDENT, so the `upper_ident COLONCOLON lower_ident`
877+
production above never fires for them. We replicate the same
878+
`ExprField (ExprVar TypeName, lower_ident)` shape so [Resolve] sees a
879+
uniform qualified-path AST. The lookahead after COLONCOLON (a lower_ident)
880+
distinguishes this from any type-position use of the same keyword.
881+
Added 2026-05-26 (Refs gitbot-fleet#148 sustainabot hand-port:
882+
Int::to_string, Int::from_string, Float::to_string, etc.). */
883+
| NAT COLONCOLON fname = lower_ident
884+
{ ExprField (ExprVar (mk_ident "Nat" $startpos($1) $endpos($1)),
885+
mk_ident fname $startpos(fname) $endpos(fname)) }
886+
| INT_T COLONCOLON fname = lower_ident
887+
{ ExprField (ExprVar (mk_ident "Int" $startpos($1) $endpos($1)),
888+
mk_ident fname $startpos(fname) $endpos(fname)) }
889+
| BOOL COLONCOLON fname = lower_ident
890+
{ ExprField (ExprVar (mk_ident "Bool" $startpos($1) $endpos($1)),
891+
mk_ident fname $startpos(fname) $endpos(fname)) }
892+
| FLOAT_T COLONCOLON fname = lower_ident
893+
{ ExprField (ExprVar (mk_ident "Float" $startpos($1) $endpos($1)),
894+
mk_ident fname $startpos(fname) $endpos(fname)) }
895+
| STRING_T COLONCOLON fname = lower_ident
896+
{ ExprField (ExprVar (mk_ident "String" $startpos($1) $endpos($1)),
897+
mk_ident fname $startpos(fname) $endpos(fname)) }
898+
| CHAR_T COLONCOLON fname = lower_ident
899+
{ ExprField (ExprVar (mk_ident "Char" $startpos($1) $endpos($1)),
900+
mk_ident fname $startpos(fname) $endpos(fname)) }
901+
/* Lowercase-module-qualified value path: `json::encode_string(s)`,
902+
`string::join(xs, sep)`, etc. The stdlib already defines lowercase
903+
modules (`stdlib/json.affine` declares `module json;`,
904+
`stdlib/string.affine` declares `module string;`) and `use json::{...}`
905+
parses because `module_path` is a list of `ident` (upper or lower).
906+
The expression-position qualified path, however, only covered
907+
`upper_ident COLONCOLON lower_ident`, leaving every call into a
908+
lowercase stdlib module as a parse error. We mirror that rule for
909+
lower-module values so [Resolve] sees the same canonical
910+
`ExprField (ExprVar Mod, fname)` AST regardless of module casing.
911+
LR(1)-safe: the only competing reduction for `lower_ident` at this
912+
position is `name = lower_ident { ExprVar ... }`, and on a COLONCOLON
913+
lookahead no rule starting from `expr_primary COLONCOLON` exists, so
914+
the parser shifts unambiguously into this rule. Added 2026-05-26
915+
(Refs gitbot-fleet#148 sustainabot hand-port: json::encode_object,
916+
string::join, etc.). */
917+
| modu = lower_ident COLONCOLON fname = lower_ident
918+
{ ExprField (ExprVar (mk_ident modu $startpos(modu) $endpos(modu)),
919+
mk_ident fname $startpos(fname) $endpos(fname)) }
874920

875921
/* Grouping and tuples */
876922
| LPAREN RPAREN { ExprLit (LitUnit (mk_span $startpos $endpos)) }
@@ -1182,10 +1228,17 @@ ident:
11821228
record field names. Only keywords that do NOT introduce shift/reduce or
11831229
reduce/reduce conflicts are listed here. HANDLE is safe because it always
11841230
requires `name COLON ty` in type-record context and `name: expr` in
1185-
expression-record context; the surrounding COLON disambiguates. *)
1231+
expression-record context; the surrounding COLON disambiguates.
1232+
TOTAL is safe by the same reasoning: as a function-decl modifier it always
1233+
appears between visibility? and FN (never after DOT, never before COLON in
1234+
a record body), and as a field name it always appears immediately before
1235+
COLON (record-field decl, record-literal field, dotted field access). The
1236+
surrounding COLON / DOT disambiguates. Added 2026-05-26 (Refs gitbot-fleet#148
1237+
sustainabot hand-port: HealthIndex.total, Recommendation.total, etc.). *)
11861238
field_name:
11871239
| id = ident { id }
11881240
| HANDLE { mk_ident "handle" $startpos $endpos }
1241+
| TOTAL { mk_ident "total" $startpos $endpos }
11891242

11901243
lower_ident:
11911244
| name = LOWER_IDENT { name }

0 commit comments

Comments
 (0)