From 297144e471a95fe0ca16532931d0f6b2b648a2a4 Mon Sep 17 00:00:00 2001 From: Alex Rocha Date: Mon, 11 Aug 2025 15:18:36 -0700 Subject: [PATCH 01/32] 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 57981e02c5b5cbc767e684dbad12762732e33fce Mon Sep 17 00:00:00 2001 From: Alex Rocha Date: Fri, 22 Aug 2025 09:52:32 -0400 Subject: [PATCH 02/32] 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 480243c88434d45721569c02e9c97a2309a8e6b5 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/32] Simplify Rust node generation with explicit naming 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 013cd9b31dd03b5945402477e82ef6a59ca57eff 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/32] Handle RBSString types 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 87ce94b2d10b47904ce746d76f2610e1abb14712 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/32] Add parse function to Rust RBS bindings 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 a9da36cce3ff7fbc4939b3d0635dafe08ae4a8cf 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/32] Handle bool primitive types 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 ba442a8123fdce2d487353c72b5d8a5d06eea279 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/32] Handle RBSSymbol types 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 35d00fcdb266bf4acde0ca0ed16bc8f52b276c30 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/32] Add Node and NodeList types to Rust RBS bindings 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 8879daa343810a295d33f840d93c85dfb2348421 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/32] Handle RBSLocation and RBSLocationList types 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 4806ca1655542f9d8f550d9d479611b071240f17 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/32] Handle rbs_node and rbs_node_list types 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 431a3e18830c1655ab8a56f456a11c62f6d961f6 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/32] Handle RBSKeyword types --- 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 94c5b2f7bf4c249ab8220f168f233a6ea84ae489 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/32] Add test demonstrating AST traversal and type checking 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 fb492d67513e77ad59dc6d2418e3d67057a1a543 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/32] Handle CommentNode types --- 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 babe55b2a5a9e827a199f8891cd983483ce66706 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/32] Handle ClassSuperNode types --- 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 e9e1e4dfe34b362b65f568359b0002ea97dfc328 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/32] Handle NamespaceNode types --- 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 3beae9f653026f3999fc638070feb72317555a87 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/32] Handle TypeNameNode types --- 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 d312d1d9ee3ed141336e1937083dc0b9d3d3bd2f 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/32] Handle BlockTypeNode types --- 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 6b3e7de4f2d97416596d1c3ebe94a0aaf5530402 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/32] Refactor Symbol and Keyword as nodes 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 a188680dada02aee5b9eae72274f509cb50d2098 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/32] Handle RBSHash types 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 2c7b79f7affbe2082aa09056c8e6bb5300ad931e 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/32] Add generated Visit trait for AST node traversal 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 48b32af556d884aefac41febc5129588fb0bdf3d 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/32] Annotate nullable pointer fields in config.yml with optional: true 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 12afcbe47190818b9bdfc57ec3edaf2b7c5432b5 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/32] Handle optional field types 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 026d89ad46e8a760eaeabf6ff70f5e5cb958bd92 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/32] Generate child node traversal in visitor functions 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 ed62f25f5f0fd09f5bacffa51447cf6fe871a60f 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/32] Generate location() accessor for each node type 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 2500def5536e1b4698efe5497dd7add33f8d276e 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/32] Use inline format args 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 27ae68339a859b147dba7d4ec337c480d6a0f976 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/32] Generate location() accessor for Node enum 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 c56eaf5773aedb8366d832b21972a02a392164d9 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/32] Small polish pass Reorder lib.rs structs alphabetically Improve bindings code formatting 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. Remove debug comment from generated bindings --- rust/ruby-rbs/build.rs | 13 +++-- rust/ruby-rbs/src/lib.rs | 100 +++++++++++++++++++-------------------- 2 files changed, 59 insertions(+), 54 deletions(-) diff --git a/rust/ruby-rbs/build.rs b/rust/ruby-rbs/build.rs index 517bc37dd..455c237d1 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> { @@ -325,14 +327,12 @@ 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)?; - // 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, " #[allow(dead_code)]")?; writeln!(file, " parser: *mut rbs_parser_t,")?; writeln!( file, @@ -369,11 +369,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 +414,7 @@ fn generate(config: &Config) -> Result<(), Box> { )?; writeln!(file, " }}")?; } + writeln!(file)?; } "rbs_location_list" => { if field.optional { @@ -444,6 +447,7 @@ fn generate(config: &Config) -> Result<(), Box> { )?; writeln!(file, " }}")?; } + writeln!(file)?; } "rbs_namespace" => { write_node_field_accessor(&mut file, field, "NamespaceNode")?; @@ -474,6 +478,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 2839682571f5af3a1a1065fc5a9155ab9a56e344 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/32] Add lifetimes 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 455c237d1..d77a10cc1 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() )?; } @@ -331,7 +335,7 @@ fn generate(config: &Config) -> Result<(), Box> { for node in &config.nodes { writeln!(file, "#[derive(Debug)]")?; - writeln!(file, "pub struct {} {{", node.rust_name)?; + writeln!(file, "pub struct {}<'a> {{", node.rust_name)?; writeln!(file, " #[allow(dead_code)]")?; writeln!(file, " parser: *mut rbs_parser_t,")?; writeln!( @@ -339,12 +343,17 @@ fn generate(config: &Config) -> Result<(), Box> { " 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)?; @@ -459,7 +468,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).{} }};", @@ -470,7 +479,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).{}) }}", @@ -499,18 +508,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, @@ -523,7 +532,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 12fe16a95cc472c3059df432149e872185baafc2 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/32] Use NonNull wrapper for parser pointers 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 d77a10cc1..4bc3903fb 100644 --- a/rust/ruby-rbs/build.rs +++ b/rust/ruby-rbs/build.rs @@ -337,7 +337,7 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!(file, "#[derive(Debug)]")?; writeln!(file, "pub struct {}<'a> {{", node.rust_name)?; writeln!(file, " #[allow(dead_code)]")?; - writeln!(file, " parser: *mut rbs_parser_t,")?; + writeln!(file, " parser: NonNull,")?; writeln!( file, " pointer: *mut {},", @@ -523,7 +523,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 548d438c9d7f9947ab867800f2f62cbfee5f9fb5 Mon Sep 17 00:00:00 2001 From: Alex Rocha Date: Tue, 13 Jan 2026 16:52:08 -0800 Subject: [PATCH 30/32] 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 4bc3903fb..b0671b797 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> {{", @@ -371,6 +373,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, @@ -381,6 +384,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, " }}")?; @@ -398,6 +402,7 @@ fn generate(config: &Config) -> Result<(), Box> { } "rbs_location" => { if field.optional { + writeln!(file, " #[must_use]")?; writeln!( file, " pub fn {}(&self) -> Option {{", @@ -415,6 +420,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, @@ -427,6 +433,7 @@ fn generate(config: &Config) -> Result<(), Box> { } "rbs_location_list" => { if field.optional { + writeln!(file, " #[must_use]")?; writeln!( file, " pub fn {}(&self) -> Option {{", @@ -444,6 +451,7 @@ fn generate(config: &Config) -> Result<(), Box> { writeln!(file, " }}")?; writeln!(file, " }}")?; } else { + writeln!(file, " #[must_use]")?; writeln!( file, " pub fn {}(&self) -> RBSLocationList {{", @@ -468,6 +476,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, @@ -479,6 +488,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 c801005334eebfb8e66a42d2cb6ccecae823a5c4 Mon Sep 17 00:00:00 2001 From: Alex Rocha Date: Tue, 13 Jan 2026 17:05:10 -0800 Subject: [PATCH 31/32] 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 86fb90a4967d1525009e26f8b0812329ec979ded Mon Sep 17 00:00:00 2001 From: Alex Rocha Date: Wed, 14 Jan 2026 12:18:38 -0800 Subject: [PATCH 32/32] 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