Skip to content

Commit

Permalink
fix Sphinx warnings
Browse files Browse the repository at this point in the history
  • Loading branch information
Mathnerd314 committed Jan 15, 2024
1 parent 66578aa commit 69e7689
Show file tree
Hide file tree
Showing 34 changed files with 2,750 additions and 955 deletions.
65 changes: 36 additions & 29 deletions docs/Commentary/Implementation/Compiler.rst
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,14 @@ Being able to break big complicated bytecode instructions down into more simple


Example: Fibonacci function
def fibonacci(n)
a, b = 0, 1
for _ in range(n): # inner loop
a, b = b, a + b # update a and b by adding them together
return a

.. code-block:: python
def fibonacci(n)
a, b = 0, 1
for _ in range(n): # inner loop
a, b = b, a + b # update a and b by adding them together
return a
The bytecode for the loop is something like this:
FOR_ITER
Expand Down Expand Up @@ -229,35 +232,39 @@ LuaJIT

see paper for benchmarks, of course multiple tiers are better, but tl;dr is copy-and-patch is a nice middle tier. It is a template JIT compiler. In particular, it works by copying over a static pre-compiled machine code "template" into executable memory, and then going through that machine code and patching up instructions that need to have runtime data encoded in them. This is sort of like the relocation phase of linking/loading an ELF file. And actually we can use LLVM to build an ELF object file and generate our templates. For example:

extern int MAGICALLY_INSERT_THE_OPARG;
extern int MAGICALLY_CONTINUE_EXECUTION(_PyInterpreterFrame *frame, PyObject **stack_pointer);
int load_fast(_PyInterpreterFrame *frame, PyObject **stack_pointer)
{
int oparg = &MAGICALLY_INSERT_THE_OPARG;
PyObject *value = frame->localsplus[oparg];
Py_INCREF(value);
*stack_pointer++ = value;
__attribute__((musttail)) return MAGICALLY_CONTINUE_EXECUTION(frame, stack_pointer);
}
.. code-block:: c
extern int MAGICALLY_INSERT_THE_OPARG;
extern int MAGICALLY_CONTINUE_EXECUTION(_PyInterpreterFrame *frame, PyObject **stack_pointer);
int load_fast(_PyInterpreterFrame *frame, PyObject **stack_pointer)
{
int oparg = &MAGICALLY_INSERT_THE_OPARG;
PyObject *value = frame->localsplus[oparg];
Py_INCREF(value);
*stack_pointer++ = value;
__attribute__((musttail)) return MAGICALLY_CONTINUE_EXECUTION(frame, stack_pointer);
}
So there are extern placeholders for inserting the oparg and continuing execution.
For the oparg, we use the address of the extern for our oparg. This generates more efficient code because the relocation inserts the constant directly, instead of needing to dereference the address.
And for continuing execution, we use LLVM's `musttail` so we get a single jump to the next opcode, and even better, if that jump happens to be of length zero, we can just skip the jump entirely. So, the object file that we get out of this looks like this:

