Skip to content

Scoped nowarn #18049

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 124 commits into
base: main
Choose a base branch
from
Open

Scoped nowarn #18049

wants to merge 124 commits into from

Conversation

Martin521
Copy link
Contributor

@Martin521 Martin521 commented Nov 22, 2024

Description

Implements Scoped Nowarn according to draft RFC FS-1146.

This PR has taken a while. I had to deal with much more complexity than I imagined when I naively volunteered to tackle the feature request. Anyway, here we are.

I have split the PR into 7 commits that can be reviewed in sequence.
All of them compile, 1 and 4 - 7 also pass all tests locally.

  1. Add the feature flag, baseline tests, and the core WarnScopes module. See src/Compiler/SyntaxTree/WarnScopes.fsi and the RFC for the functionality of the module.

  2. Add the necessary changes to lexing and parsing. Note that the warn directives can no longer be collected during parsing (since they can now appear not only in top-level modules, but anywhere). So we collect them during lexing, similar to the processing of #if/#else/#endif directives.

  3. Remove legacy #nowarn processing (but hold off AST changes)

  4. Integrate the WarnScopes functionality and test it

  5. Add warn directive trivia (but hold off AST changes)

  6. Enable warn directive trivia (which means AST changes)

  7. Remove defunct types and parameters related to former #nowarn processing (more AST changes)

There is also a separate commit for the IlVerify baseline updates (change in line numbers only)

Checklist

  • Test cases added
  • Performance benchmarks added in case of performance changes
  • Release notes entry updated
  • Create documentation update PRs (see RFC)

@Martin521 Martin521 requested a review from a team as a code owner November 22, 2024 08:58
Copy link
Contributor

github-actions bot commented Nov 22, 2024

❗ Release notes required


✅ Found changes and release notes in following paths:

Change path Release notes path Description
src/Compiler docs/release-notes/.FSharp.Compiler.Service/9.0.300.md
LanguageFeatures.fsi docs/release-notes/.Language/preview.md

@psfinaki
Copy link
Member

Hi @Martin521 - thanks for the contribution. It's a substantial effort and we appreciate it. The PR is on our radar - just keep in mind that it's big and specific, and it will take time to find capacity for it.

If anyone from the community gets to thoroughly review it, that would be valuable as well.

Thanks for your diligence and patience :)

@majocha
Copy link
Contributor

majocha commented Apr 9, 2025

ConditionalWeakTable keyed with snapshot of the source file (ISourceText ?), or something else?

I thought ParsedInput. That's where the data is created, so should stay in sync.

@majocha
Copy link
Contributor

majocha commented Apr 9, 2025

I thought ParsedInput. That's where the data is created, so should stay in sync.

This won't work, because in AdjustSeverity where the data is consumed, the only thing we have is the range of PhasedDiagnostic.

@Martin521
Copy link
Contributor Author

