Skip to main content

Using taichi.js with Minifiers

The Problem

taichi.js is a compiler. It takes Javascript code in your program and compiles them into WebGPU code. Under the hood, this is achieved by calling the .toString() on the methods you pass to ti.kernel(..). However, in production environments, we often pass the Javascript code into some sort of minifier, which often changes the source code of the functions significantly. This could often lead to compilation errors thrown by the taichi.js compiler when your program is running in release mode.

As an example, consider the following program

let fieldWithLongName = ti.field(ti.i32, 1000)
ti.addToKernelScope({ fieldWithLongName })
let k = ti.kernel(() => {
for(let i of ti.range(1000)){
fieldWithLongName[i] += 1
}
})

A Javascript minifier might decide to rename the fieldWithLongName variable because its name is too long. The minifier will likely apply a code transformation that turns the code into

let f = ti.field(ti.i32, 1000)
ti.addToKernelScope({ fieldWithLongName: f })
let k = ti.kernel(() => {
for(let i of ti.range(1000)){
f[i] += 1
}
})

Now, all of a sudden, taichi.js will start complaining that it does not recognize the variable f. This is because the transformed code still adds a variable named fieldWithLongName into the kernel scope, but the variable name within the kernel has already been modified.

This is only one example of how minifiers can mess-up the programming model of taichi.js. In general, minifiers and other code transformation tools may apply code transformations based on their own assumptions of the semantics of your code, and these assumptions often fail when taichi.js is involved.

Mitigations

There are a number of things that you can do to avoid problematic transformations by minifiers:

However, these are not guaranteed to work, and it largely depends on the specific minifier you are using.

String for the win

The ultimate mitigation, which is almost guaranteed to work, is to pass a string directly. taichi.js allows you to write this:

let fieldWithLongName = ti.field(ti.i32, 1000)
ti.addToKernelScope({ fieldWithLongName })
let k = ti.kernel(
`
() => {
for(let i of ti.range(1000)){
fieldWithLongName[i] += 1
}
}
`
)

Since you are now passing a string to ti.kernel(), no minifiers would dare modify it in any way.

Rollup

If you use rollup to bundle your project, there is a rollup-plugin-taichi which automatically turns every argument in ti.kernel() into a string. You can enable it by putting this into your rollup.config.js:

...
import taichi from "rollup-plugin-taichi"

export default {
...
plugins: [
taichi(),
]
};

This will make sure all the kernels you write are free from the tempering of minifiers. However, notice that this plugin only transforms kernel code to strings, but it does not transform any external functions your kernels use:

let f = (x) => { return x + 1 }
ti.addToKernelScope({ f })
let k = ti.kernel(() => {
f(...)
})

Here, minifiers may still wreck havoc on the lambda f. To prevent this from happening, you can either specify f as a raw string:

let f = `(x) => { return x + 1 }`

Or better yet, you can use the ti.func() function:

let f = ti.func((x) => { return x + 1 })

In taichi.js library, ti.func has no effect, and it returns any argument as is. However, rollup-plugin-taichi will identify this as a function for kernel consumption, and will transform it into a string to protect it from the hands of minifiers.