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
- 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()
}
- Compare with the documented spelling, which is refused:
probe_base_sealed := class<final> {
Value: int = 1
}
probe_sub_sealed := class(probe_base_sealed) {}
- For comparison, place
finalon 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.