First, thanks to @T-Gro and @majocha for testing and reviewing this.
Too bad that there are still mistakes in code-edit scenarios. The reason could be in my code, but I bet it is in some inconsistency in BackgroundCompiler/TransparentCompiler/IncrementalBuild. I have come across a few of them already (e.g. #18366, #18380, and one that I fixed in a recent commit to this PR).

However, I do understand your reluctance to accept code that is sensitive to certain constellations that were not problem so far but are seen as "inconsistency" by my code. (And also, I don't know how difficult it will be to find a repro for the mistake in order to fix it.)

I therefore want to try once more to find an alternative to the tcConfig.diagnosticsOptions solution. I have an idea that I need to check.

To still answer the question of @T-Gro about ApplyNoWarnsToTcConfig. This function was used for scripts and by service, but not in fsc (except for scripts). It just added any #nowarn to tcConfig.diagnosticsOptions.Warnoff, thus making it valid for the whole file. In fsc, a different mechanism was used that made #nowarn valid only after the directive. We have agreed in the RFC to change this and have the same "scoped" behavior for fsc, service and scripts. Therefore, all ApplyNoWarnsToTcConfig (and equivalent functions) were removed and the fsc mechanism (adjusted to WarnScopes) is now used for all cases.

Anyway, my ask is for some time to check the alternative that I mentioned above.

@majocha
Copy link
Contributor

majocha commented Apr 9, 2025

@Martin521,
I can repro this only with TransparentCompiler. The fact that it caches diagnostics may have something to do with it.
This is probably not very helpful, but, anyway, what I did was toggle comment / uncomment on #warnon 25 in the following sample:

module A
#nowarn 25
match None with None -> ()
#warnon 25
let x =
    let a = match None with Some () -> 1 
    match None with None -> 1 
match None with Some () -> () 
match None with None -> ()  

After few tries I was able to get it stuck in a wrong state. Making some edit that is not yet cached makes it work again.

Recording.2025-04-09.233442.mp4

@majocha
Copy link
Contributor

majocha commented Apr 9, 2025

Again this is with TransparentCompiler:

Recording.2025-04-09.234141.mp4

@Martin521
Copy link
Contributor Author

Thanks!

@majocha
Copy link
Contributor

majocha commented Apr 13, 2025

I just noticed #if DEBUG in code sometimes goes out of sync, too. I wonder if it's the same bug?

@Martin521
Copy link
Contributor Author

It seems in TransparentCompiler the file version part of the cache key is not reliably updated on edits. But I have not yet found the place where it goes wrong.

@Martin521 Martin521 marked this pull request as draft April 14, 2025 12:59
@Martin521
Copy link
Contributor Author

The above issue happens only in a certain situation in interactive (IDE) use and only when TransparentCompiler (TC) is enabled. If an edit of a warn directive makes a warning appear (or disappear) and you then revert that edit, the warning does not disappear (or appear, resp.) as you would expect.

The last commits that I have pushed should fix this (hopefully last) issue.

Here is the long story for those interested.

I first have to state again why the warn scope data are stored in tcConfig.diagnosticOptions.

Following the RFC, we want the warn directives to behave consistently across all compiler modes, i.e. in fsc-compiled .fs/.fsi files, in fsc-compiled .fsx files, in the IDEs (compiler service), and in fsi compilation (both scripts and interactive).

This means we have to replace two existing mechanisms (one in fsc and one for the other cases) by a single new one. (The fsc mechanism stores the #nowarn information during post-parse in ParsedInput and then brings it during type checking into a special diagnostics logger, which makes it available to AdjustSeverity. In all other cases, the information is collected from ParsedInput and is put into tcConfig.diagnosticOptions, which is available in AdjustSeverity.)

To use the current fsc mechanism also for the other cases turns out to be impossible. At least not before a BIG cleanup of the whole diagnostics logging and background compiler / fsi. The call hierarchies leading to post-parse and to AdjustSeverity are so deep and so broad, that threading something from one to the other is an impossible task (except for the fsc case). So, the only way left is to follow the other path and store the warn scope data for the project in tcConfig.diagnosticOptions.

This has been implemented and works nicely everywhere except for the situation mentioned above. Here is what's happening. The TC caches parse and check results. Caching is based on keys. For the parse cache, the key is (if we simplify the matter a little) just the file name and a hash of the file content (source text). If during editing the content changes, the file is parsed again (including update of the warn scope data in the project's tcConfig.diagnosticOptions) and the new parse result is cached. If for some reason the editor asks for a check of the file while the content has not changed, the results are taken from the cache and the warn scope data stays the same, which is fine. If, however, during editing, we return to a content state that has been parsed before, we get from the cache the previous parse result, but the warn scope data is not updated. This explains the issue that we have observed in exploratory testing of warn scopes in the IDEs.

What can we do now?

a) Postpone "scoped nowarn" (beyond F# 10) and first do a lot of cleanup. Remove a lot of the code duplication in the many compilation paths. Unify/simplify diagnostics logging for the different paths. Finish TC (bring it to production quality) and remove the old background compiler. Replace the current hack that implements #line directives by something sensible. Etc etc. (TBH, in hindsight, we should never have started "scoped nowarn" without the cleanup.)

b) Fix the issue by disabling parse result caching. This is a serious and simple option. I don't think it will affect performance in a significant way. Parsing takes little resources compared to type checking. (Also, ironically, ParseAndCheckFileInProject (the main entry point for the editor that leads to parsing) first calls the caching parser, then does the type checking, then does an unconditional parse (not chached) of the same file again (via ComputeScriptClosure).)

c) Fix the issue by making sure the parse cache key is changed (incremented) with every edit. This would be a fix with zero performance impact. But implementing it requires a quite deep redesign of the TC.

I have implemented b) now, leaving a) and c) to a next step.
Please let me know if there are other insights, opinions or advice.

@Martin521 Martin521 marked this pull request as ready for review April 21, 2025 07:35
@T-Gro
Copy link
Member

T-Gro commented Apr 28, 2025

Thanks for the explanation @Martin521.

If I understood it correctly, caching non-changing data is fine.
If a single change happens, we are also fine if re-parsing happens.

Which means that the only way problem can appear is if there are 2+ entries in the parsing cache, for the same file. (one current and one "to go back").

I do not like the approach to fully disable the parsing cache - there are tooling needs also for parsed results and we do have a tooling layer which simply assumes content is cached if it is not changing. (i.e. tooling happily asks for parsed results multiple times for various operations).

Could you please elaborate any possible downsides of an approach b2:

  • Keep the parsing cache, but modify it so that it cannot go back in time. i.e. it would only keep 1 current parsed result for file. If stuff changes, it is reparsed. If tooling asks dozens of times for the same file, cache result is returned.

Would there be any downsides then?

@Martin521
Copy link
Contributor Author

Let me look into "b2" (size 1 cache), it sounds like a very good idea.
(I am travelling, so it may take a few days)

@psfinaki psfinaki removed their assignment Apr 29, 2025
@Martin521
Copy link
Contributor Author

Martin521 commented Apr 30, 2025

Let me look into "b2" (size 1 cache), it sounds like a very good idea.

Making the cache single-version was easier than I thought (here).
The difficult part was to create a regression test that would reproduce the interactive behavior that we observed.

Copy link
Member

@T-Gro T-Gro left a comment

Choose a reason for hiding this comment

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

Thank you @Martin521 for your patience with fixing the outstanding IDE scenarios!
This is a big contribution and will improve the capabilities for handling warnings in F# codebases!

@T-Gro T-Gro enabled auto-merge (squash) May 1, 2025 06:11
auto-merge was automatically disabled May 1, 2025 06:14

Head branch was pushed to by a user without write access

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Breaking-change Describes a bug which is also a breaking change. needs-breaking-change-doc-created A PR needs a doc entry describing a new breaking change
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

6 participants