# Access Specifiers Access specifiers control visibility and accessibility of code elements. They provide a nuanced spectrum of access levels that reflect the complex reality of modern software development, particularly in the context of a persistent, global metaverse where code from many authors must coexist safely. Five primary visibility levels are defined that form a carefully designed hierarchy, each serving specific architectural needs. Understanding when and why to use each level is crucial for creating well-structured, maintainable code. | Specifier | Visibility | Usage | |-----------|------------|-------| | `` | Universally accessible | Members intended for external use | | `` | Only within the module (default) | Module-private implementation | | `` | Only in immediate enclosing scope | Local to class/struct | | `` | Current class and subtypes | Inheritance hierarchies | | `` | Current scope and enclosing scopes | Special use cases | | `` | Scopes with the /Verse.org, /UnrealEngine.com, and /Fortnite.com domains | `` is only usable by Epic-authored code | ## Public The `` specifier represents the broadest level of access, making an identifier universally accessible from any code that can reference the containing module or type. When you mark something as public, you're making a strong commitment about its availability and stability: ```verse PlayerManager := module: MaxPlayers:int = 100 player := class: Name:string Level:int = 1 ``` Public members form the contract between your code and the outside world. In the metaverse context, public declarations are particularly significant because they represent guarantees that extend potentially forever—once published, removing or incompatibly changing a public member breaks the promise you've made to other developers who depend on your code. The public specifier can be applied to modules, classes, interfaces, structs, enums, methods, and data members. When applied to a type definition itself, it makes the type available for use outside its defining module. When applied to members within a type, it makes those members accessible to any code that has access to an instance of that type. ## Protected The `` specifier creates a middle ground between public and private, allowing access within the defining class and any classes that inherit from it. This level exists specifically to support inheritance hierarchies while maintaining encapsulation: ```verse game_entity := class: var Position:vector3 = vector3{x:=0.0, y:=0.0, z:=0.0} var Health:int = 100 UpdatePosition(NewPos:vector3):void = set Position = NewPos OnPositionChanged() OnPositionChanged():void = {} # Overridable by subclasses player := class(game_entity): MoveToSpawn():void = UpdatePosition(GetSpawnLocation()) # Can access protected member set Health = MaxHealth # Can modify protected variable ``` Protected access enables the template method pattern and other inheritance-based designs while preventing external code from accessing implementation details that should remain within the class hierarchy. This is particularly valuable for game entities and other hierarchical structures where parent classes need to share behavior with children without exposing that behavior to the world. ## Private The `` specifier provides the strictest access control, limiting visibility to the immediately enclosing scope. Private members are truly internal implementation details that can be changed freely without affecting any external code: ```verse inventory := class: var Items:[]item = array{} var Capacity:int = 20 var CurrentWeight:float = 0.0 MaxWeight:float=20.0 AddItem(NewItem:item, At:int):void = ValidateCapacity[NewItem] set Items[At] = NewItem set CurrentWeight = CurrentWeight + NewItem.Weight ValidateCapacity(NewItem:item):void = Items.Length < Capacity CurrentWeight + NewItem.Weight <= MaxWeight ``` Private members are the building blocks of encapsulation. They allow you to maintain invariants, hide complexity, and create clean abstractions. Changes to private members never break external code, giving you the freedom to refactor and optimize implementation details as needed. ## Internal The `` specifier, which is the default access level when no specifier is provided, makes members accessible within the defining module but not outside it. This creates a natural boundary for collaborative code that needs to share implementation details without exposing them publicly: ```verse Physics := module: # Internal types and constants gravity_constant:float = 9.81 collision_detector := class: DetectCollision(A:game_entity, B:game_entity):?collision_info physics_world := class: var Entities:[]game_entity = array{} SimulateStep(DeltaTime:float):void = for (Entity : Entities): ApplyGravity(Entity, DeltaTime) CheckCollisions(Entity) ``` Internal access is ideal for module-wide utilities, shared implementation details, and helper functions that multiple classes within a module need but shouldn't be exposed to external code. It provides a clean separation between the module's public interface and its implementation machinery. ## Scoped The `` specifier creates custom access boundaries between modules or code locations. Unlike the fixed visibility levels of `public`, `internal`, and `private`, `scoped` access allows you to explicitly grant access to particular modules while excluding all others—creating a kind of "friend" relationship between program entities. ### Scoped Definitions A scoped access level is created using the `scoped{...}` expression, which takes one or more module references: ```verse Collaboration := module: # Create a scope that includes both ModuleA and ModuleB Shared := scoped{ModuleA, ModuleB} # This class is only accessible within ModuleA and ModuleB SharedResource := class: Data:int = 42 ``` The scoped definition creates an access level that can then be used as a specifier on classes, functions, variables, and other definitions. Code within any of the listed entities can access the scoped member, while code outside those modules cannot—even if it can see the containing scope. ### Cross-Module Collaboration The most powerful use of scoped access is enabling controlled collaboration between modules. A definition can be created in one module but scoped to another, making it accessible where it's needed while keeping it hidden elsewhere: ```verse Graphics := module: # Define an interface scoped to the physics module CollidableShape := interface: GetBounds():bounding_box Physics := module: using{Graphics} # Physics can implement the interface even though it's defined in graphics sphere_collider := class(CollidableShape): GetBounds():bounding_box ``` This pattern allows graphics to define contracts that physics implements without exposing those implementation details publicly. The interface exists at the boundary between the two modules but isn't part of either module's public API. You can scope a definition to multiple modules, creating a shared private space for collaboration: ```verse Gameplay := module: # This scope includes both the inventory and crafting modules SharedGameplayScope := scoped{Inventory, Crafting} # Items can be accessed by both inventory and crafting Item := class: ID:int Properties:[string]string # Factory function available to both systems CreateItem(TheID:int):Item = Item{ID:=TheID, Properties:=map{}} Inventory := module: using{Gameplay} AddToInventory(ItemID:int):void = NewItem := CreateItem(ItemID) # Can access scoped function # Implementation... Crafting := module: using{Gameplay} CraftItem(Recipe:[]int):Item = # Can create items and access their properties CreateItem(Recipe[0]) ``` ### Scoped Read or Write Access Like other access specifiers, scoped can be applied separately to read and write operations on variables: ```verse SharedScope := scoped{ModuleA, ModuleB} state_manager := class: # Public read access, but only ModuleA and ModuleB can write var GameState:game_state = game_state{} # Only ModuleA and ModuleB can read or write this internal state var SyncCounter:int = 0 ``` This pattern is particularly useful for shared state that multiple modules need to coordinate on without exposing write access publicly. ### Visibility and Access Paths An important subtlety of scoped access is that it grants access to a specific member, but doesn't make intermediate types or modules visible. To access a scoped member, you must be able to see the entire path to it: ```verse Outer := module: # Internal to outer Inner := module: # Scoped to TargetModule SharedClass := class: Value:int = 42 TargetModule := module: using{Outer} # ERROR: Can't see Outer.Inner because Inner is internal to Outer # even though SharedClass is scoped to us UseShared():void = Outer.Inner.SharedClass{} ``` For scoped access to work, either the containing scope must be accessible (public or also scoped appropriately), or the scoped member must be accessed through a public interface that exposes it. A definition can only have one scoped access level—you cannot apply multiple scoped specifiers: ```verse # ERROR: Cannot have multiple access level specifiers InvalidScope := class{} ``` ### Scoped Access and Inheritance When a class member has scoped access, overriding members in subclasses can maintain or narrow that access, following normal inheritance rules: ```verse SharedScope := scoped{ModuleA, ModuleB} base := class: # Accessible only in ModuleA and ModuleB ComputeValue():int = 42 derived := class(base): # Can override with same or more restrictive access ComputeValue():int = 100 # Now internal to this module ``` ### Using Scoped for API Boundaries Scoped access excels at creating controlled API boundaries where certain functionality should be shared between specific modules but not exposed as part of the public interface: ```verse Networking := module: # Public scope for modules that need network access NetworkScope := scoped{PlayerSystem, Matchmaking, Telemetry} # Core networking available to specific systems SendPacket(Data:[]uint8):void = # Implementation... # Internal statistics var BytesSent:int = 0 ``` This creates an explicit architectural boundary—only the modules listed in the scope can access the networking primitives, while other code must use higher-level public APIs. ### Design Considerations Scoped access represents an architectural commitment between modules. When using it effectively: - Use scoped for legitimate cross-module collaboration that doesn't belong in the public API - Keep scope definitions at the module level where they can be documented and maintained - Prefer scoping to explicit modules rather than deeply nested scopes - Consider whether protected or internal access might be simpler for your use case - Document why particular modules are included in a scope The scoped specifier fills a unique niche between internal and public access, enabling sophisticated module architectures where multiple components need to collaborate intimately without exposing those implementation details to the wider codebase. ## Separating Read and Write Access An innovative feature is the ability to apply different access specifiers to reading and writing operations on the same variable. This fine-grained control allows you to create variables that are widely readable but narrowly writable, implementing common patterns like read-only properties elegantly: ```verse game_state := class: # Public read, protected write var Score:int = 0 # Public read, private write var PlayerCount:int = 0 # Internal read, private write var SessionID:string ``` This dual-specifier system solves a common problem in object-oriented programming where you want to expose state for reading without allowing external modification. Rather than requiring getter methods or property syntax, Verse makes this pattern a first-class language feature. The syntax places the write-access specifier on the `var` keyword and the read-access specifier on the identifier itself. This visual separation makes the access levels immediately clear when reading code. The write specifier must be at least as restrictive as the read specifier — you cannot write to a variable that's privately readable but publicly writable, as this would violate basic encapsulation principles. ## Best Practices Understanding when to use each access level requires thinking about your code's architecture and evolution. The principle of least privilege suggests starting with the most restrictive access that works and only broadening it when necessary. For public APIs, every public member is a commitment. Before making something public, consider whether it truly needs to be part of your module's contract or if it's an implementation detail that happens to be needed elsewhere temporarily. Public members should be stable, well-documented, and designed for longevity. Protected access should be used thoughtfully in inheritance hierarchies. Not everything in a base class needs to be protected—only those members that form the inheritance contract between parent and child classes. Overuse of protected access can create tight coupling between classes in a hierarchy. Private access is your default for implementation details. Most helper functions, intermediate calculations, and state management should be private. This gives you maximum flexibility to refactor and optimize without breaking dependent code. The dual-specifier pattern for variables is particularly powerful for maintaining invariants. By making variables publicly readable but privately or protectively writable, you can expose state for observation while maintaining complete control over modifications: ```verse resource_manager := class: var TotalResources:int = 1000 var AllocatedResources:int = 0 var AvailableResources:int = 1000 AllocateResources(Amount:int):void = Amount <= AvailableResources set AllocatedResources = AllocatedResources + Amount set AvailableResources = AvailableResources - Amount ``` ## Annotations and Metadata Verse provides an annotation system for attaching metadata to definitions using the `@` prefix syntax. Annotations provide compiler directives and metadata that affect how code is treated during compilation and evolution. ### Built-in Annotations #### @deprecated !!! warning "Internal Feature" @deprecated attribute is currently an internal feature and cannot be used by end-users. The `@deprecated` annotation marks definitions that should no longer be used. When code references a deprecated definition, the compiler produces a warning, alerting developers to update their code: ```verse # Mark a definition as deprecated @deprecated OldFunction():void = Print("This function is deprecated") # Mark a class as deprecated @deprecated legacy_player := class: Name:string # Attempting to use deprecated code produces a warning UseDeprecated():void = OldFunction() # Warning: OldFunction is deprecated ``` Deprecated definitions can use other deprecated definitions without warnings, but non-deprecated code cannot use deprecated definitions without triggering warnings. This allows gradual migration of deprecated APIs: ```verse @deprecated OldAPI():int = 42 # Valid: deprecated can call deprecated @deprecated MigrateOldAPI():int = OldAPI() # Warning: non-deprecated calling deprecated # NewCode():int = OldAPI() ``` The `@deprecated` annotation can be applied to: - Functions and methods - Classes, interfaces, structs, and enums - Individual enum values - Data members - Modules #### @experimental !!! warning "Internal Feature" @deprecated attribute is currently an internal feature and cannot be used by end-users. The `@experimental` annotation marks features that are not yet stable and may change or be removed in future versions. Experimental features can only be used when the `AllowExperimental` package flag is enabled: ```verse # Mark a feature as experimental @experimental experimental_class := class: NewFeature:int # Using experimental features requires AllowExperimental flag # Without flag: error # With AllowExperimental:=true: allowed UseExperimental(Obj:experimental_class):void = Print("Using experimental feature") ``` Experimental definitions behave similarly to deprecated ones—experimental definitions can freely use other experimental definitions, but stable code cannot use experimental definitions unless the `AllowExperimental` flag is set. The `@experimental` annotation cannot be applied to: - Local variables - Override methods (base method's experimental status is inherited) #### @available The `@available` annotation controls when a definition becomes available based on version numbers. This enables gradual API rollout and version-specific functionality: ```verse using { /Verse.org/Native } # Required for @available # Available only in version 3000 and later @available{MinUploadedAtFNVersion := 3000} NewFeature():void = Print("New feature") # Multiple definitions can coexist for different versions @available{MinUploadedAtFNVersion := 2900} OldImplementation():int = 42 @available{MinUploadedAtFNVersion := 3000} NewImplementation():int = 100 ``` The `@available` annotation can be applied to the same kinds of definitions as `@deprecated`. ### Custom Attributes !!! warning "Internal Feature" Custom attributes are currently an internal feature and cannot be created by end-users. You can create custom attributes by inheriting from the special `attribute` class. Custom attributes allow you to attach domain-specific metadata to your code: ```verse # Define a custom attribute @attribscope_class gameplay_element := class(attribute): Category:string Priority:int # Use the custom attribute @gameplay_element{Category := "Combat", Priority := 1} weapon_system := class: Damage:int ``` #### Attribute Scopes When defining custom attributes, you must specify where they can be applied using scope annotations: - **@attribscope_class** - Can be applied to regular classes - **@attribscope_attribclass** - Can be applied to attribute classes (classes that inherit from `attribute`) - **@attribscope_enum** - Can be applied to enums - **@attribscope_interface** - Can be applied to interfaces - **@attribscope_function** - Can be applied to functions and methods - **@attribscope_data** - Can be applied to data members Example of scoped custom attributes: ```verse # Attribute that can only be applied to functions @attribscope_function performance_critical := class(attribute): MaxExecutionTimeMs:int # Attribute that can only be applied to data members @attribscope_data serializable_field := class(attribute): SerializationKey:string # Use them appropriately entity := class: @serializable_field{SerializationKey := "entity_id"} ID:int @performance_critical{MaxExecutionTimeMs := 16} Update():void ``` Attempting to use an attribute in the wrong location produces a compiler error. For example, a function-scoped attribute cannot be applied to a class. **Reading attributes:** Custom attributes are currently metadata for external tooling — the compiler, LSP, and the Unreal Editor can read and act on them, but there is no Verse API to query attributes at runtime. Attributes are used to apply rules, constraints, or extra data that tools outside the language consume, such as serialization hints, editor annotations, or performance directives. ### Getter and Setter Accessors !!! warning "Internal Feature" Getter and setter accessors are currently an internal feature and cannot be used by end-users. While not strictly annotations, the `` and `` specifiers provide a related form of metadata for controlling field access. These can be applied to both class and interface fields to define custom access logic: ```verse entity := class: # External field with custom accessors var Health:int = external{} var InternalHealth:int = 100 GetHealth(:accessor):int = InternalHealth SetHealth(:accessor, NewValue:int):void = if (NewValue >= 0, NewValue <= 100): set InternalHealth = NewValue ``` Constraints on accessors: - Must include both `` and `` - cannot have only one - The field must have `= external{}` or no default value (with archetype initialization required) - Fields with accessors cannot be overridden in subclasses - The field must be mutable (marked with `var`) - Not all types are supported for accessor fields - Accessor fields are currently only allowed in epic_internal scopes For more details on accessor patterns, see [Fields with Accessors](10_classes_interfaces.md). ### Localization The `` specifier marks definitions as localizable messages for internationalization. Localized messages use the `message` type and can be extracted for translation into different languages: ```verse # Simple localized message WelcomeMessage : message = "Welcome to the game!" # Call Localize to get the string ShowWelcome():void = Print(Localize(WelcomeMessage)) ``` #### Message Parameters Localized messages can accept parameters for dynamic content interpolation: ```verse # Message with parameter interpolation GreetPlayer(PlayerName:string) : message = "Hello, {PlayerName}!" # Use with arguments ShowGreeting(Name:string):void = Print(Localize(GreetPlayer(Name))) # Outputs: "Hello, Aldric!" (if Name = "Aldric") ``` **Supported parameter types:** - `string` - Text values - `int` - Integer values (formatted with comma separators) - `float` - Floating-point values **Parameter interpolation syntax:** - Use `{ParameterName}` to insert parameter values - Parameters can be used multiple times or not at all - Only parameter names and Unicode code points allowed in braces ```verse # Multiple parameters, some repeated ScoreMessage(Player:string, Score:int) : message = "Congratulations {Player}! Your score is {Score}. Great job, {Player}!" # Outputs: "Congratulations Alice! Your score is 1,500. Great job, Alice!" # Not all parameters required in message text OptionalParam(Name:string, Score:int) : message = "Thanks for playing!" # Score parameter ignored ``` #### Integer Formatting Integer parameters are automatically formatted with comma separators for readability: ```verse HighScore(Points:int) : message = "New record: {Points} points!" # Localize(HighScore(190091)) produces: "New record: 190,091 points!" ``` #### Named and Default Parameters Localized messages support named parameters and default values: ```verse ConfigMessage(?MaxPlayers:int = 8, ?TimeLimit:int = 300):message = "Game settings: {MaxPlayers} players, {TimeLimit} seconds" # Can be called with any combination Localize(ConfigMessage()) # Uses defaults Localize(ConfigMessage(?MaxPlayers := 16)) # Override one Localize(ConfigMessage(?TimeLimit := 600, ?MaxPlayers := 32)) # Override both ``` #### Tuple Parameters Messages can accept tuple parameters, which are destructured in the parameter list: ```verse LocationMessage(Player:string, (X:int, Y:int)) : message = "{Player} is at position ({X}, {Y})" # Call with tuple Localize(LocationMessage("Hero", (10, 20))) # Outputs: "Hero is at position (10, 20)" ``` #### String Escaping and Unicode **Unicode code points:** ```verse UnicodeMessage : message = "The letter is {0u004d}" # Outputs: "The letter is M" ``` **Escaped braces** (to show literal braces): ```verse EscapedMessage(Name:string) : message = "Use \{Name\} to insert {Name}" # Localize(EscapedMessage("value")) produces: "Use {Name} to insert value" ``` **Special characters:** ```verse SpecialChars : message = "Supports: \\r\\n\\t\\\"\\'\\#\\<\\>\\&\\~" ``` **Whitespace and comments** are allowed in interpolation: ```verse SpacedParam(Name:string) : message = "Hello { Name }" CommentedParam(Name:string) : message = "Hello {<# comment #>Name}" ``` #### Scope Requirements Localized messages **must be defined at module or snippet scope**. They cannot be defined inside functions: ```verse # Valid: module scope MyModule := module: ModuleMessage : message = "Valid" # Valid: snippet scope TopLevelMessage : message = "Valid" BadFunction():void = LocalMessage : message = "Invalid" # ERROR ``` #### Inheritance and Override Localized messages can be overridden in class hierarchies: ```verse base_ui := class: Title:message = "Base Title" Description:message = "Base description" derived_ui := class(base_ui): # Override the title message Title:message = "Derived Title" # Inherits Description from base ``` Localized messages can also be abstract: ```verse quest_base := class: # Abstract message - must be implemented by subclasses TaskDescription : message # Concrete message with default CompletionMessage : message = "Quest complete!" fetch_quest := class(quest_base): TaskDescription : message = "Collect 10 items" ``` #### Restrictions and Errors **Must use explicit type annotation:** The type annotation `: message` is required. Implicit typing is not supported: ```verse # ERROR: Missing type annotation # BadMessage := "Text" # ERROR 3639 # Valid: Explicit type GoodMessage : message = "Text" ``` **RHS must be string literal:** ```verse # ERROR: Expression not allowed # InvalidMessage : message = "A" + "B" # ERROR 3638 # Valid: Literal only ValidMessage : message = "AB" ``` **Restricted parameter types:** Not all types are supported as parameters: ```verse # ERROR: Optional types not supported # OptionalMsg(Player:?string) : message = "{Player}" # ERROR 3509 # ERROR: Custom classes not supported my_class := class{Value:int} # ClassMsg(Obj:my_class) : message = "{Object}" # ERROR 3509 ``` **Interpolation syntax restrictions:** Only parameter names and Unicode code points are allowed inside `{}`: ```verse # ERROR: Expressions not allowed # ExprMessage(Name:string) : message = "{"Hello"}" # ERROR 3652 # Valid: Parameter names only ParamMessage(Name:string) : message = "{Name}" ``` **Non-parameter identifiers are escaped:** If you reference an identifier that isn't a parameter, it gets escaped in the output: ```verse GlobalName:string = "World" RefMessage(Greeting:string) : message = "{Greeting} to {GlobalName}" # Localize(RefMessage("Hello")) produces: "Hello to \{GlobalName\}" # Note: GlobalName is escaped because it's not a parameter ``` #### Access Specifiers Localized messages support standard access specifiers: ```verse MyModule := module: PublicMessage : message = "Public message" InternalMessage : message = "Internal message" some_class := class: PrivateMessage : message = "Private message" # private not allowed in module scopes ``` #### Best Practices **Keep messages translatable:** - Use complete sentences, not fragments that might be concatenated - Avoid gender or number assumptions that don't translate well - Provide context through parameter names **Design for different languages:** - Don't assume word order - let translators rearrange parameter positions - Allow repeated parameter use for languages that need it - Keep formatting codes (like comma separators) automated **Organization:** - Group related messages in the same module - Use descriptive names that indicate message purpose - Consider using abstract base classes for message families ```verse # Good: Clear, complete, flexible PlayerJoined(PlayerName:string, TeamName:string) : message = "{PlayerName} joined team {TeamName}" # Avoid: Fragments that might be concatenated # PlayerPrefix(Name:string) : message = "Player {Name}" # JoinedSuffix(Team:string) : message = "joined {Team}" ``` ## Evolution Access specifiers play a crucial role in code evolution. Changing access levels after publication can break compatibility: - Narrowing access (public to private) breaks external code that depends on the member - Widening access (private to public) is generally safe but creates new commitments - Changing protected members affects the inheritance contract The `` specifier on classes has special compatibility requirements—once published, it cannot be added or removed, as this would affect the safety of dynamic casts throughout the codebase. When designing for long-term evolution, consider using internal access for members that might eventually become public. This allows you to test and refine APIs within your module before committing to public exposure.