Support typed Data.define members via inline RBS comments#866
Draft
julianojulio wants to merge 1 commit into
Draft
Support typed Data.define members via inline RBS comments#866julianojulio wants to merge 1 commit into
julianojulio wants to merge 1 commit into
Conversation
julianojulio
force-pushed
the
ae-task-21-typed-data-define-members-via-inline-rbs
branch
4 times, most recently
from
March 24, 2026 14:27
2de5e81 to
6c190da
Compare
Add support for typing Data.define members using inline RBS comments
on the symbol arguments:
Point = Data.define(
:x, #: Integer
:y, #: String
)
This gives Data.define members their declared types at zero runtime
cost -- the type annotations are in comments that only the static
analyzer reads.
The implementation has three parts:
1. CommentsAssociator: detect Data.define sends and capture #:
comments on each symbol argument. Stored in a new
dataDefineMembersForNode map in CommentMap.
2. SigsRewriter: when a Data.define has typed members, synthesize a
sig + bare-super def initialize into the block body. This is
the only code the Data rewriter needs to propagate types.
3. Data.cc: port Cameron Bothner's approach from sorbet#8079
to extract types from the initialize sig and create typed reader
stubs. The bare-super check ensures types only propagate when
the initialize forwards all arguments unchanged.
Design decisions (informed by jez's review of sorbet#8079):
- Bare super required: types propagate to readers only when the
initialize body is exactly bare super. When values are transformed
(e.g., super(x: x.to_i)), readers fall back to T.untyped.
- self.[] left untyped: Sorbet can't overload positional vs keyword
args, so only new/initialize gets typed.
- Explicit initialize takes precedence: if the user provides both
inline #: types AND a def initialize with sig in the block, the
explicit initialize wins.
Based on the typed Data.define approach originally designed by
Cameron Bothner in sorbet#8079.
Co-authored-by: Cameron Bothner <cbothner@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
julianojulio
force-pushed
the
ae-task-21-typed-data-define-members-via-inline-rbs
branch
from
May 29, 2026 22:56
6c190da to
15eaa0a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Add support for typing
Data.definemembers using inline RBS comments on the symbol arguments. This solves the problem that Data.define members are alwaysT.untypedwith zero runtime cost — the type annotations live in comments that only the static analyzer reads.Based on the typed Data.define approach originally designed by @cbothner in sorbet/sorbet#8079. This PR adds the RBS inline syntax (zero runtime cost) and incorporates the conservative bare-super check from @jez's review feedback on that PR.
Addresses sorbet#10055
Design decisions (informed by #8079 review)
Following @jez's principle that "the default assumption should be 'Sorbet doesn't do anything' and only if a certain set of very precise constraints are met should Sorbet do something":
Bare super required: Typed readers are only created when the
initializebody is exactly baresuper(desugars tosend(untypedSuper, [ZSuperArgs])). When the user transforms values (e.g.,super(x: x.to_i)), readers conservatively fall back toT.untyped. This is thecanCreateTypedAccessors()check ported from Rewrite Data to create typed attribute readers based on initialize sig sorbet/sorbet#8079.self.[]left untyped: Sorbet cannot overload a method to accept both positional and keyword arguments, so onlynew/initializegets typed.Explicit initialize takes precedence: If the user provides both inline
#:types AND adef initializewith a sig in the block, the explicit initialize wins and the inline types are ignored (see "Interaction with Sorbet-style annotations" below).No partial typing surprises: Un-annotated members get
T.untypedin the synthesized sig. This enables gradually annotating members while preserving old behavior for unannotated ones.Interaction with Sorbet-style annotations
The inline
#:syntax composes cleanly with all existing annotation styles. The Data.cc type extraction operates on the shared desugared AST, so it works identically regardless of whether the sig came from inline RBS types, an RBS method sig, or a traditional Sorbetsig { }block.Three ways to type Data.define members (all produce equivalent type checking when the initialize body is bare
super):Precedence when styles are mixed: If inline
#:types are present AND the block contains an explicitdef initialize(with either a Sorbet or RBS sig), the explicit initialize takes precedence and the inline types are ignored. This avoids conflicting type information:Non-bare-super fallback: When the initialize body does anything beyond bare
super(e.g., coercing values), readers fall back toT.untypedregardless of annotation style. The sig still constrains the constructor:Approach
Prism RBS pipeline (Parse →
CommentsAssociatorPrism→SigsRewriterPrism):rbs/prism/CommentsAssociatorPrism.cc: Detects rootData.define(...)/::Data.define(...)calls and captures#:comments on each symbol argument. Stored in a newdataDefineMembersForNodemap inCommentMapPrismkeyed by non-owning Prism call-node pointers.rbs/prism/SigsRewriterPrism.cc: When aData.definehas typed members, synthesizes Prism C AST nodes equivalent tosig { params(x: Integer, y: String).void }+def initialize(x:, y:) = superinto the Data.define block body. This createspm_def_node_t,pm_parameters_node_t, required keyword parameter nodes, and a forwarding-super node. The bare-super body is critical — it's whatcanCreateTypedAccessors()in Data.cc checks to propagate types.Data rewriter (
rewriter/Data.cc):getInitialize(): Finds adef initializein the block body and its preceding sig.canCreateTypedAccessors(): Checks the initialize body is exactly baresuper.getMemberType(): Extracts per-member types from the sig'sparams()call.sig { returns(Type) }before each reader method instead of the defaultT.untyped.Test coverage
test/testdata/rbs/data_define_members.rb:String?(nilable),Integer | String(union),Array[String],Hash[Symbol, Integer],boolT.untyped),T.assert_type!validation::Data.defineT.untypedtest/testdata/rbs/data_define_members_edge_cases.rb:T.untypedFoo::Data.define— correctly not rewrittensig { }+ bare-super initialize#:types + explicit Sorbet sig → explicit wins#:types + explicit RBS sig → explicit winsLimitations
Single-line syntax: The inline
#:syntax requires spreadingData.defineacross multiple lines — each member must be on its own line to have a trailing#:comment. Single-lineData.define(:x, :y)cannot have per-member inline comments (Ruby comment syntax extends to end of line). This is inherent to using trailing comments and not something we can change.Multiline types: Multiline RBS continuation (
#|) is not supported for inline member type annotations. Each member's type must fit on a single line. For complex types that don't fit, users can fall back to the explicit initialize pattern with a full method signature. This limitation is acceptable because Data.define member types are typically simple (class names, nilable wrappers, unions).self.[]untyped: Theself.[]constructor method remains untyped even when members are typed. Sorbet cannot overload a method to accept both positional and keyword arguments, so only the keyword-basednew/initializepath gets type constraints. This matches the behavior proposed in sorbet#8079.Co-authored-by: Cameron Bothner cbothner@users.noreply.github.com
Co-authored-by: Claude noreply@anthropic.com