diff --git a/spec/bindgen/parser/type_spec.cr b/spec/bindgen/parser/type_spec.cr index 995ef98..9b5593b 100644 --- a/spec/bindgen/parser/type_spec.cr +++ b/spec/bindgen/parser/type_spec.cr @@ -53,7 +53,7 @@ describe Bindgen::Parser::Type do type.reference?.should be_false type.pointer.should eq(1) type.base_name.should eq("int") - type.full_name.should eq("int*") + type.full_name.should eq("int *") end it "recognizes 'int &'" do @@ -71,7 +71,7 @@ describe Bindgen::Parser::Type do type.reference?.should be_true type.pointer.should eq(1) type.base_name.should eq("int") - type.full_name.should eq("int&") + type.full_name.should eq("int &") end it "recognizes 'const int *'" do @@ -101,9 +101,161 @@ describe Bindgen::Parser::Type do type.full_name.should eq("const int **") end + it "recognizes `Container<>`" do + type = parse("Container<>") + type.base_name.should eq("Container<>") + type.full_name.should eq("Container<>") + template = type.template + template.should_not be_nil + template.try(&.base_name).should eq("Container") + template.try(&.full_name).should eq("Container<>") + template.try(&.arguments.empty?).should be_true + end + + it "recognizes `Container`" do + type = parse("Container") + type.base_name.should eq("Container") + type.full_name.should eq("Container") + template = type.template + template.should_not be_nil + template.try(&.base_name).should eq("Container") + template.try(&.full_name).should eq("Container") + template.try(&.arguments.size).should eq(1) + template.try(&.arguments[0]).should eq(parse("int")) + end + + it "recognizes `Container`" do + type = parse("Container") + type.base_name.should eq("Container") + type.full_name.should eq("Container") + template = type.template + template.should_not be_nil + template.try(&.base_name).should eq("Container") + template.try(&.full_name).should eq("Container") + template.try(&.arguments.size).should eq(1) + template.try(&.arguments[0]).should eq(parse("const int *")) + end + + it "recognizes `Container`" do + type = parse("Container") + type.base_name.should eq("Container") + type.full_name.should eq("Container") + template = type.template + template.should_not be_nil + template.try(&.base_name).should eq("Container") + template.try(&.full_name).should eq("Container") + template.try(&.arguments.size).should eq(2) + template.try(&.arguments[0]).should eq(parse("int")) + template.try(&.arguments[1]).should eq(parse("bool")) + end + + it "recognizes `Container >`" do + type = parse("Container >") + type.base_name.should eq("Container >") + type.full_name.should eq("Container >") + template = type.template + template.should_not be_nil + template.try(&.base_name).should eq("Container") + template.try(&.full_name).should eq("Container >") + template.try(&.arguments.size).should eq(1) + template.try(&.arguments[0]).should eq(parse("Container")) + end + + it "recognizes `Container>`" do + type = parse("Container>") + type.base_name.should eq("Container>") + type.full_name.should eq("Container>") + template = type.template + template.should_not be_nil + template.try(&.base_name).should eq("Container") + template.try(&.full_name).should eq("Container >") + template.try(&.arguments.size).should eq(1) + template.try(&.arguments[0]).should eq(parse("Container")) + end + + it "recognizes `Container &>`" do + type = parse("Container &>") + type.base_name.should eq("Container &>") + type.full_name.should eq("Container &>") + template = type.template + template.should_not be_nil + template.try(&.base_name).should eq("Container") + template.try(&.full_name).should eq("Container &>") + template.try(&.arguments.size).should eq(1) + template.try(&.arguments[0]).should eq(parse("const Container &")) + end + it "supports pointer depth offset" do parse("int", 1).pointer.should eq(1) parse("int *", 1).pointer.should eq(2) + + # don't add offset to template argument types + type = parse("Container", 1) + type.pointer.should eq(1) + type.template.not_nil!.arguments[0].pointer.should eq(0) + end + end + + describe "#substitute" do + it "replaces `T` with another type" do + parse("T").substitute("T", parse("int")).should eq(parse("int")) + parse("U").substitute("T", parse("int")).should eq(parse("U")) + end + + it "combines const-ness" do + parse("const T").substitute("T", parse("int")).should eq(parse("const int")) + parse("T").substitute("T", parse("const int")).should eq(parse("const int")) + parse("const T").substitute("T", parse("const int")).should eq(parse("const int")) + end + + it "combines references" do + parse("T").substitute("T", parse("int &")).should eq(parse("int &")) + parse("T &").substitute("T", parse("int")).should eq(parse("int &")) + parse("T &").substitute("T", parse("int &")).should eq(parse("int &")) + end + + it "combines pointers" do + parse("T").substitute("T", parse("int *")).should eq(parse("int *")) + parse("T *").substitute("T", parse("int")).should eq(parse("int *")) + parse("T").substitute("T", parse("int **")).should eq(parse("int **")) + parse("T *").substitute("T", parse("int *")).should eq(parse("int **")) + parse("T **").substitute("T", parse("int")).should eq(parse("int **")) + end + + it "combines mixtures of references and pointers" do + parse("T &").substitute("T", parse("int *")).should eq(parse("int *&")) + parse("T *").substitute("T", parse("int &")).should eq(parse("int *")) + + parse("T").substitute("T", parse("int *&")).should eq(parse("int *&")) + parse("T *").substitute("T", parse("int *&")).should eq(parse("int **")) + parse("T &").substitute("T", parse("int *&")).should eq(parse("int *&")) + + parse("T *&").substitute("T", parse("int")).should eq(parse("int *&")) + parse("T *&").substitute("T", parse("int *")).should eq(parse("int **&")) + parse("T *&").substitute("T", parse("int &")).should eq(parse("int *&")) + parse("T *&").substitute("T", parse("int *&")).should eq(parse("int **&")) + end + + it "replaces `T` in template arguments" do + parse("Ref").substitute("T", parse("int")).should eq(parse("Ref")) + parse("Ref, T>").substitute("T", parse("int")).should eq(parse("Ref, int>")) + + parse("Ref").substitute("Ref", parse("int")).should eq(parse("Ref")) + end + + it "replaces `T` with another template type" do + parse("T").substitute("T", parse("U")).should eq(parse("U")) + parse("U").substitute("T", parse("U")).should eq(parse("U, U >")) + end + + it "accepts multiple simultaneous replacements" do + substs = {"T" => parse("int"), "U" => parse("char")} + parse("T").substitute(substs).should eq(parse("int")) + parse("U").substitute(substs).should eq(parse("char")) + parse("T").substitute(substs).should eq(parse("T")) + + substs = {"X" => parse("Y"), "Y" => parse("X")} + parse("T").substitute(substs).should eq(parse("T")) end end end diff --git a/src/bindgen/parser/argument.cr b/src/bindgen/parser/argument.cr index 2edbac4..f169ee9 100644 --- a/src/bindgen/parser/argument.cr +++ b/src/bindgen/parser/argument.cr @@ -101,6 +101,28 @@ module Bindgen ) end + # Performs type substitution on the type part of this argument using the + # given *replacements*. + def substitute_type(replacements : Hash(String, Type)) : Argument + Argument.new( + name: @name, + type: substitute(replacements), + has_default: @has_default, + value: @value, + ) + end + + # Substitutes all uses of *name* on the type part of this argument with + # the given *type*. + def substitute_type(name : String, with type : Type) : Argument + Argument.new( + name: @name, + type: substitute(name, type), + has_default: @has_default, + value: @value, + ) + end + # Checks if the type-part of this equals the type-part of *other*. def type_equals?(other : Type) {% for i in %i[base_name full_name const reference move builtin void pointer template] %} diff --git a/src/bindgen/parser/method.cr b/src/bindgen/parser/method.cr index 39ba371..01f5a84 100644 --- a/src/bindgen/parser/method.cr +++ b/src/bindgen/parser/method.cr @@ -98,7 +98,7 @@ module Bindgen @name, @class_name, @return_type, @arguments, @first_default_argument = nil, @access = AccessSpecifier::Public, @type = Type::MemberMethod, @const = false, @virtual = false, @pure = false, @extern_c = false, - @builtin = false, @origin = nil, @crystal_name = nil + @builtin = false, @origin = nil, @crystal_name = nil, @binding_name = nil ) end @@ -429,7 +429,7 @@ module Bindgen return unless @name == "operator++" || @name == "operator--" return unless @arguments.size == 1 && @arguments[0].full_name == "int" - fixed = Method.new( + Method.new( type: @type, name: @name, access: @access, @@ -441,9 +441,8 @@ module Bindgen arguments: [] of Argument, return_type: @return_type, crystal_name: crystal_name, + binding_name: binding_method_name, ) - fixed.binding_name = binding_method_name - fixed end # Mangled name for the C++ wrapper method name @@ -469,7 +468,7 @@ module Bindgen end case self - when .constructor? then "_CONSTRUCT" + when .constructor? then "#{@name}_CONSTRUCT" when .aggregate_constructor? then "_AGGREGATE" when .copy_constructor? then "_COPY" when .member_getter? then "#{@name}_GETTER" @@ -478,7 +477,7 @@ module Bindgen when .static_method? then "#{@name}_STATIC" when .static_getter? then "#{@name}_STATIC_GETTER" when .static_setter? then "#{@name}_STATIC_SETTER" - when .destructor? then "_DESTROY" + when .destructor? then "#{@name}_DESTROY" else @name end end @@ -630,11 +629,13 @@ module Bindgen args << l.merge(r) end - result = Method.new( + Method.new( type: @type, access: @access, name: @name, class_name: @class_name, + crystal_name: @crystal_name, + binding_name: @binding_name, arguments: args, first_default_argument: @first_default_argument, return_type: @return_type, @@ -643,9 +644,50 @@ module Bindgen pure: @pure && other.pure?, builtin: @builtin, ) + end + + # Performs type substitution on the argument and return types of this + # method using the given *replacements*. + def substitute_type(replacements : Hash(String, Parser::Type)) : Method + args = arguments.map(&.substitute_type(replacements)) + ret = @return_type.substitute(replacements) - result.crystal_name = @crystal_name - result + Method.new( + type: @type, + access: @access, + name: @name, + class_name: @class_name, + crystal_name: @crystal_name, + binding_name: @binding_name, + arguments: args, + first_default_argument: @first_default_argument, + return_type: ret, + const: @const, + virtual: @virtual, + pure: @pure, + ) + end + + # Substitutes all uses of *name* on the argument and return types of this + # method with the given *type*. + def substitute_type(name : String, with type : Type) : Method + args = arguments.map(&.substitute_type(name, type)) + ret = @return_type.substitute(name, type) + + Method.new( + type: @type, + access: @access, + name: @name, + class_name: @class_name, + crystal_name: @crystal_name, + binding_name: @binding_name, + arguments: args, + first_default_argument: @first_default_argument, + return_type: ret, + const: @const, + virtual: @virtual, + pure: @pure, + ) end end end diff --git a/src/bindgen/parser/type.cr b/src/bindgen/parser/type.cr index 299cce5..8e11642 100644 --- a/src/bindgen/parser/type.cr +++ b/src/bindgen/parser/type.cr @@ -1,3 +1,5 @@ +require "./type/cpp_type_parser" + module Bindgen module Parser # Stores information about a specific C++ type. @@ -109,43 +111,10 @@ module Bindgen ) end - # Parser for qualified C++ type-names. It's really stupid though. + # Returns a `Type` of a fully qualified C++ typename *type_name*. Extra + # pointer indirections can be set by *pointer_depth*. def self.parse(type_name : String, pointer_depth = 0) - name = type_name.strip # Clean the name - reference = false - const = false - - # Is it const-qualified? - if name.starts_with?("const ") - const = true - name = name[6..-1] # Remove `const ` - end - - # Is it a reference? - if name.ends_with?('&') - reference = true - pointer_depth += 1 - name = name[0..-2] # Remove ampersand - end - - # Is it a pointer? - while name.ends_with?('*') - pointer_depth += 1 - name = name[0..-2] # Remove star - end - - new( # Build the `Type` - const: const, - move: false, - reference: reference, - builtin: false, # Oh well - void: (name == "void"), - pointer: pointer_depth, - base_name: name.strip, - full_name: type_name, - template: nil, - nilable: false, - ) + CppTypeParser.new.parse(type_name, pointer_depth) end # Creates a `Type` describing a Crystal `Proc` type, which returns a @@ -239,8 +208,103 @@ module Bindgen end end + # Performs type substitution with the given *replacements*. + # + # Substitution is performed if this type's base name is exactly one of the + # type arguments, but not if the type is a templated type of the same + # name. Substitution is applied recursively on template type arguments. + # All substitutions are applied simultaneously. + def substitute(replacements : Hash(String, Type)) : Type + if template = @template + substitute_template(replacements, template) + elsif type = replacements[@base_name]? + substitute_base(type) + else + self + end + end + + # Substitutes all uses of *name* with the given *type*. + def substitute(name : String, with type : Type) : Type + substitute({name, type}) + end + + # :ditto: + def substitute(replacements : Tuple(String, Type)) : Type + if template = @template + substitute_template(replacements, template) + elsif @base_name == replacements[0] + substitute_base(replacements[1]) + else + self + end + end + + # Helper for `#substitute`. Performs type substitution on the type's + # template arguments. + private def substitute_template(replacements, template) : Type + typer = Cpp::Typename.new + template_args = template.arguments.map(&.substitute(replacements)) + template_base = template.base_name + template_full = typer.template_class(template_base, template_args.map(&.full_name)) + + subst_template = Template.new( + base_name: template_base, + full_name: template_full, + arguments: template_args, + ) + + typer = Cpp::Typename.new + type_ptr = @pointer - (reference? ? 1 : 0) + + Type.new( + kind: @kind, + const: @const, + reference: @reference, + move: @move, + builtin: @builtin, + void: @void, + pointer: @pointer, + base_name: template_full, + full_name: typer.full(template_full, @const, type_ptr, @reference), + template: subst_template, + nilable: @nilable, + ) + end + + # Helper for `#substitute`. Performs basic type substitution on this + # type. Const-ness, references, and pointers are propagated. + private def substitute_base(type) + const = type.const? || const? + reference = type.reference? || reference? + move = !reference && (type.move? || move?) + pointer = @pointer + type.pointer - (type.reference? && reference? ? 1 : 0) + + if @pointer > 0 && !reference? && type.reference? + reference = false + pointer -= 1 + end + + typer = Cpp::Typename.new + type_ptr = pointer - (reference ? 1 : 0) + + Type.new( + kind: type.kind, + const: const, + reference: reference, + move: move, + builtin: type.builtin?, + void: type.void?, + pointer: pointer, + base_name: type.base_name, + full_name: typer.full(type.base_name, const, type_ptr, reference), + template: type.template, + nilable: type.nilable?, + ) + end + def_equals_and_hash @base_name, @full_name, @const, @reference, @move, - @builtin, @void, @pointer, @kind, @nilable + @builtin, @void, @pointer, @kind, @nilable, @template def initialize( @base_name, @full_name, @const, @reference, @pointer, @move = false, diff --git a/src/bindgen/parser/type/cpp_type_parser.cr b/src/bindgen/parser/type/cpp_type_parser.cr new file mode 100644 index 0000000..2f8ed17 --- /dev/null +++ b/src/bindgen/parser/type/cpp_type_parser.cr @@ -0,0 +1,138 @@ +require "string_scanner" + +module Bindgen + module Parser + class Type + # Parser for qualified C++ type-names. It's really stupid though. + private class CppTypeParser + # Regex that matches the opening of a template argument list. + OPEN_RX = /([^,>]*)/ + + # Regex matching everything that does not delimit a template argument + # list. + NEITHER_RX = /[^<>]+/ + + def parse(type_name : String, pointer_depth : Int32 = 0) + parse_type(type_name, pointer_depth) + end + + # Parses a C++ type. Recursively parses all templates contained within, + # unless a template instantiation is explicitly given. + private def parse_type(type_name, pointer_depth, template = nil) : Type + name = type_name.strip # Clean the name + reference = false + const = false + pointer = 0 + + # Is it const-qualified? + if name.starts_with?("const ") + const = true + name = name[6..-1] # Remove `const ` + end + + # Is it a reference? + if name.ends_with?('&') + reference = true + pointer_depth += 1 + name = name[0..-2] # Remove ampersand + end + + # Is it a pointer? + while name.ends_with?('*') + pointer += 1 + pointer_depth += 1 + name = name[0..-2] # Remove star + end + + name = name.strip + + # Is it a template? + if template + # Adjust template name to remove `const` etc. + template = Template.new( + base_name: name.match(OPEN_RX).try(&.pre_match) || name, + full_name: name, + arguments: template.arguments, + ) + elsif name =~ OPEN_RX || name =~ CLOSE_RX + template = parse_template(name.strip) + end + + typer = Cpp::Typename.new + + Type.new( # Build the `Type` + const: const, + move: false, + reference: reference, + builtin: false, # Oh well + void: (name == "void"), + pointer: pointer_depth, + base_name: name, + full_name: typer.full(name, const, pointer, reference), + template: template, + nilable: false, + ) + end + + # Tree structure of a template. + alias TemplateTree = Type | Array(TemplateTree) + + # this won't work for some reason + # alias TemplateTree = Array(Type | TemplateTree) + + # Parses a C++ template. Recursively parses all types within. + # *type_name* is expected to be a plain type (without `const`, pointers, + # or references). + private def parse_template(type_name) : Template? + typer = Cpp::Typename.new + scanner = StringScanner.new(type_name) + top = [] of TemplateTree + stack = [top] + + until scanner.eos? + if scanner.scan(OPEN_RX) + top = [] of TemplateTree + stack << top + elsif scanner.scan(CLOSE_RX) + template_args = stack.pop.map(&.as(Type)) + + raise "Extra closing bracket" if stack.empty? + top = stack.last + template_type = top.pop? + raise "Template argument list without template name" unless + template_type.is_a?(Type) + + suffix = scanner[1] + arg_list = template_args.map(&.full_name) + base_name = template_type.full_name + full_name = "#{typer.template_class base_name, arg_list}#{suffix}".strip + template = Template.new( + base_name: base_name, + full_name: full_name, + arguments: template_args, + ) + + type = parse_type(full_name, 0, template) + top << type + elsif text = scanner.scan(NEITHER_RX) + parts = text.split(',', remove_empty: true) + types = parts.compact_map do |part| + parse_type(part.strip, 0) unless part.blank? + end + top.concat(types) + end + end + + raise "Extra opening bracket" unless stack.size == 1 + raise "Multiple top-level types" unless top.size == 1 + + top.first.as(Type).template + end + end + end + end +end