Module: Ikra::RubyIntegration

Defined in:
lib/ruby_core/core.rb,
lib/ruby_core/math.rb,
lib/ruby_core/array.rb,
lib/ruby_core/interpreter.rb,
lib/ruby_core/array_command.rb,
lib/ruby_core/ruby_integration.rb

Defined Under Namespace

Classes: CycleDetectedError, Implementation, SymbolicCycleFinder

Constant Summary collapse

TYPE_INT_COERCE_TO_FLOAT =

TODO: Handle non-singleton types

proc do |recv, other|
    if other.include?(FLOAT_S) 
        FLOAT
    elsif other.include?(INT_S)
        INT
    else
        # At least one of the types INT_S or FLOAT_S are required
        raise RuntimeError.new("Operation defined numeric values only (found #{other})")
    end
end
TYPE_INT_RETURN_INT =
proc do |recv, other|
    if !other.include?(INT_S)
        raise RuntimeError.new("Operation defined Int values only (found #{other})")
    end

    INT
end
TYPE_NUMERIC_RETURN_BOOL =
proc do |recv, other|
    if !other.include?(INT_S) && !other.include?(FLOAT_S)
        raise RuntimeError.new("Expected type Int or Float, found #{other}")
    end

    BOOL
end
TYPE_BOOL_RETURN_BOOL =
proc do |recv, other|
    if !other.include?(BOOL_S)
        raise RuntimeError.new("Expected type Bool, found #{other}")
    end

    BOOL
end
MATH =
Math.singleton_class.to_ikra_type
ALL_ARRAY_TYPES =
proc do |type|
    type.is_a?(Types::ArrayType) && !type.is_a?(Types::LocationAwareArrayType)
end
LOCATION_AWARE_ARRAY_TYPE =
proc do |type|
    # TODO: Maybe there should be an automated transfer to host side here if necessary?
    type.is_a?(Types::LocationAwareArrayType)
end
LOCATION_AWARE_ARRAY_ACCESS =
proc do |receiver, method_name, args, translator, result_type|

    recv = receiver.accept(translator.expression_translator)
    inner_type = receiver.get_type.singleton_type.inner_type.to_c_type
    index = args[0].accept(translator.expression_translator)

    "((#{inner_type} *) #{recv}.content)[#{index}]"
end
INNER_TYPE =
proc do |rcvr|
    rcvr.inner_type
end
INTERPRETER_ONLY_CLS_OBJ =

No need to do type inference or code generation, if a method is called on an on an instance of one of these classes.

[Ikra::Symbolic.singleton_class]
ALL_ARRAY_COMMAND_TYPES =
proc do |type|
    type.is_a?(Symbolic::ArrayCommand)
end
PMAP_TYPE =
proc do |rcvr_type, *args_types, send_node:|
    # TODO: Handle keyword arguments
    
    # Ensure that there is no cycle here. "Cycle" means that the same AST send node
    # was used earlier (i.e., in one of `rcvr_type`'s inputs/dependent computations).
    # In that case we have to abort type inference here, because it would not terminate.
    SymbolicCycleFinder.raise_on_cycle(rcvr_type, send_node)

    more_kw_args = {}

    if send_node.arguments.size == 1
        if !send_node.arguments.first.is_a?(AST::HashNode)
            raise ArgumentError.new("If an argument is given, it must be a Hash of kwargs.")
        end

        # Pass kwargs separately
        more_kw_args = AST::Interpreter.interpret(send_node.arguments.first)
    end

    rcvr_type.pmap(
        ast: send_node.block_argument, 
        generator_node: send_node, 
        # TODO: Fix binding
        command_binding: send_node.find_behavior_node.binding,
        **more_kw_args).to_union_type
end
PZIP_TYPE =
proc do |rcvr_type, *args_types, send_node:|
    # TODO: Support multiple arguments for `pzip`
    types = args_types[0].map do |sing_type|
        raise AssertionError.new("Singleton type expected") if sing_type.is_union_type?
        rcvr_type.pzip(sing_type, generator_node: send_node).to_union_type
    end

    types.reduce(Types::UnionType.new) do |acc, type|
        acc.expand_return_type(type)
    end