.static
00: 48 b8 00 00 00 00 00 00 00 00 movabsq $0x0, %rax
0a: 48 98 cltq
0c: 49 8b 44 c5 48 movq 0x48(%r13,%rax,8), %rax
11: 8b 08 movl (%rax), %ecx
13: ff c1 incl %ecx
15: 74 02 je 0x19 <load_fast+0x19>
17: 89 08 movl %ecx, (%rax)
19: 48 89 45 00 movq %rax, (%rbp)
1d: 48 83 c5 08 addq $0x8, %rbp
21: e9 00 00 00 00 jmp 0x26 <load_fast+0x26>
.reloc
02: R_X86_64_64 MAGICALLY_INSERT_THE_OPARG
22: R_X86_64_PLT32 MAGICALLY_CONTINUE_EXECUTION - 0x4
.. code-block:: none
.static
00: 48 b8 00 00 00 00 00 00 00 00 movabsq $0x0, %rax
0a: 48 98 cltq
0c: 49 8b 44 c5 48 movq 0x48(%r13,%rax,8), %rax
11: 8b 08 movl (%rax), %ecx
13: ff c1 incl %ecx
15: 74 02 je 0x19 <load_fast+0x19>
17: 89 08 movl %ecx, (%rax)
19: 48 89 45 00 movq %rax, (%rbp)
1d: 48 83 c5 08 addq $0x8, %rbp
21: e9 00 00 00 00 jmp 0x26 <load_fast+0x26>
.reloc
02: R_X86_64_64 MAGICALLY_INSERT_THE_OPARG
22: R_X86_64_PLT32 MAGICALLY_CONTINUE_EXECUTION - 0x4
We have the machine code, and the relocations, and we know the calling convention. And so we can take this, parse it out and put it in static header files as data, and then we can implement copy and patch for real. There is python code https://github.com/brandtbucher/cpython/tree/justin/Tools/jit (c4904e44167de6d3f7a1f985697710fd8219b3b2) that handles actually extracting all the cases, compiling each one, parsing out the ELF (by dumping with LLVM to JSON), and then generating the header files. Then the final build has no LLVM dependency and is a self-contained JIT. And because clang/LLVM is portable, you can cross-compile for all platforms from Linux, or do whatever.

Expand Down
2 changes: 1 addition & 1 deletion docs/Commentary/Implementation/CoreSyntax.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ We use a simple program, boolean "and", presented in a Haskell-ish language:
Derivation tree
~~~~~~~~~~~~~~~

See the connectives :math:`\text{B} = \text{Bool}` and :math:`\multimap` defined :ref:`above <connectives>`. :math:`\multimap` is right associative as usual. Our program then has the following derivation tree, among others (we could add a bang to the first argument, use a multiple-argument function instead of currying, expand out the identity, etc.).
See the connectives :math:`\text{B} = \text{Bool}` and :math:`\multimap` defined in :ref:`Reference/Logic:Common connectives`. :math:`\multimap` is right associative as usual. Our program then has the following derivation tree, among others (we could add a bang to the first argument, use a multiple-argument function instead of currying, expand out the identity, etc.).

.. image:: /_static/Stroscot_AND_Proof_Tree.svg

Expand Down
2 changes: 1 addition & 1 deletion docs/Commentary/Implementation/Errors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ The wording may be important. A Java editor called Decaf intercepted and re-word

StackOverflow and compiler error messages used 3 argument layouts: claim alone, a simple argument consisting of claim, grounds, and warrant, and an extended argument which is a simple argument plus backing. These layouts are multiplied times 2 depending on whether there was a resolution in the claim; my notation is that "claim" means a claim without resolution. The tested results were claim < {simple,extended}, extended < claim+resolution (claim+resolution being dubbed a non-logical "quick fix" instruction).

Per the thesis :cite:`barikErrorMessagesRational` extended arguments are mainly useful for novices and unfamiliar code. Theorizing, if the developer knows what's going on, they likely want brief messages and their preference is claim+resolution > simple > extended > others. But with an ``--explain`` flag their preference is more like extended+resolution > simple+resolution > claim+resolution > extended > simple > others. It's probably worth a survey comparing error messages of varying verbosities to confirm.
Per the thesis :cite:`barikErrorMessagesRational2018` extended arguments are mainly useful for novices and unfamiliar code. Theorizing, if the developer knows what's going on, they likely want brief messages and their preference is claim+resolution > simple > extended > others. But with an ``--explain`` flag their preference is more like extended+resolution > simple+resolution > claim+resolution > extended > simple > others. It's probably worth a survey comparing error messages of varying verbosities to confirm.

* Report errors at the right time: Generally one wants to see errors as soon as possible, using static analysis tools.

Expand Down
15 changes: 5 additions & 10 deletions docs/Commentary/Implementation/IR.rst
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ Turner writes https://www.cs.kent.ac.uk/people/staff/dat/miranda/manual/30.html:

1. easier to read - six separate chunks of information rather than one big one
2. easier to debug - each of its functions can be exercised separately, on appropriate test data, within a Miranda session
3. more robust for future development - for example if we later wish to add a second `main' function that solves a different problem by using the same five auxiliary functions in another way, we can do so without having to restructure any existing code.
3. more robust for future development - for example if we later wish to add a second ``main`` function that solves a different problem by using the same five auxiliary functions in another way, we can do so without having to restructure any existing code.
4. in the current implementation, functions defined inside a "where" clause cannot have their types explicitly specified

In practice, programmers tend to use fewer than ten parameters, but little nesting, with a mixture of parameter lifting/dropping and block floating/sinking.
Expand Down Expand Up @@ -327,16 +327,11 @@ A basic block is a mixture of jump and non-jump instructions that is complete, i

Although phi nodes were an interesting idea all the `cool kids <https://mlir.llvm.org/docs/Rationale/Rationale/#block-arguments-vs-phi-nodes>`__ are now using block arguments. Blocks arguments fit better into various analysis passes.

Blocks
======

From a user perspective there are two types of jumpable addresses:

memory - effective address computation
SIB addressing form, where the index register is not used in address calculation, Scale is ignored. Only the base and displacement are used in effective address calculation.
VSIB memory addressing


* memory - effective address computation
* SIB addressing form, where the index register is not used in address calculation, Scale is ignored. Only the base and displacement are used in effective address calculation.
* VSIB memory addressing

Memory and the program counter are virtualized as well, using labels. A label refers to a memory location with a specific block of code loaded. The blocks are not ordered, so unconditional jumps must be inserted between blocks if necessary. The block order can be determined using profiling, removing the unconditional jump that is taken most often.

Expand All @@ -355,7 +350,7 @@ There are also constraints from the ABI calling convention: https://gitlab.com/x
Values
======

Since all values are representable in memory, we could use bytes in the IR for values. But this would lose the type information. So instead we must support all the value types listed in :ref:`Values`.
Since all values are representable in memory, we could use bytes in the IR for values. But this would lose the type information. So instead we must support all the value types listed in `Values`_.

Thorin
======
Expand Down
18 changes: 18 additions & 0 deletions docs/Commentary/Implementation/Implementation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,21 @@ Steelman 1F. "The language shall be composed from features that [...] can be imp
13E. Translators for the language will be written in the language and will be able to produce code for a variety of object machines. The machine independent parts of translators should be separate from code generators. Although it is desirable, translators need not be able to execute on every object machine. The internal characteristics of the translator (i.e., the translation method) shall not be specified by the language definition or standards.

13F. Translators shall fail to translate otherwise correct programs only when the program requires more resources during translation than are available on the host machine or when the program calls for resources that are unavailable in the specified object system configuration. Neither the language nor its translators shall impose arbitrary restrictions on language features. For example, they shall not impose restrictions on the number of array dimensions, on the number of identifiers, on the length of identifiers, or on the number of nested parentheses levels.

Language
========

A near-term goal is to write Stroscot in itself. However, it has to generate code first. I originally picked JavaScript to start for a number of reasons:

* It's the fastest interpreted language available
* It has reasonably up-to-date syntax and features thanks to TC39
* A lot of the inspiring projects were written in JS (Wat, macro lambda calculus)
* LLVM compiles to JS and there are LLVM bindings available for JS
* TypeScript doesn't add much besides compilation overhead

Since then, development has shifted to Haskell, for other reasons:

* The compiler/type system prevents a lot of common errors (particularly typos, which JS doesn't detect until late)
* A lot of other compiler-theory-heavy projects are written in Haskell or similar functional languages
* I'm most familiar with Haskell.

Loading

0 comments on commit 69e7689

Please sign in to comment.