Execution model - BenoitKnecht/julia GitHub Wiki

What is a program?

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):

  1. To produce a computation result (a value)
  2. To cause a side effect
  3. To change Julia's internal state, affecting future expressions (like adding a definition). This is a special kind of side effect.
An expression may arrive from anywhere (examples include a file, the keyboard, and the network), and does not require any external context to be interpreted correctly. Its run-time behavior may of course depend on what expressions came before, but its effect on Julia's internal state (when described in terms of specific primitives to be defined later) must be well-defined regardless of context, or else cause an error. It may be possible to violate this restriction, but any code that does so is considered part of the implementation of the system (in other words, if you do this you've technically created a new language Julia+).

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.

Translation phases

  1. Lex and parse
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. Inlining. An implicit rewriting where the compiler expands some calls in place.
  9. Optimization. May include such things as SSA conversion and peephole optimizations (most importantly, removing redundant boxing/unboxing operations).
  10. Code generation
Native code resulting from this process should be cached appropriately to avoid regenerating the same code. Note that the last three steps can be thought of as extensions of the term rewriting pass.

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.

⚠️ **GitHub.com Fallback** ⚠️