end
PSTENCIL_TYPE =
proc do |rcvr_type, *args_types, send_node:|
    # TODO: Handle keyword arguments
    ruby_args = send_node.arguments.map do |node|
        AST::Interpreter.interpret(node)
    end

    more_kw_args = {}

    if args_types.size == 3
        if !ruby_args.last.is_a?(Hash)
            raise ArgumentError.new("If 3 arguments are given, the last one must be a Hash of kwargs.")
        end

        # Pass kwargs separately
        more_kw_args = ruby_args.pop
    end

    SymbolicCycleFinder.raise_on_cycle(rcvr_type, send_node)

    rcvr_type.pstencil(
        *ruby_args, 
        ast: send_node.block_argument, 
        generator_node: send_node, 
        # TODO: Fix binding
        command_binding: send_node.find_behavior_node.binding,
        **more_kw_args).to_union_type
end
PREDUCE_TYPE =
proc do |rcvr_type, *args_types, send_node:|
    # TODO: Handle keyword arguments
    
    SymbolicCycleFinder.raise_on_cycle(rcvr_type, send_node)

    rcvr_type.preduce(ast: send_node.block_argument, generator_node: send_node).to_union_type
end
LAUNCH_KERNEL =
proc do |receiver, method_name, arguments, translator, result_type|
    # The result type is the symbolically executed result of applying this
    # parallel section. The result type is an ArrayCommand.
    array_command = receiver.get_type.singleton_type

    # Translate command
    command_translator = translator.command_translator
    command_translator.push_kernel_launcher
    result = array_command.accept(command_translator)
    kernel_launcher = command_translator.pop_kernel_launcher(result)

    # Prepare kernel launchers for launch of `array_command`
    command_translator.program_builder.prepare_additional_args_for_launch(array_command)

    # Generate launch code for all kernels
    launch_code = command_translator.program_builder.build_kernel_launchers

    # Always return a device pointer. Only at the very end, we transfer data to the host.
    result_expr = kernel_launcher.kernel_result_var_name

    if Translator::ArrayCommandStructBuilder::RequireRuntimeSizeChecker.require_size_function?(array_command)

        # Size is not statically known, take information from receiver.
        # TODO: Code depends on template. `cmd` is defined in template.
        result_size = "cmd->size()"
    else
        # Size is known statically
        result_size = array_command.size.to_s
    end
            
    # Debug information
    if array_command.generator_node != nil
        debug_information = array_command.to_s + ": " + array_command.generator_node.to_s
    else
        debug_information = array_command.to_s
    end

    result = Translator.read_file(file_name: "host_section_launch_parallel_section.cpp", replacements: {
        "debug_information" => debug_information, 
        "array_command" => receiver.accept(translator.expression_translator),
        "array_command_type" => array_command.to_c_type,
        "result_size" => result_size,
        "kernel_invocation" => launch_code,
        "kernel_result" => result_expr,
        "free_memory" => command_translator.program_builder.build_memory_free_except_last})

    # Clear kernel launchers. Otherwise, we might launch them again in a later, unrelated
    # LAUNCH_KERNEL branch. This is because we reuse the same [ProgramBuilder] for an
    # entire host section.
    command_translator.program_builder.clear_kernel_launchers

    # Build all array command structs for this command
    command_translator.program_builder.add_array_command_struct(
        *Translator::ArrayCommandStructBuilder.build_all_structs(array_command))

    result
end
ARRAY_COMMAND_TO_ARRAY_TYPE =
proc do |rcvr_type, *args_types, send_node:|
    Types::LocationAwareFixedSizeArrayType.new(
        rcvr_type.result_type,
        rcvr_type.dimensions,
        location: :device).to_union_type
end
SYMBOLICALLY_EXECUTE_KERNEL =
proc do |receiver, method_name, arguments, translator, result_type|
    if !result_type.is_singleton?
        raise AssertionError.new("Singleton type expected")
    end

    # Build arguments to constructor. First one (result field) is NULL.
    constructor_args = ["NULL"]

    # Translate all inputs (receiver, then arguments to parallel section)
    constructor_args.push(receiver.accept(translator.expression_translator))

    for arg in arguments
        if arg.get_type.is_singleton? && 
            arg.get_type.singleton_type.is_a?(Symbolic::ArrayCommand)
            
            # Only ArrayCommands should show up as arguments
            constructor_args.push(arg.accept(translator.expression_translator))
        end
    end

    all_args = constructor_args.join(", ")

    # This is a hack because the type is a pointer type
    "new #{result_type.singleton_type.to_c_type[0...-2]}(#{all_args})"
