
Tom Wickham Jones and Daniel Sanchez demo the Wolfram Compiler, which JIT-compiles Wolfram Language to native code using LLVM. It ships with 30+ data structures like red-black trees and bit vectors, runs competitive with C++ STL, and lets you call external C libraries through a foreign function interface without writing boilerplate.
Questions this video answers
Tom Wickham Jones and Daniel Sanchez.
Wolfram Technology Conference 2023 (video uploaded Jan 26, 2024).
First released in Mathematica 12.
Introduced in version 12.1; the talk mentions over 30 data structures available.
Chapters
So, good morning everybody and welcome to the Wolfram technology conference for 2023. My name's Tom Wickham Jones and I'm going to give a talk about the Wolfram compiler. I've been a developer at Wolfram Research for many years and so I know lots about the system and in particular about Wolfram Compiler, which is what I'm going to talk about today. So I'm just all getting going. Right, fine. So in this talk I'm going to look at the Wolfram language. I'm going to give a basic demonstration of the compiler, how it embeds with the interpreter, which I think is interesting. Type and function declarations, data structures, programming via down values, compilation and data structures. And then my colleague Sanchez is going to give a sort of talk within a talk where he's going to show some examples of integrating with external code. So let's get started. So the Wolfram compiler, it compiles Wolfram language programs and generates native machine code and it's combining a high level language with low level execution. And it's a very large scale application of the Wolfram language. So let's keep going. Right, so Wolfram language was instantiated first in Mathematica and released in first release in 1988, and which Mathematica was very much focused on mathematical computation with all sorts of features for computation and analysis and trying to make these things accessible and ergonomic and easy. It contained a programming system at the beginning which was quite innovative at its time with a functional programming style. And the basic execution was via expression rewriting in what we call the evaluator mathematical programming. Wolf language programming involves pattern matching on expressions and this was the mechanism for function definition using these patterns. And over the years we've added many specialized optimizations. So there's a core philosophy where that we have, where we're trying to think about tasks and not specific solutions, where the, you know, and to achieve this we try to think the big picture thing, not the little small detail of one particular, you know, problem. And we also make all the components interoperate ergonomically. And this gives a lot of potential for sort of automating the functionality. So it's based around a uniform expression structure. So we have a parser that creates a Wolfram expression, which is a tree structured thing. And this expression evaluates infinitely to a fixed point. That's a bit like lambda calculus, if anybody knows about that. It's really runtime typing. So it's essentially in the programs and things. You don't give type markup and it's not a completely pure language because there are side effects. So the benefits of this uniform expression is that everything is serializable. So it can be saved in a notebook, it can be sent by email, it can be put in a file and read out of a file. And it's quite easy to overload and modify and customize. And the key thing is that the uniform structure means that programs can work in different areas. So we don't need a special programming language for graphics and another one for notebooks and another one for graphs. But there are challenges that come from this. And the challenge is that everything means something and it's easy to construct, you know, so called nonsense sine of true. But of course, you know, nonsense is very subjective and maybe there are some area of mathematics where sine of true is sine of Booleans is totally makes total sense. But even with the nonsense thing, that can mean that verification of code can be hard and efficiency can be hard. Efficiency and verification kind of go together. And as a consequence, performant code, including the core system is often not written in the Wolfram language. And so that's something I often see users prototyping in Wolfram language and then recoding in egc. Despite this, there are many very large Wolfram language applications and the Wolfram compiler is an example. So now let's start moving into the compiler. So this is a compiler for the Wolfram language. It's a long term project, it's built in the Wolfram language itself. And the idea is to leverage the latest in compiler knowledge and technology. So the project's been going for nearly 10 years now. The first release was in Mathematica 12 and it's been updated every version, including 14. And it's compiling Wolfram language code. It does not compile every, you know, all Wolfram code. Some of it is, is, you know, we're just steadily extending the things, but the things we're working on are the things that really benefit from compilation.
So.
How does it work? So it converts Wolfram language code into native machine instructions and it does this with this sort of in memory by default. The core way it works, it does this with an in memory JIT compilation system. It can also generate dynamic libraries or standalone code. But this in memory JIT thing is a core thing and I'll talk a little bit about that. It has a type inferencing system. So it figures out types and things which it needs to for this super fast code generation. And it provides a fast execution model for Wolfram language. And it's also an implementation layer itself for the Wolfram language. And we've been doing more of that and it integrates very tightly with the Wolfram language interpreter. So one of the ways that we've been developing it and testing it and using it is to compile the compiler itself. And the compiler is quite slow at generating code and so obviously it's a good candidate for, for eating our own dog food compiling itself. So the benefits that come out of the compiler are faster code execution, but you can also get a guarantee of correctness. It can fit with other co generation systems and it's a nice way to leverage otherwise inaccessible technology. It's also a very powerful way to learn about compiler technology. In fact, I think this would be a great way of writing a compiler course would be to use the Wolfram compiler because it fits with all the graphics and everything. This is a key point I like to stress. It's combining high level language specification with low level execution technology. So here's just a sort of basic demo. We have a function, a Mathematica function here, and we give it type annotations on its arguments, so they're both integers. And this is the body of the function, we pass it into function compile and we get back this compiled code function which we can feed arguments to, and we get back the answers. So we can give several functions. So here I'm giving an association of of functions and I get back an association of compiled code functions and we can extend it by adding declarations to things. So here's a function declaration and now I'm doing a function compile, but it's saying use this declaration. And there's a way of adding these declarations to a compiler environment so you can have them in one big global thing. So, and that's quite useful and we have quite a lot of functionality to do with that. So important feature of this in memory code generation is it's generating code that's optimized for this specific hardware. When we make Mathematica, or what we ship is compiled for some generic machine that's probably lower spec than your machine. And so that means it's not quite so optimized. Another detail is it's compiling all of the code. There's no library that it links to. It sort of pulls all of the source in and compiles all of it in one go. And that gives a lot of potential for cross function optimization because it's seeing all the code. And this means you can merge code, inline things, remove Redundant things and get all sorts of modern targeting SIMD instructions or getting, you know, various loop optimizations. So let's look a little bit at these code functions that we have. And so it's compiled code function, it's got its signature a little bit detailed. We can look at the input form. But one of the things we see here is we have this sort of compiled IR intermediate code. And this means that the code function can stick with the general serializable contract of mathematical expressions. So we can print it into a string and then convert it back into an expression and we get back a functioning compiled code function. So this is pretty useful because it means you could put into a notebook and send it to someone and they don't need to recompile it, it'll just work. You can also target add code for other platforms. So now has a little bit of bootstrapping but now it's made a compiled code function that works. But when we look at the compiled IR we see that it's got IR for the base system that my Mac, Mac system, but it's also got 64 bit Linux. So if I took this code function, put it on a. System and you know, Linux system, it'll work. I'm getting some questions but I think I'll answer the questions maybe at the end if, but you know, I won't, I won't, I won't forget about them. Otherwise, you know, maybe you'll have more questions. So there are other forms of output that the system can generate. And so for example, here I'm compiling something into a dynamic library that I could link in or use in Mathematica and I can generate, say here I'm generating assembler for my intel machine and I can do sort of cross compilation. So here's I'm generating assembler for a, an arm, you know, Mac when you do this cross compilation it has to do some sort of bootstrapping. So this is a different instruction set that it's, that it's using. I can even generate this. This is the underlying technology is a thing called LLVM which is like super interesting but I'm not really going to talk about that. But that's the underlying compiler technology that's being targeted. So we have this high level Wolfram language which is interpreted and it's tightly integrated with this fast low level machine specific code that the compiler can generate. So let's have a little look at the boundary because I think this is quite interesting between the compiler, between the interpreter, the evaluator and compiled code. So in the interpreter we have one uniform data type, which is an expression. We make a function call to compiled code where there's lots of types, integers, reals, booleans, strings, functions, all sorts of things. And these execute, the code executes here by native machine instructions and that have been generated by this llvm. Then the result comes back to the interpreter and in the interpreter execution is by, you know, this, this evaluator. So the thing that I think is quite interesting is this boundary between interface layer between the interpreter and the compiled code. So we start out with this expression and then we unbox it, we reconstitute the expression into the actual native types that the compiler works with. Then we do our compilation and then the result, the process is reversed. Now what's kind of interesting is that this layer here is also generated compiled code that's also been generated by the compiler. And that's a great way of extending the system and adding a lot of power to the system. So just one stage further, we have our interpreted code going into compiled code and this is compiled code that's compatible with other libraries. Very simple to link into an external library. And so for example, you could call the CUDA runtime library and I'll be giving a talk tomorrow about working with CUDA and CUDA enabled GPUs and then the results can come all the way back. And this is nice because it's giving scripting access to external libraries, which is kind of nice. Type system, that's a big area in itself. I'm not going to go into lots of detail about that, except to say that it has, it's an inferencing system, so you don't need to give type markup to every element. Typically you just put markup on the inputs to functions, maybe if you might say what the output is and all the intermediate types are sort of figured out. It's a parametric polymorphic system, which, which I'll show a little bit of what that means. But the benefit of that is it means a little bit of code goes a long way. It can be extended with new types, for example product and some types. It supports function overloading and there's quite a lot of interesting, innovative type computations that you can do either for finding out, you know, writing polymorphic code or for doing, you know, sort of, you know, doing type markup. It's quite interesting. So script run, supported types, I mean these are extensible, these are some of the sort of built in things, Boolean strings, signed and unsigned integers of different sizes including 128bit and then these complexes. So these are things that obviously you can extend types and things, but we also need to make sure that you have support for all these types and these are things that we've added nice support for. We have various container types. So we have these rectangular types, packed arrays and numeric arrays that are particularly good for numeric computation. And then we have various other general containers and we have our data structure library, which obviously many of those contain things, various utility types. So this is the name of the expression type, inert expression. We have a rule thing null and never type. And we also want to support functions. So parsing functions as arguments and things. And so here's how we represent a function type. So this is the arguments. So this takes a 64 bit integer and it returns a boolean, just giving the type. It says nothing about the implementation. So we know it's not saying anything about that, it's just saying what the contract of the function is. So type declarations, this is a product type thing we call, has a name and then it has these fields. So this is like the fields have names and types. So there's an integer 64 bit and called field one and another one called field two. Now we can also make a type that itself is parameterized. So this thing here we have, it's a container and it has a parameter arg and has two fields which also have that arg. We haven't said what arg is. It could be a string, it could be a packed array, it could be a function. That's quite powerful to leave it as open like that. By default, these types are, are reference types. So they allocate memory that they point to and they're memory managed and those sorts of things can be changed. So let's look at function declarations. So function declarations typically follow the name, type and body syntax. So this is the name is double, and this is the type, and this is the body of the function. So this is something that could be put in a compiler environment or used in function compile. And then you could refer to this function. You could also write a polymorphic function declaration. So here I haven't said what E is, but I've just said that E is some type of, and the function takes that type and it returns that type. So this function could work for integers, it could work for floats, it could work for complexes, and that's what it's going to do. So it works for all inputs for which these things are defined. Now a slightly Better definition would be to coerce this to, to have the same type of as the argument. And this is an example of these sort of type computations that you might, you might use. We also have a way to refer to functions from an external library. So this is the name, this is a declaration, this is the name of the function in the library, this is the name of the library and this is the signature. And I'll just show a quick example of this. Daniel's going to show lots more examples. So you make a declaration. This is referring to the function in the C runtime library. So it takes no arguments and returns an integer. And then we can compile that and this is how we call it. And so it takes no arguments. And that's what it's doing, it's returning some time counter. So those are declarations. We also have a component system that allows you to collect basically a library of declarations and refer to it by a single name or build it into a library or hook it into the evaluator. And I think Daniel's going to show up bit of that. So I'll answer questions. In the end. So. Right. Wolfram language support. The goal is to support Wolfram language programs. We don't support everything. It's quite a non trivial thing to say what support means because you have to say it's not just the name of a function, it's the. The arguments. There is a fallback because you can use the expression type and the documentation has examples of for example using IO so you could use import in compiled code and there's some examples of doing that. There's a thing called kernel function. We try to build native compiler support for things and we build and extend this with every release. And when we do that we're just using the techniques that we're using above. We're working now on some better introspection features that allow you to do some sort of query of these things to introspect on what's supported. But the only real way to do this is to actually try and use it and build up some knowledge like that. Right now I'm going to switch tack slightly and talk about an application of the compiler and give you some idea of what, what sorts of things you can do with this. And this is our data structure library. So this is a library of classical data structures that we've put together and they're built from the compiler, but you can use them without the compiler. And they provide some new features in Wolfram language. They have like mutable state and the data structures, we introduced them in 12.1 and we've been expanding ever since. So you can get like a listing of the data structures current, current ones, something over 30. So these are some of the core ones. This expo struct is a wrapper around expressions. There's a whole bunch of container type of data structures. There are sets, a bunch of different sets, probabilistic sets, bit sets, things such as that, various tree data structures and these a bunch of key value stores like hash tables that are kind of interesting. So many of the data structures support mutability, so changing the state is carried around inside of them. And so this introduces new possibilities, new programming possibilities that weren't there before. Another feature of them is that very often you can get very significant speed advantages from using them. We've been benchmarking them recently against standard libraries, in particular the C standard template library. And our versions of them are extremely competitive, except they're integrated into Wolfram language. So that's all pretty useful. So let's have a little look at an example of one. This is the extensible vector, which is a data structure that's designed to be efficient to apprent and prepend data. So I can create the data structure, prints out and things like that. So here I'm doing an append operation in a do loop. I'm doing that 100 times and I get back. There we go, we've appended 100 things again. Now when I look at it, the same data structure, it's now got 100 elements in it. That's what mutability means. I can get information slightly doesn't print out quite so nicely, but it's using a bit harder to see on the screen, but you can see all the operations and things here. I can say is it empty? And no, it's not. Another nice feature, the data structures is they all have a visualization that help you to understand some of what's how this is working. So for a linear layout container like this, it's not so interesting, but for some of the trees and things like that, it's quite useful. Now let's look at why a data structure like this is useful. So here's the expression version. So I can make a list and then I can call append to that list and I'm doing it 100 times. And there we go. There's the thing that we see here. But a key aspect of this is how long this takes. So here I'm appending 10,000 times to this list and it's running in 1.6 seconds. But the data structure version is running very significantly faster. And you get some idea of why that is. Because if we double the time, double the number of elements that we're appending, then we look at the time, the time has gone up by a factor of four, it's quadratic in the addition. Whereas this data structure here, when I doubled the length, what does immutable data structure means? It means I can change its state. That's. That's kind of what I'm showing now. And. Yes, so this is, this is very much faster to benefit from this. In version 14 that we're about to release, we have some new data structures. We have a red black tree, which is a balanced binary tree, and it's kind of the standard for a balanced primary tree. We've had another data structure which is similar to an av. We have an AVL tree, but the red black tree is more efficient. And again, adding these data structures, it's very common that we then go and build other data structures on top of them. And so this is like an important building block. So here's another new one that we've added. And this is a string vector. And this is a super efficient way to hold a collection of strings. So let's have a little look at the string vector. And I've created a string vector of, you know, two elements. Now, someone asked about mutability. So this is also an immutable data structure. I can't change the strings that are in this. There's no way to change that. I can't add a string, I can't take one out, I can't modify it. So this is immutable. If there was an operation to say, well, you know, stick something in the middle, that is mutating it, that's what mutability means. Ok, so going back to this string vector, we see that it's normal as a list of strings, it's got a bunch of useful operations. So here's one useful operation. This is counting in each element of the string vector how many times a string occurs. So in this first one, A appears twice and in the second one it's once. So you can do things like string matching. And here we see another new thing. So I'm doing string match on every element to see how many things start with A, and that's returning the result as a bit vector. A bit vector is a data structure and it's one that's particularly efficient for storing true and false or zeros and ones. But it's very compressed. I'll talk a little bit more about bit vectors later. I can then go and do pick on the data structure using this bit vector. And this is basically returning every, it returns another string vector and it's showing me everything that starts with an A. So why is this useful? Well, I'm generating, this is a list of words from some dictionary, nearly 40,000 of them. This is the first 10. And I'm going to put this into a string vector. So it's pretty fast to make the string vector containing these 40,000 words. But when we look, say at the storage, we see that it's very much, very efficient because it's nearly third of the storage size. And then when we do operations on this, so I want to do some sort of selecting on this list, everything that ends with U I could do select. I could use the function pick. And so that's, that's a bit faster. But if I used the string vector I would see that it's, it's, you know, very much faster yet again. So this is, excuse me, this is some of the power why we want to use this string vector. Having a data structure makes it, we're able to be faster. Now I want to look at some more programming ways you can do programming with the compiler. This is programming via downvalues. Let's have a look at this. I've written a function here, takes a list and a function and it iterates over it. And I'm going to call it in this thing here. And so it runs and gets the result. Now I'm going to make a function declaration and the function declaration is polymorphic. But now instead of an implementation for the function, I'm saying just use the down values. Just use these. This colon equals definitions that we have there. So I'm going to make that declaration and compile this. And now it says, you see, it's figured out that the result is going to be a boolean and we come along here and we get the same answer. So why is this useful? It's faster. So here I'm calling, this is the uncompiled one and this is the compiled one. So there's like three zeros here. It's massively faster. And this of course is a version of the built in function all true and it's sort of faster. That's a bit faster than the top level version. But this is masses faster. So if you're doing these sorts of things, you can gain advantages from that. And the benefits of these sorts of function declarations that you can develop top level code, but then you can move it to compiled code, which if it succeeds, compilation can often be faster. And again, using these polymorphic definitions, you don't need to have different versions for every different scenario. You can be quite abstract across things like this. And just to say, functions written like this are often very competitive with traditional compiled languages. And we're developing the text technology because this is how we are compiling the compiler. Now, to help with this transition between top level and compiled code, there's a few details I could show. So here I'm showing a function. Now, when I call works, this type is ignored in the function argument. And not only is it ignored, but it doesn't use it in any way. So this is not in any way meeting this type of. And we considered this and decided it would just be too weird to suddenly have these things failing or something like that. We have a thing called ifcompiled that can give different branches for compiled. So this is the compiled version, and this is if it's not compiled, this is uncompiled. But if I put this into function compile and executed it, it would run this version here. And there's also a thing called type hint. So again, if you want to give type hints to code, but you might want to use them in evaluator, and type hint is useful. So there I run the function and it's sort of stripped out the type hint. But now when I compile it, it sees the type hint and it says correctly that the result is going to be an integer 8, which was what we were using there. So now I'm going to have a little look at something that people often ask about that, and that is how to use data structures in compiled code. The data structures are built with the compiler. So this, you know, is relatively straightforward. So here's an example of a function that uses bit vector. Bit vector, it's a vector of bits. But rather than using an array, it uses integers to store the bits. So it has an array of integers and that's how it stores all the bits. So you get a tremendous compression of memory. And since it can use bit, you know, it's doing bit operations to do many things. Those are things that are super efficient with modern processes. So here's a function that test list. It takes a list and a predicate. It creates a bit vector, the appropriate length and it sets bits if the predicate returns true. So we can run this and there's some random data, and here's a bit vector that's Coming back out and we can see there's the bit vector and right. So that one there is positive and that's true. So it's like true or false depending whether it's positive or not. So let's compile this and I'm going to make a declaration. Here's a function that calls that declaration and compile this. And so this runs along and it's going to return, which it will do any minute, any second. There we go. So it takes a packed array and it returns a bit vector. So we can call this. There's our bit vector. Such like we see there. And let's just run this on some bigger code. Again this is takes a certain time and running this on the. So there's like two zeros going in there. So this is really significantly faster. Now we've seen returning a data structure from compiled code. Now I'm going to pass one in as an argument. Here's my function and I'm passing in the bit vector and in this function. So this is like, these are like could be a real function. I'm going to create a dynamic array and I'm going to look at every element in the list and if the bit vector is true, I'm going to copy that element into the array. So let's run this and compile this.
And.
We come back and this is going to return this returns the array. So, so there we go. And so we've got a packed array and a bit vector and we've got a dynamic array. But if you just notice the dynamic array is parameterized itself. It's a dynamic array of integer 64. So let's run this and there's our bit vector and there's our data structure. So this is a data dynamic array of integer 64s. So this is similar to the dynamic of expressions, which is if you use the sort of built in data structure that's pre compiled, you would do something like this and you can see it prints slightly differently and you know, I can. The difference being that this dynamic array can take anything. This one here is only integers of a certain size. If you want to do operations with such a thing, it's going to be necessary to write a compiled function to do that. So if I want to convert it to, you know, normal, for example, I have to make a function and then it's, you know, comes back and here we've got two and one and those are the positive elements that we have there. An alternative way that we might do this is to write slightly different function that I'm going to compile this and then I'll show you what this is doing. So this is very similar except when I do the appendix, I cast the element of the function to an inert expression and now what comes back is a dynamic array of inert expressions. So I run this and there's the bitvector and there's the dynamic array. So this is like a regular dynamic array of expressions. So it's not. Not so optimized in some ways, but it's more convenient. So that's a trade off that you're going to have to think about. So just quick summary of what I was showing there. I was showing using data structures in the compiler, creating them and returning them and then passing them in as an argument. And I was pointing out that these data structures can contain different contents. So I think now I'm going to pause and let's see, let Daniel take over and he's going to show. So I'm going to stop screen sharing. So Daniel, if you want to take over and give your thing and then at the end I'll switch back and have a summary and see how that's going. So that's me for now and Daniel's going to talk to you for a bit now.
Right. So my name is Daniel Sanchez. I work for the compiler project for some time now. And today I'm going to talk about calling external libraries from work language, give a little demo of how to do this.
Right.
So there already exists a couple of ways to do this. There might be more, but these are the ones that I'm aware of and those are workframe Library Link and the WSTP protocol. They are highly trusted and proven methods to call external libraries. But the issue, well, one of the issues that I see with them is that you have to write request proper code or some boilerplate code in another language. For instance, for LibraryLink you have to write it in C or this is a standard template for wsdp. So what I'm going to show you is how to call external libraries or functions entirely from language and the library I'm going to demo this is this ICU international component for Unicode. And it's a widely used set of C and C and even Java libraries that provide Unicode and globalization support for many applications. And in fact we already use them in functions such as transliterate and integrate in word from language. They come bundled with your mathematical installation. So these are ready to be used and test. You can see that they are found here. Okay. And the function that I want to demo. I plan to show more functions, but I don't have much time. Okay, so the function that I want to call from this library is ucarname, which as the name suggests, it retrieves the name of a Unicode character. It has five arguments, the first one being character code or character point. And as you can see here, this is of type UCART32, but if you look at the header files, this is just a typedef of an integer 32. That's fine. The second argument, I won't go into detail, I just pass a zero here. That gets me the full name. The third and fourth argument are the buffer and the buffer length in which the name will be filled in by the function. So this is a pointer to a car, and this is the length of set array. And the last argument will be a pointer to an error code and the function will fill that error code if anything failed in the execution. And here are the definitions of those types that I showed above. The last two are just enums and C. Okay, so first I'm going to show you how to do this, how to call these functions using this foreign function interface framework. This is a framework that allows program language to directly connect to external C compatible libraries. This is done fast and efficient. Here are a list of the symbols that this functionality.
Present.
And I'm going to use almost all of this in this demo. And this framework uses behind the scenes. It uses this LIBFFI library that again provides a C compatible interface for calling compile code by providing information about the function we want to call at runtime instead of at compile time. And Right, this is great because we don't have to do any compilation, we just provide the information about the function. And I should mention that this FFI in WordPress language uses this LIBFFI library thanks to the Wolfram compiler. And it's actually implemented in compile components, which I'll show at the end. Ok, so we want to call. And the first thing we should do is load this external library. External function or.
Or.
Well, we call this foreign function into the. Into your session. And we do this by using foreign function load, which is really similar to what library link library function load does.
The.
The first argument is the. The path to the library or the name of the library if your session can find this library. The second one is the name of the function that you want to call and the third one is the type signature of the function. These are the types, the argument types, and this is the return type. And I'm here saying that instead of using a UChart32, which we know that this is an integer32, I just put directly an integer32 here. The name choice, we saw that it was an enum values in C integrals, so I'm passing it cint here. This is the buffer, which is a row pointer to an unsigned car. I'm going to talk about this in a minute. This is the length of the buffer and this is the pointer to the error code. Yeah. What we get in return is the name length, which is of type integral 32. So a quick note about the name, about the name I'm passing here an 62 because that's what the actual library has inside it. And if you happen to call this ICU library from C code, you don't need to append this 62 because there's some rename shenanigans that happen in the header files. But since I'm not using the header files, I have to use the fully qualified. Right? So what types do FFI support? This is a table summarizing it. The first column is the FFI types and the second column is the C types. The corresponding C types and you will notice that from first column are shared with the compiler types because the FFI is implemented using the compiler. Right? So we have to find a way to fit a buffer to this external function. And we can do this by using row memory allocate, which will allocate. Well, we'll allocate memory of a given type of a given length, and we see here that I'm allocating 128 unsigned cars and what we get in return is a row pointer wrapped in a managed object. So I won't go into much detail about managed objects, but you should know that they are useful for when we. So, okay, they are useful when our object is no longer referenced. A certain freeing function will be called on the value that it's wrapping. So in this case we want row memory free to be called upon on rawpter when this object is no longer referenced. So that way we don't memory leak. Yeah, besides that, we also have to allocate our error code and we can do it again using raw memory allocate. And before calling the forint function, we have to make sure that the error code is correctly initialized. And to do so we can use row memory write to write a value into that pointer, which zero means there's no error. Okay, so let's actually call this function and let's get the name of this emoji here, I'll pass the character code here. The second argument is the name choice, which 0 is the full name. I'm here passing the buffer and the length which we allocated with row memory allocate and I'm here passing also the pointer to the error code. If I run this, I get back the 31, which is the name, the length of the character. We can verify that there was no error by using raw memory read, which essentially references this pointer and zero means there was no error. And we could also use rowmemory read and import the contents into a string. But a match more clean ways to use row memory import, which will import the contents of our row pointer using a certain format. And we support a couple of formats like string or byte arrays, but in this case we want to use a string. So I'm passing the name of the length here, the length of the name name. Right. So we can bundle all of this into function. This is the frame function load allocation of the buffers and the actual implementation, which you will see it's the same as what I already showed. Okay. And right. We can test this pass functions, pass character codes and test it in the ASCII region. That works fine. So what you will notice is that FFI allow us to call external code without the need to write any boilerplate code in other languages like the library LINQ framework requires. So this is what LibraryLink wrote for calling this file function would look like if you had to write it. Okay, let's go one level deeper. And we can also call the Use the word run compiler to call external code. And this is most of the functionality that the compiler exposes. Okay, yeah, let's jump into calling this ucard name. So we have type declarations in the compiler, so we can type declare macros for each of these types. So here I'm declaring those. So I'm storing these declarations in this list and I'm declaring that Uchart32 will be a macro for an integer32, which means that the compiler, whenever it sees this type, will replace it with this type of. I mentioned that enums are C integers, so that's what I have here. We also have to declare this external function, this UCAR main function. And we can do this by using library function declaration, which from already mentioned the name of the function, the library. And I'm using the signature here, but I instead of using the isin integer32, I'm using the macros that we set up. C string is also a macro for another type, which is A repo pointer of an assigned car. Okay, and now we have to write the wrapper function around this external function. And it looks kind of similar to the FFI implementation, but there's more detail. So I'm here allocating an array of 128. Characters here, I'm creating the pointer and initializing it with the zero error code. I'm here calling the external library function and I verify that there was no error. And if there was, I throw some error. Or I can return the empty string in this case, and using that buffer I can create a string using this create type instance, adding length. I don't forget to delete the memory allocated with this createtype instance. And finally I return the name. Okay, so we use I can use function compile to compile this functionality. I pass the declarations and then I pass the implementation of the wrapper code and give it some type annotation. Right. So yeah, I can here test it. I am doing a workload of all the names in the basic multilingual plane.
Right?
So, I mean, Tom mentioned this before, but there's a new way to, you know, to streamline this functionality. And this is compile component components. It's a framework for packaging and distributing compiler related functionality. And the object that holds all of this is the compile component. And we append declarations or type declarations and stuff to it using declarcompile component. So let's append the declarations that we already set up before appending in this notation. So this is the name of the compile component. I'm saying that these are the declarations that it should have. You see that it has four declarations. I'm also going to declare what the installed functions should be. So this here is an association where the key is a symbol and the run height side of the rule is the wrapper code that I showed above. So the idea is that whenever this compile component is built, this function here will be compiled into a compile called function. And when the compile component is loaded into your session, that compile function will be installed into this symbol as down code. Right. I will also make sure that the relevant external libraries are declared here. And this will be loaded whenever we load the compile component. Yeah, we can build it using build compile component, specifying the name of the component, the target directory, and the result is that we get a dynamic library. And the idea is that this library has all the functions that we compiled and specified before. So we can load this, we load the compile component I specify where my library is and that means now that my character name, which was just a symbol before, it now has some down code. You don't see it here, but you could use this internal function to verify that it has down code. And now I can call this function and pass in the character code. So just one note before finishing, I would like to talk about performance. So we already have a character name that returns the Unicode name and we can time it here using absolute or repeated timing. The FFI implementation is a bit slower, like nine times slower. But the benefits of using it, it's still really fast. The benefits of using it is that the boilerplate code and the time to set it up is extremely simple. It's not much code to write. And of course the compiler version is a lot faster than the built in one. And I should know that the character name does a bit of cheating because it has an internal dump of all the names. So this timing is just the name, the time it takes to look, look up an association. Yeah, I uploaded this demo and a bit more examples in this report if you want to take a look. But yeah, thanks. Thanks for your time.
Thank you, Daniel. So I'm just going to summarize the talk today. I gave a review of Wolfram language and talked a little bit about the compiler, some of its advantages and such like talked about how it embeds in the interpreter type and function declarations, data structures, programming with down values, compilation and data structures. And then Daniel showed some examples of calling external code. So now I'm trying to look for my networking center and I'm going to stop sharing, but I want to look at some of the. Here we go. That's what I'm looking for. Right. So thanks very much for the, you know, listening and I hope, hope that was all useful. I think that's, that's the end of the talk. But I'm going to stick around a little bit and try and answer some, some of the questions that have come in that I didn't answer all of the questions. So I think first of all, is there any Wolfram language code that can't be compiled? Yes, there's quite a lot of Wolfram language code that can't be compiled. I didn't show this, but maybe that would have been useful. I'm just going to rearrange my screen and then share again and I'll just point to some of the features. Right. I think I'm sharing my screen and in the help system we have a thing called kernel function. And this Lets you call from code into event evaluated code. And there's an example here using fit and here's an example using a. This is in the documentation in kernel function. I'm just in show. So this is using get in a file Opera IO example. So someone asked if import could be used. And you could use import via this kernel function. I mean, that would be. So you could import some data and then use it in the compiler. So that's an answer there. There was another question about a guarantee of correctness. And there's a bunch of ways that the guarantee of correctness can come. The type system makes sure that there's always. The types have to flow correctly and so you never reach a point in your computation where you suddenly don't have something. So if I have a function and boolean, go back to my boolean example and then I come and do function compile of func, it says, this is saying it's not going to suddenly run out of steam at some point. Other correctness things can be, I've got, you know, let's say I've got, you know, if arguments equals X. So here's. I've got a local variable, but there's a. It's initialized in a branch, but one branch doesn't, you know, doesn't, doesn't initialize it. So that's kind of a problem. So there's a number of more. And it's doing this at some analysis phase, so not when you execute the code, but when you're compiling it. In that sense, it's a sort of correctness guarantee. It's not an absolute guarantee of no errors. So that's not possible. Now there was another question that I thought was interesting, so somebody asked, I hope you can all see the Q and A questions. So data structures that work with compiler types do not work in the evaluator. Well, they do work in the evaluator, but they don't do all the things that the expression versions do. And it's not quite true because there's at least one compiler type, which is the expression compiler type, and they work in the expected way. Now, it would be possible. We haven't sort of formalized this, but it would be possible to build a set of functions to make a little package that could be loaded and then you would have a data structure, you know, a dynamic array of integers, and that, that, that, that might be useful. So that's, that's, that's possible. But at the moment, if you really care about this, it's probably better to return an expression data structure as as I showed in my example. Now there's a question here about C what are the possibilities C using. I can't quite. I don't quite understand how does the state of the C being various library link calls. Well you'd have to have some global. Some mechanism for keeping states in your program like global variable. That's. That's what I would say about question about C. There isn't a way to have recursive. No, no that's right. That's absolutely not the case. So I can have something like you know recursive. I can certainly have recursive functions in when compiling. Let's come and do this and for example I'm going to come and do. This takes nine Sorry, I'm just taking a little bit of time. If x greater than 10 greater than 1 we're going to have X times fact Oops times fact x minus 1 otherwise 1 so we made a definition there that's recursive and we've got this. I mean there's no restrictions like this. No. Why can this work? Maybe I need to do it like this. Oh I know yeah that's what I need to do. See, here we go right so I can. So I can. You can have recursive functions or they can be in done through function declaration is also so there's no restrictions like that. Data structures I'm looking at quite questions what's another for an unmanaged DLL in Windows is right. That's an interesting question. So somebody's asking. That's good good question is foreign function load now recommended over defined DLL function from the Netlink package and would there be any export expected performance differences? That's a good question. There's advantages and disadvantages so using defined deal using the Netlink package is using a very Microsoft specific technology so it's possible it might be a bit more optimized on Windows but I don't know that and I don't know about our binding so I don't know that one advantage of using the foreign function technology is it. It. It. You know you. You could load the DLL but you could load on you. You could work with it on Windows but its platform you know the foreign function interface works on other with other shared library formats. So if you had you know like. Like a Linux type so and a Mac die Lib it works on those so it's platform independent. I would expect there probably is a performance difference but I don't quite know which way around it would go. I mean, they are taking different code paths, so I think that's an interesting thing, but I don't fully have the answer for that now. Any more questions from anybody or shall we wrap up? I did try to explain what mutable data structures were. Yeah, I think that's. That's everything. Well, thanks everybody. We'll be uploading our talks and things so you can. So you can look at them. And I'll be giving another talk about working with gpus Cuda Nvidia gpus and how we can use the compiler for that tomorrow. But thanks very much, everybody.
More from Guy

Cheryl Strauss Einhorn | The Human Edge: Smarter Decisions in the Age of AI | Talks at Google

Rachel Glennerster | Innovation and the Center for Global Development | Talks at Google

Rebecca Winthrop | Rethinking the Purpose of Education in the Age of AI | Talks at Google

Dr. Robert Wachter | A Giant Leap: How AI Is Transforming Healthcare... | Talks at Google






