julia
73576bbf - Implement mechanism for `strict mode`

Commit
28 days ago
Implement mechanism for `strict mode` This implements support for the `strict mode` mechanism proposed in #54903. There was a desire to see an implementation of this along side #60018 since that are conceptually adjacent. However, other than some shared infrastructure, this version is farther from #60018 than the original proposal for reasons mentioned below. The motivation and basic idea are largely unchanged. # Implemented semantics The user facing interface to this mechanism is `@strict` (currently in `Base.Experimental`, but expected to be moved out in the course of the 1.14 release process. Rather than paraphrase, let me just provide the first part of the docstring that describes the semantics: ``` Set the `strict` mode for the current module to all flags specified in `flags`. Each `strict` mode flag is an independent option that disallows certain syntax or semantics that may be undesirable in *some* contexts. Package authors should consider which flags are appropriate for their package and set them at the top of the applicable module. # General semantics and philosophy Note that additional strict mode flags are not necessarily *safer* or *better* in any way; they are a reflection of of the reality that different users have different tradeoffs for their codebases and what may be a sensible restriction in a mature package could be very annoying in the REPL. As designed, there are several general guidelines that apply to strict mode flags. To the extent possible, they should be kept for future flag additions. 1. Code that evaluates without error in strict mode should also evaluate without error under the ordinary julia execution semantics. 2. Strict mode should not affect parsing. If it is desirable to disallow a particular syntax pattern, it should be recognized at the lowering stage. If this is currently not possible, the parser should be modified to emit an appropriate marker that can be checked at lowering time. 3. Strict mode is not intended for for issues that are clearly bugs. Those should instead use the syntax versioning mechanism (see [`Base.Experimental.@set_syntax_version`](@ref)). However, `strict` mode flags that gain widespread adoption may eventually be considered as candidates for syntax evolution. Strict mode flags are automatically inherited by submodules, but can be overriden by an explicit `@strict` invocation in the submodule. Strict mode flags are partitioned by world age. # Specifying flags The `flags` expression is runtime-evaluated and should evaluate to a collection of `Symbols` as specified below. In addition, nested collections of symbols are allowed and will be flattened. This is intended to support specifying strict mode flags in a central location and enforcing them across multiple dependents. ``` # Module vs Project.toml opt-ins Of particular note is that I ended up deciding on a per-module opt-in rather than a Project.toml opt in (like was originally proposed in #54903, and is implemented for syntax evolution in #60018). This is for the following reasons: 1. The #60018 experience has shown that project.toml opt ins are semantically somewhat awkward and need to be implemented both in the language and the package manager. This was fine for the syntax version, but strict mode is richer (and potentially much richer in the future) and adding this complexity into code loading seems undesirable. 2. One of the design objectives is to allow user-defined collections of strict mode flags enforced centrally across multiple packages. In this design this is easy by having a MyOrganizationBase package that defines a variable with the set of flags to enable. Doing something like this in Project.toml opens a whole can of worms on how to represent that. 3. I believe the concern about wanting to enable parse-time strict mode can be adequately addressed by having the parser emit a special marker that can then get picked up and checked against the strict mode by lowering (such marker addition possibly making use of the syntax evolution mechanism). If this is not how it works, the parser would need additional input state specifying the strict mode flags. #60018 has shown that changing parser state flags dynamically is undesirable, because people don't have a good sense of what the parse unit is. As such, I don't want the parser to look at strict mode flags at all. 4. As implemented here `@strict` inherits binding-world-age semantics. Since these are now well defined as of 1.12, this addresses a lot of the ordering concerns that were brought up in the discussion of #54903. 5. I think it may be useful to opt into certain strict mode flags for some modules in a package only (unlike the syntax version, where I don't expect this to be common). E.g. packages may define modules that define their core API or segregate their core algorithms from support code and may want more strict coding styles for such core modules. There remains a bit of a concern that this is less friendly to IDEs. I'm sympathetic to that, but the analysis required to compute the strict mode is a lot simpler than other analyses (so a language server should easily be able to do that) and I think it's outweighed particularly by the desire for user-definable collections (which requires the IDE to do some sort of analysis of however that is specified anyway). Given that, I think this mechanism is as IDE friendly as it gets, since the required capability is simply to compute the value of a constant obtained from a macro expansion (so no special strict-mode specific analysis required). # Implementation details The core of the implementation is simple, to determine the active strict mode flags, we simply look up the `_internal_module_strict_flags` binding in the appropriate module and see which flags are set. The exact types and values of this binding are explicitly and intentionally implementation details and `@strict` decides how to set it. This is inteded to allow flexibility of implementation in the future here. To faciliate the above described semantics of `@strict`, this binding has a couple of special features: 1. It gets automatically imported from a module's parent module upon module creation. 2. Unlike bindings created through syntax, invalidations from imports to `const` is permitted. Otherwise the mechanism behaves as an ordinary binding, including obeying world age semantics, and being Revise-able, etc. In particular, if you Revise the `@strict` setting in a top-level module it will automatically (through binding invalidation) be updated in all submodules. It was important to me that doing this would not leave the settings inconsistent. # Implemented strict mode flags Two strict mode flags are implemented in this PR, but they should largely be considered straw-man implementations to show how to access the flags set from within either the runtime or lowering. Everything works end-to-end, but we may want to do some extra work refining the precise semantics of these flags once we've merged the core mechanism. The reason for choosing these flags is simply that they were easy to implement. Several of the other proposed flags would require additional analysis in lowering, which should go in their own PRs. Implemented flags are as follows: ``` * `typeimports` This flag turns the 1.12 warning for implicit import of types into an error. Note that the implicit import default may be removed in a future Julia syntax iteration, in which case this flag will become a no-op for such versions. ```jldoctest julia> @Base.Experimental.strict :typeimports julia> String(x) = 1 ERROR: `@strict :typeimports` disallows extending types without explicit import in TypeImports: function Base.String must be explicitly imported to be extended ``` * `:nointliteraliterators` Disallows (at the lowering stages) literal integers as iterators in `for` loops. This protects against expressions like `for i in 10` which are commonly intended to be `for i in 1:10`. ```jldoctest julia> for i in 10 println(i) end 10 julia> @Base.Experimental.strict :nointliteraliterators julia> for i in 10 println(i) end ERROR: syntax: `@strict :nointliteraliterators` disallows integer literal iterators here around none:1 Stacktrace: [1] top-level scope @ none:1 ```
Author
Committer
Parents
Loading