Skip to content

Convert legacy local_/global_ to new mode/modality syntax#143

Open
riaqn wants to merge 8 commits into
janefrom
convert-local_-to-new-syntax
Open

Convert legacy local_/global_ to new mode/modality syntax#143
riaqn wants to merge 8 commits into
janefrom
convert-local_-to-new-syntax

Conversation

@riaqn
Copy link
Copy Markdown

@riaqn riaqn commented May 22, 2026

Format Jane Street legacy local_ / global_ annotations using the new mode / modality syntax. Conversions:

  • local_ exp(exp : @ local)
  • (local_ pat), ~(local_ pat), ?(local_ pat = e) etc. → (pat @ local) (6 sites in fmt_fun_args)
  • let local_ x = elet x @ local = e
  • { global_ c : t }{ c : t @@ global } (record fields)
  • C of global_ TC of T @@ global (constructor args)

Arrow types (local_ T -> UT @ local -> U) are handled by #142 (now in oxcaml/jane), so this PR's scope is just expressions, patterns, let-bindings, record fields, and constructor arguments.

The standard parser canonicalizes the legacy and new forms to the same AST in most places (via with_optional_mode_expr etc.), so the round-trip AST equality check passes naturally for the conversions above. One case needed extra help: the formatter's pre-existing type_constr_and_body transformation hoists modes from a function body (y : @ mode) into the function's ret_mode_annotations. The std parser distinguishes these two shapes and existing normalization only handled the Pexp_constraint(_, Some ty, []) case; this PR adds the symmetric rule for Pexp_constraint(_, None, modes).

The lb_local field on Sugar.Let_binding.t is removed; Sugar.of_let_binding now folds pvb_local=true into pvb_modes so the regular new-syntax printing path takes over.

Mixed legacy + new syntax (e.g. (local_ x @ mode)) merges the local mode into the existing modes list, so the output is a single @ ... group (x @ local mode) instead of an invalid ((x @ mode) @ local) re-extension.

Other legacy AST shapes (Pparam_val (true, ...) for plain Ppat_var, global_ attribute on record/constructor fields, Pexp_apply (extension_local, ...)) remain in the AST as the parser produced them; the formatter simply prints them in the new syntax.

Tests in local.ml exercise the six mixed-syntax pattern shapes and the three local_ (e : ...) expression shapes.

Comment thread lib/Fmt_ast.ml
Comment on lines +1726 to +1727
( fmt_pattern ~parens:false c xpat
$ fmt_if islocal "@ @@ local" ) ) )
Copy link
Copy Markdown
Author

@riaqn riaqn May 22, 2026

Choose a reason for hiding this comment

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

merge_islocal_into_existing_modes tries to push the local into the existing list of modes, and if that fails we still print @ local here.

It's dirty, but if we consistently push the local inward I encounter some "unable to stablize" error due to print-box issues.

Given that we will remove all of this in the very next roll, I think it's fine.

Comment thread lib/Fmt_ast.ml
(Params.Indent.exp_constraint c.conf)
(Params.parens_if (parens && has_attr) c.conf
( wrap_fits_breaks ~space:false c.conf "(" ")"
( fmt_expression c (sub_exp ~ctx sbody)
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

we don't push modes into sbody, causing nested parens (see added tests). Pushing it into sbody would complicate the code and probably not worthwhile, given that we are removing legacy syntax very soon anyway.

riaqn and others added 7 commits May 22, 2026 14:30
Format Jane Street legacy `local_`/`global_` annotations using the new
mode/modality syntax: `local_ exp` → `(exp : @ local)`, `(local_ pat)` →
`(pat @ local)`, `let local_ x = ...` → `let x @ local = ...`,
`global_ c : t` → `c : t @@ global`, `C of global_ T` → `C of T @@ global`,
`local_ T -> U` → `T @ local -> U` (arrow params and return).

Add normalizations in Normalize_extended_ast so the legacy and new
representations compare equal during the AST round-trip check.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The formatter's [type_constr_and_body] transformation moves a function
body's [Pexp_constraint(_, None, modes)] into the function's
[ret_mode_annotations]. The std parser distinguishes these forms, so
add a normalization rule that hoists body modes to ret_mode_annotations
when the function has no other constraints, mirroring the existing rule
for [Pexp_constraint(_, Some ty, [])].

With this in place, the extended AST normalizations for
[Pexp_apply(local_, e)], [pvb_local], record/constructor [global_], and
arrow [local_] are no longer needed -- the std parser already canonicalizes
those legacy/new representations to the same shape.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sugar.of_let_binding now always converts pvb_local=true into a
[Mode "local"] entry in pvb_modes, so the lb_local field is always
false at the formatter level. Drop the field and the now-unreachable
fmt_if lb_local " local_" emission.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When a function parameter mixes legacy [local_] with new [@ mode]
annotations, e.g., [(local_ x @ mode)], the previous code would output
[((x @ mode) @ local)] which is not valid syntax (a single mode group
can't be re-extended with another [@]).

Fold the [local] mode into the existing modes list when the pattern is
already a [Ppat_constraint], so the printer emits [(x @ mode local)]
through the existing mode-list printing path.

For patterns without an existing constraint we keep the simpler
prefix-style code, which avoids changes to comment placement (which
would otherwise increase the iteration count needed to reach a stable
formatting).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Drop the comment-relocation step (a pre-existing workaround for the old
legacy-printing path that had no Cmts.fmt call for the global_ attr's
loc) and instead thread the attr's loc directly into the synthetic
[Modality "global"]. [fmt_modal]'s [Cmts.fmt c loc] then picks up any
comments naturally.

No test refs change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Consistent with fmt_label_declaration: when converting a legacy
[global_]-attributed constructor argument to a [Modality "global"],
pass the original attribute's loc through to the synthetic modality
rather than hard-coding [Location.none]. The parser-extended creates
the [global_] attribute with [~loc:Location.none], so the rendered
output is identical -- this is purely a consistency cleanup.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
These exercise [local_ (e : t)], [local_ (e : t @ m)], and
[local_ (e : @ m)]. Current output emits nested constraints
(e.g., [((x : int) : @ local)]) -- since the legacy syntax will be
removed soon, the slightly redundant nesting is acceptable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@riaqn riaqn marked this pull request as ready for review May 22, 2026 13:30
…format

After rebasing onto oxcaml/jane (which upstreamed the [local_ T] -> [T @ local]
arrow conversion in commit d9c82d3), the [localI] block in
[fmt_arrow_param] is no longer needed: the parser-extended doesn't produce
the legacy attribute on arrow params anymore.

Also reformat as suggested by [dune build @fmt].

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@riaqn riaqn force-pushed the convert-local_-to-new-syntax branch from 105ccab to 06dbc5b Compare May 22, 2026 13:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant