Optimization makes the program's intermediate language simpler before the backends are given it. Work that can be done while compiling is: an operator on literals becomes the literal it gives, an if whose condition is known becomes the branch it takes, and code that can never run, or that only works out a value nobody uses, is dropped. An operator whose operands' types are known is marked with the intrinsic they make it, so the backends can work it out directly instead of choosing what it does while running. A lambda given to map, filter or any on an Array is inlined: the loop the library would run, with the lambda's body in it. The program does exactly what it did, with less left to do while it runs.
Given the program in the intermediate language, once every macro round is done. Gives the same program, optimized, each file's part on its own. In bootstrapping/optimize; --optimized-ir prints it, written the way --ir writes the program before it - see Intermediate Language.
It comes after the last macro round and before code generation. Earlier, the program isn't finished: a macro's use is still waiting to be replaced, and compile-time code to be run. Later, there are two programs, one for each backend, and each change would have to be made twice. Here there's one program, complete, and whatever optimization does to it both backends get, so a program compiled to WebAssembly and to JavaScript still behaves the same.
It only ever looks inside one function at a time, so each file is optimized on its own. The standard library, compiled with the program, is optimized with it.
Each function's body - and each const's, and each lambda's inside them - is walked once, bottom up: an expression's parts are optimized before it, so a fold lets the one around it fold too, and 60 * 60 + 5 becomes 3605 in one go.
+, - and * on two integer literals of one type, and every comparison of them. Only when the result fits the type: an operation that overflows is an OverflowError, which the program gives when it runs, so it's left alone. The types folded are the ones an S64 holds every value of - S8 to S64, U8 to U32 - and U64's below S64.max(). A literal whose type the checker left open is an S64, as both backends hold it. A negative result is written as neg of a literal, the way a negative literal is written in the source, and folds again where it's used. not, == and != on Boolean literals, and ++ on two text literals. if or conditional whose condition is a literal is the branch it takes - which is how and and or on literals fold, since they're lowered to conditionals. is-undefined and is-error on a literal are known too. return, break, continue or fail in the same block can run, so it's dropped. So is an if with nothing in either branch, and an evaluate of a value that does nothing but give itself - a literal, a local, a function. for _ in #builtin.forever() while ... - the way a while loop is written - goes round without an iterator: when nothing reads the items of one that never runs out, the loop is all that's left. The standard library's own Iterator.forever() is treated the same, unless the program has a function forever for Iterator of its own. + - * / == != < <= > >= on S8 to S64, U8 to U64 and F64 become ADD_U64_U64, LT_F64_F64, ... An operand's type is known when it's a local (its type is on it), a literal, an as, or another marked operator's result. An integer's / isn't marked. map, filter, filterMap, forEach, any, all or find given a lambda written there becomes the loop the standard library runs for an Array, with the lambda's body in it - see below. Nothing is folded or dropped that could fail, has an effect, or depends on what the program is running in. Operators on other values - numbers of different types, floats, a struct with its own + - are left for the running program to choose, as they are without optimization.
Calling a lambda costs more than most of what one does: xs.map(λ(x) x * 2) spends longer making the call for each item than multiplying. So a call of map, filter, filterMap, forEach, any, all or find given a lambda of one parameter, written right there, becomes the loop Array's method runs, with the lambda's body in it - its returns giving the item's result. The lambda's parameter and locals become new locals of the function, so each item's are its own, as they were.
A chain - xs.map(...).filter(...).any(...) - is one loop, building one array, or none: each item goes through every lambda in turn, rather than each lambda through every item. That's only done when it can't be told apart - when every lambda in it but one at most only works out operators, tests and literals on its item and on locals nothing changes. Otherwise each call is a loop of its own, one after the other, so what the lambdas do happens in the order it did.
Whether the value is an Array is only known while running, so the loops are run when it is, and the calls as they were written when it isn't - a Set's map gives a Set, an Iterator's is lazy. They're done to what a statement works out first - the value of a let or set, of a return, an if's condition, or what that value starts with - so the loops go before the statement and nothing in it changes place. A lambda that only means something as a function of its own - one that waits, yields or has a parameter left out - and a program with a map of its own for Array keep their calls.
An operator on its own is chosen while running: the backends check whether either side is a struct with its own +, whether it's an error, which number types meet and what they're worked in - for every a + b. A marked one skips all that when the values are what the intrinsic says. Each backend works out ADD_U64_U64 right there - two U64s added, or two small S64s with one WebAssembly i64.add - when both values are U64s and the result fits; for two F64s, always.
Otherwise the operator runs exactly as it would have: an overflow is an OverflowError reported as the operator's, not the intrinsic's. Gradual typing means a local typed U64 isn't certain to hold one, so the type only chooses which fast way to try - a wrong one costs a check, and never changes what the program does.
The WebAssembly backend uses the checker's types the same way for methods and fields. A call whose receiver's type is known - tokens.size() on an Array, cursor.peek() on a struct - calls the method that type has directly, with no Array of arguments and no dispatcher, when the receiver is of that type; a field of a struct known by its type is read straight from its place. Anything else - an error standing for the value, say - goes the usual way.
A U64 below 232 is an immediate, as a small S64 is: no allocation for an index, a size or a count, and none for the U64s an ADD_U64_U64 or a let i: U64 ≔ 0 makes.
--optimized-ir is a stage's result like any other: printed for each file, or kept in the folder --out names, as main.rfn.optimized-ir.
Optimization is on whenever the program is compiled. --no-optimize turns it off, and the backends are given the intermediate language as lowering left it - to see whether optimization is behind something a program does, since it never should be. Asking for --optimized-ir and --no-optimize together is an error.
Nothing - everything it's given has already been checked, and it changes how the program does what it does, never what.
There's no optimization step: src/6-compile-wasm and src/6-compile-js generate code from the syntax tree as it's written.
Next: Code Generation.