turing.reflect provides an API for reflectively accessing and analyzing classes, functions, and fields in the Turing runtime. This is done by analyzing the internal representations of running code to reconstruct what its source-level equivalent would be, along with manipulating the interpreter to run certain dynamically-generated instructions that would otherwise be unable to exist.
The first thing you'll need to do to get started with turing.reflect is copy the reflect folder of this project to the main directory of your Turing interpreter (where the Turing executable is located), or if you are using the newer Qt-based OpenTuring editor, the upper-level support directory containing all the Qt DLLs and the larger OpenTuring executable. Once that is complete, you can import turing.reflect into your project by inserting this line into a source file header
import "%oot/reflect/universe"
From there, you will have access to the reflectc and reflectf functions for reflecting classes and functions/procedures respectively. reflectc takes either a pointer to a class instance or the class itself (as shown below) as a argument, whereas reflectf takes the function/procedure to be reflected as an argument. To reflect a class, get a TClass instance as such
import "%oot/reflect/universe"
class MyClass
export greetMe
proc greetMe()
put "Hello World!"
end greeetMe
end MyClass
var clazz := reflectc(MyClass)
Here we have a class, MyClass, with a single procedure that will print "Hello World!". If we wanted to make an instance of that class, we could do the following
var instance := clazz -> newInstance()
MyClass(instance).greetMe() /* prints "Hello World!" */
If we wanted to invoke that greetMe procedure directly, we could also do it without making an instance of the container class by doing this
clazz -> getDeclaredFunction(1) -> invoke(0, nil) /* also prints "Hello World!" */
The code above will find the first declared procedure in the class above and invoke it with both an instance address of nil and a return address of 0. If greetMe returned a value, the return address would be the location in memory for the result to be put. Furthermore, if greetMe relied on any instance fields in MyClass, the instance address would specify the instance to use for that invocation.
A common use of reflection is retrieving metadata associated with specific code constructs in projects. Java and C# do this through the use of annotations and attributes respectively, but they both boil down to being small snippets of code that can be put next to code constructs to give them certain metadata.
The Turing language does not support these kinds of constructs out of the box, but turing.reflect supplements this shortcoming by allowing you to define custom annotations that can be applied to various parts of your code. To do this, we first have to include the annotations module by adding the following snippet to the import line of your file header
"%oot/reflect/annotations"
From there, you can create annotations by putting the word annotation before a declared empty procedure
annotation proc test
end test
annotation proc named(name: string)
end named
Both test and named can now be scanned for as annotations by turing.reflect's reflective objects. In this example
class TestSuite
test
proc testDoingSomething()
end testDoingSomething
test
named ("test for doing something else")
proc testDoingSomethingElse()
end testDoingSomethingElse
end TestSuite
we can scan the TestSuite class for all functions annotated with test, and print the names of all those functions also annotated as named
const clazz := reflectc(TestSuite)
for i: 1..clazz -> getFunctionCount()
const func := clazz -> getFunction(i)
if (func -> isAnnotationPresent(test) & func -> isAnnotationPresent(named)) then
const name := func -> getDeclaredAnnotation(named)
put string @ (name -> getElement(1)) /* prints "test for doing something else" */
end if
end for
The annotation reflection API also allows you enumerate all annotations present on any given code construct, along with the number of elements that annotation has.
To invoke functions that take arguments of some kind, TFunction's invoke function isn't good enough. Instead, TFunction defines a separate function invokeArgs to accomplish this. Similar to invoke, you pass arguments defining the function return address and class instance address to use for the invocation (these are ignored if the function has no return value or is not in a class, respectively). Take the example of this greeting function
fcn deliverGreeting(greeting: string, recipient: string, allCaps: boolean, repetitions: int): string
var s := "to " + recipient + ": "
for i: 1..repetitions
if (allCaps) then
s += Str.Upper(greeting)
else
s += greeting
end if
end for
result s
end deliverGreeting
To invoke this, we first have to include the invoke module by adding the following snippet to the import line of your file header
"%oot/reflect/invoke"
From there, invoking the function is simple
var greeting: string
var func := reflectf(deliverGreeting)
func -> invokeArgs(addr(greeting), nil)
-> with(stringArg("yo"))
-> with(stringArg("Mike"))
-> with(booleanArg(true))
-> with(intArg(5))
-> do()
put greeting /* prints "to Mike: YOYOYOYOYO" */
As you can see, the syntax for these invocations is fluent and simple. We provide four arguments to the invocation context through the with function, and call do to finally perform the invocation. This has the happy side effect of allowing arguments for invocation to be accumulated over time prior to invocation, instead of passing them all at once.
turing.reflect has a fairly broad feature set, closely mimicking Java's reflection API, including
- class instance creation
- class assignability checks
- annotation creation, placement and retrieval
- function invocation
- function return type resolution
- class function enumeration
- class field enumeration
Lower-level details regarding any reflected object are available through CodeContext objects, giving you details regarding the internal start and end positions of constructs, how many operations they are composed of, and how many lines of source code they equate to.