`<final>` written on a class name (`x<final> := class ...`) compiles without a diagnostic and does not seal the class; the public digests use that spelling 42 times

Summary

A final specifier placed on the identifier of a class definition, left of :=, is accepted by the Verse compiler and has no effect. The class can be subclassed and its members overridden, and no warning is emitted. The same specifier after the class keyword seals the class as documented (a subclass is refused with 3569). Every other definition kind rejects final in identifier position with an explicit message (3596 for struct, interface, enum, module and type definitions; 3674 for functions, constants and variables outside a class), and abstract and final_super in identifier position on a class are rejected with a corrective message (“should be used on the class macro name, like c := class<abstract> ...”). Only final on a class identifier falls through. The shipped public digests (Verse.digest.verse, UnrealEngine.digest.verse, Fortnite.digest.verse) declare 42 classes with the identifier-position spelling, which suggests the authors expected it to seal those classes; creators who copy those declarations as a template inherit a class that is open without knowing it.

Please select what you are reporting on:

Verse

What Type of Bug are you experiencing?

Verse

Steps to Reproduce

  1. Create a Verse file in any project with the following content and build.
probe_base<final> := class {
  Value: int = 1

  Go()<reads>: int = {
    Value
  }
}

probe_sub := class(probe_base) {
  Value<override>: int = 2

  Go<override>()<reads>: int = {
    Value + 1
  }
}

ProbeUse()<transacts>: int = {
  Sub := probe_sub {}
  Sub.Go()
}
  1. Compare with the documented spelling, which is refused:
probe_base_sealed := class<final> {
  Value: int = 1
}

probe_sub_sealed := class(probe_base_sealed) {}
  1. For comparison, place final on the identifier of a struct, an interface, an enum, a module, a type alias, a free function or a module-level constant: each is refused with 3596 or 3674.

Expected Result

Either probe_base<final> := class ... seals the class (so probe_sub is refused with 3569, as it is for probe_base_sealed), or the declaration is refused with a message like the one given for abstract and final_super in that position (“should be used on the class macro name, like c := class<final> ...”).

Observed Result

Step 1 builds without any diagnostic; probe_sub subclasses probe_base, overrides its field and method, and ProbeUse runs. Step 2 is refused with 3569 as documented. Step 3 is refused with 3596 or 3674 for every definition kind except a class.

Platform(s)

PC (Windows)

Additional Notes

Reproduced in UEFN 6.0.0-57819926 (Fortnite Release 42.10). The public digests shipped with the editor use the identifier-position spelling on the 42 classes below (from the v42.00 digests; every one of them accepts a subclass as far as its access level allows).

Declaration (as written in the digest) Module Digest
fort_cancel_reason_canceled<native><final><public> := class<castable>(cancel_reason) /Fortnite.com/Abilities Fortnite.digest.verse
fort_cancel_reason_ended<native><final><public> := class<castable>(cancel_reason) /Fortnite.com/Abilities Fortnite.digest.verse
fort_cancel_reason_invalid<native><final><public> := class<castable>(cancel_reason) /Fortnite.com/Abilities Fortnite.digest.verse
fort_item_ability_component<native><final><public> := class<final_super>(component) /Fortnite.com/Abilities Fortnite.digest.verse
aura_voice<final><public> := class(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
battle_gamer_mae_voice<final><public> := class(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
brite_bomber_voice<final><public> := class<epic_internal>(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
brute_gunner_voice<final><public> := class(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
chase_voice<final><public> := class(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
clover_swift_voice<final><public> := class(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
cuddle_team_leader_voice<final><public> := class<epic_internal>(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
elmira_voice<final><public> := class(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
field_commander_voice<final><public> := class(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
fishstick_voice<final><public> := class<epic_internal>(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
guild_voice<final><public> := class(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
halley_voice<final><public> := class(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
haylee_skye_voice<final><public> := class<epic_internal>(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
helsie_midnight_voice<final><public> := class(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
hope_voice<final><public> := class<epic_internal>(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
jonesy_voice<final><public> := class<epic_internal>(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
kor_voice<final><public> := class<epic_internal>(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
lexa_hexbringer_voice<final><public> := class(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
magnus_voice<final><public> := class(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
mancake_voice<final><public> := class<epic_internal>(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
mecha_team_leader_voice<final><public> := class<epic_internal>(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
midas_voice<final><public> := class<epic_internal>(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
moxie_voice<final><public> := class(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
munitions_master_voice<final><public> := class(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
nezumi_voice<final><public> := class(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
orin_voice<final><public> := class(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
peely_voice<final><public> := class<epic_internal>(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
raven_voice<final><public> := class<epic_internal>(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
red_ruin_joni_voice<final><public> := class<epic_internal>(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
revolt_voice<final><public> := class(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
sunspot_voice<final><public> := class(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
terns_voice<final><public> := class(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
the_imagined_voice<final><public> := class<epic_internal>(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
tomatohead_voice<final><public> := class<epic_internal>(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
ziggy_voice<final><public> := class<epic_internal>(voice_model) /UnrealEngine.com/Conversations UnrealEngine.digest.verse
camera_body<final><native><public> := class<computes> /Verse.org/SceneGraph Verse.digest.verse
camera_lens<final><native><public> := class<computes> /Verse.org/SceneGraph Verse.digest.verse
camera_state<final><native><public> := class<computes><epic_internal> /Verse.org/SceneGraph Verse.digest.verse

Internal bug database reference, for matching any follow-up to our records: A5QPO7.