Read @throws from library declarations in exception analysis
Why
The exception analysis knew what library functions throw only from a table kept by hand in exn_lib.ml. The table drifted: the Temporal fromStringOrThrow functions were never added (#9182), and neither were a few Intl and Belt ones. Third-party libraries and a project's own externals had no way to say what they throw.
A call now takes the exceptions named by the @throws on the declaration of the value it calls, and the table is gone. The .cmi keeps the attributes of externals and interface declarations, so their @throws reaches calls in other modules and packages, through aliases, open, include, functors and first-class modules. For a let in a module without an interface, the type checker now copies the binding's @throws into the inferred signature, as it already did for @deprecated. The stdlib and Belt declare what their throwing functions throw, and a guard in the reanalyze tests fails when a stdlib or Belt *OrThrow or *Exn declaration has no @throws(...).
modifiedAnalysis exceptions of a call to f, Exception.Event loop
rule
f) = values(f)when f is in the values tablef) = table(f)otherwisef) = declared(f) ∪ values(f)modifiedSignature inferred signature of a module without an interface, Typemod.type_structure
/** Finds the value. */@throws(Not_found)let find = x => x > 0 ? x : throw(Not_found)
rescript
modifiedReanalyze calls to annotated externals
// Lib.res@throws(JsExn)external parse: string => int = "parseInt" // App.reslet parse = s => Lib.parse(s) @throws(JsExn)let parseAnnotated = s => Lib.parse(s) let date = () => Temporal.PlainDate.fromStringOrThrow("x")
reanalyze Exception Analysis
App.res:4:5-18App.res:1:5-9App.res:6:5-8Fixes #9182
Review
- Why the union of the declaration and the values table, rather than one winning? The values table holds the
@throwson the implementation's binding, the declaration the one in its interface. Either can be the only one written, as with an annotated.resiletwhose.resisn't annotated, so the call can throw what either names. - Why does the inferred signature take only the
@throwsfamily and@deprecated, and not every attribute? A doc comment is an attribute too: in the.cmiit would make editing a comment recompile the module's dependents. A module with a.resiis unchanged, since its.cmiholds the interface's declarations. - What does a
@throwswithout a payload mean? Nothing, as on a binding. The guard requires a payload, and the stdlib's two payload-less@throwsin Stdlib_JSON.res now nameJsExn.