end
ALL_LOCATION_AWARE_ARRAY_TYPES =
proc do |type|
    type.is_a?(Types::LocationAwareArrayType)
end
LOCATION_AWARE_ARRAY_TO_HOST_ARRAY_TYPE =
proc do |rcvr_type, *args_types|
    # TODO: Should also be able to handle variable variant
    Types::LocationAwareFixedSizeArrayType.new(
        rcvr_type.inner_type,
        rcvr_type.dimensions,
        location: :host).to_union_type
end
LOCATION_AWARE_ARRAY_CALL_TYPE =
proc do |rcvr_type, *args_types|
    # Calling `__call__` on an array does not do anything
    rcvr_type.to_union_type
end
COPY_ARRAY_TO_HOST =
proc do |receiver, method_name, args, translator, result_type|
    if receiver.get_type.singleton_type.location == :host
        receiver.accept(translator.expression_translator)
    else
        c_type = receiver.get_type.singleton_type.inner_type.to_c_type

        Translator.read_file(file_name: "memcpy_device_to_host_expr.cpp", replacements: {
            "type" => c_type,
            "device_array" => receiver.accept(translator.expression_translator)})
    end
end
ARRAY_TYPE_TO_COMMAND_TYPE =
proc do |rcvr_type, *args_types, send_node:|
    rcvr_type.to_command.to_union_type
end
FREE_MEMORY_FOR_ARRAY_COMMAND =
proc do |receiver, method_name, args, translator, result_type|

    Translator.read_file(file_name: "free_memory_for_command.cpp", replacements: {
        "type" => receiver.get_type.to_c_type,
        "receiver" => receiver.accept(translator.expression_translator)})
end
INT =
Types::UnionType.create_int
FLOAT =
Types::UnionType.create_float
BOOL =
Types::UnionType.create_bool
INT_S =
INT.singleton_type
FLOAT_S =
FLOAT.singleton_type
BOOL_S =
BOOL.singleton_type
@@impls =
{}

Class Method Summary collapse

Class Method Details

.expect_singleton_args?(rcvr_type, method_name) ⇒ Boolean

Returns:

  • (Boolean)


68
69
70
# File 'lib/ruby_core/ruby_integration.rb', line 68

def self.expect_singleton_args?(rcvr_type, method_name)
    return find_impl(rcvr_type, method_name).expect_singleton_args
end

.get_implementation(receiver, method_name, arguments, translator, result_type) ⇒ Object

Returns the implementation (CUDA source code snippet) for a method with name

method_name

defined on [rcvr_type].

