8 Commits

18 changed files with 197 additions and 340 deletions

View File

@@ -1,5 +1,4 @@
baseURL = "https://danilafe.com"
languageCode = "en-us"
languageCode = "en"
title = "Daniel's Blog"
theme = "vanilla"
pygmentsCodeFences = true
@@ -11,3 +10,9 @@ summaryLength = 20
endLevel = 4
ordered = false
startLevel = 3
[languages]
[languages.en]
baseURL = "https://danilafe.com"
[languages.ru]
baseURL = "https://ru.danilafe.com"

8
content/_index.ru.md Normal file
View File

@@ -0,0 +1,8 @@
---
title: Daniel's Blog
description: Персональный блог Данилы Федорина о функциональном программировании, дизайне компиляторов, и многом другом!
---
## Привет!
Добро пожаловать на мой сайт. Здесь, я пишу на многие темы, включая фунциональное программирование, дизайн компилляторов, теорию языков программирования, и иногда компьютерные игры. Я надеюсь, что здесь вы найдете что-нибуть интересное!
Вы читаете русскою версию моего сайта. Я только недавно занялся его переводом, и до этого времени редко писал на русском. Я заранеее извиняюсь за присутствие орфографических или грамматических ошибок.

View File

@@ -0,0 +1,97 @@
---
title: Пишем Компилятор Для Функционального Языка на С++, Часть 0 - Вступление
date: 2019-08-03T01:02:30-07:00
tags: ["C and C++", "Functional Languages", "Compilers"]
description: "todo"
---
Год назад, я был записан на курс по компиляторам. Я ждал этого момента почти два учебных года: еще со времени школы меня интересовало создание языков программирования. Однако я был разочарован - заданный нам финальный проект полностью состоял из склеивания вместе написанных профессором кусочков кода. Склеив себе такой грустный компилятор, я не почувствовал бы никакой гордости. А я хотел бы гордиться всеми своими проектами.
Вместо стандартного задания, я решил -- с разрешением профессора -- написать компилятор для ленивого функционального языка, используя отличную книгу Саймона Пейтона Джоунса, _Implementing functional languages: a tutorial_. На курсе мы пользовались С++, и мой проект не был исключением. Получился прикольный маленький язык, и теперь я хочу рассказать вам, как вы тоже можете создать ваш собственный функциональный язык.
### Примечание к Русской Версии
Вы читаете русскою версию этой статьи. Оригинал ее был написан год назад, и с тех пор объем всей серии немного изменился. Я планировал описать только те части компилятора, которые я успел закончить и сдать профессору: лексический анализ, синтаксический разбор, мономорфную проверку типов, и компиляцию простых выражений с помощью LLVM. Закончив и описав все эти части, я решил продолжать разрабатывать компилятор, и описал сборку мусора, полиморфную проверку типов, полиморфные структуры данных, а также компиляцию более сложных выражений. Вместо того чтобы писать наивный перевод английской версии -- притворяясь что я не знаю о перемене моих планов -- я буду вносить в эту версию изменения соответствующие сегодняшнему состоянию компилятора. Части статей не затронутые этими изменениями я тоже не буду переводить слово в слово, иначе они будут звучать ненатурально. Тем не менее техническое содержание каждой статьи будет аналогично содержанию ее английской версии, и код будет тот же самый.
### Мотивация
Начать эту серию меня подтолкнули две причины.
Во-первых, почти все учебники и вступления к созданию компиляторов, с которыми я сталкивался, были написаны об императивных языках, часто похожих на C, C++, Python, или JavaScript. Я считаю, что в компиляции функциональных языков -- особенно ленивых -- есть много чего интересного, и все это относительно редко упоминается.
Во-вторых, меня вдохновили книги, как Software Foundations. Все содержание Software Foundations, например, написано в форме комментариев языка Coq. Таким образом, можно не только читать саму книгу, но и сразу же запускать находящийся рядом с комментариями код. Когда описываемый код под рукой, легче экспериментировать и интереснее читать. Принимая это во внимание, я выкладываю вместе с каждой статьей соответствующую версию компилятора; в самой статье описывается код именно из этой версии. Все части написанной мною программы полностью доступны.
### Обзор
Прежде чем начинать наш проект, давайте обсудим, чего мы будем добиваться, и какими способами.
#### “Классические” Стадии Компилятора
Части большинства компиляторов достаточно независимы друг от друга (по крайней мере в теории). Мы можем разделить их на следующие шаги:
* Лексический анализ
* Синтаксический разбор
* Анализ и оптимизация
* Генерация кода
Не все вышеописанные шаги встречаются в каждом компиляторе. Например, компилятор в моих статьях совсем не оптимизирует код. Также, в некоторых компиляторах присутствуют шаги не упомянутые в этом списке. Язык Idris -- как и многие другие функциональные языки -- переводится сначала в упрощённый язык “TT”, и только после этого проходит через анализ. Иногда, с целью ускорить компиляцию, несколько шагов производятся одновременно. В целом, все эти стадии помогут нам сориентироваться, но никаким образом нас не ограничат.
#### Темы, Которые Мы Рассмотрим
Мы начнем с нуля, и пошагово построим компилятор состоящий из следующих частей:
* Лексического анализа с помощью программы Flex.
* Синтаксического разбора с помощью программы Bison.
* Сначала мономорфной, а позже полиморфной проверки типов.
* Вычисления программ используя абстрактную машину G-machine.
* Компиляции абстрактных инструкций G-machine используя LLVM.
* Простого сбора мусора.
Наша цель - создать ленивый, функциональный язык.
#### Темы, Которые Мы Не Рассмотрим
Для того, чтобы создать любую нетривиальную программу, нужно иметь значительный объем опыта и знаний; одному человеку было бы сложно научить всему этому. У меня буквально не хватило бы на это времени, да и исход такой попытки был бы неблагоприятным: опытным читателям было бы труднее извлечь из статей новую информацию, а неопытным читателям все равно было бы недостаточно подробно. Вместо того, чтобы портить таким образом свои статьи, я буду полагаться на то, что вы достаточно комфортно себя чувствуете с некоторыми темами. В число этих тем входят:
* [Теория алгоритмов](https://ru.wikipedia.org/wiki/%D0%A2%D0%B5%D0%BE%D1%80%D0%B8%D1%8F_%D0%B0%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC%D0%BE%D0%B2),
более конкретно [теория автоматов](https://ru.wikipedia.org/wiki/%D0%A2%D0%B5%D0%BE%D1%80%D0%B8%D1%8F_%D0%B0%D0%B2%D1%82%D0%BE%D0%BC%D0%B0%D1%82%D0%BE%D0%B2).
Детерминированные и недетерминированные автоматы кратко упоминаются в первой статье во время лексического анализа, a синтаксический разбор мы выполним используя контекстно-свободную грамматику.
* [Функциональное программирование](https://ru.wikipedia.org/wiki/%D0%A4%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5), с легкой примесью [лямбда-исчисления](https://ru.wikipedia.org/wiki/%D0%9B%D1%8F%D0%BC%D0%B1%D0%B4%D0%B0-%D0%B8%D1%81%D1%87%D0%B8%D1%81%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5).
Мы будем пользоваться лямбда-функциями, каррированием, и системой типов Хиндли-Мильнер, которая часто встречается в языках семейства ML.
* С++. Я стараюсь писать код правильно и по последним стандартам, но я не эксперт. Я не буду объяснять синтаксис или правила С++, но разумеется буду описывать что именно делает мой код с точки зрения компиляторов.
#### Синтаксис Нашего Языка
Саймон Пейтон Джоунс, в одном из своих [~~двух~~ многочисленных](https://www.reddit.com/r/ProgrammingLanguages/comments/dsu115/compiling_a_functional_language_using_c/f6t52mh?utm_source=share&utm_medium=web2x&context=3) трудов на тему функциональных языков, отметил что большинство из этих языков по сути очень похожи друг на друга; часто, главная разница состоит именно в их синтаксисе. На данный момент, выбор синтаксиса - наша главная степень свободы. Нам точно нужно предоставить доступ к следующим вещам:
* Декларациям функций
* Вызову функций
* Арифметике
*гебраическим типам данных
* Сопоставлению с образцом
Позже, мы добавим к этому списку выражения let/in и лямбда-функции. С арифметикой разобраться не сложно - числа будут писаться просто как `3`, значения выражений как `1+2*3` будут высчитываться по обычным математическим правилам. Вызов функций ненамного сложнее. Выражение `f x` будет значить “вызов функции `f` с параметром `x`”, а `f x + g y` - “сумма значений `f x` и `g y`”. Заметьте, что вызов функций имеет приоритет выше приоритета арифметических операций.
Теперь давайте придумаем синтаксис для деклараций функций. Я предлогаю следующий вариант:
```
defn f x = { x + x }
```
А для типов данных:
```
data List = { Nil, Cons Int List }
```
Заметьте, что мы пока пользуемся мономорфными декларациями типов данных. Позже, в одиннадцатой части, мы добавим синтаксис для полиморфных деклараций.
В последнюю очередь, давайте определимся с синтаксисом сопоставления с образцом:
```
case l of {
Nil -> { 0 }
Cons x xs -> { x }
}
```
Представленная выше распечатка читается как: “если лист `l` сопоставим с `Nil`, то все выражение возвращает значение `0`; иначе, если лист сопоставим с `Cons x xs` (что, опираясь на декларацию `List`, означает, что лист состоит из значений `x`, с типом `Int`, и `xs`, с типом `List`), то выражение возвращает `x`”.
Вот и конец нашего обзора! В следующей статье, мы начнем с лексического анализа, что является первым шагом в процессе трансформации программного текста в исполняемые файлы.
### Список Статей
* Ой! Тут как-то пусто.
* Вы, наверно, читаете черновик.
* Если нет, то пожалуйста напишите мне об этом!

View File

@@ -1,311 +0,0 @@
---
title: "How Many Values Does a Boolean Have?"
date: 2020-08-21T23:05:55-07:00
tags: ["Java", "Haskell", "C and C++"]
---
A friend of mine recently had an interview for a software
engineering position. They later recounted to me the content
of the technical questions that they had been asked. Some had
been pretty standard:
* __"What's the difference between concurrency
and parallelism?"__ -- a reasonable question given that Go was
the company's language of choice.
* __"What's the difference between a method and a function?"__ --
a little more strange, in my opinion, since the difference
is of little _practical_ use.
But then, they recounted a rather interesting question:
> How many values does a bool have?
Innocuous at first, isn't it? Probably a bit simpler, in fact,
than the questions about methods and functions, concurrency
and parallelism. It's plausible that a candidate
has not done much concurrent or parallel programming in their
life, or that they came from a language in which functions
were rare and methods were ubiquitous. It's not plausible,
on the other hand, that a candidate applying to a software
engineering position has not encountered booleans.
If you're genuinely unsure about the answer to the question,
I think there's no reason for me to mess with you. The
simple answer to the question -- as far as I know -- is that a boolean
has two values. They are `true` and `false` in Java, or `True` and `False`
in Haskell, and `1` and `0` in C. A boolean value is either true or false.
So, what's there to think about? There are a few things, _ackshually_.
Let's explore them, starting from the theoretical perspective.
### Types, Values, and Expressions
Boolean, or `bool`, is a type. Broadly speaking, a type
is a property of _something_ that defines what the _something_
means and what you can do with it. That _something_ can be
several things; for our purposes, it can either be an
_expression_ in a programming language (like those in the form `fact(n)`)
or a value in that same programming language (like `5`).
Dealing with values is rather simple. Most languages have finite numbers,
usually with \\(2^{32}\\) values, which have type `int`,
`i32`, or something in a similar vein. Most languages also have
strings, of which there are as many as you have memory to contain,
and which have the type `string`, `String`, or occasionally
the more confusing `char*`. Most languages also have booleans,
as we discussed above.
The deal with expressions is a more interesting. Presumably
expressions evaluate to values, and the type of an expression
is then the type of values it can yield. Consider the following
snippet in C++:
```C
int square(int x) {
return x * x;
}
```
Here, the expression `x` is known to have type `int` from
the type signature provided by the user. Multiplication
of integers yields an integer, and so the type of `x*x` is also
of type `int`. Since `square(x)` returns `x*x`, it is also
of type `int`. So far, so good.
Okay, how about this:
```C++
int meaningOfLife() {
return meaningOfLife();
}
```
No, wait, doesn't say "stack overflow" just yet. That's no fun.
And anyway, this is technically a tail call, so maybe our
C++ compiler can avoid growing the stack. And indeed,
flicking on the `-O2` flag in this [compiler explorer example](https://godbolt.org/z/9cv4nY),
we can see that no stack growth is necessary: it's just
an infinite loop. But `meaningOfLife` will never return a value. One could say
this computation _diverges_.
Well, if it diverges, just throw the expression out of the window! That's
no `int`! We only want _real_ `int`s!
And here, we can do that. But what about the following:
```C++
inf_int collatz(inf_int x) {
if(x == 1) return 1;
if(x % 2 == 0) return collatz(x/2);
return collatz(x * 3 + 1);
}
```
Notice that I've used the fictitious type
`inf_int` to represent integers that can hold
arbitrarily large integer values, not just the 32-bit ones.
That is important for this example, and I'll explain why shortly.
The code in the example is a simulation of the process described
in the [Collatz conjecture](https://en.wikipedia.org/wiki/Collatz_conjecture).
Given an input number `x`, if the number is even, it's divided in half,
and the process continues with the halved number. If, on the other
hand, the number is odd, it's multiplied by 3, 1 is added to it,
and the process continues with _that_ number. The only way for the
process to terminate is for the computation to reach the value 1.
Why does this matter? Because as of right now, __nobody knows__
whether or not the process terminates for all possible input numbers.
We have a strong hunch that it does; we've checked a __lot__
of numbers and found that the process terminates for them.
This is why 32-bit integers are not truly sufficient for this example;
we know empirically that the function will terminate for them.
But why does _this_ matter? Well, it matters because we don't know
whether or not this function will diverge, and thus, we can't
'throw it out of the window' like we wanted to with `meaningOfLife`!
In general, it's _impossible to tell_ whether or not a program will
terminate; that is the [halting problem](https://en.wikipedia.org/wiki/Halting_problem).
So, what do we do?
It turns out to be convenient -- formally -- to treat the result of a diverging computation
as its own value. This value is usually called 'bottom', and written as \\(\\bot\\).
Since in most programming languages, you can write a nonterminating expression or
function of any type, this 'bottom' is included in _all_ types. So in fact, the
possible values of `unsigned int` are \\(\\bot, 0, 1, 2, ...\\) and so on.
As you may have by now guessed, the same is true for a boolean: we have \\(\\bot\\), `true`, and `false`.
### Haskell and Bottom
You may be thinking:
> Now he's done it; he's gone off the deep end with all that programming language
theory. Tell me, Daniel, where the heck have you ever encountered \\(\\bot\\) in
code? This question was for a software engineering interview, after all!
You're right; I haven't _specifically_ seen the symbol \\(\\bot\\) in my time
programming. But I have frequently used an equivalent notation for the same idea:
`undefined`. In fact, here's a possible definition of `undefined` in Haskell:
```
undefined = undefined
```
Just like `meaningOfLife`, this is a divergent computation! What's more is that
the type of this computation is, in Haskell, `a`. More explicitly -- and retreating
to more mathematical notation -- we can write this type as: \\(\\forall \\alpha . \\alpha\\).
That is, for any type \\(\\alpha\\), `undefined` has that type! This means
`undefined` can take on _any_ type, and so, we can write:
```Haskell
myTrue :: Bool
myTrue = True
myFalse :: Bool
myFalse = False
myBool :: Bool
myBool = undefined
```
In Haskell, this is quite useful. For instance, if one's in the middle
of writing a complicated function, and wants to check their work so far,
they can put 'undefined' for the part of the function they haven't written.
They can then compile their program; the typechecker will find any mistakes
they've made so far, but, since the type of `undefined` can be _anything_,
that part of the program will be accepted without second thought.
The language Idris extends this practice with the idea of typed holes,
where you can leave fragments of your program unwritten, and ask the
compiler what kind of _thing_ you need to write to fill that hole.
### Java and `null`
Now you may be thinking:
> This whole deal with Haskell's `undefined` is beside the point; it doesn't
really count as a value, since it's just a nonterminating
expression. What you're doing is a kind of academic autofellatio.
Alright, I can accept this criticism. Perhaps just calling a nonterminating
function a value _is_ far-fetched (even though in [denotational semantics](https://en.wikipedia.org/wiki/Denotational_semantics)
we _do_ extend types with \\(\\bot\\)). But denotational semantics are not
the only place where types are implicitly extend with an extra value;
let's look at Java.
In Java, we have `null`. At the
core language level, any function or method that accepts a class can also take `null`;
if `null` is not to that function or method's liking, it has to
explicitly check for it using `if(x == null)`.
This `null` value does not at first interact with booleans.
After all, Java's booleans are not classes. Unlike classes, which you have
to allocate using `new`, you can just throw around `true` and `false` as you see
fit. Also unlike classes, you simply can't assign `null` to a boolean value.
The trouble is, the parts of Java dealing with _generics_, which allow you to write
polymorphic functions, can't handle 'primitives' like `bool`. If you want to have an `ArrayList`
of something, that something _must_ be a class.
But what if you really _do_ want an `ArrayList` of booleans? Java solves this problem by introducing
'boxed' booleans: they're primitives wrapped in a class, called `Boolean`. This class
can then be used for generics.
But see, this is where `null` has snuck in again. By allowing `Boolean` to be a class
(thereby granting it access to generics), we've also given it the ability to be null.
This example is made especially compelling because Java supports something
they call [autoboxing](https://docs.oracle.com/javase/tutorial/java/data/autoboxing.html):
you can directly assign a primitive to a variable of the corresponding boxed type.
Consider the example:
```Java
Boolean myTrue = true;
Boolean myFalse = false;
Boolean myBool = null;
```
Beautiful, isn't it? Better yet, unlike Haskell, where you can't _really_
check if your `Bool` is `undefined` (because you can't tell whether
a non-terminating computation is as such), you can very easily
check if your `Boolean` is `true`, `false`, or `null`:
```Java
assert myTrue != myFalse;
assert myFalse != myBool;
assert myTrue != myBool;
```
We're okay to use `!=` here, instead of `equals`, because it so happens
each boxed instance of a `boolean` value
[refers to the same `Boolean` object](https://stackoverflow.com/questions/28636738/equality-of-boxed-boolean).
In fact, this means that a `Boolean` variable can have __exactly__ 3 values!
### C and Integers
Oh the luxury of having a type representing booleans in your language!
It's almost overly indulgent compared to the spartan minimalism of C.
In C, boolean conditions are represented as numbers. You can perhaps get
away with throwing around `char` or `short int`, but even then,
these types allow far more values than two!
```C
unsigned char test = 255;
while(test) test -= 1;
```
This loop will run 255 times, thereby demonstrating
that C has at least 255 values that can be used
to represent the boolean `true`.
There are other languages
with this notion of 'truthy' and 'falsey' values, in which
something not exactly `true` or `false` can be used as a condition. However,
some of them differ from C in that they also extend this idea
to equality. In JavaScript:
```JavaScript
console.assert(true == 1)
console.assert(false == 0)
```
Then, there are still exactly two distinct boolean values
modulo `==`. No such luck in C, though! We have 256 values that fit in `unsigned char`,
all of which are also distinct modulo `==`. Our boolean
variable can contain all of these values. And there is no
respite to be found with `enum`s, either. We could try define:
```C
enum bool { TRUE, FALSE };
```
Unfortunately, all this does is define `bool` to be a numeric
type that can hold at least 2 distinct values, and define
numeric constants `TRUE` and `FALSE`. So in fact, you can
_still_ write the following code:
```C
enum bool b1 = TRUE;
enum bool b2 = FALSE;
enum bool b3 = 15;
```
And so, no matter how hard you try, your 'boolean'
variable can have many, many values!
### Conclusion
I think that 'how many values does a boolean have' is a strange
question. Its purpose can be one of two things:
* The interviewer expected a long-form response such as this one.
This is a weird expectation for a software engineering candidate -
how does knowing about \\(\\bot\\), `undefined`, or `null` help in
creating software, especially if this information is irrelevant
to the company's language of choice?
* The interviewer expected the simple answer. In that case,
my previous observation applies: what software engineering
candidate has _not_ seen a boolean in their time programming?
Surely candidates are better screened before they are offered
an interview?
Despite the question's weirdness, I think that the resulting
investigation of the matter -- outside of the interview setting --
is useful, and perhaps, in a way, enlightening. It may help
one understand the design choices made in _their_ language of choice,
and how those choices shape the code that they write.
That's all I have! I hope that you found it interesting.

View File

@@ -103,17 +103,6 @@ needed to compute the final answer can exist, unsimplified, in the tree.
Why don't we draw a few graphs to get familiar with the idea?
### Visualizing Graphs and Their Reduction
__A word of caution__: the steps presented below may significantly differ
from the actual graph reduction algorithms used by modern compilers.
In particular, this section draws a lot of ideas from Simon Peyton Jones' book,
[_Implementing functional languages: a tutorial_](https://www.microsoft.com/en-us/research/publication/implementing-functional-languages-a-tutorial/).
However, modern functional compilers (i.e. GHC) use a much more
complicated abstract machine for evaluating graph-based code,
based on -- from what I know -- the [spineless tagless G-machine](https://www.microsoft.com/en-us/research/wp-content/uploads/1992/04/spineless-tagless-gmachine.pdf).
In short, this section, in order to build intuition, walks through how a functional program
evaluated using graph reduction _may_ behave; the actual details
depend on the compiler.
Let's start with something that doesn't have anything fancy. We can
take a look at the graph of the expression:

Binary file not shown.

View File

@@ -28,7 +28,6 @@ pre code {
border: $code-border;
display: block;
overflow: auto;
margin-bottom: 1rem;
td {
padding: 0;

View File

@@ -9,7 +9,7 @@ $toc-border-color: $code-border-color;
@include margin-content-left;
display: flex;
flex-direction: column;
align-items: flex-end;
align-items: end;
margin-bottom: 1rem;
em {

View File

@@ -0,0 +1,35 @@
[Home]
other = "Home"
[About]
other = "About"
[Resume]
other = "Resume"
[Tags]
other = "Tags"
[RecentPosts]
other = "Recent posts"
[AllPosts]
other = "All Posts"
[PostedOn]
other = "Posted on {{ .Date.Format \"January 2, 2006\" }}."
[TableOfContents]
other = "Table of Contents"
[ReadingTime]
other = "{{ .WordCount }} words, about {{ .ReadingTime }} minutes to read."
[Note]
other = "note"
[Tagged]
other = "Tagged \"{{ .Title }}\""
[AllTags]
other = "Below is a list of all the tags ever used on this site."

View File

@@ -0,0 +1,35 @@
[Home]
other = "Главная"
[About]
other = "О Сайте"
[Resume]
other = "Резюме"
[Tags]
other = "Метки"
[RecentPosts]
other = "Недавние статьи"
[AllPosts]
other = "Все Статьи"
[PostedOn]
other = "Статья опубликована {{ .Date.Format \"January 2, 2006\" }}."
[TableOfContents]
other = "Оглавление"
[ReadingTime]
other = "{{ .WordCount }} слов, чтение займет примерно {{ .ReadingTime }} минут."
[Note]
other = "примечание"
[Tagged]
other = "Статьи c меткой \"{{ .Title }}\""
[AllTags]
other = "Ниже приводится список всех меток на сайте."

View File

@@ -6,14 +6,14 @@
<a class="button" href="{{ $.Site.BaseURL }}/tags/{{ . | urlize }}">{{ . }}</a>
{{ end }}
</p>
<p>Posted on {{ .Date.Format "January 2, 2006" }}.</p>
<p>{{ i18n "PostedOn" . }}</p>
</div>
<div class="post-content">
{{ if not (eq .TableOfContents "<nav id=\"TableOfContents\"></nav>") }}
<div class="table-of-contents">
<div class="wrapper">
<em>Table of Contents</em>
<em>{{ i18n "TableOfContents" }}</em>
{{ .TableOfContents }}
</div>
</div>

View File

@@ -1,7 +1,7 @@
{{ define "main" }}
{{ .Content }}
Recent posts:
{{ i18n "RecentPosts" }}:
<ul class="post-list">
{{ range first 10 (where (where .Site.Pages.ByDate.Reverse "Section" "blog") ".Kind" "!=" "section") }}
{{ partial "post.html" . }}

View File

@@ -3,11 +3,11 @@
</div>
<nav>
<div class="container">
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/">{{ i18n "Home" }}</a>
<a href="/about">{{ i18n "About" }}</a>
<a href="https://github.com/DanilaFe">GitHub</a>
<a href="/Resume-Danila-Fedorin.pdf">Resume</a>
<a href="/tags">Tags</a>
<a href="/blog">All Posts</a>
<a href="/Resume-Danila-Fedorin.pdf">{{ i18n "Resume" }}</a>
<a href="/tags">{{ i18n "Tags" }}</a>
<a href="/blog">{{ i18n "AllPosts" }}</a>
</div>
</nav>

View File

@@ -1,5 +1,5 @@
<li>
<a href="{{ .Permalink }}" class="post-title">{{ .Title }}</a>
<p class="post-wordcount">{{ .WordCount }} words, about {{ .ReadingTime }} minutes to read.</p>
<p class="post-wordcount">{{ i18n "ReadingTime" . }}</p>
<p class="post-preview">{{ .Summary }} . . .</p>
</li>

View File

@@ -4,7 +4,7 @@
<label class="sidenote-label" for="numbernote-{{ $id }}">({{ $id }})</label>
<input class="sidenote-checkbox" type="checkbox" id="numbernote-{{ $id }}"></input>
<span class="sidenote-content sidenote-{{ .Get 0 }}">
<span class="sidenote-delimiter">[note:</span>
<span class="sidenote-delimiter">[{{ i18n "note" }}:</span>
{{ .Inner }}
<span class="sidenote-delimiter">]</span>
</span>

View File

@@ -2,7 +2,7 @@
<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 }}">
<span class="sidenote-delimiter">[note:</span>
<span class="sidenote-delimiter">[{{ i18n "Note" }}:</span>
{{ .Inner }}
<span class="sidenote-delimiter">]</span>
</span>

View File

@@ -1,5 +1,5 @@
{{ define "main" }}
<h2>Tagged "{{ .Title }}"</h2>
<h2>{{ i18n "Tagged" . }}</h2>
<ul class="post-list">
{{ range .Pages.ByDate.Reverse }}

View File

@@ -1,6 +1,6 @@
{{ define "main" }}
<h2>{{ .Title }}</h2>
Below is a list of all the tags ever used on this site.
<h2>{{ i18n "Tags" }}</h2>
{{ i18n "AllTags" }}
<ul>
{{ range sort .Pages "Title" }}