diff --git a/CHANGELOG.md b/CHANGELOG.md index da2eca3e3e..f8e7301d9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ #### :rocket: New Feature +- Reanalyze: add scoped `@@live`/`@@dead` annotations for marking module/file sections as live or dead. https://github.com/rescript-lang/rescript/pull/8197 + #### :bug: Bug fix - Reanalyze: fix reactive/server stale results when cross-file references change without changing dead declarations (non-transitive mode). https://github.com/rescript-lang/rescript/pull/8173 diff --git a/analysis/reanalyze/src/CollectAnnotations.ml b/analysis/reanalyze/src/CollectAnnotations.ml index ef8246daa5..2f6b53f457 100644 --- a/analysis/reanalyze/src/CollectAnnotations.ml +++ b/analysis/reanalyze/src/CollectAnnotations.ml @@ -5,7 +5,15 @@ open DeadCommon -let processAttributes ~state ~config ~doGenType ~name ~pos attributes = +type scope_default = FileAnnotations.annotated_as option + +let processAttributes ~(scope_default : scope_default) ~state ~config ~doGenType + ~name ~pos attributes = + (match scope_default with + | Some FileAnnotations.Live -> FileAnnotations.annotate_live state pos + | Some FileAnnotations.Dead -> FileAnnotations.annotate_dead state pos + | Some FileAnnotations.GenType -> FileAnnotations.annotate_gentype state pos + | None -> ()); let getPayloadFun f = attributes |> Annotation.getAttributePayload f in let getPayload (x : string) = attributes |> Annotation.getAttributePayload (( = ) x) @@ -39,6 +47,20 @@ let processAttributes ~state ~config ~doGenType ~name ~pos attributes = let collectExportLocations ~state ~config ~doGenType = let super = Tast_mapper.default in let currentlyDisableWarnings = ref false in + let currentScopeDefault : scope_default ref = ref None in + + let scopeDefaultFromToplevelAttribute (attribute : Parsetree.attribute) : + scope_default = + let attrs = [attribute] in + let getPayload (x : string) = + attrs |> Annotation.getAttributePayload (( = ) x) + in + if getPayload "dead" <> None then Some FileAnnotations.Dead + else if getPayload "live" <> None then Some FileAnnotations.Live + else if getPayload "genType" <> None then Some FileAnnotations.GenType + else None + in + let value_binding self ({vb_attributes; vb_pat} as value_binding : Typedtree.value_binding) = (match vb_pat.pat_desc with @@ -46,8 +68,8 @@ let collectExportLocations ~state ~config ~doGenType = | Tpat_alias ({pat_desc = Tpat_any}, id, {loc = {loc_start = pos}}) -> if !currentlyDisableWarnings then FileAnnotations.annotate_live state pos; vb_attributes - |> processAttributes ~state ~config ~doGenType ~name:(id |> Ident.name) - ~pos + |> processAttributes ~scope_default:!currentScopeDefault ~state ~config + ~doGenType ~name:(id |> Ident.name) ~pos | _ -> ()); super.value_binding self value_binding in @@ -58,8 +80,8 @@ let collectExportLocations ~state ~config ~doGenType = |> List.iter (fun ({ld_attributes; ld_loc} : Typedtree.label_declaration) -> toplevelAttrs @ ld_attributes - |> processAttributes ~state ~config ~doGenType:false ~name:"" - ~pos:ld_loc.loc_start) + |> processAttributes ~scope_default:!currentScopeDefault ~state + ~config ~doGenType:false ~name:"" ~pos:ld_loc.loc_start) | Ttype_variant constructorDeclarations -> constructorDeclarations |> List.iter @@ -74,14 +96,15 @@ let collectExportLocations ~state ~config ~doGenType = (fun ({ld_attributes; ld_loc} : Typedtree.label_declaration) -> toplevelAttrs @ cd_attributes @ ld_attributes - |> processAttributes ~state ~config ~doGenType:false - ~name:"" ~pos:ld_loc.loc_start) + |> processAttributes ~scope_default:!currentScopeDefault + ~state ~config ~doGenType:false ~name:"" + ~pos:ld_loc.loc_start) flds | Cstr_tuple _ -> () in toplevelAttrs @ cd_attributes - |> processAttributes ~state ~config ~doGenType:false ~name:"" - ~pos:cd_loc.loc_start) + |> processAttributes ~scope_default:!currentScopeDefault ~state + ~config ~doGenType:false ~name:"" ~pos:cd_loc.loc_start) | _ -> ()); super.type_kind self typeKind in @@ -96,36 +119,46 @@ let collectExportLocations ~state ~config ~doGenType = Typedtree.value_description) = if !currentlyDisableWarnings then FileAnnotations.annotate_live state pos; val_attributes - |> processAttributes ~state ~config ~doGenType ~name:(val_id |> Ident.name) - ~pos; + |> processAttributes ~scope_default:!currentScopeDefault ~state ~config + ~doGenType ~name:(val_id |> Ident.name) ~pos; super.value_description self value_description in let structure_item self (item : Typedtree.structure_item) = (match item.str_desc with - | Tstr_attribute attribute - when [attribute] |> Annotation.isOcamlSuppressDeadWarning -> - currentlyDisableWarnings := true + | Tstr_attribute attribute -> ( + match scopeDefaultFromToplevelAttribute attribute with + | Some _ as newDefault -> currentScopeDefault := newDefault + | None -> + if [attribute] |> Annotation.isOcamlSuppressDeadWarning then + currentlyDisableWarnings := true) | _ -> ()); super.structure_item self item in let structure self (structure : Typedtree.structure) = let oldDisableWarnings = !currentlyDisableWarnings in + let oldScopeDefault = !currentScopeDefault in super.structure self structure |> ignore; currentlyDisableWarnings := oldDisableWarnings; + currentScopeDefault := oldScopeDefault; structure in let signature_item self (item : Typedtree.signature_item) = (match item.sig_desc with - | Tsig_attribute attribute - when [attribute] |> Annotation.isOcamlSuppressDeadWarning -> - currentlyDisableWarnings := true + | Tsig_attribute attribute -> ( + match scopeDefaultFromToplevelAttribute attribute with + | Some _ as newDefault -> currentScopeDefault := newDefault + | None -> + if [attribute] |> Annotation.isOcamlSuppressDeadWarning then + currentlyDisableWarnings := true) | _ -> ()); super.signature_item self item in let signature self (signature : Typedtree.signature) = let oldDisableWarnings = !currentlyDisableWarnings in + let oldScopeDefault = !currentScopeDefault in super.signature self signature |> ignore; currentlyDisableWarnings := oldDisableWarnings; + currentScopeDefault := oldScopeDefault; signature in { diff --git a/tests/analysis_tests/tests-reanalyze/deadcode/expected/deadcode.txt b/tests/analysis_tests/tests-reanalyze/deadcode/expected/deadcode.txt index 14a41fb688..48214f3a03 100644 --- a/tests/analysis_tests/tests-reanalyze/deadcode/expected/deadcode.txt +++ b/tests/analysis_tests/tests-reanalyze/deadcode/expected/deadcode.txt @@ -1079,6 +1079,11 @@ addValueReference Newton.res:31:8 --> Newton.res:29:4 addValueReference Newton.res:31:18 --> Newton.res:29:4 addValueReference Newton.res:31:16 --> Newton.res:25:4 + Scanning OcamlWarningSuppressToplevel.cmt Source:OcamlWarningSuppressToplevel.res + addValueDeclaration +suppressed1 OcamlWarningSuppressToplevel.res:3:4 path:+OcamlWarningSuppressToplevel + addValueDeclaration +suppressed2 OcamlWarningSuppressToplevel.res:4:4 path:+OcamlWarningSuppressToplevel + addValueDeclaration +suppressed3 OcamlWarningSuppressToplevel.res:7:6 path:+OcamlWarningSuppressToplevel.M + addValueDeclaration +suppressed4 OcamlWarningSuppressToplevel.res:8:6 path:+OcamlWarningSuppressToplevel.M Scanning Opaque.cmt Source:Opaque.res addValueDeclaration +noConversion Opaque.res:5:4 path:+Opaque addValueDeclaration +testConvertNestedRecordFromOtherFile Opaque.res:11:4 path:+Opaque @@ -1312,6 +1317,26 @@ addTypeReference RepeatedLabel.res:12:16 --> RepeatedLabel.res:8:2 addValueReference RepeatedLabel.res:14:7 --> RepeatedLabel.res:12:4 Scanning RequireCond.cmt Source:RequireCond.res + Scanning ScopedAnnotationsLiveVsDead.cmt Source:ScopedAnnotationsLiveVsDead.res + addValueDeclaration +leafLive ScopedAnnotationsLiveVsDead.res:1:4 path:+ScopedAnnotationsLiveVsDead + addValueDeclaration +middleLive ScopedAnnotationsLiveVsDead.res:2:4 path:+ScopedAnnotationsLiveVsDead + addValueDeclaration +root ScopedAnnotationsLiveVsDead.res:6:6 path:+ScopedAnnotationsLiveVsDead.LiveScope + addValueDeclaration +stillDeadOutside ScopedAnnotationsLiveVsDead.res:9:4 path:+ScopedAnnotationsLiveVsDead + addValueDeclaration +leafDead ScopedAnnotationsLiveVsDead.res:11:4 path:+ScopedAnnotationsLiveVsDead + addValueDeclaration +middleDead ScopedAnnotationsLiveVsDead.res:12:4 path:+ScopedAnnotationsLiveVsDead + addValueDeclaration +root ScopedAnnotationsLiveVsDead.res:16:6 path:+ScopedAnnotationsLiveVsDead.DeadScope + addValueReference ScopedAnnotationsLiveVsDead.res:2:4 --> ScopedAnnotationsLiveVsDead.res:1:4 + addValueReference ScopedAnnotationsLiveVsDead.res:6:6 --> ScopedAnnotationsLiveVsDead.res:2:4 + addValueReference ScopedAnnotationsLiveVsDead.res:12:4 --> ScopedAnnotationsLiveVsDead.res:11:4 + addValueReference ScopedAnnotationsLiveVsDead.res:16:6 --> ScopedAnnotationsLiveVsDead.res:12:4 + Scanning ScopedAnnotationsOverride.cmt Source:ScopedAnnotationsOverride.res + addValueDeclaration +before ScopedAnnotationsOverride.res:2:6 path:+ScopedAnnotationsOverride.M + addValueDeclaration +live1 ScopedAnnotationsOverride.res:5:6 path:+ScopedAnnotationsOverride.M + addValueDeclaration +nestedLive ScopedAnnotationsOverride.res:8:8 path:+ScopedAnnotationsOverride.M.NestedInLive + addValueDeclaration +dead1 ScopedAnnotationsOverride.res:12:6 path:+ScopedAnnotationsOverride.M + addValueDeclaration +nestedDead ScopedAnnotationsOverride.res:15:8 path:+ScopedAnnotationsOverride.M.NestedInDead + addValueDeclaration +live2 ScopedAnnotationsOverride.res:19:6 path:+ScopedAnnotationsOverride.M + addValueDeclaration +afterModules ScopedAnnotationsOverride.res:22:4 path:+ScopedAnnotationsOverride Scanning Shadow.cmt Source:Shadow.res addValueDeclaration +test Shadow.res:2:4 path:+Shadow addValueDeclaration +test Shadow.res:5:4 path:+Shadow @@ -1807,9 +1832,9 @@ Forward Liveness Analysis - decls: 641 + decls: 659 roots(external targets): 122 - decl-deps: decls_with_out=371 edges_to_decls=248 + decl-deps: decls_with_out=375 edges_to_decls=252 Root (annotated): Value +Hooks.+default Root (external ref): Value +FirstClassModules.M.InnerModule2.+k @@ -1821,6 +1846,7 @@ Forward Liveness Analysis Root (external ref): Value +CreateErrorHandler2.Error2.+notification Root (annotated): Value +DeadTest.+fortyTwoButExported Root (annotated): Value +Docstrings.+grouped + Root (annotated): Value +ScopedAnnotationsOverride.M.+live1 Root (external ref): RecordLabel +DeadTest.inlineRecord.IR.b Root (annotated): Value +NestedModules.Universe.Nested2.+nested2Function Root (annotated): Value +Tuples.+marry @@ -1938,6 +1964,7 @@ Forward Liveness Analysis Root (annotated): Value +Uncurried.+callback Root (annotated): Value +TestPromise.+convert Root (external ref): Value +EmptyArray.Z.+make + Root (annotated): Value +ScopedAnnotationsLiveVsDead.LiveScope.+root Root (external ref): Value +Newton.+result Root (annotated): Value +Records.+findAllAddresses Root (annotated): Value +Variants.+id2 @@ -1947,6 +1974,7 @@ Forward Liveness Analysis Root (annotated): Value +Hooks.Inner.+make Root (annotated): Value +Uncurried.+curried3 Root (external ref): Value +OptArg.+twoArgs + Root (annotated): Value +OcamlWarningSuppressToplevel.+suppressed1 Root (external ref): RecordLabel +Records.business2.address2 Root (annotated): Value +Tuples.+testTuple Root (annotated): Value +Records.+testMyObj2 @@ -1970,12 +1998,14 @@ Forward Liveness Analysis Root (annotated): Value +TestImport.+make Root (external ref): RecordLabel +Unison.t.break Root (annotated): Value +ImportJsValue.+default + Root (annotated): Value +OcamlWarningSuppressToplevel.M.+suppressed4 Root (annotated): Value +Types.+optFunction Root (annotated): Value +Records.+getPayloadRecordPlusOne Root (annotated): Value +Types.+swap Root (annotated): Value +Types.+jsonStringify Root (annotated): RecordLabel +ImportHookDefault.props.person Root (annotated): Value +Variants.+saturday + Root (annotated): Value +OcamlWarningSuppressToplevel.M.+suppressed3 Root (annotated): Value +Records.+findAddress2 Root (annotated): Value +Records.+someBusiness Root (external ref): RecordLabel +Hooks.vehicle.name @@ -2025,6 +2055,7 @@ Forward Liveness Analysis Root (annotated): Value +NestedModules.Universe.Nested2.+nested2Value Root (annotated): Value +Records.+testMyObj Root (external ref): VariantCase DeadTypeTest.deadType.InBoth + Root (annotated): Value +ScopedAnnotationsOverride.M.+live2 Root (annotated): Value +Records.+testMyRecBsAs2 Root (annotated): Value +VariantsWithPayload.+testManyPayloads Root (annotated): Value +FirstClassModules.+someFunctorAsFunction @@ -2049,6 +2080,7 @@ Forward Liveness Analysis Root (annotated): Value +ImportJsValue.+useColor Root (annotated): Value +Records.+getPayload Root (external ref): VariantCase +Unison.break.IfNeed + Root (annotated): Value +ScopedAnnotationsOverride.M.NestedInLive.+nestedLive Root (external ref): Value +FirstClassModules.M.+y Root (annotated): Value +ModuleAliases.+testInner2 Root (annotated): RecordLabel +ImportHooks.props.person @@ -2103,6 +2135,7 @@ Forward Liveness Analysis Root (external ref): RecordLabel +VariantsWithPayload.payload.y Root (annotated): RecordLabel +ImportHookDefault.props.children Root (annotated): Value +TestModuleAliases.+testInner1 + Root (annotated): Value +OcamlWarningSuppressToplevel.+suppressed2 Root (annotated): Value +VariantsWithPayload.+testWithPayload Root (annotated): Value +Types.+testConvertNull Root (annotated): Value +Records.+getPayloadRecord @@ -2112,7 +2145,7 @@ Forward Liveness Analysis Root (annotated): Value +UseImportJsValue.+useGetProp Root (external ref): RecordLabel +Hooks.RenderPropRequiresConversion.props.renderVehicle - 300 roots found + 308 roots found Propagate: +Hooks.+default -> +Hooks.+make Propagate: DeadRT.moduleAccessPath.Root -> +DeadRT.moduleAccessPath.Root @@ -2130,6 +2163,7 @@ Forward Liveness Analysis Propagate: +DeadTest.WithInclude.t.A -> +DeadTest.WithInclude.t.A Propagate: ErrorHandler.Make.+notify -> +ErrorHandler.Make.+notify Propagate: +References.+make -> +References.R.+make + Propagate: +ScopedAnnotationsLiveVsDead.LiveScope.+root -> +ScopedAnnotationsLiveVsDead.+middleLive Propagate: +Newton.+result -> +Newton.+newton Propagate: +Newton.+result -> +Newton.+fPrimed Propagate: +Records.+findAllAddresses -> +Records.+getOpt @@ -2148,6 +2182,7 @@ Forward Liveness Analysis Propagate: DeadValueTest.+valueAlive -> +DeadValueTest.+valueAlive Propagate: +References.R.+get -> +References.R.+get Propagate: +References.R.+make -> +References.R.+make + Propagate: +ScopedAnnotationsLiveVsDead.+middleLive -> +ScopedAnnotationsLiveVsDead.+leafLive Propagate: +Newton.+newton -> +Newton.+/ Propagate: +Newton.+newton -> +Newton.+current Propagate: +Newton.+newton -> +Newton.+iterateMore @@ -2160,7 +2195,7 @@ Forward Liveness Analysis Propagate: +DeadTest.MM.+x -> +DeadTest.MM.+y Propagate: +ImportJsValue.AbsoluteValue.+getAbs -> +ImportJsValue.AbsoluteValue.+getAbs - 45 declarations marked live via propagation + 47 declarations marked live via propagation Dead VariantCase +AutoAnnotate.variant.R Dead RecordLabel +AutoAnnotate.record.variant @@ -3199,6 +3234,10 @@ Forward Liveness Analysis -> +Newton.+newton -> +Newton.+f -> +Newton.+fPrimed + Live (annotated) Value +OcamlWarningSuppressToplevel.+suppressed1 + Live (annotated) Value +OcamlWarningSuppressToplevel.+suppressed2 + Live (annotated) Value +OcamlWarningSuppressToplevel.M.+suppressed3 + Live (annotated) Value +OcamlWarningSuppressToplevel.M.+suppressed4 Dead VariantCase +Opaque.opaqueFromRecords.A Live (annotated) Value +Opaque.+noConversion Live (annotated) Value +Opaque.+testConvertNestedRecordFromOtherFile @@ -3386,6 +3425,34 @@ Forward Liveness Analysis deps: in=0 (live=0 dead=0) out=2 -> +RepeatedLabel.tabState.a -> +RepeatedLabel.tabState.b + Live (propagated) Value +ScopedAnnotationsLiveVsDead.+leafLive + deps: in=1 (live=1 dead=0) out=0 + <- +ScopedAnnotationsLiveVsDead.+middleLive (live) + Live (propagated) Value +ScopedAnnotationsLiveVsDead.+middleLive + deps: in=1 (live=1 dead=0) out=1 + <- +ScopedAnnotationsLiveVsDead.LiveScope.+root (live) + -> +ScopedAnnotationsLiveVsDead.+leafLive + Live (annotated) Value +ScopedAnnotationsLiveVsDead.LiveScope.+root + deps: in=0 (live=0 dead=0) out=1 + -> +ScopedAnnotationsLiveVsDead.+middleLive + Dead Value +ScopedAnnotationsLiveVsDead.+stillDeadOutside + Dead Value +ScopedAnnotationsLiveVsDead.+leafDead + deps: in=1 (live=0 dead=1) out=0 + <- +ScopedAnnotationsLiveVsDead.+middleDead (dead) + Dead Value +ScopedAnnotationsLiveVsDead.+middleDead + deps: in=1 (live=0 dead=1) out=1 + <- +ScopedAnnotationsLiveVsDead.DeadScope.+root (dead) + -> +ScopedAnnotationsLiveVsDead.+leafDead + Dead Value +ScopedAnnotationsLiveVsDead.DeadScope.+root + deps: in=0 (live=0 dead=0) out=1 + -> +ScopedAnnotationsLiveVsDead.+middleDead + Dead Value +ScopedAnnotationsOverride.M.+before + Live (annotated) Value +ScopedAnnotationsOverride.M.+live1 + Live (annotated) Value +ScopedAnnotationsOverride.M.NestedInLive.+nestedLive + Dead Value +ScopedAnnotationsOverride.M.+dead1 + Dead Value +ScopedAnnotationsOverride.M.NestedInDead.+nestedDead + Live (annotated) Value +ScopedAnnotationsOverride.M.+live2 + Dead Value +ScopedAnnotationsOverride.+afterModules Live (annotated) Value +Shadow.+test Live (annotated) Value +Shadow.+test Live (annotated) Value +Shadow.M.+test @@ -4595,6 +4662,30 @@ Forward Liveness Analysis RepeatedLabel.res:9:3-11 tabState.f is a record label never used to read a value + Warning Dead Value + ScopedAnnotationsLiveVsDead.res:9:1-24 + stillDeadOutside is never used + + Warning Dead Value + ScopedAnnotationsLiveVsDead.res:11:1-16 + leafDead is never used + + Warning Dead Value + ScopedAnnotationsLiveVsDead.res:12:1-25 + middleDead is never used + + Warning Dead Value + ScopedAnnotationsOverride.res:2:3-16 + M.before is never used + + Warning Dead Module + ScopedAnnotationsOverride.res:0:1 + ScopedAnnotationsOverride is a dead module as all its items are dead. + + Warning Dead Value + ScopedAnnotationsOverride.res:22:1-20 + afterModules is never used + Warning Dead Value Shadow.res:11:3-22 M.test is never used @@ -4847,4 +4938,4 @@ Forward Liveness Analysis OptArg.res:26:1-70 optional argument c of function wrapfourArgs is always supplied (2 calls) - Analysis reported 305 issues (Incorrect Dead Annotation:1, Warning Dead Exception:2, Warning Dead Module:21, Warning Dead Type:87, Warning Dead Value:174, Warning Dead Value With Side Effects:2, Warning Redundant Optional Argument:6, Warning Unused Argument:12) + Analysis reported 311 issues (Incorrect Dead Annotation:1, Warning Dead Exception:2, Warning Dead Module:22, Warning Dead Type:87, Warning Dead Value:179, Warning Dead Value With Side Effects:2, Warning Redundant Optional Argument:6, Warning Unused Argument:12) diff --git a/tests/analysis_tests/tests-reanalyze/deadcode/src/OcamlWarningSuppressToplevel.res b/tests/analysis_tests/tests-reanalyze/deadcode/src/OcamlWarningSuppressToplevel.res new file mode 100644 index 0000000000..21ed8a5e7c --- /dev/null +++ b/tests/analysis_tests/tests-reanalyze/deadcode/src/OcamlWarningSuppressToplevel.res @@ -0,0 +1,10 @@ +@@ocaml.warning("-32") + +let suppressed1 = 1 +let suppressed2 = 2 + +module M = { + let suppressed3 = 3 + let suppressed4 = 4 +} + diff --git a/tests/analysis_tests/tests-reanalyze/deadcode/src/ScopedAnnotationsLiveVsDead.res b/tests/analysis_tests/tests-reanalyze/deadcode/src/ScopedAnnotationsLiveVsDead.res new file mode 100644 index 0000000000..dc4fe26a96 --- /dev/null +++ b/tests/analysis_tests/tests-reanalyze/deadcode/src/ScopedAnnotationsLiveVsDead.res @@ -0,0 +1,17 @@ +let leafLive = 1 /* Used only by middleLive; becomes live only via propagation from a live root. */ +let middleLive = leafLive /* Used only by LiveScope.root; should be kept alive by @@live. */ + +module LiveScope = { + @@live /* From here on (inside LiveScope): treat items as if annotated @live. */ + let root = middleLive /* Live root: keeps middleLive -> leafLive alive via liveness propagation. */ +} + +let stillDeadOutside = 2 /* Outside any @@live scope and unused: should be reported as dead. */ + +let leafDead = 3 /* Only referenced from middleDead; should still be reported (dead). */ +let middleDead = leafDead /* Only referenced from DeadScope.root; @@dead does not keep it alive. */ + +module DeadScope = { + @@dead /* From here on (inside DeadScope): treat items as if annotated @dead (suppressed). */ + let root = middleDead /* Suppressed, but NOT a liveness root: middleDead/leafDead remain dead and are reported. */ +} diff --git a/tests/analysis_tests/tests-reanalyze/deadcode/src/ScopedAnnotationsOverride.res b/tests/analysis_tests/tests-reanalyze/deadcode/src/ScopedAnnotationsOverride.res new file mode 100644 index 0000000000..14fd406701 --- /dev/null +++ b/tests/analysis_tests/tests-reanalyze/deadcode/src/ScopedAnnotationsOverride.res @@ -0,0 +1,22 @@ +module M = { + let before = 1 /* Before any @@ annotation in M: should be reported as dead (unused). */ + + @@live /* Set default to live roots for the rest of M (until overridden). */ + let live1 = 1 /* Live root (suppressed) and can keep deps alive, if it referenced anything. */ + + module NestedInLive = { + let nestedLive = 1 /* Inherits @@live from M: treated as a live root (suppressed). */ + } + + @@dead /* Override default to dead (suppressed) for the rest of M (until overridden). */ + let dead1 = 1 /* Suppressed, but NOT a liveness root: would not keep dependencies alive. */ + + module NestedInDead = { + let nestedDead = 1 /* Inherits @@dead from M: suppressed. */ + } + + @@live /* Override again: back to live roots for the remainder of M. */ + let live2 = 1 /* Live root (suppressed). */ +} + +let afterModules = 1 /* Outside M: scope does not leak, so this should be reported as dead (unused). */