From aa7e6e0862954ee89fc85a2dcefe4b3d233f738f Mon Sep 17 00:00:00 2001 From: Alex Rocha Date: Mon, 11 Aug 2025 15:18:36 -0700 Subject: [PATCH 01/34] Add ruby-rbs crate This commit introduces the `ruby-rbs` crate, which will provide a safe, high-level Rust API for the RBS C library. It follows the common Rust pattern of separating the safe wrapper from the `*-sys` crate that provides the raw FFI bindings. The `ruby-rbs` crate will depend on `ruby-rbs-sys` for the unsafe C bindings and will expose a safe, idiomatic Rust interface. This commit sets up the foundation for that structure. The initial implementation includes: - The basic crate structure with its own Cargo.toml, declaring a dependency on `ruby-rbs-sys`. - A build script (`build.rs`) that will be responsible for generating safe Rust wrappers from the C API. Currently, it only generates an empty `bindings.rs` file. - The `ruby-rbs` crate is added to the main workspace `Cargo.toml`. While the interaction is not yet implemented, this setup paves the way for providing a robust Rust interface for RBS, which will improve safety and developer experience. --- rust/Cargo.lock | 7 +++++++ rust/Cargo.toml | 1 + rust/ruby-rbs/Cargo.toml | 7 +++++++ rust/ruby-rbs/build.rs | 25 +++++++++++++++++++++++++ rust/ruby-rbs/src/lib.rs | 1 + 5 files changed, 41 insertions(+) create mode 100644 rust/ruby-rbs/Cargo.toml create mode 100644 rust/ruby-rbs/build.rs create mode 100644 rust/ruby-rbs/src/lib.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 906fc5859..3d8c898d1 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -194,6 +194,13 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "ruby-rbs" +version = "0.1.0" +dependencies = [ + "ruby-rbs-sys", +] + [[package]] name = "ruby-rbs-sys" version = "0.1.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 36e83a904..60895567e 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "ruby-rbs", "ruby-rbs-sys", ] diff --git a/rust/ruby-rbs/Cargo.toml b/rust/ruby-rbs/Cargo.toml new file mode 100644 index 000000000..4e7c25d77 --- /dev/null +++ b/rust/ruby-rbs/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "ruby-rbs" +version = "0.1.0" +edition = "2024" + +[dependencies] +ruby-rbs-sys = { path = "../ruby-rbs-sys" } diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs new file mode 100644 index 000000000..898275ed9 --- /dev/null +++ b/rust/ruby-rbs/build.rs @@ -0,0 +1,25 @@ +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::Path; + +fn main() { + println!("cargo:warning=Build script is running!"); + + if let Err(err) = generate() { + panic!("build.rs failed: {err}"); + } +} + +fn generate() -> Result<(), Box> { + let out_dir = env::var("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir).join("bindings.rs"); + + let mut file = File::create(&dest_path)?; + + writeln!(file, "// Generated by build.rs")?; + writeln!(file, "// Do not edit this file directly")?; + writeln!(file, "")?; + + Ok(()) +} diff --git a/rust/ruby-rbs/src/lib.rs b/rust/ruby-rbs/src/lib.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/rust/ruby-rbs/src/lib.rs @@ -0,0 +1 @@ + From 1d812c203d63229fe0c49c15bbc5d4ba6468933a Mon Sep 17 00:00:00 2001 From: Alex Rocha Date: Fri, 22 Aug 2025 09:52:32 -0400 Subject: [PATCH 02/34] Implement AST struct generation from config.yml The build script now reads the config.yml file and generates corresponding Rust struct definitions for all RBS AST nodes. Implementation details: - Parse config.yml using serde to extract node definitions - Generate proper Rust module hierarchy from :: namespace separators - Apply Rust naming conventions: - Modules use snake_case - Structs remain PascalCase - Handle Rust reserved keywords (Use -> UseDirective, Self -> SelfType) - Smart PascalCase to snake_case conversion that correctly handles acronyms (e.g., 'AST' -> 'ast', not 'a_s_t') The generated bindings create empty struct definitions organized in the correct module hierarchy, laying the foundation for the safe Rust API that will wrap the ruby-rbs-sys FFI bindings. --- rust/Cargo.lock | 75 ++++++++++++++++++++++ rust/ruby-rbs/Cargo.toml | 4 ++ rust/ruby-rbs/build.rs | 133 +++++++++++++++++++++++++++++++++++---- rust/ruby-rbs/src/lib.rs | 2 +- 4 files changed, 200 insertions(+), 14 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 3d8c898d1..747375d18 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -78,12 +78,34 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "glob" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" + +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "itertools" version = "0.13.0" @@ -93,6 +115,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "libc" version = "0.2.174" @@ -199,6 +227,8 @@ name = "ruby-rbs" version = "0.1.0" dependencies = [ "ruby-rbs-sys", + "serde", + "serde_yaml", ] [[package]] @@ -215,6 +245,45 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "shlex" version = "1.3.0" @@ -238,6 +307,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "windows-targets" version = "0.53.2" diff --git a/rust/ruby-rbs/Cargo.toml b/rust/ruby-rbs/Cargo.toml index 4e7c25d77..9c4731d41 100644 --- a/rust/ruby-rbs/Cargo.toml +++ b/rust/ruby-rbs/Cargo.toml @@ -5,3 +5,7 @@ edition = "2024" [dependencies] ruby-rbs-sys = { path = "../ruby-rbs-sys" } + +[build-dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_yaml = "0.9" diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index 898275ed9..d064a689d 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -1,25 +1,132 @@ -use std::env; -use std::fs::File; -use std::io::Write; -use std::path::Path; +use serde::Deserialize; +use std::{env, error::Error, fs::File, io::Write, path::Path}; -fn main() { - println!("cargo:warning=Build script is running!"); +#[derive(Debug, Deserialize)] +struct Config { + nodes: Vec, +} + +#[derive(Debug, Deserialize)] +struct Node { + name: String, +} + +fn main() -> Result<(), Box> { + let config_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../config.yml") + .canonicalize()?; + + println!("cargo:rerun-if-changed={}", config_path.display()); + + let config_file = File::open(&config_path)?; + let mut config: Config = serde_yaml::from_reader(config_file)?; + + config.nodes.sort_by(|a, b| a.name.cmp(&b.name)); + generate(&config)?; + + Ok(()) +} + +fn replace_reserved_keyword(name: &str) -> &str { + match name { + "Use" => "UseDirective", + "Self" => "SelfType", + _ => name, + } +} + +fn safe_module_name(name: &str) -> String { + let name = replace_reserved_keyword(name); - if let Err(err) = generate() { - panic!("build.rs failed: {err}"); + let chars: Vec = name.chars().collect(); + let mut result = String::new(); + + for (i, &ch) in chars.iter().enumerate() { + // Insert underscore before uppercase if: + // - Not at the start + // - Previous char was lowercase OR + // - Previous was uppercase but next is lowercase + // e.g., "RBSTypes" -> "RBS_Types" -> "rbs_types" + if i > 0 && ch.is_uppercase() { + let prev_was_lower = chars[i - 1].is_lowercase(); + let next_is_lower = chars.get(i + 1).is_some_and(|c| c.is_lowercase()); + + if prev_was_lower || (chars[i - 1].is_uppercase() && next_is_lower) { + result.push('_'); + } + } + result.push(ch); } + + result.to_lowercase() } -fn generate() -> Result<(), Box> { - let out_dir = env::var("OUT_DIR").unwrap(); +fn generate(config: &Config) -> Result<(), Box> { + let out_dir = env::var("OUT_DIR")?; let dest_path = Path::new(&out_dir).join("bindings.rs"); let mut file = File::create(&dest_path)?; - writeln!(file, "// Generated by build.rs")?; - writeln!(file, "// Do not edit this file directly")?; - writeln!(file, "")?; + writeln!(file, "// Generated by build.rs from config.yml")?; + writeln!(file, "// Nodes to generate: {}", config.nodes.len())?; + writeln!(file)?; + + let mut current_path: Vec = Vec::new(); + let mut first_in_module = true; + + for node in &config.nodes { + // Parse node path (skip "RBS" prefix) + let parts: Vec<_> = node.name.split("::").skip(1).collect(); + let (modules, struct_name) = parts.split_at(parts.len() - 1); + + // Transform module and struct names + let modules: Vec = modules.iter().map(|s| safe_module_name(s)).collect(); + let struct_name = { + let name = struct_name[0]; + replace_reserved_keyword(name).to_string() + }; + + // Find where paths diverge + let common_len = current_path + .iter() + .zip(&modules) + .take_while(|(a, b)| a == b) + .count(); + + // Close old modules + for depth in (common_len..current_path.len()).rev() { + writeln!(file, "{}}}", " ".repeat(depth))?; + first_in_module = false; + } + + // Open new modules + for (depth, module) in modules.iter().enumerate().skip(common_len) { + if !first_in_module { + writeln!(file)?; + } + writeln!(file, "{}pub mod {} {{", " ".repeat(depth), module)?; + first_in_module = true; + } + + // Write struct (with spacing if not first in module) + if !first_in_module { + writeln!(file)?; + } + writeln!( + file, + "{}pub struct {} {{}}", + " ".repeat(modules.len()), + struct_name + )?; + first_in_module = false; + + current_path = modules; + } + + // Close remaining modules + for depth in (0..current_path.len()).rev() { + writeln!(file, "{}}}", " ".repeat(depth))?; + } Ok(()) } diff --git a/rust/ruby-rbs/src/lib.rs b/rust/ruby-rbs/src/lib.rs index 8b1378917..44ae78fca 100644 --- a/rust/ruby-rbs/src/lib.rs +++ b/rust/ruby-rbs/src/lib.rs @@ -1 +1 @@ - +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); From a9500746e0c317745630030ffd49968f6bd12a77 Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Wed, 22 Oct 2025 18:25:16 +0100 Subject: [PATCH 03/34] Simplify Rust node generation with explicit naming (#51) ## Add explicit Rust names for AST nodes and simplify code generation Instead of auto-generating nested module paths from RBS nested naming conventions, use explicit `rust_name` fields in `config.yml` and generate flat structs. - Add `rust_name` field to all node definitions in `config.yml` - Remove complex module/path parsing logic from build.rs - Generate flat structs (e.g., `ClassNode`) instead of nested modules - Add `Node` enum to wrap all node types This makes the generated Rust code easier to work with. --- config.yml | 68 +++++++++++++++++++++++++++++ rust/ruby-rbs/build.rs | 99 ++++++------------------------------------ 2 files changed, 81 insertions(+), 86 deletions(-) diff --git a/config.yml b/config.yml index 9679a62e7..94f58bea4 100644 --- a/config.yml +++ b/config.yml @@ -1,19 +1,23 @@ nodes: - name: RBS::AST::Annotation + rust_name: AnnotationNode fields: - name: string c_type: rbs_string - name: RBS::AST::Bool + rust_name: BoolNode expose_to_ruby: false expose_location: false fields: - name: value c_type: bool - name: RBS::AST::Comment + rust_name: CommentNode fields: - name: string c_type: rbs_string - name: RBS::AST::Declarations::Class + rust_name: ClassNode fields: - name: name c_type: rbs_type_name @@ -28,12 +32,14 @@ nodes: - name: comment c_type: rbs_ast_comment - name: RBS::AST::Declarations::Class::Super + rust_name: ClassSuperNode fields: - name: name c_type: rbs_type_name - name: args c_type: rbs_node_list - name: RBS::AST::Declarations::ClassAlias + rust_name: ClassAliasNode fields: - name: new_name c_type: rbs_type_name @@ -44,6 +50,7 @@ nodes: - name: annotations c_type: rbs_node_list - name: RBS::AST::Declarations::Constant + rust_name: ConstantNode fields: - name: name c_type: rbs_type_name @@ -54,6 +61,7 @@ nodes: - name: annotations c_type: rbs_node_list - name: RBS::AST::Declarations::Global + rust_name: GlobalNode fields: - name: name c_type: rbs_ast_symbol @@ -64,6 +72,7 @@ nodes: - name: annotations c_type: rbs_node_list - name: RBS::AST::Declarations::Interface + rust_name: InterfaceNode fields: - name: name c_type: rbs_type_name @@ -76,6 +85,7 @@ nodes: - name: comment c_type: rbs_ast_comment - name: RBS::AST::Declarations::Module + rust_name: ModuleNode fields: - name: name c_type: rbs_type_name @@ -90,12 +100,14 @@ nodes: - name: comment c_type: rbs_ast_comment - name: RBS::AST::Declarations::Module::Self + rust_name: ModuleSelfNode fields: - name: name c_type: rbs_type_name - name: args c_type: rbs_node_list - name: RBS::AST::Declarations::ModuleAlias + rust_name: ModuleAliasNode fields: - name: new_name c_type: rbs_type_name @@ -106,6 +118,7 @@ nodes: - name: annotations c_type: rbs_node_list - name: RBS::AST::Declarations::TypeAlias + rust_name: TypeAliasNode fields: - name: name c_type: rbs_type_name @@ -118,21 +131,25 @@ nodes: - name: comment c_type: rbs_ast_comment - name: RBS::AST::Directives::Use + rust_name: UseNode fields: - name: clauses c_type: rbs_node_list - name: RBS::AST::Directives::Use::SingleClause + rust_name: UseSingleClauseNode fields: - name: type_name c_type: rbs_type_name - name: new_name c_type: rbs_ast_symbol - name: RBS::AST::Directives::Use::WildcardClause + rust_name: UseWildcardClauseNode fields: - name: namespace c_type: rbs_namespace c_name: rbs_namespace - name: RBS::AST::Members::Alias + rust_name: AliasNode fields: - name: new_name c_type: rbs_ast_symbol @@ -145,6 +162,7 @@ nodes: - name: comment c_type: rbs_ast_comment - name: RBS::AST::Members::AttrAccessor + rust_name: AttrAccessorNode fields: - name: name c_type: rbs_ast_symbol @@ -161,6 +179,7 @@ nodes: - name: visibility c_type: rbs_keyword - name: RBS::AST::Members::AttrReader + rust_name: AttrReaderNode fields: - name: name c_type: rbs_ast_symbol @@ -177,6 +196,7 @@ nodes: - name: visibility c_type: rbs_keyword - name: RBS::AST::Members::AttrWriter + rust_name: AttrWriterNode fields: - name: name c_type: rbs_ast_symbol @@ -193,6 +213,7 @@ nodes: - name: visibility c_type: rbs_keyword - name: RBS::AST::Members::ClassInstanceVariable + rust_name: ClassInstanceVariableNode fields: - name: name c_type: rbs_ast_symbol @@ -201,6 +222,7 @@ nodes: - name: comment c_type: rbs_ast_comment - name: RBS::AST::Members::ClassVariable + rust_name: ClassVariableNode fields: - name: name c_type: rbs_ast_symbol @@ -209,6 +231,7 @@ nodes: - name: comment c_type: rbs_ast_comment - name: RBS::AST::Members::Extend + rust_name: ExtendNode fields: - name: name c_type: rbs_type_name @@ -219,6 +242,7 @@ nodes: - name: comment c_type: rbs_ast_comment - name: RBS::AST::Members::Include + rust_name: IncludeNode fields: - name: name c_type: rbs_type_name @@ -229,6 +253,7 @@ nodes: - name: comment c_type: rbs_ast_comment - name: RBS::AST::Members::InstanceVariable + rust_name: InstanceVariableNode fields: - name: name c_type: rbs_ast_symbol @@ -237,6 +262,7 @@ nodes: - name: comment c_type: rbs_ast_comment - name: RBS::AST::Members::MethodDefinition + rust_name: MethodDefinitionNode fields: - name: name c_type: rbs_ast_symbol @@ -253,6 +279,7 @@ nodes: - name: visibility c_type: rbs_keyword - name: RBS::AST::Members::MethodDefinition::Overload + rust_name: MethodDefinitionOverloadNode expose_location: false fields: - name: annotations @@ -260,6 +287,7 @@ nodes: - name: method_type c_type: rbs_node - name: RBS::AST::Members::Prepend + rust_name: PrependNode fields: - name: name c_type: rbs_type_name @@ -270,8 +298,11 @@ nodes: - name: comment c_type: rbs_ast_comment - name: RBS::AST::Members::Private + rust_name: PrivateNode - name: RBS::AST::Members::Public + rust_name: PublicNode - name: RBS::AST::TypeParam + rust_name: TypeParamNode fields: - name: name c_type: rbs_ast_symbol @@ -286,18 +317,21 @@ nodes: - name: unchecked c_type: bool - name: RBS::AST::Integer + rust_name: IntegerNode expose_to_ruby: false expose_location: false fields: - name: string_representation c_type: rbs_string - name: RBS::AST::String + rust_name: StringNode expose_to_ruby: false expose_location: false fields: - name: string c_type: rbs_string - name: RBS::MethodType + rust_name: MethodTypeNode fields: - name: type_params c_type: rbs_node_list @@ -306,6 +340,7 @@ nodes: - name: block c_type: rbs_types_block - name: RBS::Namespace + rust_name: NamespaceNode expose_location: false fields: - name: path @@ -313,6 +348,7 @@ nodes: - name: absolute c_type: bool - name: RBS::Signature + rust_name: SignatureNode expose_to_ruby: false expose_location: false fields: @@ -321,6 +357,7 @@ nodes: - name: declarations c_type: rbs_node_list - name: RBS::TypeName + rust_name: TypeNameNode expose_location: false fields: - name: namespace @@ -329,24 +366,35 @@ nodes: - name: name c_type: rbs_ast_symbol - name: RBS::Types::Alias + rust_name: AliasTypeNode fields: - name: name c_type: rbs_type_name - name: args c_type: rbs_node_list - name: RBS::Types::Bases::Any + rust_name: AnyTypeNode fields: - name: todo c_type: bool - name: RBS::Types::Bases::Bool + rust_name: BoolTypeNode - name: RBS::Types::Bases::Bottom + rust_name: BottomTypeNode - name: RBS::Types::Bases::Class + rust_name: ClassTypeNode - name: RBS::Types::Bases::Instance + rust_name: InstanceTypeNode - name: RBS::Types::Bases::Nil + rust_name: NilTypeNode - name: RBS::Types::Bases::Self + rust_name: SelfTypeNode - name: RBS::Types::Bases::Top + rust_name: TopTypeNode - name: RBS::Types::Bases::Void + rust_name: VoidTypeNode - name: RBS::Types::Block + rust_name: BlockTypeNode expose_location: true fields: - name: type @@ -356,16 +404,19 @@ nodes: - name: self_type c_type: rbs_node - name: RBS::Types::ClassInstance + rust_name: ClassInstanceTypeNode fields: - name: name c_type: rbs_type_name - name: args c_type: rbs_node_list - name: RBS::Types::ClassSingleton + rust_name: ClassSingletonTypeNode fields: - name: name c_type: rbs_type_name - name: RBS::Types::Function + rust_name: FunctionTypeNode expose_location: false fields: - name: required_positionals @@ -385,30 +436,36 @@ nodes: - name: return_type c_type: rbs_node - name: RBS::Types::Function::Param + rust_name: FunctionParamNode fields: - name: type c_type: rbs_node - name: name c_type: rbs_ast_symbol - name: RBS::Types::Interface + rust_name: InterfaceTypeNode fields: - name: name c_type: rbs_type_name - name: args c_type: rbs_node_list - name: RBS::Types::Intersection + rust_name: IntersectionTypeNode fields: - name: types c_type: rbs_node_list - name: RBS::Types::Literal + rust_name: LiteralTypeNode fields: - name: literal c_type: rbs_node - name: RBS::Types::Optional + rust_name: OptionalTypeNode fields: - name: type c_type: rbs_node - name: RBS::Types::Proc + rust_name: ProcTypeNode fields: - name: type c_type: rbs_node @@ -417,10 +474,12 @@ nodes: - name: self_type c_type: rbs_node - name: RBS::Types::Record + rust_name: RecordTypeNode fields: - name: all_fields c_type: rbs_hash - name: RBS::Types::Record::FieldType + rust_name: RecordFieldTypeNode expose_to_ruby: false expose_location: false fields: @@ -429,29 +488,35 @@ nodes: - name: required c_type: bool - name: RBS::Types::Tuple + rust_name: TupleTypeNode fields: - name: types c_type: rbs_node_list - name: RBS::Types::Union + rust_name: UnionTypeNode fields: - name: types c_type: rbs_node_list - name: RBS::Types::UntypedFunction + rust_name: UntypedFunctionTypeNode expose_location: false fields: - name: return_type c_type: rbs_node - name: RBS::Types::Variable + rust_name: VariableTypeNode fields: - name: name c_type: rbs_ast_symbol - name: RBS::AST::Ruby::Annotations::NodeTypeAssertion + rust_name: NodeTypeAssertionNode fields: - name: prefix_location c_type: rbs_location - name: type c_type: rbs_node - name: RBS::AST::Ruby::Annotations::ColonMethodTypeAnnotation + rust_name: ColonMethodTypeAnnotationNode fields: - name: prefix_location c_type: rbs_location @@ -460,6 +525,7 @@ nodes: - name: method_type c_type: rbs_node - name: RBS::AST::Ruby::Annotations::MethodTypesAnnotation + rust_name: MethodTypesAnnotationNode fields: - name: prefix_location c_type: rbs_location @@ -468,6 +534,7 @@ nodes: - name: vertical_bar_locations c_type: rbs_location_list - name: RBS::AST::Ruby::Annotations::SkipAnnotation + rust_name: SkipAnnotationNode fields: - name: prefix_location c_type: rbs_location @@ -476,6 +543,7 @@ nodes: - name: comment_location c_type: rbs_location - name: RBS::AST::Ruby::Annotations::ReturnTypeAnnotation + rust_name: ReturnTypeAnnotationNode fields: - name: prefix_location c_type: rbs_location diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index d064a689d..299145f81 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -9,6 +9,7 @@ struct Config { #[derive(Debug, Deserialize)] struct Node { name: String, + rust_name: String, } fn main() -> Result<(), Box> { @@ -27,40 +28,6 @@ fn main() -> Result<(), Box> { Ok(()) } -fn replace_reserved_keyword(name: &str) -> &str { - match name { - "Use" => "UseDirective", - "Self" => "SelfType", - _ => name, - } -} - -fn safe_module_name(name: &str) -> String { - let name = replace_reserved_keyword(name); - - let chars: Vec = name.chars().collect(); - let mut result = String::new(); - - for (i, &ch) in chars.iter().enumerate() { - // Insert underscore before uppercase if: - // - Not at the start - // - Previous char was lowercase OR - // - Previous was uppercase but next is lowercase - // e.g., "RBSTypes" -> "RBS_Types" -> "rbs_types" - if i > 0 && ch.is_uppercase() { - let prev_was_lower = chars[i - 1].is_lowercase(); - let next_is_lower = chars.get(i + 1).is_some_and(|c| c.is_lowercase()); - - if prev_was_lower || (chars[i - 1].is_uppercase() && next_is_lower) { - result.push('_'); - } - } - result.push(ch); - } - - result.to_lowercase() -} - fn generate(config: &Config) -> Result<(), Box> { let out_dir = env::var("OUT_DIR")?; let dest_path = Path::new(&out_dir).join("bindings.rs"); @@ -71,62 +38,22 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!(file, "// Nodes to generate: {}", config.nodes.len())?; writeln!(file)?; - let mut current_path: Vec = Vec::new(); - let mut first_in_module = true; - + // TODO: Go through all of the nodes and generate the structs to back them up for node in &config.nodes { - // Parse node path (skip "RBS" prefix) - let parts: Vec<_> = node.name.split("::").skip(1).collect(); - let (modules, struct_name) = parts.split_at(parts.len() - 1); - - // Transform module and struct names - let modules: Vec = modules.iter().map(|s| safe_module_name(s)).collect(); - let struct_name = { - let name = struct_name[0]; - replace_reserved_keyword(name).to_string() - }; - - // Find where paths diverge - let common_len = current_path - .iter() - .zip(&modules) - .take_while(|(a, b)| a == b) - .count(); - - // Close old modules - for depth in (common_len..current_path.len()).rev() { - writeln!(file, "{}}}", " ".repeat(depth))?; - first_in_module = false; - } - - // Open new modules - for (depth, module) in modules.iter().enumerate().skip(common_len) { - if !first_in_module { - writeln!(file)?; - } - writeln!(file, "{}pub mod {} {{", " ".repeat(depth), module)?; - first_in_module = true; - } - - // Write struct (with spacing if not first in module) - if !first_in_module { - writeln!(file)?; - } - writeln!( - file, - "{}pub struct {} {{}}", - " ".repeat(modules.len()), - struct_name - )?; - first_in_module = false; - - current_path = modules; + writeln!(file, "pub struct {} {{}}\n", node.rust_name)?; } - // Close remaining modules - for depth in (0..current_path.len()).rev() { - writeln!(file, "{}}}", " ".repeat(depth))?; + // Generate the Node enum to wrap all of the structs + writeln!(file, "pub enum Node {{")?; + for node in &config.nodes { + let variant_name = node + .rust_name + .strip_suffix("Node") + .unwrap_or(&node.rust_name); + + writeln!(file, " {}({}),", variant_name, node.rust_name)?; } + writeln!(file, "}}")?; Ok(()) } From 4c984cd09fb31dc4e0cf8650f6d8d1d0ed31277c Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Fri, 24 Oct 2025 12:23:54 +0100 Subject: [PATCH 04/34] Handle RBSString types (#52) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Handle rbs_string field types when generating Rust structs from config.yml. The RBSString struct wraps rbs_string_t pointers and provides an as_bytes() method that safely calculates string length using pointer arithmetic. --- rust/ruby-rbs/build.rs | 33 ++++++++++++++++++++++++++++++++- rust/ruby-rbs/src/lib.rs | 18 ++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index 299145f81..7f39c29e1 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -6,10 +6,17 @@ struct Config { nodes: Vec, } +#[derive(Debug, Deserialize)] +struct NodeField { + name: String, + c_type: String, +} + #[derive(Debug, Deserialize)] struct Node { name: String, rust_name: String, + fields: Option>, } fn main() -> Result<(), Box> { @@ -40,7 +47,31 @@ fn generate(config: &Config) -> Result<(), Box> { // TODO: Go through all of the nodes and generate the structs to back them up for node in &config.nodes { - writeln!(file, "pub struct {} {{}}\n", node.rust_name)?; + writeln!(file, "pub struct {} {{", node.rust_name)?; + if let Some(fields) = &node.fields { + for field in fields { + match field.c_type.as_str() { + "rbs_string" => writeln!(file, " {}: *const rbs_string_t,", field.name)?, + _ => eprintln!("Unknown field type: {}", field.c_type), + } + } + } + writeln!(file, "}}\n")?; + + writeln!(file, "impl {} {{", node.rust_name)?; + if let Some(fields) = &node.fields { + for field in fields { + match field.c_type.as_str() { + "rbs_string" => { + writeln!(file, " pub fn {}(&self) -> RBSString {{", field.name)?; + writeln!(file, " RBSString::new(self.{})", field.name)?; + writeln!(file, " }}")?; + } + _ => eprintln!("Unknown field type: {}", field.c_type), + } + } + } + writeln!(file, "}}\n")?; } // Generate the Node enum to wrap all of the structs diff --git a/rust/ruby-rbs/src/lib.rs b/rust/ruby-rbs/src/lib.rs index 44ae78fca..909a616f2 100644 --- a/rust/ruby-rbs/src/lib.rs +++ b/rust/ruby-rbs/src/lib.rs @@ -1 +1,19 @@ include!(concat!(env!("OUT_DIR"), "/bindings.rs")); +use ruby_rbs_sys::bindings::*; + +pub struct RBSString { + pointer: *const rbs_string_t, +} + +impl RBSString { + pub fn new(pointer: *const rbs_string_t) -> Self { + Self { pointer } + } + + pub fn as_bytes(&self) -> &[u8] { + unsafe { + let s = *self.pointer; + std::slice::from_raw_parts(s.start as *const u8, s.end.offset_from(s.start) as usize) + } + } +} From 4b3b1a32b8f8e8f2dd27166a3dcd9ba229653742 Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Tue, 28 Oct 2025 15:15:09 +0000 Subject: [PATCH 05/34] Add parse function to Rust RBS bindings (#53) The `parse` function enables parsing RBS code from Rust. This provides a safe Rust interface to the C parser, handling memory management and encoding setup. --- rust/ruby-rbs/src/lib.rs | 55 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/rust/ruby-rbs/src/lib.rs b/rust/ruby-rbs/src/lib.rs index 909a616f2..429796483 100644 --- a/rust/ruby-rbs/src/lib.rs +++ b/rust/ruby-rbs/src/lib.rs @@ -1,5 +1,44 @@ include!(concat!(env!("OUT_DIR"), "/bindings.rs")); +use rbs_encoding_type_t::RBS_ENCODING_UTF_8; use ruby_rbs_sys::bindings::*; +use std::sync::Once; + +static INIT: Once = Once::new(); + +/// Parse RBS code into an AST. +/// +/// ```rust +/// use ruby_rbs::parse; +/// let rbs_code = r#"type foo = "hello""#; +/// let signature = parse(rbs_code.as_bytes()); +/// assert!(signature.is_ok(), "Failed to parse RBS signature"); +/// ``` +pub fn parse(rbs_code: &[u8]) -> Result<*mut rbs_signature_t, String> { + unsafe { + INIT.call_once(|| { + rbs_constant_pool_init(RBS_GLOBAL_CONSTANT_POOL, 26); + }); + + let start_ptr = rbs_code.as_ptr() as *const i8; + let end_ptr = start_ptr.add(rbs_code.len()); + + let raw_rbs_string_value = rbs_string_new(start_ptr, end_ptr); + + let encoding_ptr = &rbs_encodings[RBS_ENCODING_UTF_8 as usize] as *const rbs_encoding_t; + let parser = rbs_parser_new(raw_rbs_string_value, encoding_ptr, 0, rbs_code.len() as i32); + + let mut signature: *mut rbs_signature_t = std::ptr::null_mut(); + let result = rbs_parse_signature(parser, &mut signature); + + rbs_parser_free(parser); + + if result { + Ok(signature) + } else { + Err(String::from("Failed to parse RBS signature")) + } + } +} pub struct RBSString { pointer: *const rbs_string_t, @@ -17,3 +56,19 @@ impl RBSString { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse() { + let rbs_code = r#"type foo = "hello""#; + let signature = parse(rbs_code.as_bytes()); + assert!(signature.is_ok(), "Failed to parse RBS signature"); + + let rbs_code2 = r#"class Foo end"#; + let signature2 = parse(rbs_code2.as_bytes()); + assert!(signature2.is_ok(), "Failed to parse RBS signature"); + } +} From 92e3554ab0a29eeb9637f37e1c71af310bb9c37e Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Thu, 30 Oct 2025 09:33:41 +0000 Subject: [PATCH 06/34] Handle bool primitive types (#54) Since `bool` is a primitive type with direct FFI mapping between C and Rust, we don't need a wrapper struct like we do for complex types (`rbs_string_t`, etc.). --- rust/ruby-rbs/build.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index 7f39c29e1..64908b998 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -52,6 +52,7 @@ fn generate(config: &Config) -> Result<(), Box> { for field in fields { match field.c_type.as_str() { "rbs_string" => writeln!(file, " {}: *const rbs_string_t,", field.name)?, + "bool" => writeln!(file, " {}: bool,", field.name)?, _ => eprintln!("Unknown field type: {}", field.c_type), } } @@ -67,6 +68,11 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!(file, " RBSString::new(self.{})", field.name)?; writeln!(file, " }}")?; } + "bool" => { + writeln!(file, " pub fn {}(&self) -> bool {{", field.name)?; + writeln!(file, " self.{}", field.name)?; + writeln!(file, " }}")?; + } _ => eprintln!("Unknown field type: {}", field.c_type), } } From 8aad725b6a99fa41ae1d672498ae091c78a3b5dc Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Tue, 18 Nov 2025 17:40:14 +0000 Subject: [PATCH 07/34] Handle RBSSymbol types (#56) Symbol fields in RBS AST nodes store their values as constant IDs that need to be resolved through the parser's constant pool. This safe Rust wrapper (`RBSSymbol`) maintains a reference to the parser and provides access to the symbol's name bytes, similar to how `RBSString` handles string types. The build script now generates accessors for `rbs_ast_symbol` fields that properly pass both the symbol pointer and parser reference to enable constant pool lookups. --- rust/ruby-rbs/build.rs | 14 ++++++++++++++ rust/ruby-rbs/src/lib.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index 64908b998..2e470c7c6 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -47,12 +47,17 @@ fn generate(config: &Config) -> Result<(), Box> { // TODO: Go through all of the nodes and generate the structs to back them up for node in &config.nodes { + writeln!(file, "#[allow(dead_code)]")?; // TODO: Remove this once all nodes that need parser are implemented writeln!(file, "pub struct {} {{", node.rust_name)?; + writeln!(file, " parser: *mut rbs_parser_t,")?; if let Some(fields) = &node.fields { for field in fields { match field.c_type.as_str() { "rbs_string" => writeln!(file, " {}: *const rbs_string_t,", field.name)?, "bool" => writeln!(file, " {}: bool,", field.name)?, + "rbs_ast_symbol" => { + writeln!(file, " {}: *const rbs_ast_symbol_t,", field.name)? + } _ => eprintln!("Unknown field type: {}", field.c_type), } } @@ -73,6 +78,15 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!(file, " self.{}", field.name)?; writeln!(file, " }}")?; } + "rbs_ast_symbol" => { + writeln!(file, " pub fn {}(&self) -> RBSSymbol {{", field.name)?; + writeln!( + file, + " RBSSymbol::new(self.{}, self.parser)", + field.name + )?; + writeln!(file, " }}")?; + } _ => eprintln!("Unknown field type: {}", field.c_type), } } diff --git a/rust/ruby-rbs/src/lib.rs b/rust/ruby-rbs/src/lib.rs index 429796483..d90496129 100644 --- a/rust/ruby-rbs/src/lib.rs +++ b/rust/ruby-rbs/src/lib.rs @@ -57,6 +57,32 @@ impl RBSString { } } +pub struct RBSSymbol { + pointer: *const rbs_ast_symbol_t, + parser: *mut rbs_parser_t, +} + +impl RBSSymbol { + pub fn new(pointer: *const rbs_ast_symbol_t, parser: *mut rbs_parser_t) -> Self { + Self { pointer, parser } + } + + pub fn name(&self) -> &[u8] { + unsafe { + let constant_ptr = rbs_constant_pool_id_to_constant( + &(*self.parser).constant_pool, + (*self.pointer).constant_id, + ); + if constant_ptr.is_null() { + panic!("Constant ID for symbol is not present in the pool"); + } + + let constant = &*constant_ptr; + std::slice::from_raw_parts(constant.start, constant.length) + } + } +} + #[cfg(test)] mod tests { use super::*; From e25e2723ff67d3573bd0ab11d54fd38bf8a2ab85 Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Tue, 18 Nov 2025 18:24:35 +0000 Subject: [PATCH 08/34] Add Node and NodeList types to Rust RBS bindings (#57) Refactor node structs to use pointer-based access and add NodeList iterator Changes node generation from storing individual fields to holding a single pointer to the C struct. This avoids duplicating data in Rust structs and matches the pattern used in Prism's bindings. We just maintain a thin wrapper around the C pointer and dereference it in accessor methods. Adds NodeList/NodeListIter to enable idiomatic Rust iteration over RBS's linked list structures, and implements Node::new() factory method that type-checks the C node pointer and constructs the appropriate Rust variant with proper pointer casting. Also adds convert_name() helper to generate C identifiers from RBS node names (snake_case_t for types, UPPER_CASE for enum constants). --- rust/ruby-rbs/build.rs | 100 +++++++++++++++++++++++++++++++++------ rust/ruby-rbs/src/lib.rs | 36 ++++++++++++++ 2 files changed, 121 insertions(+), 15 deletions(-) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index 2e470c7c6..3e2ab08da 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -35,6 +35,47 @@ fn main() -> Result<(), Box> { Ok(()) } +enum CIdentifier { + Type, // foo_bar_t + Constant, // FOO_BAR +} + +fn convert_name(name: &str, identifier: CIdentifier) -> String { + let type_name = name.replace("::", "_"); + let lowercase = matches!(identifier, CIdentifier::Type); + let mut out = String::new(); + let mut prev_is_lower = false; + + for ch in type_name.chars() { + if ch.is_ascii_uppercase() { + if prev_is_lower { + out.push('_'); + } + out.push(if lowercase { + ch.to_ascii_lowercase() + } else { + ch + }); + prev_is_lower = false; + } else if ch == '_' { + out.push(ch); + prev_is_lower = false; + } else { + out.push(if lowercase { + ch + } else { + ch.to_ascii_uppercase() + }); + prev_is_lower = ch.is_ascii_lowercase() || ch.is_ascii_digit(); + } + } + + if lowercase { + out.push_str("_t"); + } + out +} + fn generate(config: &Config) -> Result<(), Box> { let out_dir = env::var("OUT_DIR")?; let dest_path = Path::new(&out_dir).join("bindings.rs"); @@ -50,18 +91,11 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!(file, "#[allow(dead_code)]")?; // TODO: Remove this once all nodes that need parser are implemented writeln!(file, "pub struct {} {{", node.rust_name)?; writeln!(file, " parser: *mut rbs_parser_t,")?; - if let Some(fields) = &node.fields { - for field in fields { - match field.c_type.as_str() { - "rbs_string" => writeln!(file, " {}: *const rbs_string_t,", field.name)?, - "bool" => writeln!(file, " {}: bool,", field.name)?, - "rbs_ast_symbol" => { - writeln!(file, " {}: *const rbs_ast_symbol_t,", field.name)? - } - _ => eprintln!("Unknown field type: {}", field.c_type), - } - } - } + writeln!( + file, + " pointer: *mut {},", + convert_name(&node.name, CIdentifier::Type) + )?; writeln!(file, "}}\n")?; writeln!(file, "impl {} {{", node.rust_name)?; @@ -70,19 +104,23 @@ fn generate(config: &Config) -> Result<(), Box> { match field.c_type.as_str() { "rbs_string" => { writeln!(file, " pub fn {}(&self) -> RBSString {{", field.name)?; - writeln!(file, " RBSString::new(self.{})", field.name)?; + writeln!( + file, + " RBSString::new(unsafe {{ &(*self.pointer).{} }})", + field.name + )?; writeln!(file, " }}")?; } "bool" => { writeln!(file, " pub fn {}(&self) -> bool {{", field.name)?; - writeln!(file, " self.{}", field.name)?; + writeln!(file, " unsafe {{ (*self.pointer).{} }}", field.name)?; writeln!(file, " }}")?; } "rbs_ast_symbol" => { writeln!(file, " pub fn {}(&self) -> RBSSymbol {{", field.name)?; writeln!( file, - " RBSSymbol::new(self.{}, self.parser)", + " RBSSymbol::new(unsafe {{ (*self.pointer).{} }}, self.parser)", field.name )?; writeln!(file, " }}")?; @@ -106,5 +144,37 @@ fn generate(config: &Config) -> Result<(), Box> { } writeln!(file, "}}")?; + writeln!(file, "impl Node {{")?; + writeln!(file, " #[allow(clippy::missing_safety_doc)]")?; + writeln!( + file, + " pub unsafe fn new(parser: *mut rbs_parser_t, node: *mut rbs_node_t) -> Self {{" + )?; + writeln!(file, " match unsafe {{ (*node).type_ }} {{")?; + for node in &config.nodes { + let variant_name = node + .rust_name + .strip_suffix("Node") + .unwrap_or(&node.rust_name); + + let enum_name = convert_name(&node.name, CIdentifier::Constant); + + writeln!( + file, + " rbs_node_type::{} => Self::{}({} {{ parser, pointer: node.cast::<{}>() }}),", + enum_name, + variant_name, + node.rust_name, + convert_name(&node.name, CIdentifier::Type) + )?; + } + writeln!( + file, + " _ => panic!(\"Unknown node type: {{}}\", unsafe {{ (*node).type_ }})" + )?; + writeln!(file, " }}")?; + writeln!(file, " }}")?; + writeln!(file, "}}")?; + Ok(()) } diff --git a/rust/ruby-rbs/src/lib.rs b/rust/ruby-rbs/src/lib.rs index d90496129..a81982f42 100644 --- a/rust/ruby-rbs/src/lib.rs +++ b/rust/ruby-rbs/src/lib.rs @@ -40,6 +40,42 @@ pub fn parse(rbs_code: &[u8]) -> Result<*mut rbs_signature_t, String> { } } +pub struct NodeListIter { + parser: *mut rbs_parser_t, + current: *mut rbs_node_list_node_t, +} + +impl Iterator for NodeListIter { + type Item = Node; + + fn next(&mut self) -> Option { + if self.current.is_null() { + None + } else { + let pointer_data = unsafe { *self.current }; + let node = unsafe { Node::new(self.parser, pointer_data.node) }; + self.current = pointer_data.next; + Some(node) + } + } +} + +pub struct NodeList { + parser: *mut rbs_parser_t, + pointer: *mut rbs_node_list_t, +} + +impl NodeList { + /// Returns an iterator over the nodes. + #[must_use] + pub fn iter(&self) -> NodeListIter { + NodeListIter { + parser: self.parser, + current: unsafe { (*self.pointer).head }, + } + } +} + pub struct RBSString { pointer: *const rbs_string_t, } From 84da61f6f2684ca0ed0d66324c98a9bd557ffb27 Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Wed, 19 Nov 2025 22:39:18 +0000 Subject: [PATCH 09/34] Handle RBSLocation and RBSLocationList types (#58) Many AST nodes in `config.yml` have location fields (`rbs_location`, `rbs_location_list`). This change adds the necessary wrapper structs (`RBSLocation`, `RBSLocationList`) and updates `build.rs` to generate accessors for these fields. The `RBSLocation` wrapper includes a reference to the parser to support future functionality like source extraction. --- rust/ruby-rbs/build.rs | 22 +++++++++++++++ rust/ruby-rbs/src/lib.rs | 60 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index 3e2ab08da..84ed084dd 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -125,6 +125,28 @@ fn generate(config: &Config) -> Result<(), Box> { )?; writeln!(file, " }}")?; } + "rbs_location" => { + writeln!(file, " pub fn {}(&self) -> RBSLocation {{", field.name)?; + writeln!( + file, + " RBSLocation::new(unsafe {{ (*self.pointer).{} }}, self.parser)", + field.name + )?; + writeln!(file, " }}")?; + } + "rbs_location_list" => { + writeln!( + file, + " pub fn {}(&self) -> RBSLocationList {{", + field.name + )?; + writeln!( + file, + " RBSLocationList::new(unsafe {{ (*self.pointer).{} }}, self.parser)", + field.name + )?; + writeln!(file, " }}")?; + } _ => eprintln!("Unknown field type: {}", field.c_type), } } diff --git a/rust/ruby-rbs/src/lib.rs b/rust/ruby-rbs/src/lib.rs index a81982f42..21905538c 100644 --- a/rust/ruby-rbs/src/lib.rs +++ b/rust/ruby-rbs/src/lib.rs @@ -76,6 +76,66 @@ impl NodeList { } } +pub struct RBSLocation { + pointer: *const rbs_location_t, + #[allow(dead_code)] + parser: *mut rbs_parser_t, +} + +impl RBSLocation { + pub fn new(pointer: *const rbs_location_t, parser: *mut rbs_parser_t) -> Self { + Self { pointer, parser } + } + + pub fn start_loc(&self) -> i32 { + unsafe { (*self.pointer).rg.start.byte_pos } + } + + pub fn end_loc(&self) -> i32 { + unsafe { (*self.pointer).rg.end.byte_pos } + } +} + +pub struct RBSLocationListIter { + current: *mut rbs_location_list_node_t, + parser: *mut rbs_parser_t, +} + +impl Iterator for RBSLocationListIter { + type Item = RBSLocation; + + fn next(&mut self) -> Option { + if self.current.is_null() { + None + } else { + let pointer_data = unsafe { *self.current }; + let loc = RBSLocation::new(pointer_data.loc, self.parser); + self.current = pointer_data.next; + Some(loc) + } + } +} + +pub struct RBSLocationList { + pointer: *mut rbs_location_list, + parser: *mut rbs_parser_t, +} + +impl RBSLocationList { + pub fn new(pointer: *mut rbs_location_list, parser: *mut rbs_parser_t) -> Self { + Self { pointer, parser } + } + + /// Returns an iterator over the locations. + #[must_use] + pub fn iter(&self) -> RBSLocationListIter { + RBSLocationListIter { + current: unsafe { (*self.pointer).head }, + parser: self.parser, + } + } +} + pub struct RBSString { pointer: *const rbs_string_t, } From 6d0174a6461b0a0aa0a2f6e9a64a1e455c7c1885 Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Thu, 20 Nov 2025 18:47:08 +0000 Subject: [PATCH 10/34] Handle rbs_node and rbs_node_list types (#59) Enable nested AST traversal by exposing rbs_node and rbs_node_list fields Nested structure traversal (e.g., class members, constant types) depends on access to rbs_node and rbs_node_list fields. Making these fields accessible aligns the Rust bindings with the C API. Fields named "type" are accessible via type_ to avoid a Rust keyword collision --- rust/ruby-rbs/build.rs | 24 ++++++++++++++++++++++++ rust/ruby-rbs/src/lib.rs | 4 ++++ 2 files changed, 28 insertions(+) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index 84ed084dd..beec0238e 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -147,6 +147,30 @@ fn generate(config: &Config) -> Result<(), Box> { )?; writeln!(file, " }}")?; } + "rbs_node" => { + let name = if field.name == "type" { + "type_" + } else { + field.name.as_str() + }; + + writeln!(file, " pub fn {}(&self) -> Node {{", name)?; + writeln!( + file, + " unsafe {{ Node::new(self.parser, (*self.pointer).{}) }}", + name + )?; + writeln!(file, " }}")?; + } + "rbs_node_list" => { + writeln!(file, " pub fn {}(&self) -> NodeList {{", field.name)?; + writeln!( + file, + " NodeList::new(self.parser, unsafe {{ (*self.pointer).{} }})", + field.name + )?; + writeln!(file, " }}")?; + } _ => eprintln!("Unknown field type: {}", field.c_type), } } diff --git a/rust/ruby-rbs/src/lib.rs b/rust/ruby-rbs/src/lib.rs index 21905538c..13fb8a9ea 100644 --- a/rust/ruby-rbs/src/lib.rs +++ b/rust/ruby-rbs/src/lib.rs @@ -66,6 +66,10 @@ pub struct NodeList { } impl NodeList { + pub fn new(parser: *mut rbs_parser_t, pointer: *mut rbs_node_list_t) -> Self { + Self { parser, pointer } + } + /// Returns an iterator over the nodes. #[must_use] pub fn iter(&self) -> NodeListIter { From d28df1f39d1affea2aad64e547076a8d1f1ce7ce Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Fri, 21 Nov 2025 17:04:49 +0000 Subject: [PATCH 11/34] Handle RBSKeyword types (#60) --- rust/ruby-rbs/build.rs | 9 +++++++++ rust/ruby-rbs/src/lib.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index beec0238e..d105f247f 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -171,6 +171,15 @@ fn generate(config: &Config) -> Result<(), Box> { )?; writeln!(file, " }}")?; } + "rbs_keyword" => { + writeln!(file, " pub fn {}(&self) -> RBSKeyword {{", field.name)?; + writeln!( + file, + " RBSKeyword::new(self.parser, unsafe {{ (*self.pointer).{} }})", + field.name + )?; + writeln!(file, " }}")?; + } _ => eprintln!("Unknown field type: {}", field.c_type), } } diff --git a/rust/ruby-rbs/src/lib.rs b/rust/ruby-rbs/src/lib.rs index 13fb8a9ea..20069ebf9 100644 --- a/rust/ruby-rbs/src/lib.rs +++ b/rust/ruby-rbs/src/lib.rs @@ -183,6 +183,32 @@ impl RBSSymbol { } } +pub struct RBSKeyword { + parser: *mut rbs_parser_t, + pointer: *const rbs_keyword, +} + +impl RBSKeyword { + pub fn new(parser: *mut rbs_parser_t, pointer: *const rbs_keyword) -> Self { + Self { parser, pointer } + } + + pub fn name(&self) -> &[u8] { + unsafe { + let constant_ptr = rbs_constant_pool_id_to_constant( + &(*self.parser).constant_pool, + (*self.pointer).constant_id, + ); + if constant_ptr.is_null() { + panic!("Constant ID for keyword is not present in the pool"); + } + + let constant = &*constant_ptr; + std::slice::from_raw_parts(constant.start, constant.length) + } + } +} + #[cfg(test)] mod tests { use super::*; From 1d6e3fa7bfd2ddc4884809d95ae891a10159da74 Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Mon, 24 Nov 2025 17:13:32 +0000 Subject: [PATCH 12/34] Add test demonstrating AST traversal and type checking (#61) Adds `test_parse_integer()` which parses an integer literal type alias and traverses the AST (`TypeAlias` -> `LiteralType` -> `Integer`) using pattern matching to verify node types and extract values. This validates that the generated node wrappers enable AST traversal in pure Rust with proper type safety. Also adds `Debug` derives and refactors memory management by returning `SignatureNode` instead of raw pointer, with `Drop` impl to free parser. --- rust/ruby-rbs/build.rs | 2 ++ rust/ruby-rbs/src/lib.rs | 38 +++++++++++++++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index d105f247f..a8bdfccd2 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -89,6 +89,7 @@ fn generate(config: &Config) -> Result<(), Box> { // TODO: Go through all of the nodes and generate the structs to back them up for node in &config.nodes { writeln!(file, "#[allow(dead_code)]")?; // TODO: Remove this once all nodes that need parser are implemented + writeln!(file, "#[derive(Debug)]")?; writeln!(file, "pub struct {} {{", node.rust_name)?; writeln!(file, " parser: *mut rbs_parser_t,")?; writeln!( @@ -188,6 +189,7 @@ fn generate(config: &Config) -> Result<(), Box> { } // Generate the Node enum to wrap all of the structs + writeln!(file, "#[derive(Debug)]")?; writeln!(file, "pub enum Node {{")?; for node in &config.nodes { let variant_name = node diff --git a/rust/ruby-rbs/src/lib.rs b/rust/ruby-rbs/src/lib.rs index 20069ebf9..dae6787c2 100644 --- a/rust/ruby-rbs/src/lib.rs +++ b/rust/ruby-rbs/src/lib.rs @@ -13,7 +13,7 @@ static INIT: Once = Once::new(); /// let signature = parse(rbs_code.as_bytes()); /// assert!(signature.is_ok(), "Failed to parse RBS signature"); /// ``` -pub fn parse(rbs_code: &[u8]) -> Result<*mut rbs_signature_t, String> { +pub fn parse(rbs_code: &[u8]) -> Result { unsafe { INIT.call_once(|| { rbs_constant_pool_init(RBS_GLOBAL_CONSTANT_POOL, 26); @@ -30,16 +30,27 @@ pub fn parse(rbs_code: &[u8]) -> Result<*mut rbs_signature_t, String> { let mut signature: *mut rbs_signature_t = std::ptr::null_mut(); let result = rbs_parse_signature(parser, &mut signature); - rbs_parser_free(parser); + let signature_node = SignatureNode { + parser, + pointer: signature, + }; if result { - Ok(signature) + Ok(signature_node) } else { Err(String::from("Failed to parse RBS signature")) } } } +impl Drop for SignatureNode { + fn drop(&mut self) { + unsafe { + rbs_parser_free(self.parser); + } + } +} + pub struct NodeListIter { parser: *mut rbs_parser_t, current: *mut rbs_node_list_node_t, @@ -140,6 +151,7 @@ impl RBSLocationList { } } +#[derive(Debug)] pub struct RBSString { pointer: *const rbs_string_t, } @@ -223,4 +235,24 @@ mod tests { let signature2 = parse(rbs_code2.as_bytes()); assert!(signature2.is_ok(), "Failed to parse RBS signature"); } + + #[test] + fn test_parse_integer() { + let rbs_code = r#"type foo = 1"#; + let signature = parse(rbs_code.as_bytes()); + assert!(signature.is_ok(), "Failed to parse RBS signature"); + + let signature_node = signature.unwrap(); + if let Node::TypeAlias(node) = signature_node.declarations().iter().next().unwrap() + && let Node::LiteralType(literal) = node.type_() + && let Node::Integer(integer) = literal.literal() + { + assert_eq!( + "1".to_string(), + String::from_utf8(integer.string_representation().as_bytes().to_vec()).unwrap() + ); + } else { + panic!("No literal type node found"); + } + } } From 1614014325242ff9ae4d1cff5e89f2090afcfd44 Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Tue, 25 Nov 2025 17:50:09 +0000 Subject: [PATCH 13/34] Handle CommentNode types (#62) --- rust/ruby-rbs/build.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index a8bdfccd2..6a1dca66a 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -117,6 +117,15 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!(file, " unsafe {{ (*self.pointer).{} }}", field.name)?; writeln!(file, " }}")?; } + "rbs_ast_comment" => { + writeln!(file, " pub fn {}(&self) -> CommentNode {{", field.name)?; + writeln!( + file, + " CommentNode {{ parser: self.parser, pointer: unsafe {{ (*self.pointer).{} }} }}", + field.name + )?; + writeln!(file, " }}")?; + } "rbs_ast_symbol" => { writeln!(file, " pub fn {}(&self) -> RBSSymbol {{", field.name)?; writeln!( From 8fa54da463da2b4b6ea035a414caed132c40ffb7 Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:27:39 +0000 Subject: [PATCH 14/34] Handle ClassSuperNode types (#63) --- rust/ruby-rbs/build.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index 6a1dca66a..0dee33988 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -126,6 +126,19 @@ fn generate(config: &Config) -> Result<(), Box> { )?; writeln!(file, " }}")?; } + "rbs_ast_declarations_class_super" => { + writeln!( + file, + " pub fn {}(&self) -> ClassSuperNode {{", + field.name + )?; + writeln!( + file, + " ClassSuperNode {{ parser: self.parser, pointer: unsafe {{ (*self.pointer).{} }} }}", + field.name + )?; + writeln!(file, " }}")?; + } "rbs_ast_symbol" => { writeln!(file, " pub fn {}(&self) -> RBSSymbol {{", field.name)?; writeln!( From c7ca6d436ddac0e988f94f1629f0a3692624f58f Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Tue, 25 Nov 2025 21:07:42 +0000 Subject: [PATCH 15/34] Handle NamespaceNode types (#64) --- rust/ruby-rbs/build.rs | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index 0dee33988..ae2a205ba 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -10,6 +10,14 @@ struct Config { struct NodeField { name: String, c_type: String, + c_name: Option, +} + +impl NodeField { + fn c_name(&self) -> &str { + let name = self.c_name.as_ref().unwrap_or(&self.name); + if name == "type" { "type_" } else { name } + } } #[derive(Debug, Deserialize)] @@ -108,7 +116,7 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!( file, " RBSString::new(unsafe {{ &(*self.pointer).{} }})", - field.name + field.c_name() )?; writeln!(file, " }}")?; } @@ -122,7 +130,7 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!( file, " CommentNode {{ parser: self.parser, pointer: unsafe {{ (*self.pointer).{} }} }}", - field.name + field.c_name() )?; writeln!(file, " }}")?; } @@ -135,7 +143,7 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!( file, " ClassSuperNode {{ parser: self.parser, pointer: unsafe {{ (*self.pointer).{} }} }}", - field.name + field.c_name() )?; writeln!(file, " }}")?; } @@ -144,7 +152,7 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!( file, " RBSSymbol::new(unsafe {{ (*self.pointer).{} }}, self.parser)", - field.name + field.c_name() )?; writeln!(file, " }}")?; } @@ -153,7 +161,7 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!( file, " RBSLocation::new(unsafe {{ (*self.pointer).{} }}, self.parser)", - field.name + field.c_name() )?; writeln!(file, " }}")?; } @@ -166,7 +174,16 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!( file, " RBSLocationList::new(unsafe {{ (*self.pointer).{} }}, self.parser)", - field.name + field.c_name() + )?; + writeln!(file, " }}")?; + } + "rbs_namespace" => { + writeln!(file, " pub fn {}(&self) -> NamespaceNode {{", field.name)?; + writeln!( + file, + " NamespaceNode {{ parser: self.parser, pointer: unsafe {{ (*self.pointer).{} }} }}", + field.c_name() )?; writeln!(file, " }}")?; } @@ -181,7 +198,7 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!( file, " unsafe {{ Node::new(self.parser, (*self.pointer).{}) }}", - name + field.c_name() )?; writeln!(file, " }}")?; } @@ -190,7 +207,7 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!( file, " NodeList::new(self.parser, unsafe {{ (*self.pointer).{} }})", - field.name + field.c_name() )?; writeln!(file, " }}")?; } @@ -199,7 +216,7 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!( file, " RBSKeyword::new(self.parser, unsafe {{ (*self.pointer).{} }})", - field.name + field.c_name() )?; writeln!(file, " }}")?; } From 0f977828f4bcbaa581d1e86e0a551a57b08e748f Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Tue, 25 Nov 2025 21:19:41 +0000 Subject: [PATCH 16/34] Handle TypeNameNode types (#65) --- rust/ruby-rbs/build.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index ae2a205ba..be8210843 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -220,6 +220,15 @@ fn generate(config: &Config) -> Result<(), Box> { )?; writeln!(file, " }}")?; } + "rbs_type_name" => { + writeln!(file, " pub fn {}(&self) -> TypeNameNode {{", field.name)?; + writeln!( + file, + " TypeNameNode {{ parser: self.parser, pointer: unsafe {{ (*self.pointer).{} }} }}", + field.c_name() + )?; + writeln!(file, " }}")?; + } _ => eprintln!("Unknown field type: {}", field.c_type), } } From 0446053fdfd99560fc24917a2e139a8224e5923d Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Tue, 25 Nov 2025 21:25:35 +0000 Subject: [PATCH 17/34] Handle BlockTypeNode types (#66) --- rust/ruby-rbs/build.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index be8210843..36883f84c 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -229,6 +229,15 @@ fn generate(config: &Config) -> Result<(), Box> { )?; writeln!(file, " }}")?; } + "rbs_types_block" => { + writeln!(file, " pub fn {}(&self) -> BlockTypeNode {{", field.name)?; + writeln!( + file, + " BlockTypeNode {{ parser: self.parser, pointer: unsafe {{ (*self.pointer).{} }} }}", + field.c_name() + )?; + writeln!(file, " }}")?; + } _ => eprintln!("Unknown field type: {}", field.c_type), } } From cc67a0906036ffb3cf40ae19fd9ebb16f48cfdcd Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Fri, 28 Nov 2025 22:48:06 +0000 Subject: [PATCH 18/34] Refactor Symbol and Keyword as nodes (#67) Refactor the previous implementation of `Symbol`/`Keyword` handling to treat them as first-class nodes in the build configuration. `Keyword` and `Symbol` represent identifiers (interned strings), not traditional AST nodes. However, the C parser defines them in `rbs_node_type` (as `RBS_KEYWORD` and `RBS_AST_SYMBOL`) and treats them as nodes (`rbs_node_t*`) in many contexts (lists, hashes). Instead of manually defining `RBSSymbol`/`RBSKeyword` structs, we now inject them into the `config.yml` node list in `build.rs`. This allows them to be generated as `SymbolNode`/`KeywordNode` variants in the `Node` enum, enabling polymorphic handling (in Node lists and Hashes) --- rust/ruby-rbs/build.rs | 24 ++++++++++++++++++++---- rust/ruby-rbs/src/lib.rs | 22 ++-------------------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index 36883f84c..fb7d61661 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -37,6 +37,22 @@ fn main() -> Result<(), Box> { let config_file = File::open(&config_path)?; let mut config: Config = serde_yaml::from_reader(config_file)?; + // Keyword and Symbol represent identifiers (interned strings), not traditional AST nodes. + // However, the C parser defines them in `rbs_node_type` (RBS_KEYWORD, RBS_AST_SYMBOL) and + // treats them as nodes (rbs_node_t*) in many contexts (lists, hashes). + // We inject them into the config so they are generated as structs matching the Node pattern, + // allowing them to be wrapped in the Node enum and handled uniformly in Rust. + config.nodes.push(Node { + name: "RBS::Keyword".to_string(), + rust_name: "KeywordNode".to_string(), + fields: None, + }); + config.nodes.push(Node { + name: "RBS::AST::Symbol".to_string(), + rust_name: "SymbolNode".to_string(), + fields: None, + }); + config.nodes.sort_by(|a, b| a.name.cmp(&b.name)); generate(&config)?; @@ -148,10 +164,10 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!(file, " }}")?; } "rbs_ast_symbol" => { - writeln!(file, " pub fn {}(&self) -> RBSSymbol {{", field.name)?; + writeln!(file, " pub fn {}(&self) -> SymbolNode {{", field.name)?; writeln!( file, - " RBSSymbol::new(unsafe {{ (*self.pointer).{} }}, self.parser)", + " SymbolNode {{ parser: self.parser, pointer: unsafe {{ (*self.pointer).{} }} }}", field.c_name() )?; writeln!(file, " }}")?; @@ -212,10 +228,10 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!(file, " }}")?; } "rbs_keyword" => { - writeln!(file, " pub fn {}(&self) -> RBSKeyword {{", field.name)?; + writeln!(file, " pub fn {}(&self) -> KeywordNode {{", field.name)?; writeln!( file, - " RBSKeyword::new(self.parser, unsafe {{ (*self.pointer).{} }})", + " KeywordNode {{ parser: self.parser, pointer: unsafe {{ (*self.pointer).{} }} }}", field.c_name() )?; writeln!(file, " }}")?; diff --git a/rust/ruby-rbs/src/lib.rs b/rust/ruby-rbs/src/lib.rs index dae6787c2..86e0ff5a3 100644 --- a/rust/ruby-rbs/src/lib.rs +++ b/rust/ruby-rbs/src/lib.rs @@ -169,16 +169,7 @@ impl RBSString { } } -pub struct RBSSymbol { - pointer: *const rbs_ast_symbol_t, - parser: *mut rbs_parser_t, -} - -impl RBSSymbol { - pub fn new(pointer: *const rbs_ast_symbol_t, parser: *mut rbs_parser_t) -> Self { - Self { pointer, parser } - } - +impl SymbolNode { pub fn name(&self) -> &[u8] { unsafe { let constant_ptr = rbs_constant_pool_id_to_constant( @@ -195,16 +186,7 @@ impl RBSSymbol { } } -pub struct RBSKeyword { - parser: *mut rbs_parser_t, - pointer: *const rbs_keyword, -} - -impl RBSKeyword { - pub fn new(parser: *mut rbs_parser_t, pointer: *const rbs_keyword) -> Self { - Self { parser, pointer } - } - +impl KeywordNode { pub fn name(&self) -> &[u8] { unsafe { let constant_ptr = rbs_constant_pool_id_to_constant( From a104aca832e4c71d60894deaa92080068cc676b7 Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Fri, 28 Nov 2025 23:21:18 +0000 Subject: [PATCH 19/34] Handle RBSHash types (#68) Add support for RBS hashes (`rbs_hash_t`), which are used in Record types and Function keyword arguments --- rust/ruby-rbs/build.rs | 9 ++++ rust/ruby-rbs/src/lib.rs | 89 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index fb7d61661..ce05d2e62 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -172,6 +172,15 @@ fn generate(config: &Config) -> Result<(), Box> { )?; writeln!(file, " }}")?; } + "rbs_hash" => { + writeln!(file, " pub fn {}(&self) -> RBSHash {{", field.name)?; + writeln!( + file, + " RBSHash::new(self.parser, unsafe {{ (*self.pointer).{} }})", + field.c_name() + )?; + writeln!(file, " }}")?; + } "rbs_location" => { writeln!(file, " pub fn {}(&self) -> RBSLocation {{", field.name)?; writeln!( diff --git a/rust/ruby-rbs/src/lib.rs b/rust/ruby-rbs/src/lib.rs index 86e0ff5a3..d92e767ca 100644 --- a/rust/ruby-rbs/src/lib.rs +++ b/rust/ruby-rbs/src/lib.rs @@ -91,6 +91,47 @@ impl NodeList { } } +pub struct RBSHash { + parser: *mut rbs_parser_t, + pointer: *mut rbs_hash, +} + +impl RBSHash { + pub fn new(parser: *mut rbs_parser_t, pointer: *mut rbs_hash) -> Self { + Self { parser, pointer } + } + + /// Returns an iterator over the key-value pairs. + #[must_use] + pub fn iter(&self) -> RBSHashIter { + RBSHashIter { + parser: self.parser, + current: unsafe { (*self.pointer).head }, + } + } +} + +pub struct RBSHashIter { + parser: *mut rbs_parser_t, + current: *mut rbs_hash_node_t, +} + +impl Iterator for RBSHashIter { + type Item = (Node, Node); + + fn next(&mut self) -> Option { + if self.current.is_null() { + None + } else { + let pointer_data = unsafe { *self.current }; + let key = unsafe { Node::new(self.parser, pointer_data.key) }; + let value = unsafe { Node::new(self.parser, pointer_data.value) }; + self.current = pointer_data.next; + Some((key, value)) + } + } +} + pub struct RBSLocation { pointer: *const rbs_location_t, #[allow(dead_code)] @@ -237,4 +278,52 @@ mod tests { panic!("No literal type node found"); } } + + #[test] + fn test_rbs_hash_via_record_type() { + // RecordType stores its fields in an RBSHash via all_fields() + let rbs_code = r#"type foo = { name: String, age: Integer }"#; + let signature = parse(rbs_code.as_bytes()); + assert!(signature.is_ok(), "Failed to parse RBS signature"); + + let signature_node = signature.unwrap(); + if let Node::TypeAlias(type_alias) = signature_node.declarations().iter().next().unwrap() + && let Node::RecordType(record) = type_alias.type_() + { + let hash = record.all_fields(); + let fields: Vec<_> = hash.iter().collect(); + assert_eq!(fields.len(), 2, "Expected 2 fields in record"); + + // Build a map of field names to type names + let mut field_types: Vec<(String, String)> = Vec::new(); + for (key, value) in &fields { + let Node::Symbol(sym) = key else { + panic!("Expected Symbol key"); + }; + let Node::RecordFieldType(field_type) = value else { + panic!("Expected RecordFieldType value"); + }; + let Node::ClassInstanceType(class_type) = field_type.type_() else { + panic!("Expected ClassInstanceType"); + }; + + let key_name = String::from_utf8(sym.name().to_vec()).unwrap(); + let type_name_node = class_type.name(); + let type_name_sym = type_name_node.name(); + let type_name = String::from_utf8(type_name_sym.name().to_vec()).unwrap(); + field_types.push((key_name, type_name)); + } + + assert!( + field_types.contains(&("name".to_string(), "String".to_string())), + "Expected 'name: String'" + ); + assert!( + field_types.contains(&("age".to_string(), "Integer".to_string())), + "Expected 'age: Integer'" + ); + } else { + panic!("Expected TypeAlias with RecordType"); + } + } } From 4b15719c862b1e5d68ef2b1ac1439ee96e8996e8 Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Mon, 1 Dec 2025 19:51:49 +0000 Subject: [PATCH 20/34] Add generated Visit trait for AST node traversal (#69) Enable walking the AST by generating a `Visit` trait with per-node visitor methods. It uses double dispatch to route each node type to its corresponding visitor method. This avoids consumers needing to manually match on Node variants and allows overriding specific visits while inheriting default behaviour for others. --- rust/ruby-rbs/build.rs | 78 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 70 insertions(+), 8 deletions(-) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index ce05d2e62..f30a97c3a 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -27,6 +27,14 @@ struct Node { fields: Option>, } +impl Node { + fn variant_name(&self) -> &str { + self.rust_name + .strip_suffix("Node") + .unwrap_or(&self.rust_name) + } +} + fn main() -> Result<(), Box> { let config_path = Path::new(env!("CARGO_MANIFEST_DIR")) .join("../../config.yml") @@ -62,11 +70,12 @@ fn main() -> Result<(), Box> { enum CIdentifier { Type, // foo_bar_t Constant, // FOO_BAR + Method, // visit_foo_bar } fn convert_name(name: &str, identifier: CIdentifier) -> String { let type_name = name.replace("::", "_"); - let lowercase = matches!(identifier, CIdentifier::Type); + let lowercase = matches!(identifier, CIdentifier::Type | CIdentifier::Method); let mut out = String::new(); let mut prev_is_lower = false; @@ -94,12 +103,67 @@ fn convert_name(name: &str, identifier: CIdentifier) -> String { } } - if lowercase { + if matches!(identifier, CIdentifier::Type) { out.push_str("_t"); } out } +fn write_visit_trait(file: &mut File, config: &Config) -> Result<(), Box> { + writeln!(file, "/// A trait for traversing the AST using a visitor")?; + writeln!(file, "pub trait Visit {{")?; + writeln!( + file, + " /// Visit any node of the AST. Generally used to continue traversal" + )?; + writeln!(file, " fn visit(&mut self, node: &Node) {{")?; + writeln!(file, " match node {{")?; + + for node in &config.nodes { + let node_variant_name = node.variant_name(); + let method_name = convert_name(node_variant_name, CIdentifier::Method); + + writeln!(file, " Node::{}(it) => {{", node_variant_name)?; + writeln!(file, " self.visit_{}_node(it);", method_name,)?; + writeln!(file, " }}")?; + } + + writeln!(file, " }}")?; + writeln!(file, " }}")?; + + for node in &config.nodes { + let node_variant_name = node.variant_name(); + let method_name = convert_name(node_variant_name, CIdentifier::Method); + + writeln!(file)?; + writeln!( + file, + " fn visit_{}_node(&mut self, node: &{}Node) {{", + method_name, node_variant_name + )?; + writeln!(file, " visit_{}_node(self, node);", method_name)?; + writeln!(file, " }}")?; + } + writeln!(file, "}}")?; + writeln!(file)?; + + for node in &config.nodes { + let node_variant_name = node.variant_name(); + let method_name = convert_name(node_variant_name, CIdentifier::Method); + + writeln!(file, "#[allow(unused_variables)]")?; // TODO: Remove this once all nodes that need visitor are implemented + writeln!( + file, + "pub fn visit_{}_node(visitor: &mut V, node: &{}Node) {{", + method_name, node_variant_name + )?; + writeln!(file, "}}")?; + writeln!(file)?; + } + + Ok(()) +} + fn generate(config: &Config) -> Result<(), Box> { let out_dir = env::var("OUT_DIR")?; let dest_path = Path::new(&out_dir).join("bindings.rs"); @@ -291,18 +355,13 @@ fn generate(config: &Config) -> Result<(), Box> { )?; writeln!(file, " match unsafe {{ (*node).type_ }} {{")?; for node in &config.nodes { - let variant_name = node - .rust_name - .strip_suffix("Node") - .unwrap_or(&node.rust_name); - let enum_name = convert_name(&node.name, CIdentifier::Constant); writeln!( file, " rbs_node_type::{} => Self::{}({} {{ parser, pointer: node.cast::<{}>() }}),", enum_name, - variant_name, + node.variant_name(), node.rust_name, convert_name(&node.name, CIdentifier::Type) )?; @@ -314,6 +373,9 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!(file, " }}")?; writeln!(file, " }}")?; writeln!(file, "}}")?; + writeln!(file)?; + + write_visit_trait(&mut file, config)?; Ok(()) } From 66b569aa1716e1c01741e70e0d6c0c002cc931b3 Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Mon, 15 Dec 2025 08:57:32 -0800 Subject: [PATCH 21/34] Annotate nullable pointer fields in config.yml with optional: true (#70) Some C struct pointer fields can be NULL (super_class when no parent class, comment when no doc comment). This metadata allows our Rust codegen to generate Option return types for these accessors instead of unconditionally wrapping potentially NULL pointers. --- config.yml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/config.yml b/config.yml index 94f58bea4..27eeebb44 100644 --- a/config.yml +++ b/config.yml @@ -25,12 +25,14 @@ nodes: c_type: rbs_node_list - name: super_class c_type: rbs_ast_declarations_class_super + optional: true # NULL when no superclass (e.g., `class Foo end` vs `class Foo < Bar end`) - name: members c_type: rbs_node_list - name: annotations c_type: rbs_node_list - name: comment c_type: rbs_ast_comment + optional: true # NULL when no comment precedes the declaration - name: RBS::AST::Declarations::Class::Super rust_name: ClassSuperNode fields: @@ -47,6 +49,7 @@ nodes: c_type: rbs_type_name - name: comment c_type: rbs_ast_comment + optional: true # NULL when no comment precedes the declaration - name: annotations c_type: rbs_node_list - name: RBS::AST::Declarations::Constant @@ -58,6 +61,7 @@ nodes: c_type: rbs_node - name: comment c_type: rbs_ast_comment + optional: true # NULL when no comment precedes the declaration - name: annotations c_type: rbs_node_list - name: RBS::AST::Declarations::Global @@ -69,6 +73,7 @@ nodes: c_type: rbs_node - name: comment c_type: rbs_ast_comment + optional: true # NULL when no comment precedes the declaration - name: annotations c_type: rbs_node_list - name: RBS::AST::Declarations::Interface @@ -84,6 +89,7 @@ nodes: c_type: rbs_node_list - name: comment c_type: rbs_ast_comment + optional: true # NULL when no comment precedes the declaration - name: RBS::AST::Declarations::Module rust_name: ModuleNode fields: @@ -99,6 +105,7 @@ nodes: c_type: rbs_node_list - name: comment c_type: rbs_ast_comment + optional: true # NULL when no comment precedes the declaration - name: RBS::AST::Declarations::Module::Self rust_name: ModuleSelfNode fields: @@ -115,6 +122,7 @@ nodes: c_type: rbs_type_name - name: comment c_type: rbs_ast_comment + optional: true # NULL when no comment precedes the declaration - name: annotations c_type: rbs_node_list - name: RBS::AST::Declarations::TypeAlias @@ -130,6 +138,7 @@ nodes: c_type: rbs_node_list - name: comment c_type: rbs_ast_comment + optional: true # NULL when no comment precedes the declaration - name: RBS::AST::Directives::Use rust_name: UseNode fields: @@ -142,6 +151,7 @@ nodes: c_type: rbs_type_name - name: new_name c_type: rbs_ast_symbol + optional: true # NULL when no alias (e.g., `use Foo::Bar` vs `use Foo::Bar as Baz`) - name: RBS::AST::Directives::Use::WildcardClause rust_name: UseWildcardClauseNode fields: @@ -161,6 +171,7 @@ nodes: c_type: rbs_node_list - name: comment c_type: rbs_ast_comment + optional: true # NULL when no comment precedes the declaration - name: RBS::AST::Members::AttrAccessor rust_name: AttrAccessorNode fields: @@ -170,14 +181,17 @@ nodes: c_type: rbs_node - name: ivar_name c_type: rbs_node # rbs_ast_symbol_t, NULL or rbs_ast_bool_new(false) + optional: true # NULL when omitted (`attr_accessor foo: T`); Symbol when named (`attr_accessor foo (@bar): T`); Bool(false) when empty parens (`attr_accessor foo (): T`) - name: kind c_type: rbs_keyword - name: annotations c_type: rbs_node_list - name: comment c_type: rbs_ast_comment + optional: true # NULL when no comment precedes the declaration - name: visibility c_type: rbs_keyword + optional: true # NULL when no visibility prefix (e.g., `attr_accessor foo: T` vs `private attr_accessor foo: T`) - name: RBS::AST::Members::AttrReader rust_name: AttrReaderNode fields: @@ -187,14 +201,17 @@ nodes: c_type: rbs_node - name: ivar_name c_type: rbs_node # rbs_ast_symbol_t, NULL or rbs_ast_bool_new(false) + optional: true # NULL when omitted (`attr_reader foo: T`); Symbol when named (`attr_reader foo (@bar): T`); Bool(false) when empty parens (`attr_reader foo (): T`) - name: kind c_type: rbs_keyword - name: annotations c_type: rbs_node_list - name: comment c_type: rbs_ast_comment + optional: true # NULL when no comment precedes the declaration - name: visibility c_type: rbs_keyword + optional: true # NULL when no visibility prefix (e.g., `attr_reader foo: T` vs `private attr_reader foo: T`) - name: RBS::AST::Members::AttrWriter rust_name: AttrWriterNode fields: @@ -204,14 +221,17 @@ nodes: c_type: rbs_node - name: ivar_name c_type: rbs_node # rbs_ast_symbol_t, NULL or rbs_ast_bool_new(false) + optional: true # NULL when omitted (`attr_writer foo: T`); Symbol when named (`attr_writer foo (@bar): T`); Bool(false) when empty parens (`attr_writer foo (): T`) - name: kind c_type: rbs_keyword - name: annotations c_type: rbs_node_list - name: comment c_type: rbs_ast_comment + optional: true # NULL when no comment precedes the declaration - name: visibility c_type: rbs_keyword + optional: true # NULL when no visibility prefix (e.g., `attr_writer foo: T` vs `private attr_writer foo: T`) - name: RBS::AST::Members::ClassInstanceVariable rust_name: ClassInstanceVariableNode fields: @@ -221,6 +241,7 @@ nodes: c_type: rbs_node - name: comment c_type: rbs_ast_comment + optional: true # NULL when no comment precedes the declaration - name: RBS::AST::Members::ClassVariable rust_name: ClassVariableNode fields: @@ -230,6 +251,7 @@ nodes: c_type: rbs_node - name: comment c_type: rbs_ast_comment + optional: true # NULL when no comment precedes the declaration - name: RBS::AST::Members::Extend rust_name: ExtendNode fields: @@ -241,6 +263,7 @@ nodes: c_type: rbs_node_list - name: comment c_type: rbs_ast_comment + optional: true # NULL when no comment precedes the declaration - name: RBS::AST::Members::Include rust_name: IncludeNode fields: @@ -252,6 +275,7 @@ nodes: c_type: rbs_node_list - name: comment c_type: rbs_ast_comment + optional: true # NULL when no comment precedes the declaration - name: RBS::AST::Members::InstanceVariable rust_name: InstanceVariableNode fields: @@ -261,6 +285,7 @@ nodes: c_type: rbs_node - name: comment c_type: rbs_ast_comment + optional: true # NULL when no comment precedes the declaration - name: RBS::AST::Members::MethodDefinition rust_name: MethodDefinitionNode fields: @@ -274,10 +299,12 @@ nodes: c_type: rbs_node_list - name: comment c_type: rbs_ast_comment + optional: true # NULL when no comment precedes the declaration - name: overloading c_type: bool - name: visibility c_type: rbs_keyword + optional: true # NULL when no visibility prefix (e.g., `def foo: ...` vs `private def foo: ...`) - name: RBS::AST::Members::MethodDefinition::Overload rust_name: MethodDefinitionOverloadNode expose_location: false @@ -297,6 +324,7 @@ nodes: c_type: rbs_node_list - name: comment c_type: rbs_ast_comment + optional: true # NULL when no comment precedes the declaration - name: RBS::AST::Members::Private rust_name: PrivateNode - name: RBS::AST::Members::Public @@ -310,10 +338,13 @@ nodes: c_type: rbs_keyword - name: upper_bound c_type: rbs_node + optional: true # NULL when no upper bound (e.g., `[T]` vs `[T < Bound]`) - name: lower_bound c_type: rbs_node + optional: true # NULL when no lower bound (e.g., `[T]` vs `[T > Bound]`) - name: default_type c_type: rbs_node + optional: true # NULL when no default (e.g., `[T]` vs `[T = Default]`) - name: unchecked c_type: bool - name: RBS::AST::Integer @@ -339,6 +370,7 @@ nodes: c_type: rbs_node - name: block c_type: rbs_types_block + optional: true # NULL when no block (e.g., `() -> void` vs `() { () -> void } -> void`) - name: RBS::Namespace rust_name: NamespaceNode expose_location: false @@ -403,6 +435,7 @@ nodes: c_type: bool - name: self_type c_type: rbs_node + optional: true # NULL when no self binding (e.g., `{ () -> void }` vs `{ () [self: T] -> void }`) - name: RBS::Types::ClassInstance rust_name: ClassInstanceTypeNode fields: @@ -425,6 +458,7 @@ nodes: c_type: rbs_node_list - name: rest_positionals c_type: rbs_node + optional: true # NULL when no splat (e.g., `(String) -> void` vs `(*String) -> void`) - name: trailing_positionals c_type: rbs_node_list - name: required_keywords @@ -433,6 +467,7 @@ nodes: c_type: rbs_hash - name: rest_keywords c_type: rbs_node + optional: true # NULL when no double-splat (e.g., `() -> void` vs `(**String) -> void`) - name: return_type c_type: rbs_node - name: RBS::Types::Function::Param @@ -442,6 +477,7 @@ nodes: c_type: rbs_node - name: name c_type: rbs_ast_symbol + optional: true # NULL when param is unnamed (e.g., `(String) -> void` vs `(String name) -> void`) - name: RBS::Types::Interface rust_name: InterfaceTypeNode fields: @@ -471,8 +507,10 @@ nodes: c_type: rbs_node - name: block c_type: rbs_types_block + optional: true # NULL when proc has no block (e.g., `^() -> void` vs `^() { () -> void } -> void`) - name: self_type c_type: rbs_node + optional: true # NULL when no self binding (e.g., `^() -> void` vs `^() [self: T] -> void`) - name: RBS::Types::Record rust_name: RecordTypeNode fields: From 88cf6d002084b7a31211047501f9a685798d40be Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Tue, 16 Dec 2025 15:43:22 -0800 Subject: [PATCH 22/34] Handle optional field types (#71) Read `optional: true` annotations from `config.yml` and generate `Option` return types with null checks, so we don't crash at runtime. The extracted helper function centralizes the accessor generation logic for pointer-based field types. --- rust/ruby-rbs/build.rs | 168 ++++++++++++++++----------------------- rust/ruby-rbs/src/lib.rs | 6 +- 2 files changed, 73 insertions(+), 101 deletions(-) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index f30a97c3a..0c7e3c258 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -11,6 +11,8 @@ struct NodeField { name: String, c_type: String, c_name: Option, + #[serde(default)] + optional: bool, } impl NodeField { @@ -109,6 +111,42 @@ fn convert_name(name: &str, identifier: CIdentifier) -> String { out } +fn write_node_field_accessor( + file: &mut File, + field: &NodeField, + rust_type: &str, +) -> std::io::Result<()> { + if field.optional { + writeln!( + file, + " pub fn {}(&self) -> Option<{}> {{", + field.name, rust_type + )?; + writeln!( + file, + " let ptr = unsafe {{ (*self.pointer).{} }};", + field.c_name() + )?; + writeln!(file, " if ptr.is_null() {{")?; + writeln!(file, " None")?; + writeln!(file, " }} else {{")?; + writeln!( + file, + " Some({rust_type} {{ parser: self.parser, pointer: ptr }})" + )?; + writeln!(file, " }}")?; + } else { + writeln!(file, " pub fn {}(&self) -> {} {{", field.name, rust_type)?; + writeln!( + file, + " {} {{ parser: self.parser, pointer: unsafe {{ (*self.pointer).{} }} }}", + rust_type, + field.c_name() + )?; + } + writeln!(file, " }}") +} + fn write_visit_trait(file: &mut File, config: &Config) -> Result<(), Box> { writeln!(file, "/// A trait for traversing the AST using a visitor")?; writeln!(file, "pub trait Visit {{")?; @@ -206,75 +244,23 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!(file, " }}")?; } "rbs_ast_comment" => { - writeln!(file, " pub fn {}(&self) -> CommentNode {{", field.name)?; - writeln!( - file, - " CommentNode {{ parser: self.parser, pointer: unsafe {{ (*self.pointer).{} }} }}", - field.c_name() - )?; - writeln!(file, " }}")?; + write_node_field_accessor(&mut file, field, "CommentNode")? } "rbs_ast_declarations_class_super" => { - writeln!( - file, - " pub fn {}(&self) -> ClassSuperNode {{", - field.name - )?; - writeln!( - file, - " ClassSuperNode {{ parser: self.parser, pointer: unsafe {{ (*self.pointer).{} }} }}", - field.c_name() - )?; - writeln!(file, " }}")?; - } - "rbs_ast_symbol" => { - writeln!(file, " pub fn {}(&self) -> SymbolNode {{", field.name)?; - writeln!( - file, - " SymbolNode {{ parser: self.parser, pointer: unsafe {{ (*self.pointer).{} }} }}", - field.c_name() - )?; - writeln!(file, " }}")?; + write_node_field_accessor(&mut file, field, "ClassSuperNode")? } + "rbs_ast_symbol" => write_node_field_accessor(&mut file, field, "SymbolNode")?, "rbs_hash" => { - writeln!(file, " pub fn {}(&self) -> RBSHash {{", field.name)?; - writeln!( - file, - " RBSHash::new(self.parser, unsafe {{ (*self.pointer).{} }})", - field.c_name() - )?; - writeln!(file, " }}")?; + write_node_field_accessor(&mut file, field, "RBSHash")?; } "rbs_location" => { - writeln!(file, " pub fn {}(&self) -> RBSLocation {{", field.name)?; - writeln!( - file, - " RBSLocation::new(unsafe {{ (*self.pointer).{} }}, self.parser)", - field.c_name() - )?; - writeln!(file, " }}")?; + write_node_field_accessor(&mut file, field, "RBSLocation")?; } "rbs_location_list" => { - writeln!( - file, - " pub fn {}(&self) -> RBSLocationList {{", - field.name - )?; - writeln!( - file, - " RBSLocationList::new(unsafe {{ (*self.pointer).{} }}, self.parser)", - field.c_name() - )?; - writeln!(file, " }}")?; + write_node_field_accessor(&mut file, field, "RBSLocationList")?; } "rbs_namespace" => { - writeln!(file, " pub fn {}(&self) -> NamespaceNode {{", field.name)?; - writeln!( - file, - " NamespaceNode {{ parser: self.parser, pointer: unsafe {{ (*self.pointer).{} }} }}", - field.c_name() - )?; - writeln!(file, " }}")?; + write_node_field_accessor(&mut file, field, "NamespaceNode")?; } "rbs_node" => { let name = if field.name == "type" { @@ -282,52 +268,38 @@ fn generate(config: &Config) -> Result<(), Box> { } else { field.name.as_str() }; - - writeln!(file, " pub fn {}(&self) -> Node {{", name)?; - writeln!( - file, - " unsafe {{ Node::new(self.parser, (*self.pointer).{}) }}", - field.c_name() - )?; + if field.optional { + writeln!(file, " pub fn {name}(&self) -> Option {{")?; + writeln!( + file, + " let ptr = unsafe {{ (*self.pointer).{} }};", + field.c_name() + )?; + writeln!( + file, + " if ptr.is_null() {{ None }} else {{ Some(Node::new(self.parser, ptr)) }}" + )?; + } else { + writeln!(file, " pub fn {name}(&self) -> Node {{")?; + writeln!( + file, + " unsafe {{ Node::new(self.parser, (*self.pointer).{}) }}", + field.c_name() + )?; + } writeln!(file, " }}")?; } "rbs_node_list" => { - writeln!(file, " pub fn {}(&self) -> NodeList {{", field.name)?; - writeln!( - file, - " NodeList::new(self.parser, unsafe {{ (*self.pointer).{} }})", - field.c_name() - )?; - writeln!(file, " }}")?; - } - "rbs_keyword" => { - writeln!(file, " pub fn {}(&self) -> KeywordNode {{", field.name)?; - writeln!( - file, - " KeywordNode {{ parser: self.parser, pointer: unsafe {{ (*self.pointer).{} }} }}", - field.c_name() - )?; - writeln!(file, " }}")?; + write_node_field_accessor(&mut file, field, "NodeList")?; } + "rbs_keyword" => write_node_field_accessor(&mut file, field, "KeywordNode")?, "rbs_type_name" => { - writeln!(file, " pub fn {}(&self) -> TypeNameNode {{", field.name)?; - writeln!( - file, - " TypeNameNode {{ parser: self.parser, pointer: unsafe {{ (*self.pointer).{} }} }}", - field.c_name() - )?; - writeln!(file, " }}")?; + write_node_field_accessor(&mut file, field, "TypeNameNode")?; } "rbs_types_block" => { - writeln!(file, " pub fn {}(&self) -> BlockTypeNode {{", field.name)?; - writeln!( - file, - " BlockTypeNode {{ parser: self.parser, pointer: unsafe {{ (*self.pointer).{} }} }}", - field.c_name() - )?; - writeln!(file, " }}")?; + write_node_field_accessor(&mut file, field, "BlockTypeNode")? } - _ => eprintln!("Unknown field type: {}", field.c_type), + _ => panic!("Unknown field type: {}", field.c_type), } } } @@ -351,7 +323,7 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!(file, " #[allow(clippy::missing_safety_doc)]")?; writeln!( file, - " pub unsafe fn new(parser: *mut rbs_parser_t, node: *mut rbs_node_t) -> Self {{" + " fn new(parser: *mut rbs_parser_t, node: *mut rbs_node_t) -> Self {{" )?; writeln!(file, " match unsafe {{ (*node).type_ }} {{")?; for node in &config.nodes { diff --git a/rust/ruby-rbs/src/lib.rs b/rust/ruby-rbs/src/lib.rs index d92e767ca..cad95c118 100644 --- a/rust/ruby-rbs/src/lib.rs +++ b/rust/ruby-rbs/src/lib.rs @@ -64,7 +64,7 @@ impl Iterator for NodeListIter { None } else { let pointer_data = unsafe { *self.current }; - let node = unsafe { Node::new(self.parser, pointer_data.node) }; + let node = Node::new(self.parser, pointer_data.node); self.current = pointer_data.next; Some(node) } @@ -124,8 +124,8 @@ impl Iterator for RBSHashIter { None } else { let pointer_data = unsafe { *self.current }; - let key = unsafe { Node::new(self.parser, pointer_data.key) }; - let value = unsafe { Node::new(self.parser, pointer_data.value) }; + let key = Node::new(self.parser, pointer_data.key); + let value = Node::new(self.parser, pointer_data.value); self.current = pointer_data.next; Some((key, value)) } From ec89faab2ebfe109675d98617583f3071b94aff8 Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Wed, 17 Dec 2025 16:46:48 -0800 Subject: [PATCH 23/34] Generate child node traversal in visitor functions (#72) The Visit trait added in #69 provided the scaffolding for AST traversal, but the visitor functions were empty stubs that didn't recurse into children nodes. Without this, the visitor pattern is incomplete as we'd have to manually write traversal logic every time we want to walk the tree. This commit adds the generation of visitor functions for child node traversal. We handle four field types: - `rbs_node`: single child node - `rbs_node_list`: list of child nodes - `rbs_hash`: key-value pairs of nodes - Wrapper types (`rbs_type_name`, `rbs_namespace`, etc): each with its own visitor method Each case handles optional fields to safely skip NULL pointers --- rust/ruby-rbs/build.rs | 127 ++++++++++++++++++++++++++++++++++++++- rust/ruby-rbs/src/lib.rs | 109 +++++++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+), 1 deletion(-) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index 0c7e3c258..b2142fa86 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -185,16 +185,135 @@ fn write_visit_trait(file: &mut File, config: &Config) -> Result<(), Box `visit_type_name_node`). + let visitor_method_names: std::collections::HashMap = config + .nodes + .iter() + .map(|node| { + let c_type = convert_name(&node.name, CIdentifier::Type); + let c_type = c_type.strip_suffix("_t").unwrap_or(&c_type).to_string(); + let method = convert_name(node.variant_name(), CIdentifier::Method); + (c_type, method) + }) + .collect(); + + let is_visitable = |c_type: &str| -> bool { + matches!(c_type, "rbs_node" | "rbs_node_list" | "rbs_hash") + || visitor_method_names.contains_key(c_type) + }; + for node in &config.nodes { let node_variant_name = node.variant_name(); let method_name = convert_name(node_variant_name, CIdentifier::Method); - writeln!(file, "#[allow(unused_variables)]")?; // TODO: Remove this once all nodes that need visitor are implemented + let has_visitable_fields = node + .fields + .iter() + .flatten() + .any(|field| is_visitable(&field.c_type)); + + if !has_visitable_fields { + // If there's nothing to visit in this node, write the empty method with + // underscored parameters, and skip to the next iteration + writeln!( + file, + "pub fn visit_{method_name}_node(_visitor: &mut V, _node: &{node_variant_name}Node) {{}}" + )?; + + continue; + } + writeln!( file, "pub fn visit_{}_node(visitor: &mut V, node: &{}Node) {{", method_name, node_variant_name )?; + + if let Some(fields) = &node.fields { + for field in fields { + let field_method_name = if field.name == "type" { + "type_" + } else { + field.name.as_str() + }; + + match field.c_type.as_str() { + "rbs_node" => { + if field.optional { + writeln!( + file, + " if let Some(item) = node.{field_method_name}() {{" + )?; + writeln!(file, " visitor.visit(&item);")?; + writeln!(file, " }}")?; + } else { + writeln!(file, " visitor.visit(&node.{field_method_name}());")?; + } + } + + "rbs_node_list" => { + if field.optional { + writeln!( + file, + " if let Some(list) = node.{field_method_name}() {{" + )?; + writeln!(file, " for item in list.iter() {{")?; + writeln!(file, " visitor.visit(&item);")?; + writeln!(file, " }}")?; + writeln!(file, " }}")?; + } else { + writeln!(file, " for item in node.{field_method_name}().iter() {{")?; + writeln!(file, " visitor.visit(&item);")?; + writeln!(file, " }}")?; + } + } + + "rbs_hash" => { + if field.optional { + writeln!( + file, + " if let Some(hash) = node.{field_method_name}() {{" + )?; + writeln!(file, " for (key, value) in hash.iter() {{")?; + writeln!(file, " visitor.visit(&key);")?; + writeln!(file, " visitor.visit(&value);")?; + writeln!(file, " }}")?; + writeln!(file, " }}")?; + } else { + writeln!( + file, + " for (key, value) in node.{field_method_name}().iter() {{" + )?; + writeln!(file, " visitor.visit(&key);")?; + writeln!(file, " visitor.visit(&value);")?; + writeln!(file, " }}")?; + } + } + + _ => { + if let Some(visit_method_name) = visitor_method_names.get(&field.c_type) { + if field.optional { + writeln!( + file, + " if let Some(item) = node.{field_method_name}() {{" + )?; + writeln!( + file, + " visitor.visit_{visit_method_name}_node(&item);" + )?; + writeln!(file, " }}")?; + } else { + writeln!( + file, + " visitor.visit_{visit_method_name}_node(&node.{field_method_name}());" + )?; + } + } + } + } + } + } writeln!(file, "}}")?; writeln!(file)?; } @@ -226,6 +345,12 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!(file, "}}\n")?; writeln!(file, "impl {} {{", node.rust_name)?; + writeln!(file, " /// Converts this node to a generic node.")?; + writeln!(file, " #[must_use]")?; + writeln!(file, " pub fn as_node(self) -> Node {{")?; + writeln!(file, " Node::{}(self)", node.variant_name())?; + writeln!(file, " }}")?; + if let Some(fields) = &node.fields { for field in fields { match field.c_type.as_str() { diff --git a/rust/ruby-rbs/src/lib.rs b/rust/ruby-rbs/src/lib.rs index cad95c118..45df7145f 100644 --- a/rust/ruby-rbs/src/lib.rs +++ b/rust/ruby-rbs/src/lib.rs @@ -326,4 +326,113 @@ mod tests { panic!("Expected TypeAlias with RecordType"); } } + + #[test] + fn visitor_test() { + struct Visitor { + visited: Vec, + } + + impl Visit for Visitor { + fn visit_bool_type_node(&mut self, node: &BoolTypeNode) { + self.visited.push("type:bool".to_string()); + + crate::visit_bool_type_node(self, node); + } + + fn visit_class_node(&mut self, node: &ClassNode) { + self.visited.push(format!( + "class:{}", + String::from_utf8(node.name().name().name().to_vec()).unwrap() + )); + + crate::visit_class_node(self, node); + } + + fn visit_class_instance_type_node(&mut self, node: &ClassInstanceTypeNode) { + self.visited.push(format!( + "type:{}", + String::from_utf8(node.name().name().name().to_vec()).unwrap() + )); + + crate::visit_class_instance_type_node(self, node); + } + + fn visit_class_super_node(&mut self, node: &ClassSuperNode) { + self.visited.push(format!( + "super:{}", + String::from_utf8(node.name().name().name().to_vec()).unwrap() + )); + + crate::visit_class_super_node(self, node); + } + + fn visit_function_type_node(&mut self, node: &FunctionTypeNode) { + let count = node.required_positionals().iter().count(); + self.visited + .push(format!("function:required_positionals:{count}")); + + crate::visit_function_type_node(self, node); + } + + fn visit_method_definition_node(&mut self, node: &MethodDefinitionNode) { + self.visited.push(format!( + "method:{}", + String::from_utf8(node.name().name().to_vec()).unwrap() + )); + + crate::visit_method_definition_node(self, node); + } + + fn visit_record_type_node(&mut self, node: &RecordTypeNode) { + self.visited.push("record".to_string()); + + crate::visit_record_type_node(self, node); + } + + fn visit_symbol_node(&mut self, node: &SymbolNode) { + self.visited.push(format!( + "symbol:{}", + String::from_utf8(node.name().to_vec()).unwrap() + )); + + crate::visit_symbol_node(self, node); + } + } + + let rbs_code = r#" + class Foo < Bar + def process: ({ name: String, age: Integer }, bool) -> void + end + "#; + + let signature = parse(rbs_code.as_bytes()).unwrap(); + + let mut visitor = Visitor { + visited: Vec::new(), + }; + + visitor.visit(&signature.as_node()); + + assert_eq!( + vec![ + "class:Foo", + "symbol:Foo", + "super:Bar", + "symbol:Bar", + "method:process", + "symbol:process", + "function:required_positionals:2", + "record", + "symbol:name", + "type:String", + "symbol:String", + "symbol:age", + "type:Integer", + "symbol:Integer", + "type:bool", + ], + visitor.visited + ); + } } From 1c1f866e826872b5c78b42491b48abe1435af10b Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:44:49 -0800 Subject: [PATCH 24/34] Generate location() accessor for each node type (#74) Each node already has location data in its C struct, but it wasn't exposed through the Rust API. This adds a generated `location()` method to every node type, making it easy to get source ranges for any part of the AST. Also removing `parser` from location structs as it is not needed. --- rust/ruby-rbs/build.rs | 68 ++++++++++++++++++++++++++++++++++++++-- rust/ruby-rbs/src/lib.rs | 47 ++++++++++++++++++++------- 2 files changed, 101 insertions(+), 14 deletions(-) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index b2142fa86..950338f26 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -350,6 +350,16 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!(file, " pub fn as_node(self) -> Node {{")?; writeln!(file, " Node::{}(self)", node.variant_name())?; writeln!(file, " }}")?; + writeln!(file)?; + writeln!(file, " /// Returns the location of this node.")?; + writeln!(file, " #[must_use]")?; + writeln!(file, " pub fn location(&self) -> RBSLocation {{")?; + writeln!( + file, + " RBSLocation::new(unsafe {{ (*self.pointer).base.location }})" + )?; + writeln!(file, " }}")?; + writeln!(file)?; if let Some(fields) = &node.fields { for field in fields { @@ -379,10 +389,64 @@ fn generate(config: &Config) -> Result<(), Box> { write_node_field_accessor(&mut file, field, "RBSHash")?; } "rbs_location" => { - write_node_field_accessor(&mut file, field, "RBSLocation")?; + if field.optional { + writeln!( + file, + " pub fn {}(&self) -> Option {{", + field.name + )?; + writeln!( + file, + " let ptr = unsafe {{ (*self.pointer).{} }};", + field.c_name() + )?; + writeln!(file, " if ptr.is_null() {{")?; + writeln!(file, " None")?; + writeln!(file, " }} else {{")?; + writeln!(file, " Some(RBSLocation {{ pointer: ptr }})")?; + writeln!(file, " }}")?; + writeln!(file, " }}")?; + } else { + writeln!(file, " pub fn {}(&self) -> RBSLocation {{", field.name)?; + writeln!( + file, + " RBSLocation {{ pointer: unsafe {{ (*self.pointer).{} }} }}", + field.c_name() + )?; + writeln!(file, " }}")?; + } } "rbs_location_list" => { - write_node_field_accessor(&mut file, field, "RBSLocationList")?; + if field.optional { + writeln!( + file, + " pub fn {}(&self) -> Option {{", + field.name + )?; + writeln!( + file, + " let ptr = unsafe {{ (*self.pointer).{} }};", + field.c_name() + )?; + writeln!(file, " if ptr.is_null() {{")?; + writeln!(file, " None")?; + writeln!(file, " }} else {{")?; + writeln!(file, " Some(RBSLocationList {{ pointer: ptr }})")?; + writeln!(file, " }}")?; + writeln!(file, " }}")?; + } else { + writeln!( + file, + " pub fn {}(&self) -> RBSLocationList {{", + field.name + )?; + writeln!( + file, + " RBSLocationList {{ pointer: unsafe {{ (*self.pointer).{} }} }}", + field.c_name() + )?; + writeln!(file, " }}")?; + } } "rbs_namespace" => { write_node_field_accessor(&mut file, field, "NamespaceNode")?; diff --git a/rust/ruby-rbs/src/lib.rs b/rust/ruby-rbs/src/lib.rs index 45df7145f..0dc116263 100644 --- a/rust/ruby-rbs/src/lib.rs +++ b/rust/ruby-rbs/src/lib.rs @@ -134,27 +134,24 @@ impl Iterator for RBSHashIter { pub struct RBSLocation { pointer: *const rbs_location_t, - #[allow(dead_code)] - parser: *mut rbs_parser_t, } impl RBSLocation { - pub fn new(pointer: *const rbs_location_t, parser: *mut rbs_parser_t) -> Self { - Self { pointer, parser } + pub fn new(pointer: *const rbs_location_t) -> Self { + Self { pointer } } - pub fn start_loc(&self) -> i32 { + pub fn start(&self) -> i32 { unsafe { (*self.pointer).rg.start.byte_pos } } - pub fn end_loc(&self) -> i32 { + pub fn end(&self) -> i32 { unsafe { (*self.pointer).rg.end.byte_pos } } } pub struct RBSLocationListIter { current: *mut rbs_location_list_node_t, - parser: *mut rbs_parser_t, } impl Iterator for RBSLocationListIter { @@ -165,7 +162,7 @@ impl Iterator for RBSLocationListIter { None } else { let pointer_data = unsafe { *self.current }; - let loc = RBSLocation::new(pointer_data.loc, self.parser); + let loc = RBSLocation::new(pointer_data.loc); self.current = pointer_data.next; Some(loc) } @@ -174,12 +171,11 @@ impl Iterator for RBSLocationListIter { pub struct RBSLocationList { pointer: *mut rbs_location_list, - parser: *mut rbs_parser_t, } impl RBSLocationList { - pub fn new(pointer: *mut rbs_location_list, parser: *mut rbs_parser_t) -> Self { - Self { pointer, parser } + pub fn new(pointer: *mut rbs_location_list) -> Self { + Self { pointer } } /// Returns an iterator over the locations. @@ -187,7 +183,6 @@ impl RBSLocationList { pub fn iter(&self) -> RBSLocationListIter { RBSLocationListIter { current: unsafe { (*self.pointer).head }, - parser: self.parser, } } } @@ -435,4 +430,32 @@ mod tests { visitor.visited ); } + + #[test] + fn test_node_location_ranges() { + let rbs_code = r#"type foo = 1"#; + let signature = parse(rbs_code.as_bytes()).unwrap(); + + let declaration = signature.declarations().iter().next().unwrap(); + let Node::TypeAlias(type_alias) = declaration else { + panic!("Expected TypeAlias"); + }; + + // TypeAlias spans the entire declaration + let loc = type_alias.location(); + assert_eq!(0, loc.start()); + assert_eq!(12, loc.end()); + + // The literal "1" is at position 11-12 + let Node::LiteralType(literal) = type_alias.type_() else { + panic!("Expected LiteralType"); + }; + let Node::Integer(integer) = literal.literal() else { + panic!("Expected Integer"); + }; + + let int_loc = integer.location(); + assert_eq!(11, int_loc.start()); + assert_eq!(12, int_loc.end()); + } } From 403883585528986d596c1d6bcd6f3280b4dd70a5 Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Mon, 5 Jan 2026 09:20:42 -0800 Subject: [PATCH 25/34] Use inline format args (#73) Addressing some linting warnings --- rust/ruby-rbs/build.rs | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index 950338f26..96170bcb1 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -119,8 +119,8 @@ fn write_node_field_accessor( if field.optional { writeln!( file, - " pub fn {}(&self) -> Option<{}> {{", - field.name, rust_type + " pub fn {}(&self) -> Option<{rust_type}> {{", + field.name, )?; writeln!( file, @@ -136,11 +136,10 @@ fn write_node_field_accessor( )?; writeln!(file, " }}")?; } else { - writeln!(file, " pub fn {}(&self) -> {} {{", field.name, rust_type)?; + writeln!(file, " pub fn {}(&self) -> {rust_type} {{", field.name)?; writeln!( file, - " {} {{ parser: self.parser, pointer: unsafe {{ (*self.pointer).{} }} }}", - rust_type, + " {rust_type} {{ parser: self.parser, pointer: unsafe {{ (*self.pointer).{} }} }}", field.c_name() )?; } @@ -161,8 +160,8 @@ fn write_visit_trait(file: &mut File, config: &Config) -> Result<(), Box {{", node_variant_name)?; - writeln!(file, " self.visit_{}_node(it);", method_name,)?; + writeln!(file, " Node::{node_variant_name}(it) => {{")?; + writeln!(file, " self.visit_{method_name}_node(it);")?; writeln!(file, " }}")?; } @@ -176,10 +175,9 @@ fn write_visit_trait(file: &mut File, config: &Config) -> Result<(), Box Result<(), Box(visitor: &mut V, node: &{}Node) {{", - method_name, node_variant_name + "pub fn visit_{method_name}_node(visitor: &mut V, node: &{node_variant_name}Node) {{" )?; if let Some(fields) = &node.fields { @@ -504,7 +501,7 @@ fn generate(config: &Config) -> Result<(), Box> { .strip_suffix("Node") .unwrap_or(&node.rust_name); - writeln!(file, " {}({}),", variant_name, node.rust_name)?; + writeln!(file, " {variant_name}({}),", node.rust_name)?; } writeln!(file, "}}")?; @@ -517,14 +514,13 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!(file, " match unsafe {{ (*node).type_ }} {{")?; for node in &config.nodes { let enum_name = convert_name(&node.name, CIdentifier::Constant); + let c_type = convert_name(&node.name, CIdentifier::Type); writeln!( file, - " rbs_node_type::{} => Self::{}({} {{ parser, pointer: node.cast::<{}>() }}),", - enum_name, + " rbs_node_type::{enum_name} => Self::{}({} {{ parser, pointer: node.cast::<{c_type}>() }}),", node.variant_name(), node.rust_name, - convert_name(&node.name, CIdentifier::Type) )?; } writeln!( From 97e8cbd8e302e13c9ae4af5223e17b305567d067 Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Thu, 8 Jan 2026 10:32:04 -0800 Subject: [PATCH 26/34] Generate location() accessor for Node enum (#76) Adds `location()` accessor to the `Node` enum, delegating to each variant's `location()` method. A previous commit added `location()` to individual node types but missed the enum itself. This allows getting the location of the entire node definition when working with the `Node` enum directly. --- rust/ruby-rbs/build.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index 96170bcb1..517bc37dd 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -529,6 +529,20 @@ fn generate(config: &Config) -> Result<(), Box> { )?; writeln!(file, " }}")?; writeln!(file, " }}")?; + writeln!(file)?; + writeln!(file, " /// Returns the location of the entire node.")?; + writeln!(file, " #[must_use]")?; + writeln!(file, " pub fn location(&self) -> RBSLocation {{")?; + writeln!(file, " match self {{")?; + for node in &config.nodes { + writeln!( + file, + " Node::{}(node) => node.location(),", + node.variant_name() + )?; + } + writeln!(file, " }}")?; + writeln!(file, " }}")?; writeln!(file, "}}")?; writeln!(file)?; From 0392b630a58b4cc72f2e44d9c57c581ecc331f00 Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Tue, 13 Jan 2026 12:48:33 -0800 Subject: [PATCH 27/34] Small polish pass (#77) Reorder lib.rs structs alphabetically Improve bindings code formatting --- rust/ruby-rbs/build.rs | 9 +++- rust/ruby-rbs/src/lib.rs | 100 +++++++++++++++++++-------------------- 2 files changed, 58 insertions(+), 51 deletions(-) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index 517bc37dd..5eb249233 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -143,7 +143,9 @@ fn write_node_field_accessor( field.c_name() )?; } - writeln!(file, " }}") + writeln!(file, " }}")?; + writeln!(file)?; + Ok(()) } fn write_visit_trait(file: &mut File, config: &Config) -> Result<(), Box> { @@ -369,11 +371,13 @@ fn generate(config: &Config) -> Result<(), Box> { field.c_name() )?; writeln!(file, " }}")?; + writeln!(file)?; } "bool" => { writeln!(file, " pub fn {}(&self) -> bool {{", field.name)?; writeln!(file, " unsafe {{ (*self.pointer).{} }}", field.name)?; writeln!(file, " }}")?; + writeln!(file)?; } "rbs_ast_comment" => { write_node_field_accessor(&mut file, field, "CommentNode")? @@ -412,6 +416,7 @@ fn generate(config: &Config) -> Result<(), Box> { )?; writeln!(file, " }}")?; } + writeln!(file)?; } "rbs_location_list" => { if field.optional { @@ -444,6 +449,7 @@ fn generate(config: &Config) -> Result<(), Box> { )?; writeln!(file, " }}")?; } + writeln!(file)?; } "rbs_namespace" => { write_node_field_accessor(&mut file, field, "NamespaceNode")?; @@ -474,6 +480,7 @@ fn generate(config: &Config) -> Result<(), Box> { )?; } writeln!(file, " }}")?; + writeln!(file)?; } "rbs_node_list" => { write_node_field_accessor(&mut file, field, "NodeList")?; diff --git a/rust/ruby-rbs/src/lib.rs b/rust/ruby-rbs/src/lib.rs index 0dc116263..ba4130365 100644 --- a/rust/ruby-rbs/src/lib.rs +++ b/rust/ruby-rbs/src/lib.rs @@ -51,22 +51,19 @@ impl Drop for SignatureNode { } } -pub struct NodeListIter { - parser: *mut rbs_parser_t, - current: *mut rbs_node_list_node_t, -} - -impl Iterator for NodeListIter { - type Item = Node; +impl KeywordNode { + pub fn name(&self) -> &[u8] { + unsafe { + let constant_ptr = rbs_constant_pool_id_to_constant( + &(*self.parser).constant_pool, + (*self.pointer).constant_id, + ); + if constant_ptr.is_null() { + panic!("Constant ID for keyword is not present in the pool"); + } - fn next(&mut self) -> Option { - if self.current.is_null() { - None - } else { - let pointer_data = unsafe { *self.current }; - let node = Node::new(self.parser, pointer_data.node); - self.current = pointer_data.next; - Some(node) + let constant = &*constant_ptr; + std::slice::from_raw_parts(constant.start, constant.length) } } } @@ -91,6 +88,26 @@ impl NodeList { } } +pub struct NodeListIter { + parser: *mut rbs_parser_t, + current: *mut rbs_node_list_node_t, +} + +impl Iterator for NodeListIter { + type Item = Node; + + fn next(&mut self) -> Option { + if self.current.is_null() { + None + } else { + let pointer_data = unsafe { *self.current }; + let node = Node::new(self.parser, pointer_data.node); + self.current = pointer_data.next; + Some(node) + } + } +} + pub struct RBSHash { parser: *mut rbs_parser_t, pointer: *mut rbs_hash, @@ -150,6 +167,24 @@ impl RBSLocation { } } +pub struct RBSLocationList { + pointer: *mut rbs_location_list, +} + +impl RBSLocationList { + pub fn new(pointer: *mut rbs_location_list) -> Self { + Self { pointer } + } + + /// Returns an iterator over the locations. + #[must_use] + pub fn iter(&self) -> RBSLocationListIter { + RBSLocationListIter { + current: unsafe { (*self.pointer).head }, + } + } +} + pub struct RBSLocationListIter { current: *mut rbs_location_list_node_t, } @@ -169,24 +204,6 @@ impl Iterator for RBSLocationListIter { } } -pub struct RBSLocationList { - pointer: *mut rbs_location_list, -} - -impl RBSLocationList { - pub fn new(pointer: *mut rbs_location_list) -> Self { - Self { pointer } - } - - /// Returns an iterator over the locations. - #[must_use] - pub fn iter(&self) -> RBSLocationListIter { - RBSLocationListIter { - current: unsafe { (*self.pointer).head }, - } - } -} - #[derive(Debug)] pub struct RBSString { pointer: *const rbs_string_t, @@ -222,23 +239,6 @@ impl SymbolNode { } } -impl KeywordNode { - pub fn name(&self) -> &[u8] { - unsafe { - let constant_ptr = rbs_constant_pool_id_to_constant( - &(*self.parser).constant_pool, - (*self.pointer).constant_id, - ); - if constant_ptr.is_null() { - panic!("Constant ID for keyword is not present in the pool"); - } - - let constant = &*constant_ptr; - std::slice::from_raw_parts(constant.start, constant.length) - } - } -} - #[cfg(test)] mod tests { use super::*; From 9f719e2541aeb240de27bf99685d58e1051a6093 Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Tue, 13 Jan 2026 13:05:07 -0800 Subject: [PATCH 28/34] Add lifetimes (#78) Adds lifetimes to make borrowing relationships clearer so the Rust compiler can validate and enforce them. --- rust/ruby-rbs/build.rs | 35 +++++++++++++++++---------- rust/ruby-rbs/src/lib.rs | 52 ++++++++++++++++++++++++++-------------- 2 files changed, 56 insertions(+), 31 deletions(-) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index 5eb249233..bd0d6d2ca 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -119,7 +119,7 @@ fn write_node_field_accessor( if field.optional { writeln!( file, - " pub fn {}(&self) -> Option<{rust_type}> {{", + " pub fn {}(&self) -> Option<{rust_type}<'a>> {{", field.name, )?; writeln!( @@ -132,14 +132,18 @@ fn write_node_field_accessor( writeln!(file, " }} else {{")?; writeln!( file, - " Some({rust_type} {{ parser: self.parser, pointer: ptr }})" + " Some({rust_type} {{ parser: self.parser, pointer: ptr, marker: PhantomData }})" )?; writeln!(file, " }}")?; } else { - writeln!(file, " pub fn {}(&self) -> {rust_type} {{", field.name)?; writeln!( file, - " {rust_type} {{ parser: self.parser, pointer: unsafe {{ (*self.pointer).{} }} }}", + " pub fn {}(&self) -> {rust_type}<'a> {{", + field.name + )?; + writeln!( + file, + " {rust_type} {{ parser: self.parser, pointer: unsafe {{ (*self.pointer).{} }}, marker: PhantomData }}", field.c_name() )?; } @@ -334,19 +338,24 @@ fn generate(config: &Config) -> Result<(), Box> { for node in &config.nodes { writeln!(file, "#[allow(dead_code)]")?; // TODO: Remove this once all nodes that need parser are implemented writeln!(file, "#[derive(Debug)]")?; - writeln!(file, "pub struct {} {{", node.rust_name)?; + writeln!(file, "pub struct {}<'a> {{", node.rust_name)?; writeln!(file, " parser: *mut rbs_parser_t,")?; writeln!( file, " pointer: *mut {},", convert_name(&node.name, CIdentifier::Type) )?; + writeln!( + file, + " marker: PhantomData<&'a mut {}>", + convert_name(&node.name, CIdentifier::Type) + )?; writeln!(file, "}}\n")?; - writeln!(file, "impl {} {{", node.rust_name)?; + writeln!(file, "impl<'a> {}<'a> {{", node.rust_name)?; writeln!(file, " /// Converts this node to a generic node.")?; writeln!(file, " #[must_use]")?; - writeln!(file, " pub fn as_node(self) -> Node {{")?; + writeln!(file, " pub fn as_node(self) -> Node<'a> {{")?; writeln!(file, " Node::{}(self)", node.variant_name())?; writeln!(file, " }}")?; writeln!(file)?; @@ -461,7 +470,7 @@ fn generate(config: &Config) -> Result<(), Box> { field.name.as_str() }; if field.optional { - writeln!(file, " pub fn {name}(&self) -> Option {{")?; + writeln!(file, " pub fn {name}(&self) -> Option> {{")?; writeln!( file, " let ptr = unsafe {{ (*self.pointer).{} }};", @@ -472,7 +481,7 @@ fn generate(config: &Config) -> Result<(), Box> { " if ptr.is_null() {{ None }} else {{ Some(Node::new(self.parser, ptr)) }}" )?; } else { - writeln!(file, " pub fn {name}(&self) -> Node {{")?; + writeln!(file, " pub fn {name}(&self) -> Node<'a> {{")?; writeln!( file, " unsafe {{ Node::new(self.parser, (*self.pointer).{}) }}", @@ -501,18 +510,18 @@ fn generate(config: &Config) -> Result<(), Box> { // Generate the Node enum to wrap all of the structs writeln!(file, "#[derive(Debug)]")?; - writeln!(file, "pub enum Node {{")?; + writeln!(file, "pub enum Node<'a> {{")?; for node in &config.nodes { let variant_name = node .rust_name .strip_suffix("Node") .unwrap_or(&node.rust_name); - writeln!(file, " {variant_name}({}),", node.rust_name)?; + writeln!(file, " {variant_name}({}<'a>),", node.rust_name)?; } writeln!(file, "}}")?; - writeln!(file, "impl Node {{")?; + writeln!(file, "impl Node<'_> {{")?; writeln!(file, " #[allow(clippy::missing_safety_doc)]")?; writeln!( file, @@ -525,7 +534,7 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!( file, - " rbs_node_type::{enum_name} => Self::{}({} {{ parser, pointer: node.cast::<{c_type}>() }}),", + " rbs_node_type::{enum_name} => Self::{}({} {{ parser, pointer: node.cast::<{c_type}>(), marker: PhantomData }}),", node.variant_name(), node.rust_name, )?; diff --git a/rust/ruby-rbs/src/lib.rs b/rust/ruby-rbs/src/lib.rs index ba4130365..1ffa89513 100644 --- a/rust/ruby-rbs/src/lib.rs +++ b/rust/ruby-rbs/src/lib.rs @@ -1,6 +1,7 @@ include!(concat!(env!("OUT_DIR"), "/bindings.rs")); use rbs_encoding_type_t::RBS_ENCODING_UTF_8; use ruby_rbs_sys::bindings::*; +use std::marker::PhantomData; use std::sync::Once; static INIT: Once = Once::new(); @@ -13,7 +14,7 @@ static INIT: Once = Once::new(); /// let signature = parse(rbs_code.as_bytes()); /// assert!(signature.is_ok(), "Failed to parse RBS signature"); /// ``` -pub fn parse(rbs_code: &[u8]) -> Result { +pub fn parse(rbs_code: &[u8]) -> Result, String> { unsafe { INIT.call_once(|| { rbs_constant_pool_init(RBS_GLOBAL_CONSTANT_POOL, 26); @@ -33,6 +34,7 @@ pub fn parse(rbs_code: &[u8]) -> Result { let signature_node = SignatureNode { parser, pointer: signature, + marker: PhantomData, }; if result { @@ -43,7 +45,7 @@ pub fn parse(rbs_code: &[u8]) -> Result { } } -impl Drop for SignatureNode { +impl Drop for SignatureNode<'_> { fn drop(&mut self) { unsafe { rbs_parser_free(self.parser); @@ -51,7 +53,7 @@ impl Drop for SignatureNode { } } -impl KeywordNode { +impl KeywordNode<'_> { pub fn name(&self) -> &[u8] { unsafe { let constant_ptr = rbs_constant_pool_id_to_constant( @@ -68,33 +70,40 @@ impl KeywordNode { } } -pub struct NodeList { +pub struct NodeList<'a> { parser: *mut rbs_parser_t, pointer: *mut rbs_node_list_t, + marker: PhantomData<&'a mut rbs_node_list_t>, } -impl NodeList { +impl<'a> NodeList<'a> { pub fn new(parser: *mut rbs_parser_t, pointer: *mut rbs_node_list_t) -> Self { - Self { parser, pointer } + Self { + parser, + pointer, + marker: PhantomData, + } } /// Returns an iterator over the nodes. #[must_use] - pub fn iter(&self) -> NodeListIter { + pub fn iter(&self) -> NodeListIter<'a> { NodeListIter { parser: self.parser, current: unsafe { (*self.pointer).head }, + marker: PhantomData, } } } -pub struct NodeListIter { +pub struct NodeListIter<'a> { parser: *mut rbs_parser_t, current: *mut rbs_node_list_node_t, + marker: PhantomData<&'a mut rbs_node_list_node_t>, } -impl Iterator for NodeListIter { - type Item = Node; +impl<'a> Iterator for NodeListIter<'a> { + type Item = Node<'a>; fn next(&mut self) -> Option { if self.current.is_null() { @@ -108,33 +117,40 @@ impl Iterator for NodeListIter { } } -pub struct RBSHash { +pub struct RBSHash<'a> { parser: *mut rbs_parser_t, pointer: *mut rbs_hash, + marker: PhantomData<&'a mut rbs_hash>, } -impl RBSHash { +impl<'a> RBSHash<'a> { pub fn new(parser: *mut rbs_parser_t, pointer: *mut rbs_hash) -> Self { - Self { parser, pointer } + Self { + parser, + pointer, + marker: PhantomData, + } } /// Returns an iterator over the key-value pairs. #[must_use] - pub fn iter(&self) -> RBSHashIter { + pub fn iter(&self) -> RBSHashIter<'a> { RBSHashIter { parser: self.parser, current: unsafe { (*self.pointer).head }, + marker: PhantomData, } } } -pub struct RBSHashIter { +pub struct RBSHashIter<'a> { parser: *mut rbs_parser_t, current: *mut rbs_hash_node_t, + marker: PhantomData<&'a mut rbs_hash_node_t>, } -impl Iterator for RBSHashIter { - type Item = (Node, Node); +impl<'a> Iterator for RBSHashIter<'a> { + type Item = (Node<'a>, Node<'a>); fn next(&mut self) -> Option { if self.current.is_null() { @@ -222,7 +238,7 @@ impl RBSString { } } -impl SymbolNode { +impl SymbolNode<'_> { pub fn name(&self) -> &[u8] { unsafe { let constant_ptr = rbs_constant_pool_id_to_constant( From 9342ac0d7015e1ce0ce8b29f69e30ea640855de3 Mon Sep 17 00:00:00 2001 From: Alex Rocha <9896751+alexcrocha@users.noreply.github.com> Date: Tue, 13 Jan 2026 13:12:57 -0800 Subject: [PATCH 29/34] Use NonNull wrapper for parser pointers (#79) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced `*mut T` with `NonNull` for the parser pointer to make the ‘never null’ assumption explicit. `NonNull` represents a non-null raw pointer (a wrapper around `*mut T`) that guarantees the pointer is never null. --- rust/ruby-rbs/build.rs | 4 ++-- rust/ruby-rbs/src/lib.rs | 21 +++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index bd0d6d2ca..d0f753cae 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -339,7 +339,7 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!(file, "#[allow(dead_code)]")?; // TODO: Remove this once all nodes that need parser are implemented writeln!(file, "#[derive(Debug)]")?; writeln!(file, "pub struct {}<'a> {{", node.rust_name)?; - writeln!(file, " parser: *mut rbs_parser_t,")?; + writeln!(file, " parser: NonNull,")?; writeln!( file, " pointer: *mut {},", @@ -525,7 +525,7 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!(file, " #[allow(clippy::missing_safety_doc)]")?; writeln!( file, - " fn new(parser: *mut rbs_parser_t, node: *mut rbs_node_t) -> Self {{" + " fn new(parser: NonNull, node: *mut rbs_node_t) -> Self {{" )?; writeln!(file, " match unsafe {{ (*node).type_ }} {{")?; for node in &config.nodes { diff --git a/rust/ruby-rbs/src/lib.rs b/rust/ruby-rbs/src/lib.rs index 1ffa89513..7dcae85f4 100644 --- a/rust/ruby-rbs/src/lib.rs +++ b/rust/ruby-rbs/src/lib.rs @@ -2,6 +2,7 @@ include!(concat!(env!("OUT_DIR"), "/bindings.rs")); use rbs_encoding_type_t::RBS_ENCODING_UTF_8; use ruby_rbs_sys::bindings::*; use std::marker::PhantomData; +use std::ptr::NonNull; use std::sync::Once; static INIT: Once = Once::new(); @@ -32,7 +33,7 @@ pub fn parse(rbs_code: &[u8]) -> Result, String> { let result = rbs_parse_signature(parser, &mut signature); let signature_node = SignatureNode { - parser, + parser: NonNull::new_unchecked(parser), pointer: signature, marker: PhantomData, }; @@ -48,7 +49,7 @@ pub fn parse(rbs_code: &[u8]) -> Result, String> { impl Drop for SignatureNode<'_> { fn drop(&mut self) { unsafe { - rbs_parser_free(self.parser); + rbs_parser_free(self.parser.as_ptr()); } } } @@ -57,7 +58,7 @@ impl KeywordNode<'_> { pub fn name(&self) -> &[u8] { unsafe { let constant_ptr = rbs_constant_pool_id_to_constant( - &(*self.parser).constant_pool, + &(*self.parser.as_ptr()).constant_pool, (*self.pointer).constant_id, ); if constant_ptr.is_null() { @@ -71,13 +72,13 @@ impl KeywordNode<'_> { } pub struct NodeList<'a> { - parser: *mut rbs_parser_t, + parser: NonNull, pointer: *mut rbs_node_list_t, marker: PhantomData<&'a mut rbs_node_list_t>, } impl<'a> NodeList<'a> { - pub fn new(parser: *mut rbs_parser_t, pointer: *mut rbs_node_list_t) -> Self { + pub fn new(parser: NonNull, pointer: *mut rbs_node_list_t) -> Self { Self { parser, pointer, @@ -97,7 +98,7 @@ impl<'a> NodeList<'a> { } pub struct NodeListIter<'a> { - parser: *mut rbs_parser_t, + parser: NonNull, current: *mut rbs_node_list_node_t, marker: PhantomData<&'a mut rbs_node_list_node_t>, } @@ -118,13 +119,13 @@ impl<'a> Iterator for NodeListIter<'a> { } pub struct RBSHash<'a> { - parser: *mut rbs_parser_t, + parser: NonNull, pointer: *mut rbs_hash, marker: PhantomData<&'a mut rbs_hash>, } impl<'a> RBSHash<'a> { - pub fn new(parser: *mut rbs_parser_t, pointer: *mut rbs_hash) -> Self { + pub fn new(parser: NonNull, pointer: *mut rbs_hash) -> Self { Self { parser, pointer, @@ -144,7 +145,7 @@ impl<'a> RBSHash<'a> { } pub struct RBSHashIter<'a> { - parser: *mut rbs_parser_t, + parser: NonNull, current: *mut rbs_hash_node_t, marker: PhantomData<&'a mut rbs_hash_node_t>, } @@ -242,7 +243,7 @@ impl SymbolNode<'_> { pub fn name(&self) -> &[u8] { unsafe { let constant_ptr = rbs_constant_pool_id_to_constant( - &(*self.parser).constant_pool, + &(*self.parser.as_ptr()).constant_pool, (*self.pointer).constant_id, ); if constant_ptr.is_null() { From 37bb8e37297ed28479e0f73e13faec3b0e90bf2e Mon Sep 17 00:00:00 2001 From: Alex Rocha Date: Fri, 19 Dec 2025 17:34:21 -0800 Subject: [PATCH 30/34] Remove TODO comments from rust crate Some nodes don't use their parser field, but conditionally omitting it adds significant complexity. Keep parser on all nodes and suppress the warning on the parser field. --- rust/ruby-rbs/build.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index d0f753cae..5241a982d 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -334,11 +334,10 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!(file, "// Nodes to generate: {}", config.nodes.len())?; writeln!(file)?; - // TODO: Go through all of the nodes and generate the structs to back them up for node in &config.nodes { - writeln!(file, "#[allow(dead_code)]")?; // TODO: Remove this once all nodes that need parser are implemented writeln!(file, "#[derive(Debug)]")?; writeln!(file, "pub struct {}<'a> {{", node.rust_name)?; + writeln!(file, " #[allow(dead_code)]")?; writeln!(file, " parser: NonNull,")?; writeln!( file, From b5d0d0dda3f575e958ab1ef191d2c2215fbba9e6 Mon Sep 17 00:00:00 2001 From: Alex Rocha Date: Tue, 13 Jan 2026 16:52:08 -0800 Subject: [PATCH 31/34] Add must_use attributes to accessor methods --- rust/ruby-rbs/build.rs | 10 ++++++++++ rust/ruby-rbs/src/lib.rs | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index 5241a982d..7dbbefc5a 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -117,6 +117,7 @@ fn write_node_field_accessor( rust_type: &str, ) -> std::io::Result<()> { if field.optional { + writeln!(file, " #[must_use]")?; writeln!( file, " pub fn {}(&self) -> Option<{rust_type}<'a>> {{", @@ -136,6 +137,7 @@ fn write_node_field_accessor( )?; writeln!(file, " }}")?; } else { + writeln!(file, " #[must_use]")?; writeln!( file, " pub fn {}(&self) -> {rust_type}<'a> {{", @@ -372,6 +374,7 @@ fn generate(config: &Config) -> Result<(), Box> { for field in fields { match field.c_type.as_str() { "rbs_string" => { + writeln!(file, " #[must_use]")?; writeln!(file, " pub fn {}(&self) -> RBSString {{", field.name)?; writeln!( file, @@ -382,6 +385,7 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!(file)?; } "bool" => { + writeln!(file, " #[must_use]")?; writeln!(file, " pub fn {}(&self) -> bool {{", field.name)?; writeln!(file, " unsafe {{ (*self.pointer).{} }}", field.name)?; writeln!(file, " }}")?; @@ -399,6 +403,7 @@ fn generate(config: &Config) -> Result<(), Box> { } "rbs_location" => { if field.optional { + writeln!(file, " #[must_use]")?; writeln!( file, " pub fn {}(&self) -> Option {{", @@ -416,6 +421,7 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!(file, " }}")?; writeln!(file, " }}")?; } else { + writeln!(file, " #[must_use]")?; writeln!(file, " pub fn {}(&self) -> RBSLocation {{", field.name)?; writeln!( file, @@ -428,6 +434,7 @@ fn generate(config: &Config) -> Result<(), Box> { } "rbs_location_list" => { if field.optional { + writeln!(file, " #[must_use]")?; writeln!( file, " pub fn {}(&self) -> Option {{", @@ -445,6 +452,7 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!(file, " }}")?; writeln!(file, " }}")?; } else { + writeln!(file, " #[must_use]")?; writeln!( file, " pub fn {}(&self) -> RBSLocationList {{", @@ -469,6 +477,7 @@ fn generate(config: &Config) -> Result<(), Box> { field.name.as_str() }; if field.optional { + writeln!(file, " #[must_use]")?; writeln!(file, " pub fn {name}(&self) -> Option> {{")?; writeln!( file, @@ -480,6 +489,7 @@ fn generate(config: &Config) -> Result<(), Box> { " if ptr.is_null() {{ None }} else {{ Some(Node::new(self.parser, ptr)) }}" )?; } else { + writeln!(file, " #[must_use]")?; writeln!(file, " pub fn {name}(&self) -> Node<'a> {{")?; writeln!( file, diff --git a/rust/ruby-rbs/src/lib.rs b/rust/ruby-rbs/src/lib.rs index 7dcae85f4..52e43a3b6 100644 --- a/rust/ruby-rbs/src/lib.rs +++ b/rust/ruby-rbs/src/lib.rs @@ -55,6 +55,7 @@ impl Drop for SignatureNode<'_> { } impl KeywordNode<'_> { + #[must_use] pub fn name(&self) -> &[u8] { unsafe { let constant_ptr = rbs_constant_pool_id_to_constant( @@ -78,6 +79,7 @@ pub struct NodeList<'a> { } impl<'a> NodeList<'a> { + #[must_use] pub fn new(parser: NonNull, pointer: *mut rbs_node_list_t) -> Self { Self { parser, @@ -125,6 +127,7 @@ pub struct RBSHash<'a> { } impl<'a> RBSHash<'a> { + #[must_use] pub fn new(parser: NonNull, pointer: *mut rbs_hash) -> Self { Self { parser, @@ -171,14 +174,17 @@ pub struct RBSLocation { } impl RBSLocation { + #[must_use] pub fn new(pointer: *const rbs_location_t) -> Self { Self { pointer } } + #[must_use] pub fn start(&self) -> i32 { unsafe { (*self.pointer).rg.start.byte_pos } } + #[must_use] pub fn end(&self) -> i32 { unsafe { (*self.pointer).rg.end.byte_pos } } @@ -189,6 +195,7 @@ pub struct RBSLocationList { } impl RBSLocationList { + #[must_use] pub fn new(pointer: *mut rbs_location_list) -> Self { Self { pointer } } @@ -227,10 +234,12 @@ pub struct RBSString { } impl RBSString { + #[must_use] pub fn new(pointer: *const rbs_string_t) -> Self { Self { pointer } } + #[must_use] pub fn as_bytes(&self) -> &[u8] { unsafe { let s = *self.pointer; @@ -240,6 +249,7 @@ impl RBSString { } impl SymbolNode<'_> { + #[must_use] pub fn name(&self) -> &[u8] { unsafe { let constant_ptr = rbs_constant_pool_id_to_constant( From da2f7442ae0dc0629b79558c5c34be4bfae7a0f6 Mon Sep 17 00:00:00 2001 From: Alex Rocha Date: Tue, 13 Jan 2026 17:02:46 -0800 Subject: [PATCH 32/34] Remove debug comment from generated bindings --- rust/ruby-rbs/build.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index 7dbbefc5a..b0671b797 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -333,7 +333,6 @@ fn generate(config: &Config) -> Result<(), Box> { let mut file = File::create(&dest_path)?; writeln!(file, "// Generated by build.rs from config.yml")?; - writeln!(file, "// Nodes to generate: {}", config.nodes.len())?; writeln!(file)?; for node in &config.nodes { From 8c5832e8a7d3ab8e72c95a4436391d4c699edcf8 Mon Sep 17 00:00:00 2001 From: Alex Rocha Date: Tue, 13 Jan 2026 17:05:10 -0800 Subject: [PATCH 33/34] Credit Prism for code generation pattern --- rust/ruby-rbs/build.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index b0671b797..a6b72cdf9 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -1,6 +1,9 @@ use serde::Deserialize; use std::{env, error::Error, fs::File, io::Write, path::Path}; +// This config-driven code generation approach is inspired by Prism's ruby-prism crate. +// See: https://github.com/ruby/prism/blob/main/rust/ruby-prism/build.rs + #[derive(Debug, Deserialize)] struct Config { nodes: Vec, From 60838165859c885e18131d03bc4bd04ec4ae4778 Mon Sep 17 00:00:00 2001 From: Alex Rocha Date: Wed, 14 Jan 2026 12:18:38 -0800 Subject: [PATCH 34/34] Add missing rust_name to annotation nodes TypeApplicationAnnotation, InstanceVariableAnnotation, ClassAliasAnnotation, and ModuleAliasAnnotation also need rust_name fields for rust binding code generation. --- config.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config.yml b/config.yml index 27eeebb44..7051fef1b 100644 --- a/config.yml +++ b/config.yml @@ -594,6 +594,7 @@ nodes: - name: comment_location c_type: rbs_location - name: RBS::AST::Ruby::Annotations::TypeApplicationAnnotation + rust_name: TypeApplicationAnnotationNode fields: - name: prefix_location c_type: rbs_location @@ -604,6 +605,7 @@ nodes: - name: comma_locations c_type: rbs_location_list - name: RBS::AST::Ruby::Annotations::InstanceVariableAnnotation + rust_name: InstanceVariableAnnotationNode fields: - name: prefix_location c_type: rbs_location @@ -618,6 +620,7 @@ nodes: - name: comment_location c_type: rbs_location - name: RBS::AST::Ruby::Annotations::ClassAliasAnnotation + rust_name: ClassAliasAnnotationNode fields: - name: prefix_location c_type: rbs_location @@ -628,6 +631,7 @@ nodes: - name: type_name_location c_type: rbs_location - name: RBS::AST::Ruby::Annotations::ModuleAliasAnnotation + rust_name: ModuleAliasAnnotationNode fields: - name: prefix_location c_type: rbs_location