15 Commits

25 changed files with 679 additions and 70 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
**/build/*

View File

@@ -1,14 +1,13 @@
$basic-border: 1px solid #bfbfbf; @import "style.scss";
.gmachine-instruction { .gmachine-instruction {
display: flex; display: flex;
border: $basic-border; @include bordered-block;
border-radius: 2px;
} }
.gmachine-instruction-name { .gmachine-instruction-name {
padding: 10px; padding: 10px;
border-right: $basic-border; border-right: $standard-border;
flex-grow: 1; flex-grow: 1;
flex-basis: 20%; flex-basis: 20%;
text-align: center; text-align: center;
@@ -20,7 +19,7 @@ $basic-border: 1px solid #bfbfbf;
} }
.gmachine-inner { .gmachine-inner {
border-bottom: $basic-border; border-bottom: $standard-border;
width: 100%; width: 100%;
&:last-child { &:last-child {

View File

@@ -37,6 +37,6 @@ add_executable(compiler
# Configure compiler executable # Configure compiler executable
target_include_directories(compiler PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(compiler PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_include_directories(compiler PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) target_include_directories(compiler PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
target_include_directories(compiler PUBLIC ${LLVM_DEFINITIONS}) target_include_directories(compiler PUBLIC ${LLVM_INCLUDE_DIRS})
target_compile_definitions(compiler PUBLIC ${LLVM_DEFINITIONS}) target_compile_definitions(compiler PUBLIC ${LLVM_DEFINITIONS})
target_link_libraries(compiler ${LLVM_LIBS}) target_link_libraries(compiler ${LLVM_LIBS})

View File

@@ -1,2 +1,3 @@
defn main = { plus 320 6 } defn main = { sum 320 6 }
defn plus x y = { x + y } defn sum x y = { x + y }

View File

@@ -5,3 +5,4 @@ defn length l = {
Cons x xs -> { 1 + length xs } Cons x xs -> { 1 + length xs }
} }
} }
defn main = { length (Cons 1 (Cons 2 (Cons 3 Nil))) }

View File

@@ -0,0 +1,16 @@
data List = { Nil, Cons Int List }
defn add x y = { x + y }
defn mul x y = { x * y }
defn foldr f b l = {
case l of {
Nil -> { b }
Cons x xs -> { f x (foldr f b xs) }
}
}
defn main = {
foldr add 0 (Cons 1 (Cons 2 (Cons 3 (Cons 4 Nil)))) +
foldr mul 1 (Cons 1 (Cons 2 (Cons 3 (Cons 4 Nil))))
}

View File

@@ -0,0 +1,17 @@
data List = { Nil, Cons Int List }
defn sumZip l m = {
case l of {
Nil -> { 0 }
Cons x xs -> {
case m of {
Nil -> { 0 }
Cons y ys -> { x + y + sumZip xs ys }
}
}
}
}
defn ones = { Cons 1 ones }
defn main = { sumZip ones (Cons 1 (Cons 2 (Cons 3 Nil))) }

View File

@@ -108,12 +108,12 @@ void output_llvm(llvm_context& ctx, const std::string& filename) {
std::error_code ec; std::error_code ec;
llvm::raw_fd_ostream file(filename, ec, llvm::sys::fs::F_None); llvm::raw_fd_ostream file(filename, ec, llvm::sys::fs::F_None);
if (ec) { if (ec) {
std::cerr << "Could not open output file: " << ec.message() << std::endl; throw 0;
} else { } else {
llvm::TargetMachine::CodeGenFileType type = llvm::TargetMachine::CGFT_ObjectFile; llvm::TargetMachine::CodeGenFileType type = llvm::TargetMachine::CGFT_ObjectFile;
llvm::legacy::PassManager pm; llvm::legacy::PassManager pm;
if (targetMachine->addPassesToEmitFile(pm, file, NULL, type)) { if (targetMachine->addPassesToEmitFile(pm, file, NULL, type)) {
std::cerr << "Unable to emit target code" << std::endl; throw 0;
} else { } else {
pm.run(ctx.module); pm.run(ctx.module);
file.close(); file.close();
@@ -136,7 +136,6 @@ void gen_llvm(const std::vector<definition_ptr>& prog) {
for(auto& definition : prog) { for(auto& definition : prog) {
definition->gen_llvm_second(ctx); definition->gen_llvm_second(ctx);
} }
llvm::verifyModule(ctx.module);
ctx.module.print(llvm::outs(), nullptr); ctx.module.print(llvm::outs(), nullptr);
output_llvm(ctx, "program.o"); output_llvm(ctx, "program.o");
} }

View File

@@ -2,12 +2,11 @@
title: Compiling a Functional Language Using C++, Part 0 - Intro title: Compiling a Functional Language Using C++, Part 0 - Intro
date: 2019-08-03T01:02:30-07:00 date: 2019-08-03T01:02:30-07:00
tags: ["C and C++", "Functional Languages", "Compilers"] tags: ["C and C++", "Functional Languages", "Compilers"]
draft: true
--- ---
During my last academic term, I was enrolled in a compilers course. During my last academic term, I was enrolled in a compilers course.
We had a final project - develop a compiler for a basic Python subset, We had a final project - develop a compiler for a basic Python subset,
using LLVM. It was a little boring - virtually nothing about the compiler using LLVM. It was a little boring - virtually nothing about the compiler
was __not__ covered in class, and it felt more like putting two puzzles was __not__ covered in class, and it felt more like putting two puzzle
pieces together than building a real project. pieces together than building a real project.
Instead, I chose to implement a compiler for a functional programming language, Instead, I chose to implement a compiler for a functional programming language,
@@ -138,4 +137,6 @@ Here are the posts that I've written so far for this series:
* [Typechecking]({{< relref "03_compiler_typechecking.md" >}}) * [Typechecking]({{< relref "03_compiler_typechecking.md" >}})
* [Small Improvements]({{< relref "04_compiler_improvements.md" >}}) * [Small Improvements]({{< relref "04_compiler_improvements.md" >}})
* [Execution]({{< relref "05_compiler_execution.md" >}}) * [Execution]({{< relref "05_compiler_execution.md" >}})
* [Compilation]({{< relref "06_compiler_semantics.md" >}}) * [Compilation]({{< relref "06_compiler_compilation.md" >}})
* [Runtime]({{< relref "07_compiler_runtime.md" >}})
* [LLVM]({{< relref "08_compiler_llvm.md" >}})

View File

@@ -2,7 +2,6 @@
title: Compiling a Functional Language Using C++, Part 1 - Tokenizing title: Compiling a Functional Language Using C++, Part 1 - Tokenizing
date: 2019-08-03T01:02:30-07:00 date: 2019-08-03T01:02:30-07:00
tags: ["C and C++", "Functional Languages", "Compilers"] tags: ["C and C++", "Functional Languages", "Compilers"]
draft: true
--- ---
It makes sense to build a compiler bit by bit, following the stages we outlined in It makes sense to build a compiler bit by bit, following the stages we outlined in
the first post of the series. This is because these stages are essentially a pipeline, the first post of the series. This is because these stages are essentially a pipeline,
@@ -48,7 +47,7 @@ are fairly simple - one or more digits is an integer, a few letters together
are a variable name. In order to be able to efficiently break text up into are a variable name. In order to be able to efficiently break text up into
such tokens, we restrict ourselves to __regular languages__. A language such tokens, we restrict ourselves to __regular languages__. A language
is defined as a set of strings (potentially infinite), and a regular is defined as a set of strings (potentially infinite), and a regular
language for which we can write a __regular expression__ to check if language is one for which we can write a __regular expression__ to check if
a string is in the set. Regular expressions are a way of representing a string is in the set. Regular expressions are a way of representing
patterns that a string has to match. We define regular expressions patterns that a string has to match. We define regular expressions
as follows: as follows:
@@ -77,7 +76,7 @@ Let's see some examples. An integer, such as 326, can be represented with \\([0-
This means, one or more characters between 0 or 9. Some (most) regex implementations This means, one or more characters between 0 or 9. Some (most) regex implementations
have a special symbol for \\([0-9]\\), written as \\(\\setminus d\\). A variable, have a special symbol for \\([0-9]\\), written as \\(\\setminus d\\). A variable,
starting with a lowercase letter and containing lowercase or uppercase letters after it, starting with a lowercase letter and containing lowercase or uppercase letters after it,
can be written as \\(\[a-z\]([a-z]+)?\\). Again, most regex implementations provide can be written as \\(\[a-z\]([a-zA-Z]+)?\\). Again, most regex implementations provide
a special operator for \\((r_1+)?\\), written as \\(r_1*\\). a special operator for \\((r_1+)?\\), written as \\(r_1*\\).
So how does one go about checking if a regular expression matches a string? An efficient way is to So how does one go about checking if a regular expression matches a string? An efficient way is to
@@ -115,8 +114,8 @@ represent numbers directly into numbers, and do other small tasks.
So, what tokens do we have? From our arithmetic definition, we see that we have integers. So, what tokens do we have? From our arithmetic definition, we see that we have integers.
Let's use the regex `[0-9]+` for those. We also have the operators `+`, `-`, `*`, and `/`. Let's use the regex `[0-9]+` for those. We also have the operators `+`, `-`, `*`, and `/`.
`-` is simple enough: the corresponding regex is `-`. We need to The regex for `-` is simple enough: it's just `-`. However, we need to
preface our `/`, `+` and `*` with a backslash, though, since they happen to also be modifiers preface our `/`, `+` and `*` with a backslash, since they happen to also be modifiers
in flex's regular expressions: `\/`, `\+`, `\*`. in flex's regular expressions: `\/`, `\+`, `\*`.
Let's also represent some reserved keywords. We'll say that `defn`, `data`, `case`, and `of` Let's also represent some reserved keywords. We'll say that `defn`, `data`, `case`, and `of`

View File

@@ -2,7 +2,6 @@
title: Compiling a Functional Language Using C++, Part 2 - Parsing title: Compiling a Functional Language Using C++, Part 2 - Parsing
date: 2019-08-03T01:02:30-07:00 date: 2019-08-03T01:02:30-07:00
tags: ["C and C++", "Functional Languages", "Compilers"] tags: ["C and C++", "Functional Languages", "Compilers"]
draft: true
--- ---
In the previous post, we covered tokenizing. We learned how to convert an input string into logical segments, and even wrote up a tokenizer to do it according to the rules of our language. Now, it's time to make sense of the tokens, and parse our language. In the previous post, we covered tokenizing. We learned how to convert an input string into logical segments, and even wrote up a tokenizer to do it according to the rules of our language. Now, it's time to make sense of the tokens, and parse our language.
@@ -38,7 +37,7 @@ $$
In practice, there are many ways of using a CFG to parse a programming language. Various parsing algorithms support various subsets In practice, there are many ways of using a CFG to parse a programming language. Various parsing algorithms support various subsets
of context free languages. For instance, top down parsers follow nearly exactly the structure that we had. They try to parse of context free languages. For instance, top down parsers follow nearly exactly the structure that we had. They try to parse
a nonterminal by trying to match each symbol in its body. In the rule \\(S \\rightarrow \\alpha \\beta \\gamma\\), it will a nonterminal by trying to match each symbol in its body. In the rule \\(S \\rightarrow \\alpha \\beta \\gamma\\), it will
first try to match \\(alpha\\), then \\(beta\\), and so on. If one of the three contains a nonterminal, it will attempt to parse first try to match \\(\\alpha\\), then \\(\\beta\\), and so on. If one of the three contains a nonterminal, it will attempt to parse
that nonterminal following the same strategy. However, this leaves a flaw - For instance, consider the grammar that nonterminal following the same strategy. However, this leaves a flaw - For instance, consider the grammar
$$ $$
\\begin{align} \\begin{align}
@@ -105,7 +104,7 @@ A\_{add} & \\rightarrow A\_{add}-A\_{mult} \\\\\\
A\_{add} & \\rightarrow A\_{mult} A\_{add} & \\rightarrow A\_{mult}
\\end{align} \\end{align}
$$ $$
The first rule matches another addition, added to the result of another addition. We use the addition in the body The first rule matches another addition, added to the result of a multiplication. Similarly, the second rule matches another addition, from which the result of a multiplication is then subtracted. We use the \\(A\_{add}\\) on the left side of \\(+\\) and \\(-\\) in the body
because we want to be able to parse strings like `1+2+3+4`, which we want to view as `((1+2)+3)+4` (mostly because because we want to be able to parse strings like `1+2+3+4`, which we want to view as `((1+2)+3)+4` (mostly because
subtraction is [left-associative](https://en.wikipedia.org/wiki/Operator_associativity)). So, we want the top level subtraction is [left-associative](https://en.wikipedia.org/wiki/Operator_associativity)). So, we want the top level
of the tree to be the rightmost `+` or `-`, since that means it will be the "last" operation. You may be asking, of the tree to be the rightmost `+` or `-`, since that means it will be the "last" operation. You may be asking,
@@ -150,7 +149,7 @@ What's the last \\(C\\)? We also want a "thing" to be a case expression. Here ar
$$ $$
\\begin{align} \\begin{align}
C & \\rightarrow \\text{case} \\; A\_{add} \\; \\text{of} \\; \\{ L\_B\\} \\\\\\ C & \\rightarrow \\text{case} \\; A\_{add} \\; \\text{of} \\; \\{ L\_B\\} \\\\\\
L\_B & \\rightarrow R \\; , \\; L\_B \\\\\\ L\_B & \\rightarrow R \\; L\_B \\\\\\
L\_B & \\rightarrow R \\\\\\ L\_B & \\rightarrow R \\\\\\
R & \\rightarrow N \\; \\text{arrow} \\; \\{ A\_{add} \\} \\\\\\ R & \\rightarrow N \\; \\text{arrow} \\; \\{ A\_{add} \\} \\\\\\
N & \\rightarrow \\text{lowerVar} \\\\\\ N & \\rightarrow \\text{lowerVar} \\\\\\

View File

@@ -1,7 +1,6 @@
--- ---
title: Compiling a Functional Language Using C++, Part 3 - Type Checking title: Compiling a Functional Language Using C++, Part 3 - Type Checking
date: 2019-08-06T14:26:38-07:00 date: 2019-08-06T14:26:38-07:00
draft: true
tags: ["C and C++", "Functional Languages", "Compilers"] tags: ["C and C++", "Functional Languages", "Compilers"]
--- ---
I think tokenizing and parsing are boring. The thing is, looking at syntax I think tokenizing and parsing are boring. The thing is, looking at syntax

View File

@@ -1,7 +1,6 @@
--- ---
title: Compiling a Functional Language Using C++, Part 4 - Small Improvements title: Compiling a Functional Language Using C++, Part 4 - Small Improvements
date: 2019-08-06T14:26:38-07:00 date: 2019-08-06T14:26:38-07:00
draft: true
tags: ["C and C++", "Functional Languages", "Compilers"] tags: ["C and C++", "Functional Languages", "Compilers"]
--- ---
We've done quite a big push in the previous post. We defined We've done quite a big push in the previous post. We defined

View File

@@ -1,7 +1,6 @@
--- ---
title: Compiling a Functional Language Using C++, Part 5 - Execution title: Compiling a Functional Language Using C++, Part 5 - Execution
date: 2019-08-06T14:26:38-07:00 date: 2019-08-06T14:26:38-07:00
draft: true
tags: ["C and C++", "Functional Languages", "Compilers"] tags: ["C and C++", "Functional Languages", "Compilers"]
--- ---
{{< gmachine_css >}} {{< gmachine_css >}}
@@ -47,7 +46,7 @@ defn snd p = {
P x y -> { y } P x y -> { y }
} }
} }
defn slow x = { returns x after waiting for 4 seconds } defn slow x = { returns x after waiting for 1 second }
defn main = { fst (P (slow 320) (slow 6)) } defn main = { fst (P (slow 320) (slow 6)) }
``` ```
@@ -559,7 +558,9 @@ rule to Unwind:
{{< /gmachine_inner >}} {{< /gmachine_inner >}}
{{< /gmachine >}} {{< /gmachine >}}
Just one more! Sometimes, it's possible for a tree node to reference itself. Just a couple more special-purpose instructions, and we're done!
Sometimes, it's possible for a tree node to reference itself.
For instance, Haskell defines the For instance, Haskell defines the
[fixpoint combinator](https://en.wikipedia.org/wiki/Fixed-point_combinator) [fixpoint combinator](https://en.wikipedia.org/wiki/Fixed-point_combinator)
as follows: as follows:
@@ -587,9 +588,27 @@ We can allocate an indirection on the stack, and call Update on it when
we've constructed a node. While we're constructing the tree, we can we've constructed a node. While we're constructing the tree, we can
refer to the indirection when a self-reference is required. refer to the indirection when a self-reference is required.
Lastly, we also define a Pop instruction, which just removes
some number of nodes from the stack. We want this because
calling Update at the end of a function modifies a node further up the stack,
leaving anything on top of the stack after that node as scratch work. We get
rid of that scratch work simply by popping it.
{{< gmachine "Pop" >}}
{{< gmachine_inner "Before">}}
\( \text{Pop} \; n : i \quad a_1, a_2, ..., a_n : s \quad d \quad h \quad m \)
{{< /gmachine_inner >}}
{{< gmachine_inner "After" >}}
\( i \quad s \quad d \quad h \quad m \)
{{< /gmachine_inner >}}
{{< gmachine_inner "Description" >}}
Pop \(n\) nodes from the stack.
{{< /gmachine_inner >}}
{{< /gmachine >}}
That's it for the instructions. Knowing them, however, doesn't That's it for the instructions. Knowing them, however, doesn't
tell us what to do with our `ast` structs. We'll need to define tell us what to do with our `ast` structs. We'll need to define
rules to translate trees into these instructions, and I've already rules to translate trees into these instructions, and I've already
alluded to this when we went over `double 326`. alluded to this when we went over `double 326`.
However, this has already gotten pretty long, However, this has already gotten pretty long,
so we'll do it in the next post: [Part 6 - Compilation]({{< relref "06_compiler_semantics.md" >}}). so we'll do it in the next post: [Part 6 - Compilation]({{< relref "06_compiler_compilation.md" >}}).

View File

@@ -1,7 +1,6 @@
--- ---
title: Compiling a Functional Language Using C++, Part 6 - Compilation title: Compiling a Functional Language Using C++, Part 6 - Compilation
date: 2019-08-06T14:26:38-07:00 date: 2019-08-06T14:26:38-07:00
draft: true
tags: ["C and C++", "Functional Languages", "Compilers"] tags: ["C and C++", "Functional Languages", "Compilers"]
--- ---
In the previous post, we defined a machine for graph reduction, In the previous post, we defined a machine for graph reduction,
@@ -286,7 +285,7 @@ struct, called `type_data`:
When we create types from `definition_data`, we tag the corresponding constructors: When we create types from `definition_data`, we tag the corresponding constructors:
{{< codelines "C++" "compiler/06/definition.cpp" 53 69 >}} {{< codelines "C++" "compiler/06/definition.cpp" 54 71 >}}
Ah, but adding constructor info to the type doesn't solve the problem. Ah, but adding constructor info to the type doesn't solve the problem.
Once we performed type checking, we don't keep Once we performed type checking, we don't keep
@@ -407,11 +406,12 @@ We also add a `compile` method to definitions, since they contain
our AST nodes. The method is empty for `defn_data`, and our AST nodes. The method is empty for `defn_data`, and
looks as follows for `definition_defn`: looks as follows for `definition_defn`:
{{< codelines "C++" "compiler/06/definition.cpp" 44 51 >}} {{< codelines "C++" "compiler/06/definition.cpp" 44 52 >}}
Notice that we terminate the function with Update. This Notice that we terminate the function with Update and Pop. This
will turn the `ast_app` node that served as the "root" will turn the `ast_app` node that served as the "root"
of the application into an indirection to the value that we have computed. of the application into an indirection to the value that we have computed.
Doing so will also remove all "scratch work" from the stack.
In essense, this is how we can lazily evaluate expressions. In essense, this is how we can lazily evaluate expressions.
Finally, we make a function in our `main.cpp` file to compile Finally, we make a function in our `main.cpp` file to compile
@@ -436,12 +436,16 @@ PushInt(320)
PushGlobal(plus) PushGlobal(plus)
MkApp() MkApp()
MkApp() MkApp()
Update(0)
Pop(0)
Push(1) Push(1)
Push(1) Push(1)
PushGlobal(+) PushGlobal(plus)
MkApp() MkApp()
MkApp() MkApp()
Update(2)
Pop(2)
``` ```
The first sequence of instructions is clearly `main`. It creates The first sequence of instructions is clearly `main`. It creates
@@ -474,13 +478,14 @@ Jump(
PushGlobal(length) PushGlobal(length)
MkApp() MkApp()
PushInt(1) PushInt(1)
PushGlobal(+) PushGlobal(plus)
MkApp() MkApp()
MkApp() MkApp()
Slide(2) Slide(2)
) )
Update(1) Update(1)
Pop(1)
``` ```
We push the first (and only) parameter onto the stack. We then make We push the first (and only) parameter onto the stack. We then make
@@ -496,4 +501,4 @@ into G-machine code. We're not done, however - our computers
aren't G-machines. We'll need to compile our G-machine code to aren't G-machines. We'll need to compile our G-machine code to
__machine code__ (we will use LLVM for this), implement the __machine code__ (we will use LLVM for this), implement the
__runtime__, and develop a __garbage collector__. We'll __runtime__, and develop a __garbage collector__. We'll
tackle the first of these in the next post - see you there! tackle the first of these in the next post - [Part 7 - Runtime]({{< relref "07_compiler_runtime.md" >}}).

View File

@@ -1,7 +1,6 @@
--- ---
title: Compiling a Functional Language Using C++, Part 7 - Runtime title: Compiling a Functional Language Using C++, Part 7 - Runtime
date: 2019-08-06T14:26:38-07:00 date: 2019-08-06T14:26:38-07:00
draft: true
tags: ["C and C++", "Functional Languages", "Compilers"] tags: ["C and C++", "Functional Languages", "Compilers"]
--- ---
Wikipedia has the following definition for a __runtime__: Wikipedia has the following definition for a __runtime__:
@@ -158,4 +157,4 @@ that going, we be able to compile our code!
Next time, we will start work on converting our G-machine instructions Next time, we will start work on converting our G-machine instructions
into machine code. We will set up LLVM and get our very first into machine code. We will set up LLVM and get our very first
fully functional compiled programs in Part 8 - LLVM. fully functional compiled programs in [Part 8 - LLVM]({{< relref "08_compiler_llvm.md" >}}).

View File

@@ -1,7 +1,6 @@
--- ---
title: Compiling a Functional Language Using C++, Part 8 - LLVM title: Compiling a Functional Language Using C++, Part 8 - LLVM
date: 2019-10-30T22:16:22-07:00 date: 2019-10-30T22:16:22-07:00
draft: true
tags: ["C and C++", "Functional Languages", "Compilers"] tags: ["C and C++", "Functional Languages", "Compilers"]
--- ---
@@ -54,8 +53,6 @@ a `Module` object, which represents some collection of code and declarations
{{< codeblock "C++" "compiler/08/llvm_context.hpp" >}} {{< codeblock "C++" "compiler/08/llvm_context.hpp" >}}
{{< todo >}} Explain creation functions. {{< /todo >}}
We include the LLVM context, builder, and module as members We include the LLVM context, builder, and module as members
of the context struct. Since the builder and the module need of the context struct. Since the builder and the module need
the context, we initialize them in the constructor, where they the context, we initialize them in the constructor, where they
@@ -118,10 +115,46 @@ for specifying the body of `node_base` and `node_app`.
There's still more functionality packed into `llvm_context`. There's still more functionality packed into `llvm_context`.
Let's next take a look into `custom_function`, and Let's next take a look into `custom_function`, and
the `create_custom_function` method. Why do we need the `create_custom_function` method. Why do we need
these? these? To highlight the need for the custom class,
let's take a look at `instruction_pushglobal` which
occurs at the G-machine level, and then at `alloc_global`,
which will be a function call generated as part of
the PushGlobal instruction. `instruction_pushglobal`'s
only member variable is `name`, which stands for
the name of the global function it's referencing. However,
`alloc_global` requires an arity argument! We can
try to get this information from the `llvm::Function`
corresponding to the global we're trying to reference,
but this doesn't get us anywhere: as far as LLVM
is concerned, any global function only takes one
parameter, the stack. The rest of the parameters
are given through that stack, and their number cannot
be easily deduced from the function alone.
Instead, we decide to store global functions together
with their arity. We thus create a class to combine
these two things (`custom_function`), define
a map from global function names to instances
of `custom_function`, and add a convenience method
(`create_custom_function`) that takes care of
constructing an `llvm::Function` object, creating
a `custom_function`, and storing it in the map.
The implementation for `custom_function` is
straightforward:
{{< codelines "C++" "compiler/08/llvm_context.cpp" 234 252 >}}
We create a function type, then a function, and finally
initialize a `custom_function`. There's one thing
we haven't seen yet in this function, which is the
`BasicBlock` class. We'll get to what basic blocks
are shortly, but for now it's sufficient to
know that the basic block gives us a place to
insert code.
This isn't the end of our `llvm_context` class: it also This isn't the end of our `llvm_context` class: it also
has a variety of `create_*` methods! Let's take a look has a variety of other `create_*` methods! Let's take a look
at their signatures. Most return either `void`, at their signatures. Most return either `void`,
`llvm::ConstantInt*`, or `llvm::Value*`. Since `llvm::ConstantInt*`, or `llvm::Value*`. Since
`llvm::ConstantInt*` is a subclass of `llvm::Value*`, let's `llvm::ConstantInt*` is a subclass of `llvm::Value*`, let's
@@ -168,7 +201,7 @@ Assigned to each variable is `llvm::Value`. The LLVM documentation states:
It's important to understand that `llvm::Value` __does not store the result of the computation__. It's important to understand that `llvm::Value` __does not store the result of the computation__.
It rather represents how something may be computed. 1 is a value because it computed by It rather represents how something may be computed. 1 is a value because it computed by
just returning. `x + 1` is a value because it is computed by adding the value inside of just returning 1. `x + 1` is a value because it is computed by adding the value inside of
`x` to 1. Since we cannot modify a variable once we've declared it, we will `x` to 1. Since we cannot modify a variable once we've declared it, we will
keep assigning intermediate results to new variables, constructing new values keep assigning intermediate results to new variables, constructing new values
out of values that we've already specified. out of values that we've already specified.
@@ -251,36 +284,27 @@ represented by the pointer, while the second offset
gives the index of the field we want to access. We gives the index of the field we want to access. We
want to dereference the pointer (`num_pointer[0]`), want to dereference the pointer (`num_pointer[0]`),
and we want the second field (`1`, when counting from 0). and we want the second field (`1`, when counting from 0).
Thus, we call CreateGEP with these offsets and our pointers. Thus, we call `CreateGEP` with these offsets and our pointers.
This still leaves us with a pointer to a number, rather This still leaves us with a pointer to a number, rather
than the number itself. To dereference the pointer, we use than the number itself. To dereference the pointer, we use
`CreateLoad`. This gives us the value of the number node, `CreateLoad`. This gives us the value of the number node,
which we promptly return. which we promptly return.
Let's envision a `gen_llvm` method on the `instruction` struct. This concludes our implementation of the `llvm_context` -
We need access to all the other functions from our runtime, it's time to move on to the G-machine instructions.
such as `stack_init`, and functions from our program such
as `f_custom_function`. Thus, we need access to our
`llvm_context`. The current basic block is part
of the builder, which is part of the context, so that's
also taken care of. There's only one more thing that we will
need, and that's access to the `llvm::Function` that's
currently being compiled. To understand why, consider
the signature of `f_main` from the previous post:
```C ### G-machine Instructions to LLVM IR
void f_main(struct stack*);
```
The function takes a stack as a parameter. What if Let's now envision a `gen_llvm` method on the `instruction` struct,
we want to try use this stack in a method call, like which will turn the still-abstract G-machine instruction
`stack_push(s, node)`? We need to have access to the into tangible, close-to-metal LLVM IR. As we've seen
LLVM representation of the stack parameter. The easiest in our implementation of `llvm_context`, to access the stack, we need access to the first
way to do this is to use `llvm::Function::arg_begin()`, argument of the function we're generating. Thus, we need this method
which gives the first argument of the function. We thus to accept the function whose instructions are
carry the function pointer throughout our code generation being converted to LLVM. We also pass in the
methods. `llvm_context`, since it contains the LLVM builder,
context, module, and a map of globally declared functions.
With these things in mind, here's the signature for `gen_llvm`: With these things in mind, here's the signature for `gen_llvm`:
@@ -288,4 +312,267 @@ With these things in mind, here's the signature for `gen_llvm`:
virtual void gen_llvm(llvm_context&, llvm::Function*) const; virtual void gen_llvm(llvm_context&, llvm::Function*) const;
``` ```
{{< todo >}} Fix pointer type inconsistencies. {{< /todo >}} Let's get right to it! `instruction_pushint` gives us an easy
start:
{{< codelines "C++" "compiler/08/instruction.cpp" 17 19 >}}
We create an LLVM integer constant with the value of
our integer, and push it onto the stack.
`instruction_push` is equally terse:
{{< codelines "C++" "compiler/08/instruction.cpp" 37 39 >}}
We simply peek at the value of the stack at the given
offset (an integer of the same size as `size_t`, which
we create using `create_size`). Once we have the
result of the peek, we push it onto the stack.
`instruction_pushglobal` is more involved. Let's take a look:
{{< codelines "C++" "compiler/08/instruction.cpp" 26 30 >}}
First, we retrive the `custom_function` associated with
the given global name. We then create an LLVM integer
constant representing the arity of the function,
and then push onto the stack the result of `alloc_global`,
giving it the function and arity just like it expects.
`instruction_pop` is also short, and doesn't require much
further explanation:
{{< codelines "C++" "compiler/08/instruction.cpp" 46 48 >}}
Some other instructions, such as `instruction_update`,
`instruction_pack`, `instruction_split`, `instruction_slide`,
`instruction_alloc` and `instruction_eval` are equally as simple,
and we omit them for the purpose of brevity.
What remains are two "meaty" functions, `instruction_jump` and
`instruction_binop`. Let's start with the former:
{{< codelines "C++" "compiler/08/instruction.cpp" 101 123 >}}
This is the one and only function in which we have to take
care of control flow. Conceptually, depending on the tag
of the `node_data` at the top of the stack, we want
to pick one of many branches and jump to it.
As we discussed, a basic block has to be executed in
its entirety; since the branches of a case expression
are mutually exclusive (only one of them is executed in any given case),
we have to create a separate basic block for each branch.
Given these blocks, we then want to branch to the correct one
using the tag of the node on top of the stack.
This is exactly what we do in this function. We first peek
at the node on top of the stack, and use `CreateGEP` through
`unwrap_data_tag` to get access to its tag. What we then
need is LLVM's switch instruction, created using `CreateSwitch`.
We must provide the switch with a "default" case in case
the tag value is something we don't recognize. To do this,
we create a "safety" `BasicBlock`. With this new safety
block in hand, we're able to call `CreateSwitch`, giving it
the tag value to switch on, the safety block to default to,
and the expected number of branches (to optimize memory allocation).
Next, we create a vector of blocks, and for each branch,
we append to it a corresponding block `branch_block`, into
which we insert the LLVM IR corresponding to the
instructions of the branch. No matter the branch we take,
we eventually want to come back to the same basic block,
which will perform the usual function cleanup via Update and Slide.
We re-use the safety block for this, and use `CreateBr` at the
end of each `branch_block` to perform an unconditional jump.
After we create each of the blocks, we use the `tag_mappings`
to add cases to the switch instruction, using `addCase`. Finally,
we set the builder's insertion point to the safety block,
meaning that the next instructions will insert their
LLVM IR into that block. Since we have all branches
jump to the safety block at the end, this means that
no matter which branch we take in the case expression,
we will still execute the subsequent instructions as expected.
Let's now look at `instruction_binop`:
{{< codelines "C++" "compiler/08/instruction.cpp" 139 150 >}}
In this instruction, we pop and unwrap two integers from
the stack (assuming they are integers). Depending on
the type of operation the instruction is set to, we
then push the result of the corresponding LLVM
instruction. `PLUS` calls LLVM's `CreateAdd` to insert
addition, `MINUS` calls `CreateSub`, and so on. No matter
what the operation was, we push the result onto the stack.
That's all for our instructions! We're so very close now. Let's
move on to compiling definitions.
### Definitions to LLVM IR
As with typechecking, to allow for mutually recursive functions,
we need to be able each global function from any other function.
We then take the same approah as before, going in two passes.
This leads to two new methods for `definition`:
```C++
virtual void gen_llvm_first(llvm_context& ctx) = 0;
virtual void gen_llvm_second(llvm_context& ctx) = 0;
```
The first pass is intended to register all functions into
the `llvm_context`, making them visible to other functions.
The second pass is used to actually generate the code for
each function, now having access to all the other global
functions. Let's see the implementation for `gen_llvm_first`
for `definition_defn`:
{{< codelines "C++" "compiler/08/definition.cpp" 58 60 >}}
Since `create_custom_function` already creates a function
__and__ registers it with `llvm_context`, this is
all we need. Note that we created a new member variable
for `definition_defn` which stores this newly created
function. In the second pass, we will populate this
function with LLVM IR from the definition's instructions.
We actually create functions for each of the constructors
of data types, but they're quite special: all they do is
pack their arguments! Since they don't need access to
the other global functions, we might as well create
their bodies then and there:
{{< codelines "C++" "compiler/08/definition.cpp" 101 112 >}}
Like in `definition_defn`, we use `create_custom_function`.
However, we then use `SetInsertPoint` to configure our builder to insert code into
the newly created function (which already has a `BasicBlock`,
thanks to that one previously unexplained line in `create_custom_function`!).
Since we decided to only include the Pack instruction, we generate
a call to it directly using `create_pack`. We follow this
up with `CreateRetVoid`, which tells LLVM that this is
the end of the function, and that it is now safe to return
from it.
Great! We now implement the second pass of `gen_llvm`. In
the case of `definition_defn`, we do almost exactly
what we did in the first pass of `definition_data`:
{{< codelines "C++" "compiler/08/definition.cpp" 62 68 >}}
As for `definition_data`, we have nothing to do in the
second pass. We're done!
### Getting Results
We're almost there. Two things remain. The first: our implementation
of `ast_binop`, implement each binary operation as simply a function call:
`+` calls `f_plus`, and so on. But so far, we have not implemented
`f_plus`, or any other binary operator function. We do this
in `main.cpp`, creating a function `gen_llvm_internal_op`:
{{< codelines "C++" "compiler/08/main.cpp" 70 83 >}}
We create a simple function body. We then append G-machine
instructions that take each argument, evaluate it,
and then perform the corresponding binary operation.
With these instructions in the body, we insert
them into a new function, just like we did in our code
for `definition_defn` and `definition_data`.
Finally, we write our `gen_llvm` function that we will
call from `main`:
{{< codelines "C++" "compiler/08/main.cpp" 125 141 >}}
It first creates the functions for
`+`, `-`, `*`, and `/`. Then, it calls the first
pass of `gen_llvm` on all definitions, followed
by the second pass. Lastly, it uses LLVM's built-in
functionality to print out the generated IR in
our module, and then uses a function `output_llvm`
to create an object file ready for linking.
To be very honest, I took the `output_llvm` function
almost entirely from instructional material for my university's
compilers course. The gist of it, though, is: we determine
the target architecture and platform, specify a "generic" CPU,
create a default set of options, and then generate an object file.
Here it is:
{{< codelines "C++" "compiler/08/main.cpp" 85 123 >}}
We now add a `generate_llvm` call to `main`.
Are we there?
Let's try to compile our first example, `works1.txt`. The
file:
{{< rawblock "compiler/08/examples/works1.txt" >}}
We run the following commands in our build directory:
```
./compiler < ../examples/work1.txt
gcc -no-pie ../runtime.c program.o
./a.out
```
Nothing happens. How anticlimactic! Our runtime has no way of
printing out the result of the evaluation. Let's change that:
{{< codelines "C++" "compiler/08/runtime.c" 157 183 >}}
Rerunning our commands, we get:
```
Result: 326
```
The correct result! Let's try it with `works2.txt`:
{{< rawblock "compiler/08/examples/works2.txt" >}}
And again, we get the right answer:
```
Result: 326
```
This is child's play, though. Let's try with something
more complicated, like `works3.txt`:
{{< rawblock "compiler/08/examples/works3.txt" >}}
Once again, our program does exactly what we intended:
```
Result: 3
```
Alright, this is neat, but we haven't yet confirmed that
lazy evaluation works. How about we try it with
`works5.txt`:
{{< rawblock "compiler/08/examples/works5.txt" >}}
Yet again, the program works:
```
Result: 9
```
At last, we have a working compiler!
While this is a major victory, we are not yet
finished with the compiler altogether. While
we allocate nodes whenever we need them, we
have not once uttered the phrase `free` in our
runtime. Our language works, but we have no way
of comparing numbers, no lambdas, no `let/in`.
In the next several posts, we will improve
our compiler to properly free unused memory
usign a __garbage collector__, implement
lambda functions using __lambda lifting__,
and use our Alloc instruction to implement `let/in` expressions. See
you there!

View File

@@ -2,7 +2,6 @@
title: "Thoughts on Better Explanations" title: "Thoughts on Better Explanations"
date: 2019-10-12T00:33:02-07:00 date: 2019-10-12T00:33:02-07:00
tags: ["Language Server Protocol"] tags: ["Language Server Protocol"]
draft: true
--- ---
How do you explain how to write a program? How do you explain how to write a program?

178
content/blog/sidenotes.md Normal file
View File

@@ -0,0 +1,178 @@
---
title: JavaScript-Free Sidenotes in Hugo
date: 2019-12-07T00:23:34-08:00
tags: ["Website", "Hugo", "CSS"]
---
A friend recently showed me a website, the design of which I really liked:
Gwern Branwen's [personal website](https://www.gwern.net/index). In particular,
I found that __sidenotes__ were a feature that I didn't even know I needed.
A lot of my writing seems to use small parenthesized remarks (like this), which,
although it doesn't break the flow in a grammatical sense, lengthens the
sentence, and makes it harder to follow. Since I do my best to write content
to help explain stuff (like the [compiler series]({{ relref "00_compiler_intro.md" }})),
making sentences __more__ difficult to understand is a no-go.
So, what do they look like?
{{< sidenote "right" "example-note" "Here's an example sidenote." >}}
This is this example note's content.
{{< /sidenote >}}
If you're on a mobile device, the content is hidden by default: there's no
"side" on which the note fits. In this case, you can click or tap the underlined
portion of the text, which is the part to which the sidenote is related or refers to.
Otherwise, the example sidenote should be visible
{{< sidenote "left" "left-note" "on the right side of the screen." >}}
Sidenotes can also appear on the left of the screen, to help prevent situations
in which there are too many sidenotes and not enough space.
{{< /sidenote >}}
A major goal of mine in implementing these sidenotes was to avoid the use of JavaScript.
This is driven by my recent installation of uMatrix. uMatrix is an extension
that blocks JavaScript loaded from domains other than the one you're visiting.
To my frustration, a lot of the websites I commonly visit ended up broken:
[Canvas](https://github.com/instructure/canvas-lms), Discord, YouTube, and
Disquss all fail catastrophically when they aren't allowed to load dozens of scripts
from various sources. Out of spite, I want my site to work without any JavaScript,
and these notes are no exception.
### Implementation
Some of this work has been inspired by
[this article](https://www.kooslooijesteijn.net/blog/semantic-sidenotes).
The first concern was not having to write raw HTML to add the side notes,
but this is fairly simple with Hugo's shortcodes: I write the HTML once in
a new `sidenote` shortcode, then call the shortcode from my posts. The next
issue is a matter of HTML standards. Markdown rendering generates `<p>` tags.
According the to spec, `<p>` tags cannot have a block element inside
them. When you _try_ to put a block element, such as `<div>` inside `<p>`,
the browser will automatically close the `<p>` tag, breaking the rest of the page.
So, even though conceptually (and visually) the content of the sidenote is a block,
{{< sidenote "right" "markdown-note" "it has to be inside an inline element." >}}
There's another consequence to this. Hugo implements Markdown inside shortcodes
by rendering the "inner" part of the shortcode, substituting the result into the
shortcode's definition, and then finally placing that into the final output. Since
the Markdown renderer wraps text in paragraphs, which are block elements, the
inside of the shortcode ends up with block elements. The same tag-closing issue
manifests, and the view ends up broken. So, Markdown cannot be used inside sidenotes.
{{< /sidenote >}}
That's not too bad, overall. We end up with a shortcode definition as follows:
```HTML
<span class="sidenote">
<label class="sidenote-label" for="{{ .Get 1 }}">{{ .Get 2 }}</label>
<input class="sidenote-checkbox" type="checkbox" id="{{ .Get 1 }}"></input>
<span class="sidenote-content sidenote-{{ .Get 0 }}">
{{ .Inner }}
</span>
</span>
```
As Koos points out, "label" works as a semantic tag for the text that references
the sidenote. It also helps us with the checkbox
`<input>`, which we will examine later. Since it will receive its own style,
the inner content of the sidenote is wrapped in another `<span>`. Let's
get started on styling the parts of a sidenote, beginning with the content:
```SCSS
.sidenote-content {
display: block;
position: absolute;
width: $sidenote-width;
box-sizing: border-box;
margin-top: -1.5em;
&.sidenote-right {
right: 0;
margin-right: -($sidenote-width + $sidenote-offset);
}
// ...
}
```
As you can see from the sidenotes on this page, they are displayed as a block.
We start with that, then switch the sidenotes to be positioned absolutely, so that
we can place them exactly to the right of the content, and then some. We also
make sure that the box is sized __exactly__ the amount in `$sidenote-width`, by
ensuring that the border and padding are included in the size calculation
using `border-box`. We also hide the checkbox:
```SCSS
.sidenote-checkbox {
display: none;
}
```
Finally, let's make one more adjustment to the sidenote and its label: when
you hover over one of them, the other will change its appearence slightly,
so that you can tell which note refers to which label. We can do so by detecting
hover of the parent element:
```SCSS
.sidenote {
&:hover {
.sidenote-label { /* style for the label */ }
.sidenote-content { /* style for the sidenote */ }
}
}
```
### Hiding and Showing
So far, it's hard to imagine where JavaScript would come in. If you were
always looking at the page from a wide-screen machine, it wouldn't at all.
Unfortunately phones don't leave a lot of room for margins and sidenotes, so to
make sure that these notes are visible to mobile users, we want to show
them inline. Since the entire idea of sidenotes is to present more information
__without__ interrupting the main text, we don't want to plop something down
in the middle of the screen by default. So we hide sidenotes, and show them only
when their label is clicked.
Gwern's site doesn't show the notes on mobile at all (when simulated using Firefox's
responsive design mode), and Koos uses JavaScript to toggle the sidenote text. We will
go another route.
This is where the checkbox `<input>` comes in. When the `<input>` checkbox is
checked, we show the sidenote text, as a block, in the middle of the page. When
it is not checked, we keep it hidden. Of course, keeping a checkbox in the middle
of the page is not pretty, so we keep it hidden. Rather than clicking the checkbox
directly,
{{< sidenote "right" "accessibility-note" "the users can click the text that refers to the sidenote," >}}
I'm not sure about the accessibility of such an arrangement. The label is semantic, sure, but
the checkbox is more sketchy. Take this design with a grain of salt.
{{< /sidenote >}} which happens
to also be a label for the checkbox input. Clicking the label toggles the checkbox,
and with it the display of the sidenote. We can use the following CSS to
get that to work:
```SCSS
.sidenote-content {
// ...
@media screen and
(max-width: $container-width + 2 * ($sidenote-width + 2 * $sidenote-offset)) {
position: static;
margin-top: 10px;
margin-bottom: 10px;
width: 100%;
display: none;
.sidenote-checkbox:checked ~ & {
display: block;
}
&.sidenote-right {
margin-right: 0px;
}
// ...
}
// ...
}
```
We put the position back to `static`, and add margins on the top and bottom of the node.
We keep the `display` to `none`, unless the checkbox contained in the sidenote span
is checked. Finally, we reset the margin we created earlier, since we're not moving this
note anywhere.
### Conclusion
Here, we've implemented sidenotes in Hugo with zero JavaScript. They work well on
both mobile and desktop devices, though their accessibility is, at present,
somewhat uncertain.

View File

@@ -1,7 +1,6 @@
--- ---
title: Switching to a Static Site Generator title: Switching to a Static Site Generator
date: 2019-08-05T01:13:58-07:00 date: 2019-08-05T01:13:58-07:00
draft: true
tags: ["Website"] tags: ["Website"]
--- ---
A long time ago, I decided to try out Jekyll for my website. However, it all felt too A long time ago, I decided to try out Jekyll for my website. However, it all felt too

View File

@@ -0,0 +1,70 @@
@import "style.scss";
$sidenote-width: 350px;
$sidenote-offset: 15px;
.sidenote {
&:hover {
.sidenote-label {
background-color: $primary-color;
color: white;
}
.sidenote-content {
border: 2px dashed;
padding: 9px;
border-color: $primary-color;
}
}
}
.sidenote-label {
border-bottom: 2px solid $primary-color;
}
.sidenote-checkbox {
display: none;
}
.sidenote-content {
display: block;
position: absolute;
width: $sidenote-width;
margin-top: -1.5em;
&.sidenote-right {
right: 0;
margin-right: -($sidenote-width + $sidenote-offset);
}
&.sidenote-left {
left: 0;
margin-left: -($sidenote-width + $sidenote-offset);
}
@media screen and
(max-width: $container-width + 2 * ($sidenote-width + 2 * $sidenote-offset)) {
position: static;
margin-top: 10px;
margin-bottom: 10px;
width: 100%;
display: none;
.sidenote-checkbox:checked ~ & {
display: block;
}
&.sidenote-left {
margin-left: 0px;
}
&.sidenote-right {
margin-right: 0px;
}
}
@include bordered-block;
padding: 10px;
box-sizing: border-box;
text-align: left;
}

View File

@@ -3,9 +3,16 @@ $primary-color: #36e281;
$primary-color-dark: darken($primary-color, 10%); $primary-color-dark: darken($primary-color, 10%);
$code-color: #f0f0f0; $code-color: #f0f0f0;
$code-color-dark: darken($code-color, 10%); $code-color-dark: darken($code-color, 10%);
$border-color: #bfbfbf;
$font-heading: "Lora", serif; $font-heading: "Lora", serif;
$font-body: "Raleway", serif; $font-body: "Raleway", serif;
$font-code: "Inconsolata", monospace; $font-code: "Inconsolata", monospace;
$standard-border: 1px solid $border-color;
@mixin bordered-block {
border: $standard-border;
border-radius: 2px;
}
body { body {
font-family: $font-body; font-family: $font-body;
@@ -15,11 +22,16 @@ body {
text-align: justify; text-align: justify;
} }
main {
position: relative;
}
h1, h2, h3, h4, h5, h6 { h1, h2, h3, h4, h5, h6 {
margin-bottom: .1em; margin-bottom: .1em;
margin-top: .5em; margin-top: .5em;
font-family: $font-heading; font-family: $font-heading;
font-weight: normal; font-weight: normal;
text-align: left;
a { a {
color: black; color: black;
@@ -60,6 +72,7 @@ pre code {
color: white; color: white;
transition: color 0.25s; transition: color 0.25s;
transition: background-color 0.25s; transition: background-color 0.25s;
text-align: left;
&:focus { &:focus {
outline: none; outline: none;

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="{{ .Site.Language.Lang }}">
{{- partial "head.html" . -}} {{- partial "head.html" . -}}
<body> <body>
{{- partial "header.html" . -}} {{- partial "header.html" . -}}

View File

@@ -6,7 +6,9 @@
<link href="https://fonts.googleapis.com/css?family=Inconsolata|Lora|Raleway" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Inconsolata|Lora|Raleway" rel="stylesheet">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css"> <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css">
{{ $style := resources.Get "scss/style.scss" | resources.ToCSS | resources.Minify }} {{ $style := resources.Get "scss/style.scss" | resources.ToCSS | resources.Minify }}
{{ $sidenotes:= resources.Get "scss/sidenotes.scss" | resources.ToCSS | resources.Minify }}
<link rel="stylesheet" href="{{ $style.Permalink }}"> <link rel="stylesheet" href="{{ $style.Permalink }}">
<link rel="stylesheet" href="{{ $sidenotes.Permalink }}">
<script src='https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML' async></script> <script src='https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML' async></script>
{{ template "_internal/google_analytics.html" . }} {{ template "_internal/google_analytics.html" . }}

View File

@@ -0,0 +1,7 @@
<span class="sidenote">
<label class="sidenote-label" for="{{ .Get 1 }}">{{ .Get 2 }}</label>
<input class="sidenote-checkbox" type="checkbox" id="{{ .Get 1 }}"></input>
<span class="sidenote-content sidenote-{{ .Get 0 }}">
{{ .Inner }}
</span>
</span>