This method also receives references to the receiver AST node and to AST nodes for arguments. In most cases, these AST nodes are directly translated to source code using ‘translator` (a [Translator::ASTTranslator]). However, if an implementation is given through a block ([Proc]), the implementation might decide to not use the translation (e.g., translation of parallel sections in host sections).

receiver

must have a singleton type.



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/ruby_core/ruby_integration.rb', line 82

def self.get_implementation(receiver, method_name, arguments, translator, result_type)
    impl = find_impl(receiver.get_type.singleton_type, method_name)
    source = impl.implementation

    if source.is_a?(Proc)
        source = source.call(receiver, method_name, arguments, translator, result_type)
    end

    sub_code = arguments.map do |arg| arg.accept(translator.expression_translator) end
    sub_types = arguments.map do |arg| arg.get_type end

    if impl.pass_self
        sub_code.insert(0, receiver.accept(translator.expression_translator))
        sub_types.insert(0, receiver.get_type)
    end

    sub_indices = (0...source.length).find_all do |index| 
        source[index] == "#" 
    end
    substitutions = {}
    sub_indices.each do |index|
        if source[index + 1] == "F"
            # Insert float
            arg_index = source[index + 2].to_i

            if arg_index >= sub_code.size
                raise ArgumentError.new("Argument missing: Expected at least #{arg_index + 1}, found #{sub_code.size}")
            end

            substitutions["\#F#{arg_index}"] = code_argument(FLOAT_S, sub_types[arg_index], sub_code[arg_index])
        elsif source[index + 1] == "I"
            # Insert integer
            arg_index = source[index + 2].to_i

            if arg_index >= sub_code.size
                raise ArgumentError.new("Argument missing: Expected at least #{arg_index + 1}, found #{sub_code.size}")
            end

            substitutions["\#I#{arg_index}"] = code_argument(INT_S, sub_types[arg_index], sub_code[arg_index])
        elsif source[index + 1] == "B"
            # Insert integer
            arg_index = source[index + 2].to_i

            if arg_index >= sub_code.size
                raise ArgumentError.new("Argument missing: Expected at least #{arg_index + 1}, found #{sub_code.size}")
            end

            substitutions["\#B#{arg_index}"] = code_argument(BOOL_S, sub_types[arg_index], sub_code[arg_index])
        elsif source[index + 1] == "N"
            # Numeric, coerce integer to float
            arg_index = source[index + 2].to_i

            if arg_index >= sub_code.size
                raise ArgumentError.new("Argument missing: Expected at least #{arg_index + 1}, found #{sub_code.size}")
            end

            if sub_types[arg_index].include?(FLOAT_S)
                expected_type = FLOAT_S
            else
                expected_type = INT_S
            end

            substitutions["\#N#{arg_index}"] = code_argument(expected_type, sub_types[arg_index], sub_code[arg_index])
        else
            arg_index = source[index + 1].to_i

            if arg_index >= sub_code.size
                raise ArgumentError.new("Argument missing: Expected at least #{arg_index + 1}, found #{sub_code.size}")
            end

            substitutions["\##{arg_index}"] = sub_code[arg_index]
        end
    end

    substitutions.each do |key, value|
        # Do not use `gsub!` here!
        source = source.gsub(key, value)
    end
    
    return source
end

.get_return_type(rcvr_type, method_name, *arg_types, send_node: nil) ⇒ Object

Retrieves the return type of a method invocation for receiver type [rcvr_type], selector [method_name], and argument types [arg_types].

In addition, this method accepts an optional parameter [node] containing the send node (abstract syntax tree node). That node is passed to type inference procs. This is required for symbolic execution of array commands inside host sections.



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/ruby_core/ruby_integration.rb', line 170

def self.get_return_type(rcvr_type, method_name, *arg_types, send_node: nil)
    return_type = find_impl(rcvr_type, method_name).return_type
    num_params = find_impl(rcvr_type, method_name).num_params

    if return_type.is_a?(Proc)
        # Return type depends on argument types
        if num_params.is_a?(Fixnum) && num_params != arg_types.size
            raise ArgumentError.new(
                "#{num_params} arguments expected but #{arg_types.size} given")
        elsif num_params.is_a?(Range) && !num_params.include?(arg_types.size)
            raise ArgumentError.new(
                "#{num_params} arguments expected but #{arg_types.size} given")
        else
            if send_node == nil
                return return_type.call(rcvr_type, *arg_types)
            else
                return return_type.call(rcvr_type, *arg_types, send_node: send_node)
            end
        end
    else
        return return_type
    end
end

.has_implementation?(rcvr_type, method_name) ⇒ Boolean

Returns:

  • (Boolean)


60
61
62
# File 'lib/ruby_core/ruby_integration.rb', line 60

def self.has_implementation?(rcvr_type, method_name)
    return find_impl(rcvr_type, method_name) != nil
end

.implement(rcvr_type, method_name, return_type, num_params, impl, pass_self: true, expect_singleton_args: false) ⇒ Object



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/ruby_core/ruby_integration.rb', line 43

def self.implement(
    rcvr_type, 
    method_name, 
    return_type, 
    num_params, 
    impl, 
    pass_self: true, 
    expect_singleton_args: false)

    @@impls[rcvr_type][method_name] = Implementation.new(
        num_params: num_params,
        return_type: return_type,
        implementation: impl,
        pass_self: pass_self,
        expect_singleton_args: expect_singleton_args)
end

.is_interpreter_only?(type) ⇒ Boolean

Returns:

  • (Boolean)


8
9
10
11
12
13
14
# File 'lib/ruby_core/interpreter.rb', line 8

def self.is_interpreter_only?(type)
    if !type.is_a?(Types::ClassType)
        return false
    end

    return INTERPRETER_ONLY_CLS_OBJ.include?(type.cls)
end

.should_pass_self?(rcvr_type, method_name) ⇒ Boolean

Returns:

  • (Boolean)


64
65
66
# File 'lib/ruby_core/ruby_integration.rb', line 64

def self.should_pass_self?(rcvr_type, method_name)
    return find_impl(rcvr_type, method_name).pass_self
end