Copy a dependency's signature only when it is read
Why
Env keeps each loaded compilation unit's signature as a lazy copy of the .cmi signature with its binders renamed (Subst.signature). normalize_path forced that copy just to learn that a compilation unit is not a module alias, which it never is; normalize_path is reached for every value reference such as Belt.Array.map, for abstract dependency types in Ctype.try_expand_once, and from Includemod and Printtyp. These copies were about 4-6 % of a clean build's CPU time on real projects (ui: 0.62 s of 13.3 s with one domain).
normalize_path now only looks the unit up (keeping the .cmi load, import and CRC checks) and reserves the stamps and type ids the copy would take, so every later identifier and type node gets the same stamp and id as before. The copy is made with exactly those stamps and ids when the signature is first read (find_module, scrape_alias, fold_modules, read_signature); on a 552-module app 2,921 copies are reserved and 10 are ever made.
How many stamps and ids a copy takes is learnt from the first copy of the same .cmi contents, keyed by the CRC create_cmi wrote, and kept for the process. If a reserved copy ever takes a different number, it is thrown away and made again with fresh stamps and ids, so a wrong count can change ids but never makes two nodes share one.
Review
- Are the artifacts the same? Yes: stamps and type ids are the same by construction. JS, .cmi, .cmj, .ast and .cmt are byte-identical to master on the 5,398-artifact corpus (default and one domain), and a unit test checks that deferred copies of every runtime and Belt interface marshal to the same bytes as eager ones, with the same stamps and ids created around them.
- Can the copy differ when it is made later? Only if the .cmi graph changed in between; the type checker only works on copies of it, and closed object types' shared mutability cells, the one part it can change, do not change what is copied.