Execution model - BenoitKnecht/julia GitHub Wiki
The only interface for running code in Julia is handing it an expression to evaluate.
An expression is like a sentence in English; it's a language unit that expresses a complete thought.
Expressions have three primary purposes (although there is no external difference between expressions serving these purposes):
- To produce a computation result (a value)
- To cause a side effect
- To change Julia's internal state, affecting future expressions (like adding a definition). This is a special kind of side effect.
A file is just a way of storing a series of expressions that usually need to be evaluated together. However, sometimes it is convenient to use a file as a more meaningful semantic grouping. This can be supported by allowing end-of-file to close some kinds of blocks, such as a module declaration.
- Lex and parse
- Syntax de-sugaring. These are structural transformations for converting convenient high-level syntax into core forms. Initially, these will only exist internally (like the parser). It is not clear if macros at this level should be exposed to the user.
- Name resolution. We figure out how each user-written identifier is bound, i.e. which local scope it belongs to or which module it is from.
- Delaying. We locate chunks of code that cannot (or should not) be translated further until the types of certain variables are known. Imagine wrapping expressions in a form like (wait-for-types (a b c ...) code). This is the end of the "static" phases. This idea is kind of half-baked at the moment.
- Evaluation begins. Special forms like "if" invoke evaluation recursively in an obvious way. Everything else is a function call, a wait-for-types form, or a block of native code. A call is handled by looking up the correct method, evaluating and binding arguments, and evaluating the function body. A block of native code is handled by executing it. A wait-for-types form is handled by looking at the types of the parameterizing variables and proceeding to the next translation phase.
- Type inference is performed on the body of a wait-for-types form. If another wait-for-types form is encountered and we know the types of its parameters, we can move inside the form.
- Term rewriting. The expression is expanded using a library of patterns involving types. Expansions may not do anything that changes the typing of the code. Think of them as optimizations and clarifications. Hopefully it is possible to enforce this.
- Inlining. An implicit rewriting where the compiler expands some calls in place.
- Optimization. May include such things as SSA conversion and peephole optimizations (most importantly, removing redundant boxing/unboxing operations).
- Code generation
There is a notion of "required" and "optional" passes. Passes 1, 2, 3, and 5 are necessary in any implementation of the language. If macros are part of the language then pass 7 is required. Passes 8 and 9 are most likely always optional. For static typing, pass 6 is required, otherwise it is optional. If an interpreter is present, then passes 4 and 10 are optional. Otherwise, pass 5 only handles the native code case. If we support "fallback" code generation where the pass 10 back-end runs without type information, then pass 4 is optional.
Pass 4 requires pass 6 to remove the wait-for-types forms.
If all macros refer to types then pass 7 requires pass 6 as well.
In a strong, statically typed variant you would only need wait-for-types forms around the whole bodies of functions, and failing to determine a type in pass 6 would be an error.