diff --git a/typed_python/compiler/introspect.py b/typed_python/compiler/introspect.py new file mode 100644 index 000000000..829f71e33 --- /dev/null +++ b/typed_python/compiler/introspect.py @@ -0,0 +1,132 @@ +# Copyright 2017-2019 typed_python Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""introspect.py + +Contains helper functions for exploring the objects and syntax trees generatied in the process +of going from python -> native IR -> LLVM IR. +""" + +import typed_python +import typed_python.compiler.python_to_native_converter as python_to_native_converter + +from typed_python import Runtime, Function + + +def getNativeIRString( + typedFunc: Function, args=None, kwargs=None +) -> str: + """ + Given a function compiled with Entrypoint, return a text representation + of the generated native (one layer prior to LLVM) code. + + Args: + typedFunc (Function): a decorated python function. + args (Optional(list)): these optional args should be the Types of the functions' positional arguments + kwargs (Optional(dict)): these keyword args should be the Types of the functions' keyword arguments + + Returns: + A string for the function bodies generated (including constructors and destructors) + """ + converter = Runtime.singleton().llvm_compiler.converter + + function_name = getFullFunctionNameWithArgs(typedFunc, args, kwargs) + # relies on us maintaining our naming conventions (tests would break otherwise) + output_str = "" + for key, value in converter._function_definitions.items(): + if function_name in key: + output_str += f"Function {key}" + "_" * 20 + "\n" + output_str += str(value.body.body) + "\n" + output_str += "_" * 80 + "\n" + + if not output_str: + raise ValueError( + "no matching function definitions found - has the code been compiled (and run)?" + ) + + return output_str + + +def getLLVMString( + typedFunc: Function, args=None, kwargs=None +) -> str: + """ + Given a function compiled with Entrypoint, return a text representation + of the generated LLVM code. + + Args: + typedFunc (Function): a decorated python function. + args (Optional(list)): these optional args should be the Types of the functions' positional arguments + kwargs (Optional(dict)): these keyword args should be the Types of the functions' keyword arguments + + Returns: + A string for the function bodies generated (including constructors and destructors) + """ + converter = Runtime.singleton().llvm_compiler.converter + + function_name = getFullFunctionNameWithArgs(typedFunc, args, kwargs) + + output_str = "" + for key, value in converter._functions_by_name.items(): + if function_name in key: + output_str += f"Function {key}" + "_" * 20 + "\n" + output_str += str(value) + "\n" + output_str += "_" * 80 + "\n" + + return output_str + + +def getFullFunctionNameWithArgs(funcObj, argTypes, kwargTypes): + """ + Given a Function and a set of types, compile the function to generate the unique name + for that function+argument combination. + + Args: + funcObj (Function): a typed_python Function. + argTypes (List): a list of the position arguments for the function. + kwargTypes (Dict): a key:value mapping for the functions' keywords arguments. + """ + assert isinstance(funcObj, typed_python._types.Function) + typeWrapper = lambda t: python_to_native_converter.typedPythonTypeToTypeWrapper(t) + funcObj = typed_python._types.prepareArgumentToBePassedToCompiler(funcObj) + argTypes = [typeWrapper(a) for a in argTypes] if argTypes is not None else [] + kwargTypes = ( + {k: typeWrapper(v) for k, v in kwargTypes.items()} + if kwargTypes is not None + else {} + ) + + overload_index = 0 + overload = funcObj.overloads[overload_index] + + ExpressionConversionContext = ( + typed_python.compiler.expression_conversion_context.ExpressionConversionContext + ) + argumentSignature = ( + ExpressionConversionContext.computeFunctionArgumentTypeSignature( + overload, argTypes, kwargTypes + ) + ) + + if argumentSignature is not None: + callTarget = ( + Runtime() + .singleton() + .compileFunctionOverload( + funcObj, overload_index, argumentSignature, argumentsAreTypes=True + ) + ) + return callTarget.name + else: + raise ValueError("no signature found.") diff --git a/typed_python/compiler/introspect_test.py b/typed_python/compiler/introspect_test.py new file mode 100644 index 000000000..d02ae93dc --- /dev/null +++ b/typed_python/compiler/introspect_test.py @@ -0,0 +1,70 @@ +# Copyright 2017-2019 typed_python Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +import unittest + +from typed_python import Entrypoint, ListOf +from typed_python.compiler import introspect + + +class TestRuntime(unittest.TestCase): + + # the compilation itself is handled in other tests. + def naive_sum(someList, startingInt): + for x in someList: + startingInt += x + return startingInt + + compiled = Entrypoint(naive_sum) + + def test_ir_compile_and_output_string(self): + for test_func in [introspect.getNativeIRString, introspect.getLLVMString]: + output_text = test_func( + TestRuntime.compiled, args=[ListOf(int), int], kwargs=None + ) + assert 'naive_sum' in output_text + + def test_ir_throw_error_if_uncompiled(self): + for test_func in [introspect.getNativeIRString, introspect.getLLVMString]: + with pytest.raises(AssertionError): + _ = test_func( + TestRuntime.naive_sum, args=[ListOf(int), int], kwargs=None) + + def test_introspect_handles_tp_class(self): + """Full TP class""" + pass + + def test_introspect_distinguishes_overloads(self): + """Check that we can obtain the correct IRs given multiple overloads.""" + for test_func in [introspect.getNativeIRString, introspect.getLLVMString]: + overload_one = test_func( + TestRuntime.compiled, args=[ListOf(int), int], kwargs=None) + overload_two = test_func( + TestRuntime.compiled, args=[ListOf(float), float], kwargs=None) + assert overload_one != overload_two + + def test_introspect_handles_kwargs_correctly_(self): + for test_func in [introspect.getNativeIRString, introspect.getLLVMString]: + output_text = test_func( + TestRuntime.compiled, args=[ListOf(int)], kwargs={'startingInt': int} + ) + assert 'naive_sum' in output_text + + def test_introspect_rejects_invalid_args(self): + for test_func in [introspect.getNativeIRString, introspect.getLLVMString]: + with pytest.raises(ValueError): + _ = test_func(TestRuntime.compiled, args=[ListOf(int), int, int]) + with pytest.raises(ValueError): + _ = test_func(TestRuntime.compiled, args=[ListOf(int)], kwargs={'test': int})