diff --git a/docs/2020/01/24/php-generator.html b/docs/2020/01/24/php-generator.html new file mode 100644 index 0000000..f082947 --- /dev/null +++ b/docs/2020/01/24/php-generator.html @@ -0,0 +1,250 @@ +Reinventing the Wheel: PHP Generators +

Blog

Reading time: +

+ +

Reinventing the Wheel: PHP Generators +

+ +

First thing first. How a generator works? +

+ +

Starting back at C +

+Let's create a function that each time we call it we get the next number of the fibonacci sequence. + +
+int fibonacci()
+{
+    static int a = 0;
+    static int b = 1;
+    int aux = b;
+    b = a + b;
+    a = aux;
+    return a;
+}
+
+ +If we call fibonacci(), the first time we'll get 1, the second time 1, the third 2, the fourth 3, and so on... + +This happens because we declared variables a, b to be static. This means that they mantain the value after the function returns. Normally, what happens (if we don't declare a variable as static) is that the variables inside the function don't mantain the values of the last execution. + + +

First generator for PHP +

+The equivalent function in PHP is pretty similar to C's approach. + +
+
+
+function fibonacci()
+{
+    static $a = 0;
+    static $b = 1;
+    $aux = $b;
+    $b = $a + $b;
+    $a = $aux;
+    return $a;
+}
+
+$out = [];
+
+for ($i = 1; $i <= 10; $i++) {
+    $out[] = fibonacci();
+}
+
+echo implode(', ', $out) . "\n";
+
+/*
+Output: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
+*/
+
+ +Let's compare this to the real PHP version using yield. + +
+
+
+function fibonacci($N)
+{
+    $a = 0;
+    $b = 1;
+    for ($i = 0; $i < $N; $i++) {
+        $aux = $b;
+        $b = $a + $b;
+        $a = $aux;
+        yield $a;
+    }
+}
+
+$out = [];
+
+foreach (fibonacci(10) as $fib) {
+    $out[] = $fib;
+}
+
+echo implode(', ', $out) . "\n";
+
+/*
+Output: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
+*/
+
+ + +

Creating a custom version of PHP yield +

+This is my own version using the library parallel and channels (probably uses yield internally). + +
+
+
+class MyGenerator implements Iterator
+{
+    private $chan;
+    private $current;
+    private $iteratorFn;
+    private $runtime;
+    private $key = -1;
+    private $valid = true;
+
+    public function __construct($iteratorFn)
+    {
+        $this->iteratorFn = $iteratorFn;
+        $this->runtime = new \parallel\Runtime();
+        $channel = new \parallel\Channel();
+
+        $this->runtime->run(function() use ($iteratorFn, $channel) {
+            $iteratorFn(function ($val) use ($channel) {
+                $channel->send($val);
+            });
+            $channel->close();
+        });
+
+        $this->chan = $channel;
+        $this->next();
+    }
+
+    public function current()
+    {
+        return $this->current;
+    }
+
+    public function next()
+    {
+        try {
+            ++$this->key;
+            $val = $this->chan->recv();
+            $this->current = $val;
+        } catch (\parallel\Channel\Error\Closed $e) {
+            $this->valid = false;
+        }
+        return $this->current;
+    }
+
+    public function key() {return $this->key;}
+    public function valid() {return $this->valid;}
+    public function rewind() {}
+}
+
+
+function fibonacci($N)
+{
+    return new MyGenerator(function ($yield) use ($N) {
+        $a = 0;
+        $b = 1;
+        for ($i = 0; $i < $N; $i++) {
+            $aux = $b;
+            $b = $a + $b;
+            $a = $aux;
+            $yield($a);
+        }
+    });
+}
+
+$out = [];
+
+foreach (fibonacci(10) as $fib) {
+    $out[] = $fib;
+}
+
+echo implode(', ', $out) . "\n";
+
+ + +

Performance comparison: PHP vs Custom +

+ +
Tested code +
+
+for ($i = 0; $i < 1000; ++$i) {
+    foreach (fibonacci(100) as $fib) {
+        $out[] = $fib;
+    }
+}
+
+ + +
yield version +
+
+real    0m0,083s
+user    0m0,059s
+sys     0m0,023s
+
+ + +
MyGenerator version +
+
+real    0m2,138s
+user    0m1,426s
+sys     0m1,363s
+
+ +So, it's aproximately 26 times slower :-) + +
\ No newline at end of file diff --git a/docs/2020/02/12/crafting-interpreters.html b/docs/2020/02/12/crafting-interpreters.html new file mode 100644 index 0000000..7c9a068 --- /dev/null +++ b/docs/2020/02/12/crafting-interpreters.html @@ -0,0 +1,64 @@ +Crafting interpreters +

Blog

Reading time: +

+ +

Crafting interpreters +

+I've just finished the section 2 of the book _Crafting Interpreters_, and I wanted to upload it to github right away. + +Take a look at the source code. + +Beside the lox specification I've added: + +
  • The keyword until that is a variation of while loops (as in ruby). also +
  • print is a function instead of a statement. +
  • eval function that let's you eval source code in runtime. +
  • Class methods. + +I'll implement another language interpreter, this time using golang and with a syntax similar to ruby. + +
  • \ No newline at end of file diff --git a/docs/2020/02/18/sudoku-solver.html b/docs/2020/02/18/sudoku-solver.html new file mode 100644 index 0000000..cf4944f --- /dev/null +++ b/docs/2020/02/18/sudoku-solver.html @@ -0,0 +1,207 @@ +Sudoku Solver +

    Blog

    Reading time: +

    + +

    Sudoku Solver +

    +I wanted to make my own sudoku solver to challenge myself. + +Im not a sudoku player so my approach is a brute force scan of possible combinations sort-of. + +I just know the basic rules: + +
  • Numbers 1-9 are allowed. +
  • Numbers in the same row cannot be repeated. +
  • Numbers in the same column cannot be repeated. +
  • Numbers in the 3x3 square cannot be repeated. + +The first thing i did was to build a some classes that calculates the possible values a cell can have if it's empty, based on the constraints. + +I came up with 3 classes: + +
  • Board that stores the entire board. +
  • BoardSlice that stores a slice of a board. An object of this type is returned when a Board is sliced (method __getitem__). +
  • Cell that stores the value of a single cell and calculates all possible values a cell can take. + +The class Cell receives a board, the coordinates on the board, and the value that holds. Also has the method options that uses python set data structure to calculate the posibilites. + +If you look at the following snippet you can see that the method options +generates the sets: options that contains all possible options (1-9), row that contains all the numbers that are in the same row, column that contains all the numbers that are in the same column and square that contains all the numbers that are in the same 3x3 square. The return value is options without all the used values. + +
    +class Cell:
    +    def __init__(self, b, i, j, value):
    +        self.b = b
    +        self.value = value
    +        self.i = i
    +        self.j = j
    +
    +    def options(self):
    +        if self.value != 0:
    +            return {self.value}
    +        options = set(range(1, 10))
    +        row = set(map(lambda x: x.value, self.b[self.i]))
    +        column = set(map(lambda x: x.value, self.b[:][self.j]))
    +        def to_square(k): return slice((k // 3) * 3, (k // 3) * 3 + 3)
    +        square = set(
    +            map(lambda x: x.value,
    +                self.b[to_square(self.i)][to_square(self.j)]))
    +        return options - row - column - square - {0}
    +
    + +To make easier the implementation of the square I used the class BoardSlice that contains a slice of a board and implements the magic method __getitem__. + +
    +class BoardSlice:
    +    def __init__(self, board_slice):
    +        self.board_slice = board_slice
    +
    +    def __getitem__(self, items):
    +        if type(items) == slice:
    +            return (el for row in self.board_slice for el in row[items])
    +        if type(items) == int:
    +            return (row[items] for row in self.board_slice)
    +        raise KeyError
    +
    + +The base class: Board contains the board and a copy method that copies all the values and creates a new Board object. This is necessary to avoid messing with object references and have a clean object when needed. + +
    +class Board:
    +    def __init__(self, board):
    +        self.board = [[Cell(self, i, j, value)
    +                       for (j, value) in enumerate(row)] for (i, row) in enumerate(board)]
    +
    +    def copy(self):
    +        return Board(((cell.value for cell in row) for row in self.board))
    +
    +    def __getitem__(self, items):
    +        if type(items) == int:
    +            return self.board[items]
    +        if type(items) == slice:
    +            return BoardSlice(self.board[items])
    +        raise KeyError
    +
    +    def __repr__(self):
    +        return repr(self.board)
    +
    + +With these tools the next step is to solve the problem! + +My idea was to generate a mixed iterative-recursive algorithm. + +The first pass will be iterative, and if needed, the second pass will be recursive. + + +
  • Iterative pass +
    +Iterates over the whole board and calculates the options that each cell can have. If a cell has only one option set that option on the cell and set a flag to repeat the iterative pass, if has 0 options return None meaning that the board has no solutions, and if has more than one option store the options for the recursive pass. + +If the loop ends and we found that no cell has more than one option then we solved the board! + +The idea of this first step is to solve an _easy_ board quickly. + + +
    Recursive pass +
    +If the iterative pass ends and we found that a cell has more than one option then we try all that options and call solve again! + +If solve returns a board that means we've found the solution! + +If solve returns None (back at the iterative passs) we have to try with another options. + + +
    BoardSolver +
    +The class is pretty straightforward. + +
    +class SudokuSolver:
    +    @staticmethod
    +    def solve(board):
    +        b = board.copy()
    +        # First pass: Iterative
    +        board_map = {}
    +        exhaust = False
    +        while not exhaust:
    +            exhaust = True
    +            for i in range(9):
    +                for j in range(9):
    +                    cell = b[i][j]
    +                    if cell.value == 0:
    +                        options = cell.options()
    +                        if len(options) == 1:
    +                            cell.value = options.pop()
    +                            exhaust = False
    +                        elif len(options) == 0:
    +                            return None
    +                        elif len(board_map) == 0:
    +                            board_map[(i, j)] = options
    +
    +        # Second pass: Recursive
    +        for ((i, j), options) in board_map.items():
    +            for op in options:
    +                b[i][j].value = op
    +                solved = SudokuSolver.solve(b)
    +                if solved:
    +                    return solved
    +            return None
    +
    +        return b
    +
    + + +
    Conclusions +
    +Actually my implementation is not a brute force algorithm, is a search algorithm, that searches the path to solving a board. Because it doesn't try all values on all cells nonsensically, it rather tries _some_ options for a given cell and advances to the next option as _soon_ as it detects that it's not the correct path. + + +

    Source +

    +Take a look at the source code. + +
    \ No newline at end of file diff --git a/docs/2020/02/21/grotsky-part1.html b/docs/2020/02/21/grotsky-part1.html new file mode 100644 index 0000000..c556c8d --- /dev/null +++ b/docs/2020/02/21/grotsky-part1.html @@ -0,0 +1,244 @@ +Grotsky Part 1: Syntax +

    Blog

    Reading time: +

    + +

    Grotsky Part 1: Syntax +

    + +

    Syntax Restrictions +

    +
  • No use of semicolon ; +
  • Block statements delimited by begin and end +
  • Function definition using fn keyword +
  • Logic operators in plain english or, and, not +
  • Conditional statements use the following keywords: if, elif, else +
  • There is no switch statement +
  • Class definition with class keyword +
  • Arithmetic operations: *, /, -, +, ^ +
  • Grouping with parentheses () +
  • Native support for python-like lists and dictionaries: [], {} +
  • Support for enhanced for loop: for i, el in array +
  • Keywords and identifiers can only use alphabethic characters + + +
  • Primitives +

    +
  • nil +
  • Integers +
  • Floats +
  • Booleans +
  • Strings +
  • Lists +
  • Dictionaries + + +
  • Example of functions and operations +

    +
    +## Arithmethic
    +print(2^10 - 2323*3)
    +# Output: -5945
    +print(2^(12*3+400/-4+10*5/2))
    +# Output: 1.8189894035458565e-12
    +
    +## Logic
    +print(true or false)
    +# Output: true (short circuit)
    +print(false and true)
    +# Output: false (short circuit)
    +
    +## Conditionals
    +if 3 > 2 or (1 < 3 and 2 == 2) begin
    +    print('Condition is true')
    +end
    +elif 3 == 4 begin
    +    print('Condition 2 is true')
    +end
    +else begin
    +    print('Conditions are false')
    +end
    +
    +## Lists
    +for i in [1, 2, 3, 4] begin
    +    print(i)
    +end
    +
    +let lst = [1, 2, 3, 4]
    +lst[0] = -1
    +print(lst) # Output: [-1, 2, 3, 4]
    +print(lst[1:3]) # Output: [2, 3]
    +
    +## Dictionaries
    +# (dictionaries and lists not allowed as keys)
    +let dct = {
    +    "Key1": "Val1",
    +    2: "Val2",
    +    true: false
    +}
    +for key, val in dct begin
    +    print(key, val)
    +end
    +
    +## Functions
    +fn square(x)
    +begin
    +    return x^2
    +end
    +
    +fn operate(x, operation)
    +begin
    +    return operation(x)
    +end
    +
    +## Clojure
    +fn makeCounter()
    +begin
    +    let n = 0
    +    return fn() begin
    +        n = n+1
    +        return n
    +    end
    +end
    +
    +## Classes
    +class Counter
    +begin
    +    init(start) begin
    +        self.start = start
    +    end
    +    count() begin
    +        self.start = self.start+1
    +        return self.start
    +    end
    +end
    +
    +class CounterTwo
    +begin
    +    count() begin
    +        return super.count()*2
    +    end
    +end
    +
    + + +

    Syntax definition +

    +Let's build a syntax definition in backus naur format that will be easy to parse with a recursive descent parser. + + +
    Expresions +
    +
    +expression       assignment;
    +list             "[" arguments? "]";
    +dictionary       "{" dict_elements? "}";
    +dict_elements    keyval ("," keyval)*;
    +keyval           expression ":" expression;
    +assignment       (call ".")? IDENTIFIER "=" assignment | access;
    +access           logic_or ("[" slice "]")*;
    +logic_or         logic_and ("or" logic_and)*;
    +logic_and        equality ("and" equality)*;
    +equality         comparison (("!=" | "==") comparison)*;
    +comparison       addition ((">" | ">=" | "<" | "<=") addition)*;
    +addition         multiplication (("-" | "+") multiplication)*;
    +multiplication   power (("/" | "*") power)*;
    +power            unary ("^" unary)*;
    +unary            ("not" | "-") unary | call;
    +call             primary ("(" arguments? ")" | "." IDENTIFIER)*;
    +arguments        expression ("," expression)*;
    +slice            (":" expression)
    +                | (":" expression ":" expression)
    +                | (":" ":" expression)
    +                | expression
    +                | (expression ":")
    +                | (expression ":" expression)
    +                | (expression ":" ":" expression)
    +                | (expression ":" expression ":" expression);
    +primary          NUMBER
    +                | STRING
    +                | "false"
    +                | "true"
    +                | "nil"
    +                | IDENTIFIER
    +                | "(" expression ")"
    +                | fnAnon
    +                | list
    +                | dictionary;
    +fnAnon           "fn" "(" parameters? ")" block;
    +
    + + +
    Statements +
    +
    +program         declaration* EOF;
    +declaration     classDecl | funDecl | varDecl | statement;
    +classDecl       "class" IDENTIFIER ( "<" IDENTIFIER )? "begin" methodDecl* "end" NEWLINE;
    +methodDecl      "class"? function;
    +funDecl         "fn" function ;
    +function        IDENTIFIER "(" parameters? ")" block ;
    +parameters      IDENTIFIER ( "," IDENTIFIER )* ;
    +varDecl         "let" IDENTIFIER ("=" expression)? NEWLINE;
    +statement       forStmt
    +                | ifStmt
    +                | returnStmt
    +                | whileStmt
    +                | exprStmt
    +                | block;
    +exprStmt        expression NEWLINE;
    +forStmt         "for"  (classicFor | newFor) statement;
    +classicFor      (varDecl | exprStmt | ",") expression? "," expression?;
    +newFor          IDENTIFIER ("," IDENTIFIER)? "in" expression;
    +ifStmt          "if" expression statement ("elif" expression statement)* ("else" statement)?;
    +returnStmt      "return" expression? NEWLINE;
    +whileStmt       "while" expression statement;
    +block           "begin" NEWLINE declaration* "end" NEWLINE;
    +
    + +That's it! The next step is to build a lexer and a parser. + +
    \ No newline at end of file diff --git a/docs/2020/03/15/grotsky-part2.html b/docs/2020/03/15/grotsky-part2.html new file mode 100644 index 0000000..931ee15 --- /dev/null +++ b/docs/2020/03/15/grotsky-part2.html @@ -0,0 +1,219 @@ +Grotsky Part 2: Parsing expressions +

    Blog

    Reading time: +

    + +

    Grotsky Part 2: Parsing expressions +

    + +

    Expressions +

    +Parsing an expression like 1+2*3 requires a complex representation on memory. Just looking at it we think that it's pretty simple, but there is some hidden hierarchy that we have to pay attention to, like the fact that first we have to compute 2*3 and then add 1 to it. + +To represent that in a data structure the best thing we can come up to is a tree, as seen in the next figure: + +![image](/assets/images/grotsky-part2/AST.png) + +As you can see the leaves of the tree are literals and the root and intermediate nodes are operations that have to be applied from the bottom up. That means that we traverse the tree until we reach the bottom and start computing the results by going up. + + +

    Defining node types +

    +> Not all operations are created equal. + +We have to define how each node fits into the tree. + +I'll use the following syntax: Binary -> left expr, operator token, right expr. Which means that a binary operation (as we have seen in the image before) links to 2 expressions (left and right) and stores 1 value (operator). + + +
    Let's define all posible operations on literals +
    +
    +Literal -> value object
    +# 1, "asd", 5.2, true, false
    +
    +Binary -> left expr, operator token, right expr
    +# 1+2, 3*3, 4^2+1
    +
    +Grouping -> expression expr
    +# (1+2)
    +
    +Logical -> left expr, operator token, right expr
    +# true or false, false and true
    +
    +Unary: operator token, right expr
    +# not true, -5
    +
    +List -> elements []expr
    +# [1, 2, 3, [4], "asd"]
    +
    +Dictionary -> elements []expr
    +# {"a": 1, "b": 2, 3: 4}
    +
    +Access -> object expr, slice expr
    +# [1, 2, 3][0], {"a":1}["a"]
    +
    +Slice -> first expr, second expr, third expr
    +# [1, 2, 3, 4, 5, 6][1:4:2]
    +
    + + +

    Traversing the abstract syntax tree +

    +To traverse the syntax tree we need a pattern that's uniform and easily scalable when we have to add other types of expressions and statements. + +For that we'll use the Visitor Pattern. + + +

    Visitor Pattern +

    +First we need an interface for the expression that allows a visitor to visit it. + +
    +type expr interface {
    +    accept(exprVisitor) interface{}
    +}
    +
    + +An expression visitor should have a method for each kind of expression it has to visit. + +
    +type exprVisitor interface {
    +    visitLiteralExpr(expr expr) interface{}
    +    visitBinaryExpr(expr expr) interface{}
    +    visitGroupingExpr(expr expr) interface{}
    +    visitLogicalExpr(expr expr) interface{}
    +    visitUnaryExpr(expr expr) interface{}
    +    visitListExpr(expr expr) interface{}
    +    visitDictionaryExpr(expr expr) interface{}
    +    visitAccessExpr(expr expr) interface{}
    +    visitSliceExpr(expr expr) interface{}
    +}
    +
    + +Then we have to define a type for each kind of expression that implements expr interface. For example, this is the implementation for a binary expression: + +
    +type binaryExpr struct {
    +    left expr
    +    operator *token
    +    right expr
    +}
    +
    +func (s *binaryExpr) accept(visitor exprVisitor) R {
    +    return visitor.visitBinaryExpr(s)
    +}
    +
    + +For all other expressions the definition is practically the same. + + +

    String Visitor +

    +To finish this chapter, let's define a visitor that allows you to print the syntax tree in a lisp-like syntax, ex: (+ 1 2). + +Here is the implementation of the string visitor for a binary expression: + +
    +type stringVisitor struct{}
    +
    +func (v stringVisitor) visitBinaryExpr(expr expr) R {
    +    binary := expr.(*binaryExpr)
    +    return fmt.Sprintf("(%s %v %v)", binary.operator.lexeme, binary.left.accept(v), binary.right.accept(v))
    +}
    +
    + + +

    Grotsky expression +

    +You can check out the state of the Grotsky project right here: https://github.com/mliezun/grotsky. + +Grotsky it's able to parse and print all types of expressions defined in this article right now. + + +

    Expressions +

    +Examples of operations supported: + +
    +# Math operations
    +1+2*3^2-(4+123)/2.6
    +=> (- (+ 1 (* 2 (^ 3 2))) (/ (+ 4 123) 2.6))
    +
    +# Logical operations
    +true or false
    +=> (or true false)
    +
    +# Comparisons
    +1 == 1 and (1 > 3 or 11/5.5 <= 3+2^2 and 1 != 2)
    +=> (and (== 1 1) (or (> 1 3) (and (<= (/ 11 5.5) (+ 3 (^ 2 2))) (!= 1 2))))
    +
    +# Lists
    +[1, 2, [3], "asd"]
    +=> (list 1 2 (list 3) "asd")
    +
    +# List slicing
    +[1,2,3,4][1:3][::2][0]
    +=> (#0 (#::2 (#1:3 (list 1 2 3 4))))
    +
    +# Dictionary
    +{
    +    1: 2,
    +    3: 4,
    +    "asd": 3.14
    +}
    +=> (dict 1=>2 3=>4 "asd"=>3.14)
    +
    +# Dictionary key lookup
    +{"key":0.6}["key"]
    +=> (#"key" (dict "key"=>0.6))
    +
    + +That's it for now. In the next chapter we'll traverse the tree but instead of printing we'll execute the operations listed before. + +If you have questions or suggestions please get in touch. + +
    \ No newline at end of file diff --git a/docs/2020/04/01/grotsky-part3.html b/docs/2020/04/01/grotsky-part3.html new file mode 100644 index 0000000..fa7d182 --- /dev/null +++ b/docs/2020/04/01/grotsky-part3.html @@ -0,0 +1,321 @@ +Grotsky Part 3: Interpreting +

    Blog

    Reading time: +

    + +

    Grotsky Part 3: Interpreting +

    + +

    It's slow! +

    +My interpreter it's really, really, wait for it... _Really slow_. + +An example of a bad performing grotsky code: + +
    +# fib: calculates the n-th fibonacci number recursively
    +fn fib(n) begin
    +    if n < 2 return n
    +    return fib(n-2) + fib(n-1)
    +end
    +println(fib(30))
    +
    + + +
    Running the code +
    +
    +$ time ./grotsky examples/fib.g
    +
    + +Gives a wooping result of: + +
    +832040
    +
    +real    0m11,154s
    +user    0m11,806s
    +sys     0m0,272s
    +
    + +Almost twelve seconds!!! + +Comparing with a similar python code + +
    +def fib(n):
    +    if n < 2: return n
    +    return fib(n-2) + fib(n-1)
    +print(fib(30))
    +
    + +Gives a result of: + +
    +832040
    +
    +real    0m0,423s
    +user    0m0,387s
    +sys     0m0,021s
    +
    + +That means, my interpreter is at least 20 times slower than Cpython. + + +
    Why is it so slow? +
    +Here is an explanation. + +As the person from the first comment states, go garbage collector is not well suited for this kind of scenario with heavy allocation of objects. + +> Go's GC is not generational, so allocation requires (comparatively speaking) much more work. It's also tuned for low latency (smallest pause when GC has to stop the program) at the expense of throughput (i.e. total speed). This is the right trade-off for most programs but doesn't perform optimally on micro-benchmarks that measure throughtput. + +Setting the gc percent at 800 (100 by default) more than halves the time that the function takes to compute: + +
    +$ time GOGC=800 ./grotsky examples/fib.g
    +832040
    +
    +real    0m5,110s
    +user    0m5,182s
    +sys     0m0,061s
    +
    + + +

    Interpreting functions +

    +Callable interface + +
    +type callable interface {
    +	arity() int
    +	call(exec *exec, arguments []interface{}) interface{}
    +}
    +
    + +_All grotsky functions must be an object that implements the callable interface._ + +For that I defined two kind of structs: + +
    +type function struct {
    +	declaration   *fnStmt
    +	closure       *env
    +	isInitializer bool
    +}
    +
    +type nativeFn struct {
    +	arityValue int
    +	callFn  func(exec *exec, arguments []interface{}) interface{}
    +}
    +
    + + +
    nativeFn +
    +Let's you define standard functions available on all grotsky interpreters. Line println. + +
    +func (n *nativeFn) arity() int {
    +	return n.arityValue
    +}
    +
    +func (n *nativeFn) call(exec *exec, arguments []interface{}) interface{} {
    +	return n.callFn(exec, arguments)
    +}
    +
    + +From that, println would be pretty straight forward: + +
    +...
    +
    +var println nativeFn
    +println.arityValue = 1
    +println.callFn = func(exec *exec, arguments []interface{}) interface{} {
    +    fmt.Println(arguments[0])
    +    return nil
    +}
    +...
    +
    + + +
    Ordinary grotsky functions +
    +For ordinary grotsky functions the things are a little bit messier. + +First I got to introduce the environment that is an object that holds map[string]interface{} as a dictionary for variables in the local scope and a pointer to another environment that contains variables for the outer scope. + +
    +type env struct {
    +	state *state
    +
    +	enclosing *env
    +	values    map[string]interface{}
    +}
    +
    +func newEnv(state *state, enclosing *env) *env {
    +	return &env{
    +		state:     state,
    +		enclosing: enclosing,
    +		values:    make(map[string]interface{}),
    +	}
    +}
    +
    +func (e *env) get(name *token) interface{} {
    +	if value, ok := e.values[name.lexeme]; ok {
    +		return value
    +	}
    +	if e.enclosing != nil {
    +		return e.enclosing.get(name)
    +	}
    +	e.state.runtimeErr(errUndefinedVar, name)
    +	return nil
    +}
    +
    +func (e *env) define(name string, value interface{}) {
    +	e.values[name] = value
    +}
    +
    + +As you can see, the define method creates a variable on the local scope, and the get methods tries to retrieve a variable first from the local scope and then from the outer scope. + +Let's see how functions are implemented. + +
    +func (f *function) arity() int {
    +	return len(f.declaration.params)
    +}
    +
    +func (f *function) call(exec *exec, arguments []interface{}) (result interface{}) {
    +	env := newEnv(exec.state, f.closure)
    +	for i := range f.declaration.params {
    +		env.define(f.declaration.params[i].lexeme, arguments[i])
    +	}
    +
    +	defer func() {
    +		if r := recover(); r != nil {
    +			if returnVal, isReturn := r.(returnValue); isReturn {
    +				result = returnVal
    +			} else {
    +				panic(r)
    +			}
    +		}
    +	}()
    +
    +	exec.executeBlock(f.declaration.body, env)
    +
    +	return nil
    +}
    +
    + +Function arity is pretty simple. + +The function call takes an exec object, that is no more than an instance of the interpreter, and the arguments to the function as an array of objects. Then creates a new environment the is surrounded by the environment local to the function definition and defines all the function parameters. Then comes the tricky part, first there is a deferred call to an anonymous function, let's ignore that for a moment, in the end, the function executeBlock gets called. Let's see what that function does: + +
    +func (e *exec) executeBlock(stmts []stmt, env *env) {
    +	previous := e.env
    +	defer func() {
    +		e.env = previous
    +	}()
    +	e.env = env
    +	for _, s := range stmts {
    +		e.execute(s)
    +	}
    +}
    +
    + +What's happening here is that the interpreter steps into the new environment, saving the previous environment in a variable, and execute all given statements, after that it restores the environment to the previous one. Exactly as a function does. + + +
    What happens when you hit a return +
    +
    +type returnValue interface{}
    +
    +...
    +
    +func (e *exec) visitReturnStmt(stmt *returnStmt) R {
    +	if stmt.value != nil {
    +		panic(returnValue(stmt.value.accept(e)))
    +	}
    +	return nil
    +}
    +
    + +When you get to a return node in the ast, the nodes panics with a return value. This has to do with the fact that you need to go up the call stack and finish the execution of the function, otherwise the function will keep it's execution. + +That's the reason of the deferred function we forgot a couple seconds ago: + +
    +func (f *function) call(exec *exec, arguments []interface{}) (result interface{}) {
    +    ...
    +
    +    defer func() {
    +		if r := recover(); r != nil {
    +			if returnVal, isReturn := r.(returnValue); isReturn {
    +				result = returnVal
    +			} else {
    +				panic(r)
    +			}
    +		}
    +    }()
    +
    +    ...
    +}
    +
    + +This function recovers from a panic. If the value recovered is of type returnValue it recovers successfully and sets the result value of the function call to the return value, else it panics again. + + +

    Hasta la vista, baby +

    +That's it for now. There are a lot of nifty stuff to keep talking about. But I think it's enough for now. + +Remember to check out the source code. And stay tuned for more. + +
    \ No newline at end of file diff --git a/docs/2020/04/11/custom-malloc.html b/docs/2020/04/11/custom-malloc.html new file mode 100644 index 0000000..2ace0ea --- /dev/null +++ b/docs/2020/04/11/custom-malloc.html @@ -0,0 +1,250 @@ +Writing your own C malloc and free +

    Blog

    Reading time: +

    + +

    Writing your own C malloc and free +

    + +

    Challenge +

    +This challenge comes from the book Crafting Interpreters by Bob Nystrom. And can be found in Chapter 14 - Challenge 3. + +The challenge goes: + +> You are allowed to call malloc() once, at the beginning of the interpreters execution, to allocate a single big block of memory which your reallocate() function has access to. It parcels out blobs of memory from that single region, your own personal heap. Its your job to define how it does that. + + +

    Check out this +comparison of malloc() to S3.

    Solution +

    +As stated in the challenge I'll be using a big chunk of _contiguous_ memory. The main idea of my solution is to store the blocks of memory in the array prepending a header with metadata. + +
    + _______________________________________________
    +|head_0|block_0 ... |head_1|block_1    ...      |
    + 
    +
    + +The structure of the header is pretty similar to that of a linked list. + +
    +struct block_meta
    +{
    +    size_t size;
    +    struct block_meta *next;
    +    int free;
    +};
    +
    +#define META_SIZE sizeof(struct block_meta)
    +
    + +It stores the size of the block, a pointer to the next block and a flag to mark wether it's free or not. + +Then, a function to traverse the list of blocks and find if there is any freed block is needed: + +
    +void *first_block = NULL;
    +
    +struct block_meta *find_free_block(struct block_meta **last, size_t size)
    +{
    +    struct block_meta *current = first_block;
    +    while (current && !(current->free && current->size >= size))
    +    {
    +        *last = current;
    +        current = current->next;
    +    }
    +    return current;
    +}
    +
    + +This function receives a double pointer to a block_meta struct called last that at the end of the execution should be pointing to the last node of the list and a size_t variable that indicates the minimum size that the block needs to be. + + +
    Memory initialization +
    +Two functions are needed to handle the big chunk of memory, one to initialize and the other to free it. + +
    +void initMemory();
    +void freeMemory();
    +
    + +To implement initMemory I've decided that I would ask for the maximum amount of memory that I could get from the OS. + +
    +#define MINREQ 0x20000
    +
    +// Big block of memory
    +void *memory = NULL;
    +// Position where the last block ends
    +size_t endpos = 0;
    +
    +void initMemory()
    +{
    +    size_t required = PTRDIFF_MAX;
    +    while (memory == NULL)
    +    {
    +        memory = malloc(required);
    +        if (required < MINREQ)
    +        {
    +            if (memory)
    +            {
    +                free(memory);
    +            }
    +            printf("Cannot allocate enough memory\n");
    +            exit(ENOMEM);
    +        }
    +        required >>= 1;
    +    }
    +}
    +
    +void freeMemory()
    +{
    +    free(memory);
    +}
    +
    + +As you can see, initMemory starts trying to allocate the maximum amount a memory allowed, and starts to divide that amount by 2 every time the allocation fails. If there isn't at least 128KB of memory available the program crashes with ENOMEM. + +Now that we have our chunk of memory ready to go, we can start to start giving blocks away. + +
    +struct block_meta *request_block(size_t size)
    +{
    +    struct block_meta *last = NULL;
    +    struct block_meta *block = find_free_block(&last, size);
    +    if (block)
    +    {
    +        block->free = 0;
    +        return block;
    +    }
    +    // Append new block to list
    +    block = memory + endpos;
    +    endpos += META_SIZE + size;
    +    if (last)
    +    {
    +        last->next = block;
    +    }
    +    else
    +    {
    +        first_block = block;
    +    }
    +    block->free = 0;
    +    block->next = NULL;
    +    block->size = size;
    +    return block;
    +}
    +
    + +How request_block works: + +1. Tries to find a free block with enough space. If there is one, it is set as occupied and returns that block. +2. If there isn't a free block available. It adds a new block with enough space at the end of memory (the big chunk). +3. If this is the first call, points the head of the list to the recently created block, else point the last node to the block. +4. Set the new block as occupied, set the size and next to null. Then return the new block. + +With this function, implementing malloc and free is pretty easy: + +
    +void *my_malloc(size_t size)
    +{
    +    struct block_meta *block = request_block(size);
    +    return block + 1;
    +}
    +
    +void my_free(void *ptr)
    +{
    +    struct block_meta *block = ptr - META_SIZE;
    +    block->free = 1;
    +}
    +
    + +To finish the challenge, I have to implement realloc, that is a little bit more tricky. + +
    +void *my_realloc(void *ptr, size_t size)
    +{
    +    if (!ptr)
    +    {
    +        return my_malloc(size);
    +    }
    +    struct block_meta *block = ptr - META_SIZE;
    +    if (block->size >= size)
    +    {
    +        return block + 1;
    +    }
    +    uint8_t *newptr = my_malloc(size);
    +    size_t i;
    +    for (i = 0; i < (block->size < size ? block->size : size); i++)
    +    {
    +        newptr[i] = ((uint8_t *)ptr)[i];
    +    }
    +    block->free = 1;
    +    return newptr;
    +}
    +
    + +How realloc works: + +
  • If the pointer to reallocate is null, works just like malloc. +
  • If the given size is bigger than the prior size, it allocates a bigger block and copies all data from the original block to the new block. +
  • If the given size is smaller than the prior size, it allocates a smaller block and copies just the data that fits into the smaller block. + + +
  • New challenge +

    +In my implementation I used a linked list where each node holds a pointer to the next, but given that I have control over the _entire_ memory this actualy isn't necessary. + +My challenge to you is that you remove the pointer to next from the block_meta struct. + + +

    Resources +

    +
  • https://danluu.com/malloc-tutorial/ +
  • http://www.craftinginterpreters.com/chunks-of-bytecode.html + +
  • \ No newline at end of file diff --git a/docs/2020/04/19/mysql-python.html b/docs/2020/04/19/mysql-python.html new file mode 100644 index 0000000..f3d6526 --- /dev/null +++ b/docs/2020/04/19/mysql-python.html @@ -0,0 +1,284 @@ +Executing python code from MySQL Server +

    Blog

    Reading time: +

    + +

    Executing python code from MySQL Server +

    + +

    Trying py_eval +

    + +
    Generate a list of integers from 0 to 10 +
    +
    +> select py_eval('[i for i in range(10)]') list;
    ++--------------------------------+
    +| list                           |
    ++--------------------------------+
    +| [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] |
    ++--------------------------------+
    +
    + + +
    Generate a dictionary (json object) from a list of dicts +
    +
    +> select replace(py_eval('{ str(user["id"]) : user for user in [{"id": 33, "name": "John"}, {"id": 44, "name": "George"}] }'), "'", '"') dict;
    ++------------------------------------------------------------------------+
    +| dict                                                                   |
    ++------------------------------------------------------------------------+
    +| {"33": {"id": 33, "name": "John"}, "44": {"id": 44, "name": "George"}} |
    ++------------------------------------------------------------------------+
    +
    + +Replace is needed here, because python uses single quotes for dictionaries. + + +
    Make a function that receives a json array and a key and sorts the array by key +
    +
    +DROP function IF EXISTS sort_by_key;
    +DELIMITER $$
    +CREATE FUNCTION sort_by_key (arr json, k text)
    +RETURNS json
    +BEGIN
    +    RETURN replace(py_eval(CONCAT("sorted(", arr, ", key=lambda e: e['", k, "'])")), "'", '"');
    +END$$
    +DELIMITER ;
    +
    + +Test + +
    +> select sort_by_key('[{"a":2}, {"a":1}, {"a": 722}, {"a": 0}]', 'a') sorted;
    ++--------------------------------------------+
    +| sorted                                     |
    ++--------------------------------------------+
    +| [{"a": 0}, {"a": 1}, {"a": 2}, {"a": 722}] |
    ++--------------------------------------------+
    +
    + + +

    How to write a MySQL UDF +

    +There is a pretty good guide at the MySQL 8.0 Reference Manual. I'll give you a brief explanation so you can start quickly, but reading the full guide is highly recomended. + +MySQL's UDFs are written in C++ and need to follow certain conventions so they can be recognized as such. + +In our case, we want our MySQL function to be called py_eval, so we have to define the following C++ functions: + +
  • py_eval_init or py_eval_deinit +
  • py_eval + +**py_eval_init**: (Optional) Initializes memory and data structures for the function execution. + +**py_eval**: Executes the actual function, in our case evaluates a python expression. + +**py_eval_deinit**: (Optional) If any memory was allocated in the init function, this is the place where we free it. + +For py_eval we only need **py_eval_init** and **py_eval**. + + +
  • Functions signatures +

    +
    +bool py_eval_init(UDF_INIT *initid, UDF_ARGS *args,
    +                             char *message);
    +
    +char *py_eval(UDF_INIT *, UDF_ARGS *args, char *result,
    +                         unsigned long *res_length, unsigned char *null_value,
    +                         unsigned char *);
    +
    + +These are the standard definitions for MySQL functions that return string, as is the case of py_eval. To be able to declare this functions, you need to have the definition of UDF_INIT and UDF_ARGS, you can find that at the source code of mysql server -> right here. + + +

    Evaluating python expression +

    +For evaluating python expression, we'll be using pybind11. That gives us the ability to directly access the python interpreter and execute code. + + +
    Example +
    +Make sure you have g++ installed. Try executing: g++ --help. And some version of python running of your system, for this tutorial I'll be using version _3.8_. + +
    +$ mkdir py_eval && cd py_eval
    +$ git clone https://github.com/pybind/pybind11
    +
    + +Create a new file called main.cpp with the following content: + +
    +#include "pybind11/include/pybind11/embed.h"
    +#include "pybind11/include/pybind11/eval.h"
    +#include 
    +
    +namespace py = pybind11;
    +
    +py::scoped_interpreter guard{}; // We need this to keep the interpreter alive
    +
    +int main(void) {
    +    auto obj = py::eval("[i for i in range(10)]");
    +    std::cout << std::string(py::str(obj)) << std::endl;
    +}
    +
    + +To run the example we have to compile the file. + +First, we need the compilation flags. + +
    +$ pkg-config python-3.8 --libs --cflags
    +-I/usr/include/python3.8
    +
    + +Then, we can compile and run our code with the following. + +
    +$ g++ main.cpp -I/usr/include/python3.8 -lpython3.8
    +$ ./a.out
    +[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    +
    + + +

    Putting all together +

    +Download udf types to the project folder. + +
    +$ wget https://raw.githubusercontent.com/mysql/mysql-server/8.0/include/mysql/udf_registration_types.h
    +
    + +Create a new file called py_eval.cpp, with the following content: + +
    c++
    +#include "pybind11/include/pybind11/embed.h"
    +#include "pybind11/include/pybind11/eval.h"
    +#include "udf_registration_types.h"
    +#include 
    +
    +namespace py = pybind11;
    +
    +py::scoped_interpreter guard{}; // We need this to keep the interpreter alive
    +
    +extern "C" bool py_eval_init(UDF_INIT *initid, UDF_ARGS *args,
    +                             char *message)
    +{
    +    // Here we can check if we received one argument
    +    if (args->arg_count != 1)
    +    {
    +        // The function returns true if there is an error,
    +        // the error message is copied to the message arg.
    +        strcpy(message, "py_eval must have one argument");
    +        return true;
    +    }
    +    // Cast the passed argument to string
    +    args->arg_type[0] = STRING_RESULT;
    +    initid->maybe_null = true; /* The result may be null */
    +    return false;
    +}
    +
    +extern "C" char *py_eval(UDF_INIT *, UDF_ARGS *args, char *result,
    +                         unsigned long *res_length, unsigned char *null_value,
    +                         unsigned char *)
    +{
    +    // Evaluate the argument as a python expression
    +    auto obj = py::eval(args->args[0]);
    +    // Cast the result to std::string
    +    std::string res_str = std::string(py::str(obj));
    +
    +    // Copy the output string from py::eval to the result argument
    +    strcpy(result, res_str.c_str());
    +
    +    // Set the length of the result string
    +    *res_length = res_str.length();
    +
    +    return result;
    +}
    +
    + +Then, we have to compile the project as a shared library, and move it to the plugin folder of mysql (in your case, it could be located in some other directory). + +
    +$ g++ -I/usr/include/python3.8 -lpython3.8 -shared -fPIC -o py_eval.so py_eval.cpp
    +$ sudo cp py_eval.so /usr/lib/mysql/plugin/
    +
    + +Now, it's time to try it from mysql. + +First, connect to your server as root. + +
    +$ sudo mysql -uroot
    +
    + +Create and test the function. + +
    +> create function py_eval returns string soname 'py_eval.so';
    +Query OK, 0 rows affected (0.029 sec)
    +
    +> select py_eval('[i for i in range(10)]') list;
    ++--------------------------------+
    +| list                           |
    ++--------------------------------+
    +| [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] |
    ++--------------------------------+
    +1 row in set (0.001 sec)
    +
    + + +

    Future +

    +There is a lot to do, for example there is no error control on the function execution. The python expression that we are trying to evaluate could fail causing a server reboot. Also, there is some extra work to do to be able to use import. And there are many concerns regarding concurrency issues. + +If you want to contribute to improve execution of python code on mysql server, please go to my github project and make a PR. + +I hope you enjoyed this tutorial and come back soon for more. + +
    \ No newline at end of file diff --git a/docs/2020/12/17/grotsky-getmyip.html b/docs/2020/12/17/grotsky-getmyip.html new file mode 100644 index 0000000..9bc0eb0 --- /dev/null +++ b/docs/2020/12/17/grotsky-getmyip.html @@ -0,0 +1,233 @@ +Grotsky Part 4: Writing a service to get your public IP +

    Blog

    Reading time: +

    + +

    Writing a service to get your public IP +

    +Grotsky (my toy programming language) finally can be used to make something useful. + +In this post I want to show you how I made a service that let's your retrieve your public IP as a response to a HTTP Request. + + +

    Show me the code +

    +Let's start by building the http request handler. + +The service will be deployed to heroku. Heroku passes the port that the http server has to listen as an environment variable named PORT. + + +
    Let's get the server up and running +
    +
    +let listen = ":8092"
    +let port = env.get("PORT")
    +if port != "" {
    +    listen = ":" + port
    +}
    +
    +io.println("Listen " + listen)
    +http.listen(listen)
    +
    + +We listen by default at the port 8092 and if the environment variable is given we change it. + +Then we print what is the port and start the server with http.listen. That blocks the execution and starts the server. + +Grotsky interpreter is written in Go, and uses Go's standard http server. Each requests is handled by a goroutine, but because Grotsky is single threaded only one goroutine executes at any given point in time. + +When a request is received the goroutine has to hold the GIL (Global Interrupt Lock) to be able to give control to the interpreter. + + +
    Now lets add some code to handle requests +
    +
    +fn getIP(rq, rs) {
    +    io.println("Request from --> " + rq.address)
    +    rs.write(200, rq.address)
    +}
    +
    +http.handler("/", getIP)
    +
    +let listen = ":8092"
    +let port = env.get("PORT")
    +if port != "" {
    +    listen = ":" + port
    +}
    +
    +io.println("Listen " + listen)
    +http.listen(listen)
    +
    + +Now we have something interesting to try out! + +What we've done is to log and write back as response the address of the device that is doing the request. + +To try it out you need to download grotsky. + +
    +$ go get github.com/mliezun/grotsky/cmd/grotsky
    +
    + +Save the Grotsky code under a filed called getip.g and the execute it using the grotsky interpreter: + +
    +$ go run $(go env GOPATH)/src/github.com/mliezun/grotsky/cmd/grotsky getip.g
    +
    + +Output: +
    +Listen :8092
    +
    + +Now you can make a request to see if it is working + +
    +$ curl localhost:8092
    +
    + +Output: +
    +[::1]:43464
    +
    + +We see that the address contains the port we want to split it and show just the IP. + + + +
    Let's write a couple functions to do that +
    +
    +fn findReversed(string, char) {
    +    for let i = string.length-1; i > -1; i = i - 1 {
    +        if string[i] == char {
    +            return i
    +        }
    +    }
    +    return -1
    +}
    +
    +fn parseIP(address) {
    +    let ix = findReversed(address, ":")
    +    return address[:ix]
    +}
    +
    + +The function findReversed finds the first index where char appears in string starting from the end. + +The function parseIP uses findReversed to obtain the index where ":" splits the IP and the PORT and uses that index to return just the IP address. + + +
    Now we can send just the IP address +
    +
    +fn getIP(rq, rs) {
    +    let address = parseIP(rq.address)
    +    io.println("Request from --> " + address)
    +    rs.write(200, address)
    +}
    +
    + +Add the two functions at the beginning of the file and modify the getIP function. + +Restart the server and now if you make a request you should get just the IP. + +
    +$ curl localhost:8092
    +[::1]
    +
    + +Voila! + + + +
    We have just one last issue: Proxies! +
    +Our service will probably sit behind a proxy, so we need to read the address from a special header X-Forwarded-For. + +Let's implement that! + +
    +fn getIP(rq, rs) {
    +    let address = parseIP(rq.address)
    +    let forwarded = rq.headers["X-Forwarded-For"]
    +    if forwarded != nil {
    +        address = forwarded[0]
    +    }
    +    io.println("Request from --> " + address)
    +    rs.write(200, address)
    +}
    +
    + +We read the header from the request and if X-Forwarded-For is present we sent that as a response to the user. + + +
    Our work is complete. Let's try it! +
    +
    +$ curl localhost:8092 -H 'X-Forwarded-For: 8.8.8.8'
    +8.8.8.8
    +
    +$ curl localhost:8092
    +[::1]
    +
    + +Well done. Now you can deploy it to Heroku (that's up to you) or any other cloud platform. + +I have my own version running under: https://peaceful-lowlands-45821.herokuapp.com/ + + + +

    Deployed to Fly.io after Heroku killed the free plan: +http://morning-breeze-4255.fly.dev +

    Try it from your command line +
    +
    +$ curl http://morning-breeze-4255.fly.dev
    +
    + + +
    \ No newline at end of file diff --git a/docs/2021/04/01/mlisp-wasm.html b/docs/2021/04/01/mlisp-wasm.html new file mode 100644 index 0000000..325ba0d --- /dev/null +++ b/docs/2021/04/01/mlisp-wasm.html @@ -0,0 +1,187 @@ +Mlisp: My own lisp implementation compiled to WASM +

    Blog

    Reading time: +

    + +

    Mlisp, My own lisp implementation +

    +Mlisp a tiny lispy language based on the book Build Your Own Lisp. + +The interpreter is written in C and compiled directly to WASM. You can try it in this page by openning the developer console of your browser and typing Mlisp.interpret("+ 2 2") or using the repl shown below. + + +

    Interface +

    +To be able to access C functions from your browser you have to export them. Let's see how we can define a function that is exported. + +
    +#if __EMSCRIPTEN__
    +EMSCRIPTEN_KEEPALIVE
    +#endif
    +int mlisp_init();
    +
    + +When compilen with emcc the emscripten compiler to wasm, you have to add EMSCRIPTEN_KEEPALIVE macro before your function so it doesn't get optimized away. + +The exported functions in this project are: + +
    +int mlisp_init();
    +char *mlisp_interpret(char *input);
    +void mlisp_cleanup();
    +
    + +The project is then compiled with: + +
    +emcc -std=c99  -Wall -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]'
    +
    + +That means that you would be able to access the exported functions using a cwrap that let's you wrap a C function call from a Javascript function call. + +This compilation generates two files mlisp.js and mlisp.wasm. + +The javascript file defines a Module that provides useful tool to access exported functions. + + +

    Let's start using it +

    +
    +const Mlisp = {
    +    init: Module.cwrap('mlisp_init', 'number', []),
    +    interpret: Module.cwrap('mlisp_interpret', 'string', ['string']),
    +    cleanup: Module.cwrap('mlisp_cleanup', 'void', []),
    +};
    +
    +// Init interpreter
    +Mlisp.init();
    +
    +// Run some commands
    +console.log(Mlisp.interpret("+ 2 2"));
    +
    +// Cleanup interpreter
    +Mlisp.cleanup();
    +
    + + +

    Automated Build & Release from github +

    +I made a github workflow for this project to automatically build and release so you can retrieve them from Github. + + + +

    REPL +

    + + + + +
    +
    + + Input some commands"> +
    +
    + + + + + + +

    Interesting commands to try out +

    +
  • foldl: Fold left (same as reduce left) + - (foldl + 0 {1 2 3 4 5}): Sum of elements +
  • filter + - (filter (\ {e} {> e 3}) {1 2 3 4 5 6}): Elements bigger than 3 +
  • map + - (foldl * 1 (map (\ {e} {* e 2}) {1 1 1 1 1})): Multiply elements by 2 and then multiply all elements + + +
  • \ No newline at end of file diff --git a/docs/2021/10/04/new-blog-engine.html b/docs/2021/10/04/new-blog-engine.html new file mode 100644 index 0000000..4d982c2 --- /dev/null +++ b/docs/2021/10/04/new-blog-engine.html @@ -0,0 +1,118 @@ +I created a programming language and this blog is powered by it +

    Blog

    Reading time: +

    I created a programming language and this blog is powered by it

    Why did I do it?

    Mostly for fun.

    If you follow my blog or take a look at some of the posts that I made, you will see that I was +building a programming language called +Grotksy. Just a toy programming language that I made based on the book +Crafting Interpreters, which I totally recommend buying and reading if you haven't yet. +

    I wanted to build something interesting but simple enough that could be made with Grotsky. +I tought that replacing Jekyll with my own engine was a task worth a try. +There is nothing groundbreaking or innovative being made here, just a little experimentation. +

    I have to give credit to the project +lith, because the 'templating' engine for the blog is inspired by it. +

    How did I do it?

    That's a good question.

    Originally, this blog was powered by Jekyll, that translated markdown to html and hosted + by Github Pages. I decided that I was going to build a templating engine and generate html + to keep things simple. +

    But also, as a challenge I made a simple HTTP server to use as a dev server when trying the blog locally. +

    HTTP Server

    For the purpose of having a custom HTTP Server I had to add support for TCP sockets to the language. +I wrapped the go standard library in some functions and exposed that to the Grotsky language. +In grotsky looks something like this +

    +let socket = net.listenTcp(host + ":" + port)
    +let conn
    +while true {
    +	try {
    +		conn = socket.accept()
    +	} catch err {
    +		io.println("Error accepting new connection", err)
    +		continue
    +	}
    +	try {
    +		# Call function that handles connection
    +		handleConn(conn)
    +	} catch err {
    +		io.println("Error handling connection", err)
    +	}
    +	try {
    +		conn.close()
    +	} catch err {}
    +}
    +				
    +

    This means that the server listens on a socket, accepts connections, writes some text/bytes to the connection + and then closes the connection. +

    Template Engine

    The templating engine is built using the native support Grotsky provide for lists. +A regular page for the blog looks like this: +

    +let base = import("../base.gr")
    +# Create new Post Object
    +let post = base.Post(
    +	"Title",
    +	"Brief description of blog post.",
    +	"Author Name",
    +	"descriptive,tags",
    +	[
    +		[
    +			"h2",
    +			[],
    +			[
    +				"Title"
    +			]
    +		],
    +		[
    +			"div",
    +			["class", "content"],
    +			[
    +				"Content line 1",
    +				"Content line 2",
    +			]
    +		]
    +	]
    +)
    +			

    It's pretty straightforward: the first element of the list is the html tag, the second is +an array of properties for the tag and the last one is a list that contains what will be +the *content* of enclosed by the tags. +

    Resources

    If you want to take a peek, the source code for both projects is available on github: +

    \ No newline at end of file diff --git a/docs/2021/12/31/playing-with-js.html b/docs/2021/12/31/playing-with-js.html new file mode 100644 index 0000000..ffb2ac4 --- /dev/null +++ b/docs/2021/12/31/playing-with-js.html @@ -0,0 +1,355 @@ +Playing with Javascript Proxies (getters/setters) +

    Blog

    Reading time: +

    Playing with Javascript Proxies (getters/setters)

    Overview

    Happy New Year!

    This is my final post for the 2021. This year I didn't post that much, but a lot of work was put into + the blog to rewrite it using +Grotksy. +I hope everyone has a great 2022 and that next year is much better than the last one. +

    The inspiration for this blog post comes from the idea of building a tiny db that feels more natural to Javscript. +All the databases that I've seen make a heavy use of methods like: +db.get(), +db.put(), +db.scan(), +db.query(). + And many others that Im sure you have seen. +I think it would be great to see something like: +

    const db = getDb("...")
    +// Create new user
    +const u = {username: "jdoe", email: "jdoe@example.com", id: 100}
    +// Store new user in the database
    +db.objects.users[u.username] = u
    +// Commit the changes to the database
    +db.actions.save()
    +

    In this blog post we will be building a much simpler version that stores everything in memory. Each change + made to the objects will be stored in a log (called layers) and the final object will be composed of all + the small layers present in the log. +

    Defining a proxy

    We need to implement some generic getters/setters. +

    const objects = new Proxy({}, {
    +    get: function(obj, prop) {
    +        validate(prop, null)
    +        // Implementation
    +    },
    +    set: function(obj, prop, value) {
    +        validate(prop, value)
    +        // Implementation
    +    }
    +})
    +

    Let's define the validation function. In this case we want the objects to be able to be serialized to JSON. +

    +const validate = (prop, value) => {
    +    // Make sure that the property and value are serializable
    +    // JSON.stringify throws an error if not serializable
    +    const l = {}
    +    l[prop] = value
    +    JSON.stringify(l)
    +    return l
    +}
    +
    +

    This empty proxy will validate that the values and prop are serializable and do nothing else. Now we can start +building on top of it. +

    Building a tree to hold everything together

    We need a root object where we will store all the changes that are applied to an object. +We will have a sort of tree structure to hold everything together. +It will look something like this: +

    +              rootObject({})  -> layers([{users: {jdoe: ...}}, {tokens: {tk1: ...}}])
    +                    |
    +        --------------------------
    +        |                        |
    + child(.users{})          child(.tokens{})
    +        |                        |
    +       ...                      ...
    +
    +

    The root object contains the layers with all the changes made from the beginning of the existence of the object. +Each time a property of the root object is accessed a child is returned that internally holds a reference to the root. +This way we can go through the entire chain of access and be able to reach the stored layers. +By chain of access I mean the following: objects.users.jdoe.metadata.login.ip. +As you can see, we need to traverse through many objects to be able to reach the ip field. But the layer that contains + the information is only stored in the root, so each child needs to mantain a reference to the parent to be able to reach + the root node. +

    Let's define a simple function to be able to create a new rootObject. +

    +const wrapObject = (parent, key, current) => {
    +    const rootObj = {
    +        parent: Object.freeze(parent),
    +        layers: [Object.freeze({'value': current, 'previous': null})],
    +        pushLayer (l) {}, // Push new layer
    +        getLayer (ks) {}, // Get layer where information is stored based on given keys
    +        getValue (k) {} // Get value that matches given key
    +    }
    +
    +    const rootProxy = {
    +        get: function(obj, prop) {
    +            validate(prop, null)
    +            const val = rootObj.getValue(prop)
    +            if (typeof val == 'object') {
    +                // If the value is an object we need to have a child instance
    +                // with a reference to the parent
    +                return wrapObject(rootObj, prop, val).objects
    +            }
    +            // If the value is other kind like a number or string we can safely return that
    +            return val
    +        },
    +        set: function(obj, prop, value) {
    +            const l = validate(prop, value)
    +            // Add new layer to the rootObj
    +            rootObj.pushLayer({'value': l})
    +        }
    +    }
    +
    +    return {
    +        actions: {
    +            revert () {
    +                // Deleting the last layer will revert the changes
    +                const pop = rootObj.layers[rootObj.layers.length-1]
    +                rootObj.layers.splice(rootObj.layers.length-1, rootObj.layers.length)
    +                return pop
    +            }
    +        },
    +        objects: new Proxy({}, rootProxy)
    +    }
    +}
    +
    +

    Handling layers

    The layer format: +

    +const layer = {
    +    value: {status: 'active'},
    +    previous: null // Reference to a previous layer that has the key 'status' in it
    +}
    +
    +
    The layers are stored in an array, each layer holds the value and a reference to the previous layer + that set a value for the same key (in this case the key was 'status'). Also the layers form a simple + linked list through the 'previous' reference. That way we have the entire history of a given key. +

    We would need a function to be able to tell if an object has a list of nested keys. Trust me for now, you'll see. +

    +const nestedExists = (obj, ks) => {
    +    for (let j = 0; j < ks.length; j++) {
    +        let k = ks[j];
    +        if (!(k in obj)) {
    +            return false
    +        }
    +        obj = obj[k]
    +    }
    +    return true
    +}
    +
    +
    In this function we receive an object and a list of keys, we start accessing the first internal object with the first key + and we keep doing the same till we make sure that all the keys are present. +

    Now we're almost done. Let's define the functions for handling the store and retrieval of layers. +

    +const rootObj = {
    +    parent: Object.freeze(parent),
    +    layers: [Object.freeze({'value': current, 'previous': null})],
    +    pushLayer (l) {
    +        // If this is a child object we need to build the entire chain of access
    +        // from the bottom up
    +        if (parent) {
    +            const ll = {}
    +            ll[key] = l['value']
    +            // Search for a previous layer modifying the same key
    +            const previous = parent.getLayer([key])
    +            // Tell the parent object to push the new layer
    +            parent.pushLayer(Object.freeze({'value': ll, previous}))
    +        } else {
    +            // We are in the root object, add the layer to the array
    +            this.layers.push(Object.freeze(l))
    +        }
    +    },
    +    getLayer (ks) {
    +        // Search through the entire list of layers to see if one contains all the keys
    +        // that we are looking for. Start from the end of the array (top of the stack)
    +        for (let i = this.layers.length - 1; i >= 0; i--) {
    +            let v = nestedExists(this.layers[i]['value'], ks)
    +            if (v) {
    +                return this.layers[i]
    +            }
    +        }
    +        if (parent) {
    +            // If we are in a child object, look through all the previous layers
    +            // and see if the key we're looking for is contained in one of them.
    +            let ll = parent.getLayer([key].concat(ks))
    +            while (ll) {
    +                let a = nestedExists(ll['value'][key], ks)
    +                if (a) {
    +                    return Object.freeze({'value': ll['value'][key]})
    +                }
    +                ll = ll.previous
    +            }
    +        }
    +    },
    +    getValue (k) {
    +        // Straightforward, get layer and return value
    +        const l = this.getLayer([k])
    +        if (l) {
    +            return Object.freeze(l['value'][k])
    +        }
    +    }
    +}
    +
    +
    That's all we need. We can create a new object and start adding and modifying properties. Each change will be added + to the end of the log and worked out when a property is accessed. +

    Wrapping Up

    Let's try the final result. The source code is loaded in this page, so you can open a dev console in the browser and + try for yourself. +

    +const store = wrapObject(null, null, {})
    +
    +// Create new user
    +const user = {username: 'jdoe', email: 'jdoe@example.com', name: 'John Doe', id: 100}
    +
    +// Add new user
    +store.objects.users = {}
    +store.objects.users[user.username] = user
    +
    +// Print user email
    +console.log(store.objects.users.jdoe.email)
    +
    +// Change user email and print
    +store.objects.users.jdoe.email = 'jdoe2@example.com'
    +console.log(store.objects.users.jdoe.email)
    +
    +// Revert last change and print email again
    +store.actions.revert()
    +console.log(store.objects.users.jdoe.email)
    +
    +

    That's it for now. We defined a Javascript object that contains the entire history of changes that were made to itself. +And at any point we can revert the changes and go back to a previous state. +Everything is stored in an array and is easily serializable. +If we wanted to take this to the next level, each change could be written to a persistence storage (s3, sqlite, mysql, ...) +

    The full source code is available in a +public gist. +

    \ No newline at end of file diff --git a/docs/2022/05/05/rust-blake-mysql.html b/docs/2022/05/05/rust-blake-mysql.html new file mode 100644 index 0000000..b2e5a57 --- /dev/null +++ b/docs/2022/05/05/rust-blake-mysql.html @@ -0,0 +1,58 @@ +Blake3 hash plugin for MySQL written in Rust +

    Blog

    Reading time: +

    Implementing a blake3 hash plugin for MySQL in Rust

    It's been long since I've written something, I wanted to bring you some new stuff, so here you have a short blog post. +I encourage you to try this new plugin that uses this +blake3 hash implementation. +Blake3 is secure, unlike MD5 and SHA-1. And secure against length extension, unlike SHA-2. +Start using it and create an issue in the github repo if you would like a feature implemented! +

    Checkout +blake-udf source code.

    How to use

    Download and install MySQL plugin

    $ wget 'https://github.com/mliezun/blake-udf/releases/download/v0.1.0/libblake_udf.so'
    +$ mv libblake_udf.so /usr/lib/mysql/plugin/
    +

    Load UDF in MySQL

    $ mysql -uroot -p -e 'create function blake3_hash returns string soname "libblake_udf.so";'
    +

    Execute function

    $ mysql --binary-as-hex=0 -uroot -p -e 'select blake3_hash("a");'
    +
    Output: 17762fddd969a453925d65717ac3eea21320b66b54342fde15128d6caf21215f +
    \ No newline at end of file diff --git a/docs/2022/07/20/cloud-outdated-release.html b/docs/2022/07/20/cloud-outdated-release.html new file mode 100644 index 0000000..ef06dce --- /dev/null +++ b/docs/2022/07/20/cloud-outdated-release.html @@ -0,0 +1,137 @@ +Webscraping as a side project +

    Blog

    Reading time: +

    Webscraping as a side project

    A friend and I were looking for a side project to work together. We realized we both faced a similar problem. +

    Let's use AWS Lambda Python runtime as an example. +AWS will send out emails when a version is at the end of life making it difficult to stay +on the latest if desired. +Plus, reacting to them usually means you are many many versions behind already. +

    Our journey started. +We made a list of providers for the MVP: AWS, GCP and Azure. +Then a list of the services that have versions (for example S3 doesn't have versions). +After that we realized that we could get some versions using APIs. +Other services exclusively require webscraping. +

    We support 46 services and counting. +Take a look at +Cloud Outdated and subscribe to get notified. +If you are looking for a service that's not there +contact us. +

    Picking a language, framework and platform

    We're both Python programmers. The choice was obvious. +"Let's use Python and Django framework for the job" we said. +We didn't want to spend our innovation tokens on new language/framework. +So we chose Boring Technology. +

    For the db we spent our first innovation token. +We decided to go with the flashy new serverless postgres-compatible +CockroachDB. +

    On the hosting side we're using AWS Lambda. Taking advantage of the free compute time. +Helps mantaining the costs down. +

    Make webscraping reliable

    A webpage that's being scraped can change at any time. First thing we did was account for those edge cases. +We created a custom exception that is triggered when something changed. So that we can react to that downstream. +

    +class ScrapingError(Exception):
    +    pass
    +
    +
    We wanted to keep the implementation simple. Each service is scraped by a single function. +The signature of the function is something like +aws_lambda_python() -> List[Version]. +All the implementations follow a similar pattern: +
    +def aws_lambda_python():
    +    # Read versions from aws docs website:
    +    # https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html
    +
    +    if not found_versions:
    +        raise ScrappingError
    +
    +    # Process and return versions
    +
    +
    That's ^ what we call a poll function. +

    We pass poll functions through a polling class that handles all the errors and results. +When we detect an scraping error we have a special case. We send an email with the details +of what happened. Because the problem is something that requires manual action. We receive +that email in our personal inboxes and fix the problem ASAP. +

    The poll class that handles all the magic behind cloud outdated is actually very simple: +

    +class PollService:
    +    def __init__(self, service: Service, poll_fn: Callable):
    +        self.poll_fn = poll_fn
    +        # Some other attributes...
    +
    +    def poll(self):
    +        try:
    +            results = self.poll_fn()
    +            self.process_results(results)
    +        except ScrapingError as e:
    +            notify_operator(
    +                f"{type(e).__name__} at line {e.__traceback__.tb_lineno} of {__file__}: {e.__str__()}"
    +            )
    +
    +    def process_results(self, results):
    +        # if results contains new versions:
    +        #     save new versions to db
    +        # if results contains deprecated versions:
    +        #     set versions in db as depreacted
    +
    +

    That's the hearth of Cloud Outdated. +After that we have to send notifications to subscribed users. +That part is trivial. +We send an email that contains the difference between what was last sent to a user and what we +have stored in the db at the moment. +

    Last toughts

    Having a side project is usually a good idea. +For us has been a journey were we got to know some new stuff (CockroachDB). +We also learned about how to build a product and keep a MVP mentality. +The most difficult challenge we face is to bring more users to the platform. +

    We'd love to see more people subscribed. +If this blogpost sparked your interest go to +Cloud Outdated and subscribe to start getting emails. +

    See you next time! +

    \ No newline at end of file diff --git a/docs/2022/09/20/branchable-mysql.html b/docs/2022/09/20/branchable-mysql.html new file mode 100644 index 0000000..38c47c1 --- /dev/null +++ b/docs/2022/09/20/branchable-mysql.html @@ -0,0 +1,214 @@ +Branchable MySQL: Managing multiple dev environments +

    Blog

    Reading time: +

    Branchable MySQL: Managing multiple dev environments

    When teams start to grow, having a single dev environment becomes an issue. People start stepping on each others toes. +A common problem is that two people want to apply incompatible migrations on the database. That problem is impossible +to fix if folks are working on parallel branches. +If we can have a database for each branch of a project, that will remove much of the pain of having multiple devs applying +changes to the db.

    There are already projects that solve this problem: +PlanetScale and +Neon. +

    A common case where this problem arises is when two devs want to add a column to the same table. +

    Two devs applying changes to the same table in the database.

    We have a +people table in the database. One of the devs wants to add the +last_name column and the other one wants to add the +address. +

    Dev1's code thinks the table will have 3 columns after he applies his operation: +id, name, last_name. +

    Dev2's code also thinks the table will have 3 columns: +id, name, address. +

    In reality the table will have 4 columns. +So neither of them will be able to run their code unless they talk to each other and figure out how to make this work. +

    This is far from ideal. +

    What we want instead, is that each one of them can develop their features independently. +

    Two devs applying changes to the same table in different database branches.

    They both apply to the same table, but each table lives on an instance that was 'replicated' from the original. +

    How can we implement the ideal case?

    MySQL writes data (by default) to the directory +/var/lib/mysql/data. +

    We can use an +Union filesystem. And configure MySQL to use a different directory to read and write data. +

    That way we can have a feature/user-last-name 'branch' read and write data from a directory like +/app/user-last-name/mysql/data. +

    And a feature/user-address 'branch' read and write data from a directory like +/app/user-address/mysql/data. +

    Those branches can be mounted using fuse-overlayfs by executing the following commands: +

    +# Directory /app/base contains data from the original branch
    +
    +fuse-overlayfs -o lowerdir=/app/base,upperdir=/app/user-last-name,workdir=/tmp/user-last-name overlayfs /app/user-last-name
    +
    +fuse-overlayfs -o lowerdir=/app/base,upperdir=/app/user-address,workdir=/tmp/user-address overlayfs /app/user-address
    +
    +

    This means both 'branches' of the database are able to coexist and have different schemas during their lifetime. +

    Experimenting with a use case

    I had this idea in my head for months. I finally convinced myself that it was worth a shot. +

    I decided to do a little implementation using Docker and python FastAPI. +Exposing a simple interface so that it's easy to create and delete branches. +

    The project is live on github +branchable-mysql. +

    The container image is published on Docker Hub +branchable-mysql. +

    To start using the image let's create a docker-compose.yml file. +

    version: "3"
    +
    +services:
    +  mysql:
    +    image: mliezun/branchable-mysql
    +    platform: linux/amd64
    +    privileged: true
    +    restart: always
    +    volumes:
    +      - appdata:/app/
    +
    +volumes:
    +  appdata:
    +
    +

    Then you can execute +docker-compose up and the MySQL server should start running. +

    After that, connect easily to the db +docker compose exec mysql mysql -uroot -h127.0.0.1 --skip-password -P33061. You should enter to an interactive mysql console. +

    Let's create an initial schema, a table and insert some data so that we can see how branching works. +On the console that we just opened execute: +

    +mysql> create schema s1;
    +Query OK, 1 row affected (0.01 sec)
    +
    +mysql> use s1;
    +Database changed
    +mysql> create table people (id int primary key auto_increment, name varchar(255) not null);
    +Query OK, 0 rows affected (0.07 sec)
    +
    +mysql> insert into people select 0, 'Miguel';
    +Query OK, 1 row affected (0.02 sec)
    +Records: 1  Duplicates: 0  Warnings: 0
    +
    +mysql> select * from people;
    ++----+--------+
    +| id | name   |
    ++----+--------+
    +|  1 | Miguel |
    ++----+--------+
    +1 row in set (0.00 sec)
    +
    +

    That's enough for now, we're ready to start creating branches. +

    On a separate terminal, without closing the previous mysql interactive console, execute: +

    +docker compose exec mysql /app/scripts/create_branch.sh base feature/user-last-name
    +
    +{"branch_name":"feature/user-last-name","base_branch":"base","port":33062}
    +
    +

    Now you can login to the new database branch using port 33062 +docker compose exec mysql mysql -uroot -h127.0.0.1 --skip-password -P33062

    +mysql> use s1;
    +Reading table information for completion of table and column names
    +You can turn off this feature to get a quicker startup with -A
    +
    +Database changed
    +mysql> alter table people add column last_name varchar(255) not null;
    +Query OK, 0 rows affected (0.03 sec)
    +Records: 0  Duplicates: 0  Warnings: 0
    +
    +mysql> select * from people;
    ++----+--------+-----------+
    +| id | name   | last_name |
    ++----+--------+-----------+
    +|  1 | Miguel |           |
    ++----+--------+-----------+
    +1 row in set (0.00 sec)
    +
    +

    In a new terminal we can create a another branch: +

    +docker compose exec mysql /app/scripts/create_branch.sh base feature/user-address
    +
    +{"branch_name":"feature/user-address","base_branch":"base","port":33063}
    +
    +

    Then connect using port 33063 +docker compose exec mysql mysql -uroot -h127.0.0.1 --skip-password -P33063

    +mysql> use s1;
    +Reading table information for completion of table and column names
    +You can turn off this feature to get a quicker startup with -A
    +
    +Database changed
    +mysql> alter table people add column last_name varchar(255) not null;
    +Query OK, 0 rows affected (0.03 sec)
    +Records: 0  Duplicates: 0  Warnings: 0
    +
    +mysql> select * from people;
    ++----+--------+
    +| id | name   |
    ++----+--------+
    +|  1 | Miguel |
    ++----+--------+
    +1 row in set (0.00 sec)
    +
    +mysql> alter table people add column address varchar(255) not null;
    +Query OK, 0 rows affected (0.02 sec)
    +Records: 0  Duplicates: 0  Warnings: 0
    +
    +mysql> select * from people;
    ++----+--------+---------+
    +| id | name   | address |
    ++----+--------+---------+
    +|  1 | Miguel |         |
    ++----+--------+---------+
    +1 row in set (0.00 sec)
    +
    +

    As you can see, we have 3 servers running at the same time, each one with different schemas. +

    This is great for local development and for having branch-aware dev environments. +

    Final thoughts

    I hope you find this blogpost useful. +If you want to start using branchable-mysql go ahead. +If you encounter any issues please report them in the github repo or create a pull request. +

    \ No newline at end of file diff --git a/docs/2022/09/22/heroku-to-fly.html b/docs/2022/09/22/heroku-to-fly.html new file mode 100644 index 0000000..04ceea2 --- /dev/null +++ b/docs/2022/09/22/heroku-to-fly.html @@ -0,0 +1,116 @@ +Migrate from Heroku to Fly.io +

    Blog

    Reading time: +

    How to migrate from Heroku to Fly.io

    A couple weeks ago Heroku announced the removal. I have plenty of projects running on free dynos. + I have taken some time to move my code to Fly.io. And also I've written a little tutorial of how to perform the migration.

    I'll use one of my public repos as an example +https://github.com/mliezun/getmyip. It's a simple service that returns the IP from which you're making the request. It's useful when you want to know + your public IP. +

    That project ^ was covered in a previous +blogpost. +

    Migration instructions

    The first thing we need to do is to remove heroku from the remotes. +Inside your project run: +

    git remote remove heroku

    If you have a heroku.yml file, delete it. +

    rm -rf heroku.yml

    Then, we're ready to start using fly. +There are tutorials on the +official fly.io docs for many frameworks and languages. We're going to be following the one for a Docker app, + since it's the most general case. +

    First thing you need to do is create an account in +Fly.io if you don't have one yet. +

    Once you created your account, install the flyctl command line tool. + After that, login by running the following command: +

    flyctl auth login

    After you've logged in to your account, you're ready to launch your application. + Execute the next command and follow the interactive setup. +

    $ flyctl launch
    +Scanning source code
    +Detected a Dockerfile app
    +? App Name (leave blank to use an auto-generated name): 
    +Automatically selected personal organization: Miguel Liezun
    +? Select region: mia (Miami, Florida (US))
    +Created app morning-breeze-4255 in organization personal
    +Wrote config file fly.toml
    +? Would you like to set up a Postgresql database now? No
    +? Would you like to deploy now? Yes
    +Deploying morning-breeze-4255
    +==> Validating app configuration
    +--> Validating app configuration done
    +Services
    +TCP 80/443 -> 8080
    +Remote builder fly-builder-green-pond-8004 ready
    +==> Creating build context
    +--> Creating build context done
    +==> Building image with Docker
    +--> docker host: 20.10.12 linux x86_64
    +...
    +

    Make sure your app listens to port 8080, that's the default for fly apps. + You can change the port inside the file fly.toml if you want +, just search for the internal port and change it. Remember to run launch again if you change the port. +

    # fly.toml file generated for morning-breeze-4255 on 2022-09-21T21:50:20-03:00
    +
    +app = "morning-breeze-4255"
    +kill_signal = "SIGINT"
    +kill_timeout = 5
    +processes = []
    +
    +[env]
    +
    +[experimental]
    +  allowed_public_ports = []
    +  auto_rollback = true
    +
    +[[services]]
    +  http_checks = []
    +  internal_port = 8080 # <- Put your desired port here
    +# ...
    +

    Finally, you only need to open the app and enjoy! +You migrated your first app from heroku to fly :-) +

    $ flyctl open
    +opening http://morning-breeze-4255.fly.dev ...
    +

    Access the newly deployed 'getmyip' service using the link +http://morning-breeze-4255.fly.dev. +

    \ No newline at end of file diff --git a/docs/2022/11/26/grotsky-quine.html b/docs/2022/11/26/grotsky-quine.html new file mode 100644 index 0000000..3748f43 --- /dev/null +++ b/docs/2022/11/26/grotsky-quine.html @@ -0,0 +1,103 @@ +How to write a program that can replicate itself +

    Blog

    Reading time: +

    How to write a program that can replicate itself

    Grotsky is a toy programming language that I made for fun. Today we're visinting the concept of Quines, +a.k.a. self replicating programs. It's said that any turing-complete language should be able to write a program that replicates + itself. And grotsky is no exception.

    Read more about grotsky in previous blogposts: +

    Quines are very easy to write. The language that you're using needs to be able to do a couple things: +

    • Write to a file or stdout (print)
    • Support for string arrays
    • Translate numbers/integers to character ascii representation
    • Concatenate strings
    • Loop through arrays from arbitrary indexes

    Super simple quine: less than 30 lines of code

    let tabChar = 9
    +let quoteChar = 34
    +let commaChar = 44
    +let code = [
    +	"let tabChar = 9",
    +	"let quoteChar = 34",
    +	"let commaChar = 44",
    +	"let code = [",
    +	"]",
    +	"for let i = 0; i < 4; i = i+1 {",
    +	"	io.println(code[i])",
    +	"}",
    +	"for let i = 0; i < code.length; i = i+1 {",
    +	"	io.println(strings.chr(tabChar) + strings.chr(quoteChar) + code[i] + strings.chr(quoteChar) + strings.chr(commaChar))",
    +	"}",
    +	"for let i = 4; i < code.length; i = i+1 {",
    +	"	io.println(code[i])",
    +	"}",
    +]
    +for let i = 0; i < 4; i = i+1 {
    +	io.println(code[i])
    +}
    +for let i = 0; i < code.length; i = i+1 {
    +	io.println(strings.chr(tabChar) + strings.chr(quoteChar) + code[i] + strings.chr(quoteChar) + strings.chr(commaChar))
    +}
    +for let i = 4; i < code.length; i = i+1 {
    +	io.println(code[i])
    +}
    +
    +

    Now we can use grotksy cli to run the program and compare the output to the original source. +

    Save the original source to a file called +quine.gr then run the following commands: +

    +$ grotsky quine.gr > quine_copy.gr
    +$ cmp quine.gr quine_copy.gr
    +$ echo $?
    +0
    +
    +

    If you see a 0 as the final output that means the files are the same. + Otherwise if you saw an error message or a different output, that means something has gone wrong. +

    How exciting is this?!! +We've just written a program that gives itself as an output. +That sounds impossible when you hear it for the first time. But it was actually pretty easy! +

    Source code available here: +https://gist.github.com/mliezun/c750ba701608850bd86d646a3ebf700f. +

    Grotsky cli binary available here: +https://github.com/mliezun/grotsky/releases/tag/v0.0.6

    \ No newline at end of file diff --git a/docs/2023/04/08/redis-clone.html b/docs/2023/04/08/redis-clone.html new file mode 100644 index 0000000..942c6b1 --- /dev/null +++ b/docs/2023/04/08/redis-clone.html @@ -0,0 +1,601 @@ +Writing a Redis clone in Go from scratch +

    Blog

    Reading time: +

    Writing a Redis clone in Go from scratch

    In this post we're going to write a basic Redis clone in Go that implements the most simple commands: GET, +SET, DEL and QUIT. At the end you'll know how to parse a byte stream from a live TCP connection, and hopefully have a working +implementation of Redis.

    What's intersting about this project is that it's production ready (not really). + It's being used in production in an old Web app that I made for a client in 2017. + It has been running for a few months now without issues.

    I mantain that app to this day and I charge like 50 bucks a month for it. I do it because + Im friends with the person that uses the app.

    Long story short, the app's backend is written in PHP and uses Redis for caching, only GET, SET and DEL commands. + I asked my friend if I could replace it with my custom version and said yes, so I decided to give it a go.

    If you're looking for C/C++ implementation, go check out this book.

    What we'll be building

    If you go to the +command list on redis webpage you'll see that there are 463 commands to this day (maybe more if you're in the future). +

    That's a crazy number. Here, we're only implementing 4 commands: GET, SET, DEL, QUIT, + the other 459 commands are left as an exercise to the reader. +

    GET

    GET key
    +

    Returns the value referenced by key. + If the key does not exist then nil is returned. +

    SET

    SET command gains more features on newer versions of Redis. We're going to implement one that has all features + that were realeased up until version 6.0.0. +

    SET key value [NX | XX] [EX seconds | PX milliseconds]
    +

    Stores value as a string that is referenced by key. + Overwrites any data that was previously referenced by the key. +

    Options

    • EX seconds -- Set the specified expire time, in seconds.
    • PX milliseconds -- Set the specified expire time, in seconds.
    • NX -- Only set the key if it does not already exist.
    • XX -- Only set the key if it already exist.

    DEL

    DEL key [key ...]
    +

    Takes 'any' amount of keys as input and removes all of them from storage. If a key doesn't exist it is ignored. + Returns the amount of keys that were deleted. +

    QUIT

    QUIT
    +

    When receiving this command the server closes the connection. It's useful for interactive sessions. + For production environments the client should close the connection without sending any commands. +

    Examples

    Let's start an interactive session of redis to test some commands. + We can install redis-server with docker and run it locally. + Then we can use telnet to connect directly via TCP. + Open a terminal and execute the following instructions: +

    $ docker run -d --name redis-server -p 6379:6379 redis:alpine
    +
    +$ telnet 127.0.0.1 6379
    +Trying 127.0.0.1...
    +Connected to localhost.
    +Escape character is '^]'.
    +

    At this point the prompt should be waiting for you to write something. We're gonna test a couple of commands. + In the code boxes below the first line is the command, following lines are the response. +

    GET a
    +$-1
    +
    ^ That weird $-1 is the special nil value. Which means there's nothing stored here. +

    set a 1
    ++OK
    +
    ^ First thing to notice here is that we can use lowercase version of SET. + Also, when the command is successful returns +OK. +

    set b 2
    ++OK
    +

    SET c 3
    ++OK
    +
    ^ Just storing a couple more values. +

    GET a
    +$1
    +1
    +
    ^ Here the response is returned in two lines. First line is the length of the string. Second line + is the actual string. +

    get b
    +$1
    +2
    +
    ^ We can also use lowercase version of GET, I bet commands are case-insensitive. +

    get C
    +$-1
    +
    ^ Testing with uppercase C gives a nil. Keys seem to be case-sensitive, probably values too. + That makes sense. +

    del a b c
    +:3
    +
    ^ Deleting everything returns the amount of keys deleted. Integers are indicated by ':'. +

    quit
    ++OK
    +Connection closed by foreign host.
    +
    ^ When we send QUIT, the server closes the connection and we're back to our terminal session. +

    With those tests we have enough information to start building. We learned a little bit about the + redis protocol and what the responses look like. +

    Sending commands

    Until now we've been using the inline version of redis command. + There's another kind that follows the +RESP (Redis serialization protocol).

    The RESP protocol is quite similar to what we've seen in the examples above. +The most important addition is arrays. Let's see a Client<>Server interaction + using arrays. +

    Client

    *2
    +$3
    +GET
    +$1
    +a
    +
    Server
    $-1
    +
    The server response looks the same as in the inline version. + But what the client sends looks very different: +

    • In this case, the first thing the client sends is '*' followed by the number of elements in the array, + so '*2' indicates that there are 2 elements in the array and they would be found in the following lines. +
    • After that we have '$3' which means we're expecting the first element to be a string of length 3. + Next line is the actual string, in our case is the command 'GET'. +
    • The next value is also a string and is the key passed to the command. +

    That's almost everything we need to start building a client. There's one last thing: error responses. +

    -Example error message
    +-ERR unknown command 'foo'
    +-WRONGTYPE Operation against a key holding the wrong kind of value
    +
    A response that starts with a '-' is considered an error. The first word is the error type. + We'll only gonna be using 'ERR' as a generic response. +

    RESP protocol is what client libraries use to communicate with Redis. + With all that in our toolbox we're ready to start building. +

    Receiving connections

    A crucial part of our serve is the ability to receive client's information. + The way that this is done is that the server listens on a TCP port and waits + for client connections. Let's start building the basic structure. +

    Create a new go module, open main.go and create a main function as follows. +

    package main
    +
    +import (
    +	"bufio"
    +	"fmt"
    +	"log"
    +	"net"
    +	"strconv"
    +	"strings"
    +	"sync"
    +	"time"
    +)
    +
    +var cache sync.Map
    +
    +func main() {
    +	listener, err := net.Listen("tcp", ":6380")
    +	if err != nil {
    +		log.Fatal(err)
    +	}
    +	log.Println("Listening on tcp://0.0.0.0:6380")
    +
    +	for {
    +		conn, err := listener.Accept()
    +		log.Println("New connection", conn)
    +		if err != nil {
    +			log.Fatal(err)
    +		}
    +
    +		go startSession(conn)
    +	}
    +}
    +

    After declaring the package and imports, we create a global sync.Map that would be our cache. + That's where keys are gonna be stored and retrieved. +

    On the main function we start listening on port 6380. After that we have an infinite loop that accepts + new connections and spawns a goroutine to handle the session. +

    Session handling

    // startSession handles the client's session. Parses and executes commands and writes
    +// responses back to the client.
    +func startSession(conn net.Conn) {
    +	defer func() {
    +		log.Println("Closing connection", conn)
    +		conn.Close()
    +	}()
    +	defer func() {
    +		if err := recover(); err != nil {
    +			log.Println("Recovering from error", err)
    +		}
    +	}()
    +	p := NewParser(conn)
    +	for {
    +		cmd, err := p.command()
    +		if err != nil {
    +			log.Println("Error", err)
    +			conn.Write([]uint8("-ERR " + err.Error() + "\r\n"))
    +			break
    +		}
    +		if !cmd.handle() {
    +			break
    +		}
    +	}
    +}
    +

    It's super important that we close the connection when things are done. That's why we set a deferred function, + to close the connection when the session finishes. +

    After that we handle any panics using recover. We do this mainly because at some point we might be reading from + a connection that was closed by the client. And we don't want the entire server to die in case of an error. +

    Then we create a new parser and start trying to parse commands from the live connection. If we encounter an error + we write the error message back to the client and we finish the session. +

    When cmd.handle() returns false (signaling end of session) we break the loop and the session finishes. +

    Parsing commands

    Basic parser structure: +

    // Parser contains the logic to read from a raw tcp connection and parse commands.
    +type Parser struct {
    +	conn net.Conn
    +	r    *bufio.Reader
    +	// Used for inline parsing
    +	line []byte
    +	pos  int
    +}
    +
    +// NewParser returns a new Parser that reads from the given connection.
    +func NewParser(conn net.Conn) *Parser {
    +	return &Parser{
    +		conn: conn,
    +		r:    bufio.NewReader(conn),
    +		line: make([]byte, 0),
    +		pos:  0,
    +	}
    +}
    +

    This is pretty straight-forward. We store a reference to the connection, a reader and then some + attributes that will help us with parsing. +

    The NewParser() function should be used as a contructor for Parser objects. +

    We need some helper functions that will make parsing easier: +

    func (p *Parser) current() byte {
    +	if p.atEnd() {
    +		return '\r'
    +	}
    +	return p.line[p.pos]
    +}
    +
    +func (p *Parser) advance() {
    +	p.pos++
    +}
    +
    +func (p *Parser) atEnd() bool {
    +	return p.pos >= len(p.line)
    +}
    +
    +func (p *Parser) readLine() ([]byte, error) {
    +	line, err := p.r.ReadBytes('\r')
    +	if err != nil {
    +		return nil, err
    +	}
    +	if _, err := p.r.ReadByte(); err != nil {
    +		return nil, err
    +	}
    +	return line[:len(line)-1], nil
    +}
    +

    Also quite simple. +

    • current(): Returns the character being pointed at by pos inside the line.
    • advance(): Point to the next character in the line.
    • atEnd(): Indicates if we're at the end of the line.
    • readLine(): Reads the input from the connection up to the carriage return char. Skips the '\n' char.

    Parsing strings

    In Redis we can send commands like so: +

    SET text "quoted \"text\" here"
    +

    This means we need a way to handle \, " chars inside a string. +

    For that we need a special parsing function that will handle strings: +

    // consumeString reads a string argument from the current line.
    +func (p *Parser) consumeString() (s []byte, err error) {
    +	for p.current() != '"' && !p.atEnd() {
    +		cur := p.current()
    +		p.advance()
    +		next := p.current()
    +		if cur == '\\' && next == '"' {
    +			s = append(s, '"')
    +			p.advance()
    +		} else {
    +			s = append(s, cur)
    +		}
    +	}
    +	if p.current() != '"' {
    +		return nil, errors.New("unbalanced quotes in request")
    +	}
    +	p.advance()
    +	return
    +}
    +

    From the functions that we've declared up to this point it's pretty clear that our parser + will be reading the input line by line. And the consuming the line one char at a time. +

    The way consumeString() works is quite tricky. + It assumes that the initial " has been consumed before entering the function. + And it consumes all characters in the current line up until it reaches the closing quotes character + or the end of the line. +

    Inside the loop we can see that we're reading the current character and advancing the pointer, then + the next character. + When the user is sending an escaped quote inside the string we detect that by checking the current + and the next characters. + In this special case we end up advancing the pointer twice. Because we consumed two: chars the backslash + and the quote. But we added only one char to the output: ". +

    We append all other characters to the output buffer. +

    When the loop finishes, if we're not pointing to the end quote char, that means that the user + sent an invalid command and we return an error. +

    Otherwise we advance the pointer and return normally. +

    Parsing commands

    // command parses and returns a Command.
    +func (p *Parser) command() (Command, error) {
    +	b, err := p.r.ReadByte()
    +	if err != nil {
    +		return Command{}, err
    +	}
    +	if b == '*' {
    +		log.Println("resp array")
    +		return p.respArray()
    +	} else {
    +		line, err := p.readLine()
    +		if err != nil {
    +			return Command{}, err
    +		}
    +		p.pos = 0
    +		p.line = append([]byte{}, b)
    +		p.line = append(p.line, line...)
    +		return p.inline()
    +	}
    +}
    +

    We read the first character sent by the client. If it's an asterisk we handle it + using the RESP protocol. Otherwise we assume that it's an inline command. +

    Let's start by parsing the inline commands first. +

    // Command implements the behavior of the commands.
    +type Command struct {
    +	args []string
    +	conn net.Conn
    +}
    +
    +// inline parses an inline message and returns a Command. Returns an error when there's
    +// a problem reading from the connection or parsing the command.
    +func (p *Parser) inline() (Command, error) {
    +	// skip initial whitespace if any
    +	for p.current() == ' ' {
    +		p.advance()
    +	}
    +	cmd := Command{conn: p.conn}
    +	for !p.atEnd() {
    +		arg, err := p.consumeArg()
    +		if err != nil {
    +			return cmd, err
    +		}
    +		if arg != "" {
    +			cmd.args = append(cmd.args, arg)
    +		}
    +	}
    +	return cmd, nil
    +}
    +

    This is also quite easy to skim through. We skip any leading whitespace + in case the user sent something like ' GET a'. +

    We create a new Command object with a reference to the session connection. +

    While we're not at the end of the line we consume args and append them to the + arg list of the command object if they are not empty. +

    Consuming arguments

    // consumeArg reads an argument from the current line.
    +func (p *Parser) consumeArg() (s string, err error) {
    +	for p.current() == ' ' {
    +		p.advance()
    +	}
    +	if p.current() == '"' {
    +		p.advance()
    +		buf, err := p.consumeString()
    +		return string(buf), err
    +	}
    +	for !p.atEnd() && p.current() != ' ' && p.current() != '\r' {
    +		s += string(p.current())
    +		p.advance()
    +	}
    +	return
    +}
    +

    Same as before we consume any leading whitespace. +

    If we find a quoted string we call our function from before: consumeString(). +

    We append all characters to the output until we reach a carriage return \r, a whitespace + or the end of the line. +

    Parsing RESP protocol

    // respArray parses a RESP array and returns a Command. Returns an error when there's
    +// a problem reading from the connection.
    +func (p *Parser) respArray() (Command, error) {
    +	cmd := Command{}
    +	elementsStr, err := p.readLine()
    +	if err != nil {
    +		return cmd, err
    +	}
    +	elements, _ := strconv.Atoi(string(elementsStr))
    +	log.Println("Elements", elements)
    +	for i := 0; i < elements; i++ {
    +		tp, err := p.r.ReadByte()
    +		if err != nil {
    +			return cmd, err
    +		}
    +		switch tp {
    +		case ':':
    +			arg, err := p.readLine()
    +			if err != nil {
    +				return cmd, err
    +			}
    +			cmd.args = append(cmd.args, string(arg))
    +		case '$':
    +			arg, err := p.readLine()
    +			if err != nil {
    +				return cmd, err
    +			}
    +			length, _ := strconv.Atoi(string(arg))
    +			text := make([]byte, 0)
    +			for i := 0; len(text) <= length; i++ {
    +				line, err := p.readLine()
    +				if err != nil {
    +					return cmd, err
    +				}
    +				text = append(text, line...)
    +			}
    +			cmd.args = append(cmd.args, string(text[:length]))
    +		case '*':
    +			next, err := p.respArray()
    +			if err != nil {
    +				return cmd, err
    +			}
    +			cmd.args = append(cmd.args, next.args...)
    +		}
    +	}
    +	return cmd, nil
    +}
    +

    As we know, the leading asterisk has already been consumed from the connection input. + So, at this point, the first line contains the number of elements to be consumed. + We read that into an integer. +

    We create a for loop with that will parse all the elements in the array. + We consume the first character to detect which kind of element we need to consume: int, string or array. +

    The int case is quite simple, we just read until the rest of the line. +

    The array case is also quite simple, we call respArray() and append the args of the result, + to the current command object. +

    For strings we read the first line and get the size of the string. + We keep reading lines until we have read the indicated amount of characters. +

    Handling commands

    This is the 'fun' part of the implementation. Were our server becomes alive. + In this section we'll implement the actual functionality of the commands. +

    Let's start with the cmd.handle() function that we saw in handleSession(). +

    // handle Executes the command and writes the response. Returns false when the connection should be closed.
    +func (cmd Command) handle() bool {
    +	switch strings.ToUpper(cmd.args[0]) {
    +	case "GET":
    +		return cmd.get()
    +	case "SET":
    +		return cmd.set()
    +	case "DEL":
    +		return cmd.del()
    +	case "QUIT":
    +		return cmd.quit()
    +	default:
    +		log.Println("Command not supported", cmd.args[0])
    +		cmd.conn.Write([]uint8("-ERR unknown command '" + cmd.args[0] + "'\r\n"))
    +	}
    +	return true
    +}
    +

    Needs no further explanation. Let's implement the easiest command: QUIT. +

    // quit Used in interactive/inline mode, instructs the server to terminate the connection.
    +func (cmd *Command) quit() bool {
    +	if len(cmd.args) != 1 {
    +		cmd.conn.Write([]uint8("-ERR wrong number of arguments for '" + cmd.args[0] + "' command\r\n"))
    +		return true
    +	}
    +	log.Println("Handle QUIT")
    +	cmd.conn.Write([]uint8("+OK\r\n"))
    +	return false
    +}
    +

    If any extra arguments were passed to QUIT, it returns an error. +

    Otherwise write +OK to the client and return false. + Which if you remember handleSession() is the value to indicate that the session has finished. + After that the connection will be automatically closed. +

    The next easieast command is DEL +

    // del Deletes a key from the cache.
    +func (cmd *Command) del() bool {
    +	count := 0
    +	for _, k := range cmd.args[1:] {
    +		if _, ok := cache.LoadAndDelete(k); ok {
    +			count++
    +		}
    +	}
    +	cmd.conn.Write([]uint8(fmt.Sprintf(":%d\r\n", count)))
    +	return true
    +}
    +

    Iterates through all the keys passed, deletes the ones that exists and + writes back to the client the amount of keys deleted. +

    Returns true, which means the connection is kept alive. +

    Handling GET

    // get Fetches a key from the cache if exists.
    +func (cmd Command) get() bool {
    +	if len(cmd.args) != 2 {
    +		cmd.conn.Write([]uint8("-ERR wrong number of arguments for '" + cmd.args[0] + "' command\r\n"))
    +		return true
    +	}
    +	log.Println("Handle GET")
    +	val, _ := cache.Load(cmd.args[1])
    +	if val != nil {
    +		res, _ := val.(string)
    +		if strings.HasPrefix(res, "\"") {
    +			res, _ = strconv.Unquote(res)
    +		}
    +		log.Println("Response length", len(res))
    +		cmd.conn.Write([]uint8(fmt.Sprintf("$%d\r\n", len(res))))
    +		cmd.conn.Write(append([]uint8(res), []uint8("\r\n")...))
    +	} else {
    +		cmd.conn.Write([]uint8("$-1\r\n"))
    +	}
    +	return true
    +}
    +

    As before, we validate that the correct number of arguments were passed to the command. +

    We load the value from the global variable cache. +

    If the value is nil we write back to the client the special $-1. +

    When we have a value we cast it as string and unquote it in case it's quoted. + Then we write the length as the first line of the response and the string as the + second line of the response. +

    Handling SET

    This is the most complicated command that we'll implement. +

    // set Stores a key and value on the cache. Optionally sets expiration on the key.
    +func (cmd Command) set() bool {
    +	if len(cmd.args) < 3 || len(cmd.args) > 6 {
    +		cmd.conn.Write([]uint8("-ERR wrong number of arguments for '" + cmd.args[0] + "' command\r\n"))
    +		return true
    +	}
    +	log.Println("Handle SET")
    +	log.Println("Value length", len(cmd.args[2]))
    +	if len(cmd.args) > 3 {
    +		pos := 3
    +		option := strings.ToUpper(cmd.args[pos])
    +		switch option {
    +		case "NX":
    +			log.Println("Handle NX")
    +			if _, ok := cache.Load(cmd.args[1]); ok {
    +				cmd.conn.Write([]uint8("$-1\r\n"))
    +				return true
    +			}
    +			pos++
    +		case "XX":
    +			log.Println("Handle XX")
    +			if _, ok := cache.Load(cmd.args[1]); !ok {
    +				cmd.conn.Write([]uint8("$-1\r\n"))
    +				return true
    +			}
    +			pos++
    +		}
    +		if len(cmd.args) > pos {
    +			if err := cmd.setExpiration(pos); err != nil {
    +				cmd.conn.Write([]uint8("-ERR " + err.Error() + "\r\n"))
    +				return true
    +			}
    +		}
    +	}
    +	cache.Store(cmd.args[1], cmd.args[2])
    +	cmd.conn.Write([]uint8("+OK\r\n"))
    +	return true
    +}
    +

    As always, first thing we do is validate the number of arguments. + But in this case, SET is more tricky than the others. +

    When more than 3 arguments are passed we check for the NX or XX flags and handle them accordingly. +

    • NX -- Only set the key if it does not already exist.
    • XX -- Only set the key if it already exist.

    Then we parse the expiration flags if any. We'll see how that's done in a second. +

    After handling all those special cases we finally store the key and value in the cache, + write the +OK response and return true to keep the connection alive. +

    Expiration

    // setExpiration Handles expiration when passed as part of the 'set' command.
    +func (cmd Command) setExpiration(pos int) error {
    +	option := strings.ToUpper(cmd.args[pos])
    +	value, _ := strconv.Atoi(cmd.args[pos+1])
    +	var duration time.Duration
    +	switch option {
    +	case "EX":
    +		duration = time.Second * time.Duration(value)
    +	case "PX":
    +		duration = time.Millisecond * time.Duration(value)
    +	default:
    +		return fmt.Errorf("expiration option is not valid")
    +	}
    +	go func() {
    +		log.Printf("Handling '%s', sleeping for %v\n", option, duration)
    +		time.Sleep(duration)
    +		cache.Delete(cmd.args[1])
    +	}()
    +	return nil
    +}
    +

    We read the option and the expiration value, then we compute the duration + for each case and we spawn a new goroutine that sleeps for that amount of + time and the deletes the key from the cache. +

    This is not the most efficient way to do it, but it's simple and it works for us. +

    Working server

    At this point we have an usable implementation of Redis. +

    Let's start the server the server and test it. +

    $ go run main.go
    +2023/04/08 21:09:40 Listening on tcp://0.0.0.0:6380
    +

    On a different terminal connect to the server. +

    $ telnet 127.0.0.1 6380
    +GET a
    +$-1
    +set a "test \"quotes\" are working"
    ++OK
    +get a
    +$25
    +test "quotes" are working
    +

    It's alive!! Go have fun. +

    If you'd like to access the source code of this project there's a public gist + containing all of the code displayed here. +

    Link to source code

    \ No newline at end of file diff --git a/docs/2023/06/02/rewrite-grotsky-rust.html b/docs/2023/06/02/rewrite-grotsky-rust.html new file mode 100644 index 0000000..2074b55 --- /dev/null +++ b/docs/2023/06/02/rewrite-grotsky-rust.html @@ -0,0 +1,75 @@ +Rewrite my toy language interpreter in Rust +

    Blog

    Reading time: +

    Rewrite my toy language interpreter in Rust

    Im rewriting Grotsky (my toy programming language) in Rust, the previous implementation +was done in Go. The goal of the rewrite is to improve my Rust skills, and to improve the performance of Grotsky, +by at least 10x.

    As of this writting the Rust implementation is 4x faster than the Golang implementation.

    I've rewritten the Lexer, Parser and part of the Runtime. Enough for run this code and measure + how long it takes for each implementation to finish:

    let a = 1
    +while a < 100000000 {
    +    a = a + 1
    +}
    +

    I was inspired by Mitchel Hashimoto's post: +My Approach to Building Large Technical Projects. And I want to create a roadmap of little projects to reach my goal of having a full-fledged interpreter in Rust. +

    Goal

    Until now Grotsky has been running on a Tree-based interpreter. My goal is that at the end of the rewrite I + will have a Bytecode interpreter. +

    First I want to rewrite the Tree-based interpreter in Rust and achieve at least 10x performance improvement. +

    Then figure out if I want to use a Register based or Stack based bytecode interpreter. +

    Also, I would like to have a stable bytecode representation to be able to compile programs to a binary format + that can be shipped as is or packaged into a binary. +

    Finally, it's time Grotsky gets a REPL. +

    Roadmap

    Believe it or not, Grotsky it's pretty big. It has support for reading and writting files, sockets and more + on the stdlib. Which means I have a huge task ahead of me. +

    First thing I want to do is to have some sort of automated testing setup that can compare Rust vs Go implementation. + Right now all test are written as Go unit test, I need to make them agnostic of language backend. +

    • Jun 9: Have a complete setup of automated tests for correctness and performance.
    • Jun 16: Language runtime (without stdlib) rewritten in Rust.
    • Jun 23: Finish migrating stdlib and publish results (new blog post). Use new implementation for blog engine.
    • Jun 30: Decide which kind of bytecode interpreter to use, then make a design and plan for the implementation.
    • Jul 7: Have a working bytecode interpreter that is able to run the program shown in this post ^. Just a simple while loop. Compare performance and share progress.
    • Jul 14: Add support for functions and closures.
    • Jul 21: Finish runtime without stdlib in bytecode interpreter.
    • Jul 28: Implement stdlib in bytecode interpreter. Share results.
    • Aug 5: Add ability to compile to bytecode and run from bytecode.
    • Aug 12: Add REPL and finish up project.

    Im gonna have lots of fun with this project and Im sure next time a post here I'll be a changed man. + Surely gonna learn a lot, Im excited about what lies ahead. +

    \ No newline at end of file diff --git a/docs/2023/07/15/the-end-of-a-side-project.html b/docs/2023/07/15/the-end-of-a-side-project.html new file mode 100644 index 0000000..98c1bd6 --- /dev/null +++ b/docs/2023/07/15/the-end-of-a-side-project.html @@ -0,0 +1,97 @@ +The end of a side project +

    Blog

    Reading time: +

    The end of a side project

    Cloud Outdated was a personalized digest of updates for cloud services. It's sad to see it go, + but it was a fun project to work on, learn some new stuff and collab with a friend. + There are some takeaways from this that I'd like to share. +

    Building something simple is hard

    From the beginning we were trying to build a simple product to get notified when new versions + of Python were supported in AWS Lambda. But we figured we could support other things too, like + all Lambda runtimes, then also GCP and Azure, then more services of each one. +Features started piling up pretty quickly. +

    When building stuff I try to think of every edge case, even the most improbable ones, and make + the software infalible. Of course, it's impossible to succeed at that, software will always be fallible. +And this premature optimization ends up making the project more complex than it should be. +

    We planned to work on this for a 1 or 2 months, and it ended up taking 6+ months :-). +

    My takeaway here is: start building the dumbest thing possible that gets the job done, then adjust as needed. +

    Getting users is hard

    We're killing this project because nobody uses it. +And nobody except us has used it since it was launched more than a year ago. +Some people subscribed but never even opened an email. +

    We tried to advertise in our social media and post it in different builders communities. +But that will get you so far if you're not an influencer that has the right audience. +

    We thought that we'll get more traffic from organic search or people telling their friends about this. +But in the end I think nobody really needed something like this that much. +

    My takeaway here is: building something that people really want and getting the product to the hands + of the people that want it is very complicated. You should think very deeply about what problem your + product is solving and how your users will find you. +

    The money

    This has costed like $200 (US dollars) since inception. For two people that's like $100 each. +It's not a lot, but for something that has no users that's quite expensive. +

    We used Lambdas to serve this project. +I feel like we were promised that the cloud and serverless are cheap and easy solution. +But in my opinion it doesn't seem to be the case. +It's definitely not easier nor cheaper than having a PHP website in this case. +Im sure there are other cases in which it makes more sense. +

    This is also a reason of why we're killing the project. +Our service started failing because after a deploy dependencies were updated and the code + was to big to fit on a Lambda. +It would have been a lot of work to fix it, so we decided to kill it and save some bucks every month. +

    For personal projects which you're not sure how they're going to scale, + I think serverless is probably not the right choice. +Serverless make sense if you're going to have huge burst of traffics and don't want + to handle a lot of infra. +

    My takeaway here is: beware of the cloud, if you're just building a small side project + or don't have huge infra needs stick with the cheapest providers (Hostinger, PythonAnywhere, Hetzner) + and avoid cloud providers (GCP, Azure, AWS). +

    Final thougths

    If I haven't made this clear enough, building a successful product is *hard*. +There are many things to think about when starting, and the technical stuff hopefully + is the easy part. +I think this are the 3 most important lessons that I've learned working on this: +

    • Build the dumbest thing that does the job, improve as needed.
    • Think deeply about what problem are you solving and how you're going to deliver the solution to the people that need it.
    • Beware of the cloud, if possible use a cheaper provider. It will save you money and headaches.

    \ No newline at end of file diff --git a/docs/2023/09/23/grotsky-rust-part2.html b/docs/2023/09/23/grotsky-rust-part2.html new file mode 100644 index 0000000..3ca2095 --- /dev/null +++ b/docs/2023/09/23/grotsky-rust-part2.html @@ -0,0 +1,75 @@ +Rewrite my toy language interpreter in Rust, an update +

    Blog

    Reading time: +

    Rewrite my toy language interpreter in Rust, an update

    Im rewriting Grotsky (my toy programming language) in Rust, the previous implementation +was done in Go. The goal of the rewrite is to improve my Rust skills, and to improve the performance of Grotsky, +by at least 10x.

    In a +previous post + I've outlined a plan to migrate Grotsky to a Rust based platform. +

    I was suposed to finish it more than a month ago :'). +

    Of course, I wasn't able to do it. +

    I think my original estimation was actually very flawed, but I also didn't put enough work. +

    I failed my estimation, because I decided to skip the step of first writing a Tree-based interpreter in Rust. + To go directly to a Register-based virtual machine. +

    The reason that is taking me so long is that I travel a lot. I've been a digital nomad for more than a year now. + And when I finish working I prefer to go outside and explore the place where Im staying. + I go to a lot of interesting places: at this time Im in Turkey and heading to Hong Kong, after that Im going to South Korea. + So, it makes sense to actually experience the places where Im staying than to stay inside writing code. +

    Im here to propose a new roadmap that is more achivable with the time I have to work on this. + And the idea is to finish it before the end of the year. +

    Plus, I recently heard some advice that I think it's worth to try: Work on someone for 15 minutes every day. + I do have 15 minutes everyday where Im probably scrolling through social media or just consuming content. + I can make better use of that time by putting it into this project. +

    Updated Roadmap

    • Sep 30: Publish a blogpost about the memory model of the Rust implementation.
    • Oct 15: Migrate automated tests to the new backend and make sure they pass.
    • Oct 30: Implement stdlib in bytecode interpreter. Share results.
    • Nov 15: Add ability to compile to bytecode and run from bytecode.
    • Dec 30: Finish up everything and publish a blogpost update before end of year.

    This is gonna be rough, because of the traveling and being jetlaged all the time. + But I think the roadmap is easy enough so I can do it. +

    Thanks for reading. Please, leave a comment about your experience and struggle with side projects. +

    \ No newline at end of file diff --git a/docs/2023/11/23/grotsky-rust-part3.html b/docs/2023/11/23/grotsky-rust-part3.html new file mode 100644 index 0000000..307c3ae --- /dev/null +++ b/docs/2023/11/23/grotsky-rust-part3.html @@ -0,0 +1,187 @@ +I rewrote my toy language interpreter in Rust +

    Blog

    Reading time: +

    I rewrote my toy language interpreter in Rust

    Im rewriting Grotsky (my toy programming language) in Rust, the previous implementation +was done in Go. The goal of the rewrite is to improve my Rust skills, and to improve the performance of Grotsky, +by at least 10x. This has been a serious of posts, this one is the latest one. Hopefully the best and most insightful +of them all.

    In previous posts: +

    I've outlined a plan to migrate Grotsky to a Rust based platform. +

    + Originally, my plan was very ambitious and I thought I would be able to finish the transition + in like two months. + In reality it took five months :-) + +

    Performance improvement

    I was aiming at a 10x improvement. In reality is not that much. + I ran various benchmarks and get at most a 4x improvement. + Which is not great, but also not that bad given that I know very little + of how to do high performance Rust. + The interpreter is written in the most dumbest and easiest way I managed to do it. +

    Let's look at some numbers for different programs. +

    Loop program

    let start = io.clock()
    +fn test() {
    +    let a = 1
    +    while a < 10000000 {
    +        a = a + 1
    +    }
    +}
    +test()
    +io.println(io.clock() - start)
    +

    The result is: +

    trial #1
    +build/grotsky   best 5.135s  3.877x time of best
    +build/grotsky-rs   best 1.325s  287.6800% faster
    +trial #2
    +build/grotsky   best 5.052s  3.814x time of best
    +build/grotsky-rs   best 1.325s  281.4182% faster
    +trial #3
    +build/grotsky   best 5.035s  3.802x time of best
    +build/grotsky-rs   best 1.325s  280.1663% faster
    +trial #4
    +build/grotsky   best 5.003s  3.777x time of best
    +build/grotsky-rs   best 1.325s  277.6831% faster
    +trial #5
    +build/grotsky   best 5.003s  3.777x time of best
    +build/grotsky-rs   best 1.325s  277.6831% faster
    +

    Recusive fibonacci

    let start = io.clock()
    +fn fib(n) {
    +	if n <= 2 {
    +		return n
    +	}
    +	return fib(n-2) + fib(n-1)
    +}
    +fib(28)
    +io.println(io.clock() - start)
    +

    The result is: +

    trial #1
    +build/grotsky   best 0.8409s  294.5155% faster
    +build/grotsky-rs   best 3.317s  3.945x time of best
    +trial #2
    +build/grotsky   best 0.8168s  271.3829% faster
    +build/grotsky-rs   best 3.033s  3.714x time of best
    +trial #3
    +build/grotsky   best 0.797s  245.6835% faster
    +build/grotsky-rs   best 2.755s  3.457x time of best
    +trial #4
    +build/grotsky   best 0.7784s  249.9964% faster
    +build/grotsky-rs   best 2.724s  3.5x time of best
    +trial #5
    +build/grotsky   best 0.7784s  249.9964% faster
    +build/grotsky-rs   best 2.724s  3.5x time of best
    +

    In this case is like 3.5x slower. This is due to function calls. + Im not very well versed in Rust, so on each call im copying a lot of + data over and over. In the go implementation everything is just pointers + so there's less copying. +

    Compile to bytecode

    With the Rust implementation, generating and compiling to bytecode was added. + Now it's possible to generate a bytecode file to later read it. + This is a way of distributing files without giving away source code and also + a little bit more performant because you skip parsing and compilation phases. +

    How it works: +

    grotsky compile example.gr  # Compile file
    +grotsky example.grc  # Run compiled file
    +

    Memory model

    Grotsky is a reference-counted language. We're using Rust's Rc and RefCell to keep track of values. +

    pub struct MutValue<T>(pub Rc<RefCell<T>>);
    +
    +impl<T> MutValue<T> {
    +    pub fn new(obj: T) -> Self {
    +        MutValue::<T>(Rc::new(RefCell::new(obj)))
    +    }
    +}
    +
    +pub enum Value {
    +    Class(MutValue<ClassValue>),
    +    Object(MutValue<ObjectValue>),
    +    Dict(MutValue<DictValue>),
    +    List(MutValue<ListValue>),
    +    Fn(MutValue<FnValue>),
    +    Native(NativeValue),
    +    Number(NumberValue),
    +    String(StringValue),
    +    Bytes(BytesValue),
    +    Bool(BoolValue),
    +    Slice(SliceValue),
    +    Nil,
    +}
    +
    +pub enum Record {
    +    Val(Value),
    +    Ref(MutValue<Value>),
    +}
    +
    +pub struct VM {
    +    pub activation_records: Vec<Record>,
    +}
    +

    Most of the simple values are just stored as-is: Native (builtin functions), Number, String, + Bytes, Bool, Slice and Nil. +

    For the other complex values we need to use 'pointers' which in this case are MutValue. +

    Then the Grotsky VM uses Records which can be a plain Value or a reference to a Value. + The records are registers, each function has up to 255 registers. + The reference to values are used to store upvalues. + A register is turned into an upvalue when a variable is closed by another function. +

    This implementation ends up being very slow, but easy to manage. Because Rust stdlib + does all the work. +

    Using Rust in this blogpost

    As you may know, this blog is powered by grotsky. + Im happy to say that I successfully migrated from grotsky to grostky-rs as the backend for the blog. + And what you're reading now is generated by the latest implementation of the language using Rust. +

    Even for local development the Rust version is used. Which means Im using a TCP server and an HTTP + implementation written in Grotsky. +

    Closing remarks

    This has been a great learning, Im happy to have finished because it required a lot of effort. + Im not gonna announce any new work on this interpreter but I would like to keep adding stuff. + Improving it further to make it more performant and more usable. +

    In the end I encourage everyone to try it and also start their own project. Is always cool to see + what everyone else is doing. +

    Thanks for reading and please leave a comment. +

    \ No newline at end of file diff --git a/docs/2023/12/25/favourite-advent-of-code-2023.html b/docs/2023/12/25/favourite-advent-of-code-2023.html new file mode 100644 index 0000000..2c759ac --- /dev/null +++ b/docs/2023/12/25/favourite-advent-of-code-2023.html @@ -0,0 +1,412 @@ +Day 20. My favourite problem from Advent of Code 2023 +

    Blog

    Reading time: +

    Day 20 +

    Im gonna briefly describe the problem here, but if you want to see the real thing go check it out https://adventofcode.com/2023/day/20. +

    I like it because it involves some simple electronic devices that are wired together and send pulses/signals to each other. In this problem you have to make sure to correctly propagate the signals and simulate the behaviour of the devices. +

    There are two devices that have a very distinct behaviour: +

  • Flip flops: similar to a T flip-flop electronic device.
  • +
  • Conjunctions: similar to a NAND gate with memory on its inputs.
  • +

    In this problem, Flip flops are initially off and whenever they reiceve a low pulse they toggle between on/off. Each time it toggles state it sends a pulse as an output. When turned off sends a low pulse, when turned on sends a high pulse. +

    Conjunction modules remember the most recent pulse on each input. By default it remembers a low pulse for all inputs. When a pulse is received it updates the memory for that input. Then, if it remembers high pulses for all inputs, it sends a low pulse; otherwise, it sends a high pulse. +

    There is also some "dummy" modules: +

  • Broadcaster: has 1 input and N outputs. It replicates the input in all its outputs.
  • +
  • Button: when pressed sends a low pulse. The button is always connected as the broadcaster input. This is similar to a normally closed switch.
  • +
  • Test module: module that receive and process inputs but has no output.
  • +

    One important thing to have in mind is that modules only send output pulses when they receive a pulse as input. +

    Problem input +

    The example input looks something like this: +
    +broadcaster -> a, b, c
    +%a -> b
    +%b -> c
    +%c -> inv
    +&inv -> a
    +
    +

    There will always be just one Broadcaster module called "broadcaster" that has the Button connected as input. In this case it has module's "a", "b" and "c" connected to its output. +

    The arrow -> indicates what modules are connected to the output of the module to the left. +

    Lines that start with % means the module is a Flip flop, for example: %a -> b indicates that there's a flip flop called "a" whose output is connected to module's "b" input. +

    Lines that start with & means the module is a Conjunction, for example: &inv -> a indicates that there's a conjunction called "inv" whose output is connected to module's "a" input. +

    Let's analyze how this circuit behaves once the button is pushed: +
    +button -0-> broadcaster
    +broadcaster -0-> a
    +broadcaster -0-> b
    +broadcaster -0-> c
    +a -1-> b
    +b -1-> c
    +c -1-> inv
    +inv -0-> a
    +a -0-> b
    +b -0-> c
    +c -0-> inv
    +inv -1-> a
    +
    +In this example 8 low (0) pulses and 4 high (1) pulses are sent. +

    Part 1 +

    To solve the first part we need to calculate the multiplication between high and low pulses sent between devices. +

    In the previous example that would be 8*4=32. +

    But this time we don't push the button only once, but we push it a 1000 times. Each time we push the button we wait until all signals propagate and the circuit settles into a state before pushing the button again. +

    Solution +

    First I started by modelling the devices as objects. Starting with a single base class that has most of the common behaviour. +
    +from abc import ABC
    +measure_pulses = {0: 0, 1: 0}
    +class Module(ABC):
    +    def __init__(self, name: str):
    +        self.name = name
    +        self.outputs = []
    +
    +    def receive_pulse(self, mod: "Module", pulse: int) -> list[tuple["Module", int]]:
    +        measure_pulses[pulse] += 1
    +        print(f"{mod and mod.name} -{pulse}-> {self.name}")
    +        return self.process_pulse(mod, pulse)
    +
    +    def connect_output(self, mod: "Module"):
    +        self.outputs.append(mod)
    +
    +    def propagate_pulse(self, pulse: int):
    +        mods = []
    +        for m in self.outputs:
    +            mods.append((m, pulse))
    +        return mods
    +
    +    def process_pulse(self, mod: "Module", pulse: int):
    +        raise NotImplementedError()
    +
    +    def __str__(self) -> str:
    +        return f"{self.__class__.__name__}(name={self.name})"
    +
    +    def __repr__(self) -> str:
    +        return str(self)
    +
    +

    What we see here is that we expect all modules to have a name and outputs. See __init__(), __str__(), __repr__() and connect_output(). +

    Each module can receive a pulse 0 or 1 from another module. See receive_pulse(). Each time we process a pulse we record it in a global dict called measure_pulses. +

    Also we leave process_pulse() to be defined by each particular module type. +

    We have a method that returns a list of all modules to which signals should be propagated. See propagate_pulse(). +

    Let's start by the easiest module type: +
    +class TestModule(Module):
    +    def process_pulse(self, mod: "Module", pulse: int):
    +        return []
    +
    +Give that it's a dummy module, it doesn't do anything when it receives an input. +
    +class Broadcaster(Module):
    +    def process_pulse(self, mod: "Module", pulse: int):
    +        return super().propagate_pulse(pulse)
    +
    +As expected the Broadcaster always propagates the received input to all its outputs. +
    +class FlipFlop(Module):
    +    def __init__(self, name: str):
    +        super().__init__(name)
    +        self.state = 0
    +
    +    def process_pulse(self, mod: "Module", pulse: int):
    +        if pulse == 0:
    +            self.state = (self.state + 1) % 2
    +            return super().propagate_pulse(self.state)
    +        return []
    +
    +

    The flip flop start initially turned off. See self.state = 0 in __init__(). +

    In process_pulse() we implement the behaviour: +

  • If receives a low pulse, toggles the state and sends a pulse equals to the state to all its outputs.
  • +
  • Otherwise it doesn't do anything.
  • +
    +class Conjunction(Module):
    +    def __init__(self, name: str):
    +        super().__init__(name)
    +        self.memory = {}
    +
    +    def remember_input(self, mod: Module):
    +        self.memory[mod.name] = 0
    +
    +    def process_pulse(self, mod: Module, pulse: int):
    +        self.memory[mod.name] = pulse
    +        if all(self.memory.values()):
    +            return self.propagate_pulse(0)
    +        return self.propagate_pulse(1)
    +
    +

    The conjunction initializes its memory as empty. See __init__(). +

    Each time a module is plugged in as an input it remembers it as OFF (0). See remember_input(). +

    The way it processes pulses is by first recording the pulse for the input in its memory. Then if all inputs are 1s it sends a 0 pulse to all its outputs. +

    Otherwise it sends a 1 pulse to all its outputs. +

    At this point we have all our building blocks for solving this problem. We only need to parse the input and something that pushes the button and makes sure signals are propagated to the end. +

    Parsing modules is straightforward: +
    +def parse_modules(modules: list) -> dict[str, Module]:
    +    modules_by_name = {}
    +    outputs_by_name = {}
    +
    +    # Parse all modules into their correspondig class and store
    +    # them in a dict.
    +    for m in modules:
    +        module_type = m[0]
    +        module_outputs = [o.strip() for o in m[1].split(",") if o.strip()]
    +        if module_type.startswith("broadcaster"):
    +            modules_by_name[module_type] = Broadcaster(module_type)
    +            outputs_by_name[module_type] = module_outputs
    +        elif module_type.startswith("%"):
    +            modules_by_name[module_type[1:]] = FlipFlop(module_type[1:])
    +            outputs_by_name[module_type[1:]] = module_outputs
    +        elif module_type.startswith("&"):
    +            modules_by_name[module_type[1:]] = Conjunction(module_type[1:])
    +            outputs_by_name[module_type[1:]] = module_outputs
    +    # Once all the modules are parsed use connect their outputs.
    +
    +    # If the module doesn't exist at this point is a TestModule.
    +    # If the module is a Conjunction, call remember_input().
    +    for name, outputs in outputs_by_name.items():
    +        for mod_name in outputs:
    +            mod = modules_by_name.get(mod_name, TestModule(mod_name))
    +            modules_by_name[name].connect_output(mod)
    +            if isinstance(mod, Conjunction):
    +                mod.remember_input(modules_by_name[name])
    +
    +    return modules_by_name
    +
    +

    If we parse our example using that function we will receive a dictionary as its output. Keys are module names and values are the objects representing the module. +

    If we parse the example we get something like this: +

    +example = """broadcaster -> a, b, c
    +%a -> b
    +%b -> c
    +%c -> inv
    +&inv -> a"""
    +example_modules = [m.split(" -> ") for m in example.splitlines() if m.strip()]
    +print(parse_modules(example_modules))
    +
    +# Output
    +{
    +    'broadcaster': Broadcaster(name=broadcaster),
    +    'a': FlipFlop(name=a),
    +    'b': FlipFlop(name=b),
    +    'c': FlipFlop(name=c),
    +    'inv': Conjunction(name=inv)
    +}
    +
    +Then we need a function that pushes the button and makes sure all signals are propagated: +
    +def push_button(modules_by_name: dict[str, Module]):
    +    broad = modules_by_name["broadcaster"]
    +    queue = [(broad, broad.receive_pulse(None, 0))]
    +    while queue:
    +        current, signals = queue.pop(0)
    +        for mod, pulse in signals:
    +            queue.append((mod, mod.receive_pulse(current, pulse)))
    +
    +

    Here, we lookup the broadcaster module by name. And send a pulse (note that we pass None as the module because we didn't implement a button class) to the broadcaster. +

    We store the current module (broadcaster) along with all the propagated signals (return value from receive_pulse()) in a queue to be processed. +

    While the signal queue to be processed is not empty we do the following: +

  • Extract the first element of the queue.
  • +
  • Go trough all the signals that this element is sending.
  • +
  • Send the pulses to each corresponding module and store them in the queue to be processed.
  • +

    This process will stop when all responses from receive_pulse() are empty and there are no more signals added to the queue. +

    If we run this for our example: +

    +example_modules = parse_modules(example_modules)
    +push_button(example_modules)
    +
    +# Output
    +None -0-> broadcaster
    +broadcaster -0-> a
    +broadcaster -0-> b
    +broadcaster -0-> c
    +a -1-> b
    +b -1-> c
    +c -1-> inv
    +inv -0-> a
    +a -0-> b
    +b -0-> c
    +c -0-> inv
    +inv -1-> a
    +
    +

    It looks the same as when we analyzed the example above!! +

    We're ready for processing our problems input. (Remeber to comment out the print statement inside receive_pulse()). +

    +modules = open("input.txt", "r").read().strip()
    +modules = [m.split(" -> ") for m in modules.splitlines() if m.strip()]
    +modules = parse_modules(modules)
    +
    +for _ in range(1000):
    +    push_button(modules)
    +
    +print("result:", measure_pulses[0] * measure_pulses[1])
    +
    +# Output
    +result: x
    +
    +Based on your problem input x will be the solution. +

    Part 2 +

    This part as always is much trickier than the first part. It doesn't involve much code changes, just figuring out a way of avoiding large computations. +

    For this part, the problem tells us that there's a module called rx. And we need to find out the lowest amount of button pulses that will make the rx module receive a low pulse. +

    As I have learned troughout this entire challenge, just nahively letting it run and see when the rx module gets a low signal will get me nowhere. It will run forever. +

    So, taking a look at the input and see what the rx module is actually connected to might provide some guidance. +

    Following is for my case (I don't know if all problem inputs are the same). Looking up "rx" in the input I find a single line: +
    +...
    +&lv -> rx
    +...
    +
    +

    That means rx is a TestModule (a dummy module that has nothing connected to its output). And that has only one input: a Conjunction called lv. +

    Ok, that feels like progress. Let's see what lv is connected to: +
    +...
    +&st -> lv
    +&tn -> lv
    +&hh -> lv
    +&dt -> lv
    +...
    +
    +

    Other 4 Conjunctions are connected as inputs of lv. That's interesting. Because lv is a Conjuction it means that to send the low pulse required by rx it should receive all high pulses from its inputs. +

    The solution from here is kind of intuitive at this point. If we figure out how many button pulses does it take for each of the input devices to send a 1 signal we can multiply them together and get the result. +

    I'll explain better. Let's say st sends a 1 on every button push, tn sends a 1 every second button push (this means you have to press the button twice to get tn to send a 1 as an output), hh sends a 1 every fourth button push and dt sends a 1 every eighth button push. +

    So it looks like this: +
    +module | pushes
    +---------------
    +  st   |   1
    +  tn   |   2
    +  hh   |   4
    +  dt   |   8
    +
    +

    In this example, if we push the button 8 times. They are all gonna send a high pulse. Because 8 is divisible by 1, 2, 4 and 8. +

    If the table were different: +
    +module | pushes
    +---------------
    +  st   |   1
    +  tn   |   3
    +  hh   |   5
    +  dt   |   7
    +
    +

    In this case there's no obvious number of times we should push the button. But if we multiply the numbers together we get a number that is divisible by every number in the table. Pushing the button 1 * 3 * 5 * 7 = 105 times will make all the outputs send a 1, and consequently rx will receive a 0. +

    Solution +

    What we need to do then is to figure after out how many button presses we get a 1 on each of those modules. +
    +from collections import defaultdict
    +
    +# Store number of button presses in a global variable
    +ITERATIONS = 0
    +# Store high pulses for target modules
    +OUT_PULSES = defaultdict(list)
    +
    +class Conjunction(Module):
    +    def __init__(self, name: str):
    +        super().__init__(name)
    +        self.memory = {}
    +
    +    def remember_input(self, mod: Module):
    +        self.memory[mod.name] = 0
    +
    +    def start_recording(self):
    +        self.recording = True
    +
    +    def record(self):
    +        if hasattr(self, "recording"):
    +            OUT_PULSES[self.name].append(ITERATIONS)
    +
    +    def process_pulse(self, mod: Module, pulse: int):
    +        self.memory[mod.name] = pulse
    +        if all(self.memory.values()):
    +            return self.propagate_pulse(0)
    +        self.record()
    +        return self.propagate_pulse(1)
    +
    +

    We introduced 2 new methods to the conjunction module: start_recording() and record(). The first just initializes a bool attribute. And the second makes sure to only record high pulses for objects that have been initialized (method start_recording() called). +

    Also introduced 2 global variables: ITERATIONS to keep track of button pushes and OUT_SIGNALS to track each time one of the modules outputs a high pulse. +

    Now we need to make those specific modules record their outputs: +
    +# Get the "lv" module by name
    +lv = modules["lv"]
    +lv_inputs = [modules[k] for k in lv.memory.keys()]
    +for m in lv_inputs:
    +    m.start_recording()
    +
    +I wasn't sure if the cycle was always going to be the same, so just to be sure I did 100_000 button pushes and recorded all the "1" outputs for those modules. +
    +for i in range(100_000):
    +    ITERATIONS += 1
    +    push_button(modules)
    +print(OUT_PULSES)
    +
    +# Output
    +{'hh': [3769, 7538, 11307, 15076, 18845, 22614, 26383, 30152, 33921, 37690, 41459, 45228, 48997, 52766, 56535, 60304, 64073, 67842, 71611, 75380, 79149, 82918, 86687, 90456, 94225, 97994], 'tn': [3863, 7726, 11589, 15452, 19315, 23178, 27041, 30904, 34767, 38630, 42493, 46356, 50219, 54082, 57945, 61808, 65671, 69534, 73397, 77260, 81123, 84986, 88849, 92712, 96575], 'st': [3929, 7858, 11787, 15716, 19645, 23574, 27503, 31432, 35361, 39290, 43219, 47148, 51077, 55006, 58935, 62864, 66793, 70722, 74651, 78580, 82509, 86438, 90367, 94296, 98225], 'dt': [4079, 8158, 12237, 16316, 20395, 24474, 28553, 32632, 36711, 40790, 44869, 48948, 53027, 57106, 61185, 65264, 69343, 73422, 77501, 81580, 85659, 89738, 93817, 97896]}
    +
    +We can observe that for each module we have a periodicity given by: +
    +hh = n*3769
    +tn = n*3863
    +st = n*3929
    +dt = n*4079
    +
    +

    This means we can just multiply the first element of each list for each module and we'll get our result. +

    In my case it was: +
    +accum = 1
    +for name, pulses in OUT_PULSES.items():
    +    accum *= pulses[0]
    +print("result:", accum)
    +
    +# Output
    +result: 233338595643977
    +
    +

    Edit: +As some people have pointed out in the +HN discussion, just multiplying the numbers together only works because the numbers are +coprimes + and the correct solution is to use +LCM. +

    Closing words +

    This problem is my favourite because it has a few characteristics that I personally enjoy: +

  • It's based on real world stuff. In this case electronic devices (which is also a plus because they're fun).
  • +
  • It can be easily translated to an OOP approach which makes it easy to implement and understand.
  • +
  • To solve the second part you need to look at the data and make a solution for your particular input.
  • +
  • It doesn't involve any Graph traversal or specific Math, Calculus or Algebra knowledge. Or any obscure CS algorithm.
  • +

    In the end this is one of my favourites because to solve it you just have to understand the problem and understand the data. +

    Link to my github project with the solutions https://github.com/mliezun/aoc. +

    \ No newline at end of file diff --git a/docs/2024/01/04/new-markdown-generator.html b/docs/2024/01/04/new-markdown-generator.html new file mode 100644 index 0000000..191b86a --- /dev/null +++ b/docs/2024/01/04/new-markdown-generator.html @@ -0,0 +1,97 @@ +Generating posts using markdown +

    Blog

    Reading time: +

    +

    +

    Generating posts using markdown +

    +

    This is pretty standard for Github pages. But in this case, the parser has been written by me. It takes some subset of markdown and compiles it to HTML. +

    Only what you see in this post is what's supported. +

    Code blocks +

    +

    Standard code block: +

    Hello, this is a code block!
    +

    +

    Syntax highlighting: +

    from functools import reduce, partial
    +import operator
    +
    +mul = partial(reduce, operator.mul)
    +print("Factorial of 5:", mul(range(1, 6)))
    +

    +

    Unordered list +

    +

    • This +
    • is an +
    • unordered list +

    Bolds, Italics and Inline code +

    +

    Some text can be bolded, while some other can be in Italics. +

    But the best is to have print("inline code"). +

    Links and images +

    +

    Link to the blog source code where you can see how the parser works (tldr: is awful). +

    Picture of NYC: +

    Picture of NYC +

    HEADINGS +

    +

    There's a thing you haven't noticed so far. There's support for different kinds of headings. You can see them in increasing order here: +

    Microscopic +

    +

    Small +

    +

    Good +

    +

    Pretty good +

    +

    Big +

    +

    Good bye! +

    +

    Thanks for checking out the blog. I've done this to reduce the complexity of creating new blogposts. It was a headache before. +

    Hopefully now I'll write more. Stay tuned. +

    +

    \ No newline at end of file diff --git a/docs/assets/css/style.css b/docs/assets/css/style.css new file mode 100644 index 0000000..8a2ebea --- /dev/null +++ b/docs/assets/css/style.css @@ -0,0 +1,114 @@ +body { + font-family: "Ibarra Real Nova", serif; + font-size: 14pt; + color: #1d1d1d; + background-color: #eeeedd; +} + +.body-wrapper { + display: flex; + justify-content: center; +} + +.body-content { + padding: 20px; + width: 45%; +} + +@media (max-width: 1024px) { + .body-content { + width: 85%; + } +} + +@media (min-width: 1025px) and (max-width: 1440px) { + .body-content { + width: 65%; + } +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: #000000; +} + +h4 { + font-size: 1.07em; +} + +h5 { + font-size: 0.95em; +} + +article a { + font-size: 14.25pt; +} + + +a:link { + color: #000000; + font-weight: bold; +} + +a:visited { + color: #000000; + text-decoration: none; +} + +.triple-quote { + font-family: "PT Mono", monospace !important; + font-size: 13pt; + color: #444; + background-color: #002f5617; + padding: 30px; + border-radius: 6px; + overflow: auto; +} + +.single-quote { + font-family: "PT Mono", monospace !important; + font-size: 11pt; + overflow-wrap: break-word; + color: #444; + background-color: #002f5617; + /* padding: 2px; */ + border-radius: 6px; + padding-left: 6px; + padding-right: 6px; + padding-top: 2px; + padding-bottom: 2px; +} + +footer { + margin-top: 30px; +} + +.footer-wrapper { + display: flex; + justify-content: space-between; +} + +.footer-socials a { + padding: 5px; +} + +.aboutme { + font-size: 16px; + line-height: 24px; + border: 2px solid rgba(0, 0, 0, 0.2); + border-radius: 4px; + padding: 10px 20px; + width: 75%; + margin-left: auto; + margin-right: auto; + margin-top: 5vh; + margin-bottom: 2.5em; +} + +li { + margin-bottom: 10px; +} \ No newline at end of file diff --git a/docs/assets/images/branchable-mysql/diagram1.png b/docs/assets/images/branchable-mysql/diagram1.png new file mode 100644 index 0000000..97f068c Binary files /dev/null and b/docs/assets/images/branchable-mysql/diagram1.png differ diff --git a/docs/assets/images/branchable-mysql/diagram2.png b/docs/assets/images/branchable-mysql/diagram2.png new file mode 100644 index 0000000..efb6316 Binary files /dev/null and b/docs/assets/images/branchable-mysql/diagram2.png differ diff --git a/docs/assets/images/favicon.ico b/docs/assets/images/favicon.ico new file mode 100644 index 0000000..f1e1326 Binary files /dev/null and b/docs/assets/images/favicon.ico differ diff --git a/docs/assets/images/grotsky-part2/AST.png b/docs/assets/images/grotsky-part2/AST.png new file mode 100644 index 0000000..ab5261b Binary files /dev/null and b/docs/assets/images/grotsky-part2/AST.png differ diff --git a/docs/assets/images/kandinsky.jpeg b/docs/assets/images/kandinsky.jpeg new file mode 100644 index 0000000..6356a6a Binary files /dev/null and b/docs/assets/images/kandinsky.jpeg differ diff --git a/docs/assets/images/nyc.jpg b/docs/assets/images/nyc.jpg new file mode 100644 index 0000000..62e9a7b Binary files /dev/null and b/docs/assets/images/nyc.jpg differ diff --git a/docs/assets/images/socials/github.png b/docs/assets/images/socials/github.png new file mode 100644 index 0000000..e9d8e37 Binary files /dev/null and b/docs/assets/images/socials/github.png differ diff --git a/docs/assets/images/socials/linkedin.png b/docs/assets/images/socials/linkedin.png new file mode 100644 index 0000000..677eaf7 Binary files /dev/null and b/docs/assets/images/socials/linkedin.png differ diff --git a/docs/assets/images/socials/twitter.png b/docs/assets/images/socials/twitter.png new file mode 100644 index 0000000..419715e Binary files /dev/null and b/docs/assets/images/socials/twitter.png differ diff --git a/docs/assets/images/socials/x.ico b/docs/assets/images/socials/x.ico new file mode 100644 index 0000000..717824c Binary files /dev/null and b/docs/assets/images/socials/x.ico differ diff --git a/docs/assets/mlisp/mlisp.js b/docs/assets/mlisp/mlisp.js new file mode 100644 index 0000000..8b90ca0 --- /dev/null +++ b/docs/assets/mlisp/mlisp.js @@ -0,0 +1 @@ +var Module=typeof Module!=="undefined"?Module:{};var moduleOverrides={};var key;for(key in Module){if(Module.hasOwnProperty(key)){moduleOverrides[key]=Module[key]}}var arguments_=[];var thisProgram="./this.program";var quit_=function(status,toThrow){throw toThrow};var ENVIRONMENT_IS_WEB=false;var ENVIRONMENT_IS_WORKER=false;var ENVIRONMENT_IS_NODE=false;var ENVIRONMENT_IS_SHELL=false;ENVIRONMENT_IS_WEB=typeof window==="object";ENVIRONMENT_IS_WORKER=typeof importScripts==="function";ENVIRONMENT_IS_NODE=typeof process==="object"&&typeof process.versions==="object"&&typeof process.versions.node==="string";ENVIRONMENT_IS_SHELL=!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_NODE&&!ENVIRONMENT_IS_WORKER;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var read_,readAsync,readBinary,setWindowTitle;var nodeFS;var nodePath;if(ENVIRONMENT_IS_NODE){if(ENVIRONMENT_IS_WORKER){scriptDirectory=require("path").dirname(scriptDirectory)+"/"}else{scriptDirectory=__dirname+"/"}read_=function shell_read(filename,binary){if(!nodeFS)nodeFS=require("fs");if(!nodePath)nodePath=require("path");filename=nodePath["normalize"](filename);return nodeFS["readFileSync"](filename,binary?null:"utf8")};readBinary=function readBinary(filename){var ret=read_(filename,true);if(!ret.buffer){ret=new Uint8Array(ret)}assert(ret.buffer);return ret};if(process["argv"].length>1){thisProgram=process["argv"][1].replace(/\\/g,"/")}arguments_=process["argv"].slice(2);if(typeof module!=="undefined"){module["exports"]=Module}process["on"]("uncaughtException",function(ex){if(!(ex instanceof ExitStatus)){throw ex}});process["on"]("unhandledRejection",abort);quit_=function(status){process["exit"](status)};Module["inspect"]=function(){return"[Emscripten Module object]"}}else if(ENVIRONMENT_IS_SHELL){if(typeof read!="undefined"){read_=function shell_read(f){return read(f)}}readBinary=function readBinary(f){var data;if(typeof readbuffer==="function"){return new Uint8Array(readbuffer(f))}data=read(f,"binary");assert(typeof data==="object");return data};if(typeof scriptArgs!="undefined"){arguments_=scriptArgs}else if(typeof arguments!="undefined"){arguments_=arguments}if(typeof quit==="function"){quit_=function(status){quit(status)}}if(typeof print!=="undefined"){if(typeof console==="undefined")console={};console.log=print;console.warn=console.error=typeof printErr!=="undefined"?printErr:print}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!=="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(scriptDirectory.indexOf("blob:")!==0){scriptDirectory=scriptDirectory.substr(0,scriptDirectory.lastIndexOf("/")+1)}else{scriptDirectory=""}{read_=function(url){var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.send(null);return xhr.responseText};if(ENVIRONMENT_IS_WORKER){readBinary=function(url){var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=function(url,onload,onerror){var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=function(){if(xhr.status==200||xhr.status==0&&xhr.response){onload(xhr.response);return}onerror()};xhr.onerror=onerror;xhr.send(null)}}setWindowTitle=function(title){document.title=title}}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.warn.bind(console);for(key in moduleOverrides){if(moduleOverrides.hasOwnProperty(key)){Module[key]=moduleOverrides[key]}}moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["quit"])quit_=Module["quit"];var STACK_ALIGN=16;function alignMemory(size,factor){if(!factor)factor=STACK_ALIGN;return Math.ceil(size/factor)*factor}var wasmBinary;if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];var noExitRuntime=Module["noExitRuntime"]||true;if(typeof WebAssembly!=="object"){abort("no native wasm support detected")}var wasmMemory;var ABORT=false;var EXITSTATUS;function assert(condition,text){if(!condition){abort("Assertion failed: "+text)}}function getCFunc(ident){var func=Module["_"+ident];assert(func,"Cannot call unknown function "+ident+", make sure it is exported");return func}function ccall(ident,returnType,argTypes,args,opts){var toC={"string":function(str){var ret=0;if(str!==null&&str!==undefined&&str!==0){var len=(str.length<<2)+1;ret=stackAlloc(len);stringToUTF8(str,ret,len)}return ret},"array":function(arr){var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string")return UTF8ToString(ret);if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i=endIdx))++endPtr;if(endPtr-idx>16&&heap.subarray&&UTF8Decoder){return UTF8Decoder.decode(heap.subarray(idx,endPtr))}else{var str="";while(idx>10,56320|ch&1023)}}}return str}function UTF8ToString(ptr,maxBytesToRead){return ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):""}function stringToUTF8Array(str,heap,outIdx,maxBytesToWrite){if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx}function stringToUTF8(str,outPtr,maxBytesToWrite){return stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite)}function lengthBytesUTF8(str){var len=0;for(var i=0;i=55296&&u<=57343)u=65536+((u&1023)<<10)|str.charCodeAt(++i)&1023;if(u<=127)++len;else if(u<=2047)len+=2;else if(u<=65535)len+=3;else len+=4}return len}function writeArrayToMemory(array,buffer){HEAP8.set(array,buffer)}var buffer,HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateGlobalBufferAndViews(buf){buffer=buf;Module["HEAP8"]=HEAP8=new Int8Array(buf);Module["HEAP16"]=HEAP16=new Int16Array(buf);Module["HEAP32"]=HEAP32=new Int32Array(buf);Module["HEAPU8"]=HEAPU8=new Uint8Array(buf);Module["HEAPU16"]=HEAPU16=new Uint16Array(buf);Module["HEAPU32"]=HEAPU32=new Uint32Array(buf);Module["HEAPF32"]=HEAPF32=new Float32Array(buf);Module["HEAPF64"]=HEAPF64=new Float64Array(buf)}var INITIAL_MEMORY=Module["INITIAL_MEMORY"]||16777216;var wasmTable;var __ATPRERUN__=[];var __ATINIT__=[];var __ATMAIN__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!FS.init.initialized)FS.init();TTY.init();callRuntimeCallbacks(__ATINIT__)}function preMain(){FS.ignorePermissions=false;callRuntimeCallbacks(__ATMAIN__)}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function getUniqueRunDependency(id){return id}function addRunDependency(id){runDependencies++;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}}function removeRunDependency(id){runDependencies--;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}Module["preloadedImages"]={};Module["preloadedAudios"]={};function abort(what){if(Module["onAbort"]){Module["onAbort"](what)}what+="";err(what);ABORT=true;EXITSTATUS=1;what="abort("+what+"). Build with -s ASSERTIONS=1 for more info.";var e=new WebAssembly.RuntimeError(what);throw e}function hasPrefix(str,prefix){return String.prototype.startsWith?str.startsWith(prefix):str.indexOf(prefix)===0}var dataURIPrefix="data:application/octet-stream;base64,";function isDataURI(filename){return hasPrefix(filename,dataURIPrefix)}var fileURIPrefix="file://";function isFileURI(filename){return hasPrefix(filename,fileURIPrefix)}var wasmBinaryFile="mlisp.wasm";if(!isDataURI(wasmBinaryFile)){wasmBinaryFile=locateFile(wasmBinaryFile)}function getBinary(file){try{if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}else{throw"both async and sync fetching of the wasm failed"}}catch(err){abort(err)}}function getBinaryPromise(){if(!wasmBinary&&(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER)){if(typeof fetch==="function"&&!isFileURI(wasmBinaryFile)){return fetch(wasmBinaryFile,{credentials:"same-origin"}).then(function(response){if(!response["ok"]){throw"failed to load wasm binary file at '"+wasmBinaryFile+"'"}return response["arrayBuffer"]()}).catch(function(){return getBinary(wasmBinaryFile)})}else{if(readAsync){return new Promise(function(resolve,reject){readAsync(wasmBinaryFile,function(response){resolve(new Uint8Array(response))},reject)})}}}return Promise.resolve().then(function(){return getBinary(wasmBinaryFile)})}function createWasm(){var info={"a":asmLibraryArg};function receiveInstance(instance,module){var exports=instance.exports;Module["asm"]=exports;wasmMemory=Module["asm"]["j"];updateGlobalBufferAndViews(wasmMemory.buffer);wasmTable=Module["asm"]["l"];addOnInit(Module["asm"]["k"]);removeRunDependency("wasm-instantiate")}addRunDependency("wasm-instantiate");function receiveInstantiatedSource(output){receiveInstance(output["instance"])}function instantiateArrayBuffer(receiver){return getBinaryPromise().then(function(binary){var result=WebAssembly.instantiate(binary,info);return result}).then(receiver,function(reason){err("failed to asynchronously prepare wasm: "+reason);abort(reason)})}function instantiateAsync(){if(!wasmBinary&&typeof WebAssembly.instantiateStreaming==="function"&&!isDataURI(wasmBinaryFile)&&!isFileURI(wasmBinaryFile)&&typeof fetch==="function"){return fetch(wasmBinaryFile,{credentials:"same-origin"}).then(function(response){var result=WebAssembly.instantiateStreaming(response,info);return result.then(receiveInstantiatedSource,function(reason){err("wasm streaming compile failed: "+reason);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(receiveInstantiatedSource)})})}else{return instantiateArrayBuffer(receiveInstantiatedSource)}}if(Module["instantiateWasm"]){try{var exports=Module["instantiateWasm"](info,receiveInstance);return exports}catch(e){err("Module.instantiateWasm callback failed with error: "+e);return false}}instantiateAsync();return{}}var tempDouble;var tempI64;function callRuntimeCallbacks(callbacks){while(callbacks.length>0){var callback=callbacks.shift();if(typeof callback=="function"){callback(Module);continue}var func=callback.func;if(typeof func==="number"){if(callback.arg===undefined){wasmTable.get(func)()}else{wasmTable.get(func)(callback.arg)}}else{func(callback.arg===undefined?null:callback.arg)}}}function setErrNo(value){HEAP32[___errno_location()>>2]=value;return value}var PATH={splitPath:function(filename){var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:function(parts,allowAboveRoot){var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:function(path){var isAbsolute=path.charAt(0)==="/",trailingSlash=path.substr(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(function(p){return!!p}),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:function(path){var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.substr(0,dir.length-1)}return root+dir},basename:function(path){if(path==="/")return"/";path=PATH.normalize(path);path=path.replace(/\/$/,"");var lastSlash=path.lastIndexOf("/");if(lastSlash===-1)return path;return path.substr(lastSlash+1)},extname:function(path){return PATH.splitPath(path)[3]},join:function(){var paths=Array.prototype.slice.call(arguments,0);return PATH.normalize(paths.join("/"))},join2:function(l,r){return PATH.normalize(l+"/"+r)}};function getRandomDevice(){if(typeof crypto==="object"&&typeof crypto["getRandomValues"]==="function"){var randomBuffer=new Uint8Array(1);return function(){crypto.getRandomValues(randomBuffer);return randomBuffer[0]}}else if(ENVIRONMENT_IS_NODE){try{var crypto_module=require("crypto");return function(){return crypto_module["randomBytes"](1)[0]}}catch(e){}}return function(){abort("randomDevice")}}var PATH_FS={resolve:function(){var resolvedPath="",resolvedAbsolute=false;for(var i=arguments.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?arguments[i]:FS.cwd();if(typeof path!=="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=path.charAt(0)==="/"}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(function(p){return!!p}),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:function(from,to){from=PATH_FS.resolve(from).substr(1);to=PATH_FS.resolve(to).substr(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i0){result=buf.slice(0,bytesRead).toString("utf-8")}else{result=null}}else if(typeof window!="undefined"&&typeof window.prompt=="function"){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else if(typeof readline=="function"){result=readline();if(result!==null){result+="\n"}}if(!result){return null}tty.input=intArrayFromString(result,true)}return tty.input.shift()},put_char:function(tty,val){if(val===null||val===10){out(UTF8ArrayToString(tty.output,0));tty.output=[]}else{if(val!=0)tty.output.push(val)}},flush:function(tty){if(tty.output&&tty.output.length>0){out(UTF8ArrayToString(tty.output,0));tty.output=[]}}},default_tty1_ops:{put_char:function(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output,0));tty.output=[]}else{if(val!=0)tty.output.push(val)}},flush:function(tty){if(tty.output&&tty.output.length>0){err(UTF8ArrayToString(tty.output,0));tty.output=[]}}}};function mmapAlloc(size){var alignedSize=alignMemory(size,16384);var ptr=abort();while(size=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage:function(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr:function(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.timestamp);attr.mtime=new Date(node.timestamp);attr.ctime=new Date(node.timestamp);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr:function(node,attr){if(attr.mode!==undefined){node.mode=attr.mode}if(attr.timestamp!==undefined){node.timestamp=attr.timestamp}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup:function(parent,name){throw FS.genericErrors[44]},mknod:function(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename:function(old_node,new_dir,new_name){if(FS.isDir(old_node.mode)){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}}delete old_node.parent.contents[old_node.name];old_node.parent.timestamp=Date.now();old_node.name=new_name;new_dir.contents[new_name]=old_node;new_dir.timestamp=old_node.parent.timestamp;old_node.parent=new_dir},unlink:function(parent,name){delete parent.contents[name];parent.timestamp=Date.now()},rmdir:function(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.timestamp=Date.now()},readdir:function(node){var entries=[".",".."];for(var key in node.contents){if(!node.contents.hasOwnProperty(key)){continue}entries.push(key)}return entries},symlink:function(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink:function(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read:function(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length8){throw new FS.ErrnoError(32)}var parts=PATH.normalizeArray(path.split("/").filter(function(p){return!!p}),false);var current=FS.root;var current_path="/";for(var i=0;i40){throw new FS.ErrnoError(32)}}}}return{path:current_path,node:current}},getPath:function(node){var path;while(true){if(FS.isRoot(node)){var mount=node.mount.mountpoint;if(!path)return mount;return mount[mount.length-1]!=="/"?mount+"/"+path:mount+path}path=path?node.name+"/"+path:node.name;node=node.parent}},hashName:function(parentid,name){var hash=0;for(var i=0;i>>0)%FS.nameTable.length},hashAddNode:function(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode:function(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode:function(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode,parent)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode:function(parent,name,mode,rdev){var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode:function(node){FS.hashRemoveNode(node)},isRoot:function(node){return node===node.parent},isMountpoint:function(node){return!!node.mounted},isFile:function(mode){return(mode&61440)===32768},isDir:function(mode){return(mode&61440)===16384},isLink:function(mode){return(mode&61440)===40960},isChrdev:function(mode){return(mode&61440)===8192},isBlkdev:function(mode){return(mode&61440)===24576},isFIFO:function(mode){return(mode&61440)===4096},isSocket:function(mode){return(mode&49152)===49152},flagModes:{"r":0,"r+":2,"w":577,"w+":578,"a":1089,"a+":1090},modeStringToFlags:function(str){var flags=FS.flagModes[str];if(typeof flags==="undefined"){throw new Error("Unknown file open mode: "+str)}return flags},flagsToPermissionString:function(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions:function(node,perms){if(FS.ignorePermissions){return 0}if(perms.indexOf("r")!==-1&&!(node.mode&292)){return 2}else if(perms.indexOf("w")!==-1&&!(node.mode&146)){return 2}else if(perms.indexOf("x")!==-1&&!(node.mode&73)){return 2}return 0},mayLookup:function(dir){var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate:function(dir,name){try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete:function(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen:function(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&512){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},MAX_OPEN_FDS:4096,nextfd:function(fd_start,fd_end){fd_start=fd_start||0;fd_end=fd_end||FS.MAX_OPEN_FDS;for(var fd=fd_start;fd<=fd_end;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStream:function(fd){return FS.streams[fd]},createStream:function(stream,fd_start,fd_end){if(!FS.FSStream){FS.FSStream=function(){};FS.FSStream.prototype={object:{get:function(){return this.node},set:function(val){this.node=val}},isRead:{get:function(){return(this.flags&2097155)!==1}},isWrite:{get:function(){return(this.flags&2097155)!==0}},isAppend:{get:function(){return this.flags&1024}}}}var newStream=new FS.FSStream;for(var p in stream){newStream[p]=stream[p]}stream=newStream;var fd=FS.nextfd(fd_start,fd_end);stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream:function(fd){FS.streams[fd]=null},chrdev_stream_ops:{open:function(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;if(stream.stream_ops.open){stream.stream_ops.open(stream)}},llseek:function(){throw new FS.ErrnoError(70)}},major:function(dev){return dev>>8},minor:function(dev){return dev&255},makedev:function(ma,mi){return ma<<8|mi},registerDevice:function(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:function(dev){return FS.devices[dev]},getMounts:function(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push.apply(check,m.mounts)}return mounts},syncfs:function(populate,callback){if(typeof populate==="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err("warning: "+FS.syncFSRequests+" FS.syncfs operations in flight at once, probably just doing extra work")}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}mounts.forEach(function(mount){if(!mount.type.syncfs){return done(null)}mount.type.syncfs(mount,populate,done)})},mount:function(type,opts,mountpoint){var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type:type,opts:opts,mountpoint:mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount:function(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);Object.keys(FS.nameTable).forEach(function(hash){var current=FS.nameTable[hash];while(current){var next=current.name_next;if(mounts.indexOf(current.mount)!==-1){FS.destroyNode(current)}current=next}});node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup:function(parent,name){return parent.node_ops.lookup(parent,name)},mknod:function(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name||name==="."||name===".."){throw new FS.ErrnoError(28)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},create:function(path,mode){mode=mode!==undefined?mode:438;mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir:function(path,mode){mode=mode!==undefined?mode:511;mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree:function(path,mode){var dirs=path.split("/");var d="";for(var i=0;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]};LazyUint8Array.prototype.setDataGetter=function LazyUint8Array_setDataGetter(getter){this.getter=getter};LazyUint8Array.prototype.cacheLength=function LazyUint8Array_cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=function(from,to){if(from>to)throw new Error("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)throw new Error("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);if(typeof Uint8Array!="undefined")xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}else{return intArrayFromString(xhr.responseText||"",true)}};var lazyArray=this;lazyArray.setDataGetter(function(chunkNum){var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]==="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]==="undefined")throw new Error("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true};if(typeof XMLHttpRequest!=="undefined"){if(!ENVIRONMENT_IS_WORKER)throw"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc";var lazyArray=new LazyUint8Array;Object.defineProperties(lazyArray,{length:{get:function(){if(!this.lengthKnown){this.cacheLength()}return this._length}},chunkSize:{get:function(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}});var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url:url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};var keys=Object.keys(node.stream_ops);keys.forEach(function(key){var fn=node.stream_ops[key];stream_ops[key]=function forceLoadLazyFile(){FS.forceLoadFile(node);return fn.apply(null,arguments)}});stream_ops.read=function stream_ops_read(stream,buffer,offset,length,position){FS.forceLoadFile(node);var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i>2]=stat.dev;HEAP32[buf+4>>2]=0;HEAP32[buf+8>>2]=stat.ino;HEAP32[buf+12>>2]=stat.mode;HEAP32[buf+16>>2]=stat.nlink;HEAP32[buf+20>>2]=stat.uid;HEAP32[buf+24>>2]=stat.gid;HEAP32[buf+28>>2]=stat.rdev;HEAP32[buf+32>>2]=0;tempI64=[stat.size>>>0,(tempDouble=stat.size,+Math.abs(tempDouble)>=1?tempDouble>0?(Math.min(+Math.floor(tempDouble/4294967296),4294967295)|0)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+40>>2]=tempI64[0],HEAP32[buf+44>>2]=tempI64[1];HEAP32[buf+48>>2]=4096;HEAP32[buf+52>>2]=stat.blocks;HEAP32[buf+56>>2]=stat.atime.getTime()/1e3|0;HEAP32[buf+60>>2]=0;HEAP32[buf+64>>2]=stat.mtime.getTime()/1e3|0;HEAP32[buf+68>>2]=0;HEAP32[buf+72>>2]=stat.ctime.getTime()/1e3|0;HEAP32[buf+76>>2]=0;tempI64=[stat.ino>>>0,(tempDouble=stat.ino,+Math.abs(tempDouble)>=1?tempDouble>0?(Math.min(+Math.floor(tempDouble/4294967296),4294967295)|0)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+80>>2]=tempI64[0],HEAP32[buf+84>>2]=tempI64[1];return 0},doMsync:function(addr,stream,len,flags,offset){var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},doMkdir:function(path,mode){path=PATH.normalize(path);if(path[path.length-1]==="/")path=path.substr(0,path.length-1);FS.mkdir(path,mode,0);return 0},doMknod:function(path,mode,dev){switch(mode&61440){case 32768:case 8192:case 24576:case 4096:case 49152:break;default:return-28}FS.mknod(path,mode,dev);return 0},doReadlink:function(path,buf,bufsize){if(bufsize<=0)return-28;var ret=FS.readlink(path);var len=Math.min(bufsize,lengthBytesUTF8(ret));var endChar=HEAP8[buf+len];stringToUTF8(ret,buf,bufsize+1);HEAP8[buf+len]=endChar;return len},doAccess:function(path,amode){if(amode&~7){return-28}var node;var lookup=FS.lookupPath(path,{follow:true});node=lookup.node;if(!node){return-44}var perms="";if(amode&4)perms+="r";if(amode&2)perms+="w";if(amode&1)perms+="x";if(perms&&FS.nodePermissions(node,perms)){return-2}return 0},doDup:function(path,flags,suggestFD){var suggest=FS.getStream(suggestFD);if(suggest)FS.close(suggest);return FS.open(path,flags,0,suggestFD,suggestFD).fd},doReadv:function(stream,iov,iovcnt,offset){var ret=0;for(var i=0;i>2];var len=HEAP32[iov+(i*8+4)>>2];var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2];var len=HEAP32[iov+(i*8+4)>>2];var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr}return ret},varargs:undefined,get:function(){SYSCALLS.varargs+=4;var ret=HEAP32[SYSCALLS.varargs-4>>2];return ret},getStr:function(ptr){var ret=UTF8ToString(ptr);return ret},getStreamFromFD:function(fd){var stream=FS.getStream(fd);if(!stream)throw new FS.ErrnoError(8);return stream},get64:function(low,high){return low}};function ___sys_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=SYSCALLS.get();if(arg<0){return-28}var newStream;newStream=FS.open(stream.path,stream.flags,0,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=SYSCALLS.get();stream.flags|=arg;return 0}case 12:{var arg=SYSCALLS.get();var offset=0;HEAP16[arg+offset>>1]=2;return 0}case 13:case 14:return 0;case 16:case 8:return-28;case 9:setErrNo(28);return-1;default:{return-28}}}catch(e){if(typeof FS==="undefined"||!(e instanceof FS.ErrnoError))abort(e);return-e.errno}}function ___sys_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(op){case 21509:case 21505:{if(!stream.tty)return-59;return 0}case 21510:case 21511:case 21512:case 21506:case 21507:case 21508:{if(!stream.tty)return-59;return 0}case 21519:{if(!stream.tty)return-59;var argp=SYSCALLS.get();HEAP32[argp>>2]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21531:{var argp=SYSCALLS.get();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;return 0}case 21524:{if(!stream.tty)return-59;return 0}default:abort("bad ioctl syscall "+op)}}catch(e){if(typeof FS==="undefined"||!(e instanceof FS.ErrnoError))abort(e);return-e.errno}}function ___sys_open(path,flags,varargs){SYSCALLS.varargs=varargs;try{var pathname=SYSCALLS.getStr(path);var mode=varargs?SYSCALLS.get():0;var stream=FS.open(pathname,flags,mode);return stream.fd}catch(e){if(typeof FS==="undefined"||!(e instanceof FS.ErrnoError))abort(e);return-e.errno}}function _emscripten_memcpy_big(dest,src,num){HEAPU8.copyWithin(dest,src,src+num)}function abortOnCannotGrowMemory(requestedSize){abort("OOM")}function _emscripten_resize_heap(requestedSize){var oldSize=HEAPU8.length;abortOnCannotGrowMemory(requestedSize)}function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS==="undefined"||!(e instanceof FS.ErrnoError))abort(e);return e.errno}}function _fd_read(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamFromFD(fd);var num=SYSCALLS.doReadv(stream,iov,iovcnt);HEAP32[pnum>>2]=num;return 0}catch(e){if(typeof FS==="undefined"||!(e instanceof FS.ErrnoError))abort(e);return e.errno}}function _fd_seek(fd,offset_low,offset_high,whence,newOffset){try{var stream=SYSCALLS.getStreamFromFD(fd);var HIGH_OFFSET=4294967296;var offset=offset_high*HIGH_OFFSET+(offset_low>>>0);var DOUBLE_LIMIT=9007199254740992;if(offset<=-DOUBLE_LIMIT||offset>=DOUBLE_LIMIT){return-61}FS.llseek(stream,offset,whence);tempI64=[stream.position>>>0,(tempDouble=stream.position,+Math.abs(tempDouble)>=1?tempDouble>0?(Math.min(+Math.floor(tempDouble/4294967296),4294967295)|0)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[newOffset>>2]=tempI64[0],HEAP32[newOffset+4>>2]=tempI64[1];if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS==="undefined"||!(e instanceof FS.ErrnoError))abort(e);return e.errno}}function _fd_write(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamFromFD(fd);var num=SYSCALLS.doWritev(stream,iov,iovcnt);HEAP32[pnum>>2]=num;return 0}catch(e){if(typeof FS==="undefined"||!(e instanceof FS.ErrnoError))abort(e);return e.errno}}var FSNode=function(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.mounted=null;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.node_ops={};this.stream_ops={};this.rdev=rdev};var readMode=292|73;var writeMode=146;Object.defineProperties(FSNode.prototype,{read:{get:function(){return(this.mode&readMode)===readMode},set:function(val){val?this.mode|=readMode:this.mode&=~readMode}},write:{get:function(){return(this.mode&writeMode)===writeMode},set:function(val){val?this.mode|=writeMode:this.mode&=~writeMode}},isFolder:{get:function(){return FS.isDir(this.mode)}},isDevice:{get:function(){return FS.isChrdev(this.mode)}}});FS.FSNode=FSNode;FS.staticInit();function intArrayFromString(stringy,dontAddNull,length){var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array}var asmLibraryArg={"c":___sys_fcntl64,"h":___sys_ioctl,"i":___sys_open,"e":_emscripten_memcpy_big,"f":_emscripten_resize_heap,"b":_fd_close,"g":_fd_read,"d":_fd_seek,"a":_fd_write};var asm=createWasm();var ___wasm_call_ctors=Module["___wasm_call_ctors"]=function(){return(___wasm_call_ctors=Module["___wasm_call_ctors"]=Module["asm"]["k"]).apply(null,arguments)};var ___errno_location=Module["___errno_location"]=function(){return(___errno_location=Module["___errno_location"]=Module["asm"]["m"]).apply(null,arguments)};var _mlisp_init=Module["_mlisp_init"]=function(){return(_mlisp_init=Module["_mlisp_init"]=Module["asm"]["n"]).apply(null,arguments)};var _mlisp_interpret=Module["_mlisp_interpret"]=function(){return(_mlisp_interpret=Module["_mlisp_interpret"]=Module["asm"]["o"]).apply(null,arguments)};var _mlisp_cleanup=Module["_mlisp_cleanup"]=function(){return(_mlisp_cleanup=Module["_mlisp_cleanup"]=Module["asm"]["p"]).apply(null,arguments)};var stackSave=Module["stackSave"]=function(){return(stackSave=Module["stackSave"]=Module["asm"]["q"]).apply(null,arguments)};var stackRestore=Module["stackRestore"]=function(){return(stackRestore=Module["stackRestore"]=Module["asm"]["r"]).apply(null,arguments)};var stackAlloc=Module["stackAlloc"]=function(){return(stackAlloc=Module["stackAlloc"]=Module["asm"]["s"]).apply(null,arguments)};Module["cwrap"]=cwrap;var calledRun;function ExitStatus(status){this.name="ExitStatus";this.message="Program terminated with exit("+status+")";this.status=status}dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function run(args){args=args||arguments_;if(runDependencies>0){return}preRun();if(runDependencies>0){return}function doRun(){if(calledRun)return;calledRun=true;Module["calledRun"]=true;if(ABORT)return;initRuntime();preMain();if(Module["onRuntimeInitialized"])Module["onRuntimeInitialized"]();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(function(){setTimeout(function(){Module["setStatus"]("")},1);doRun()},1)}else{doRun()}}Module["run"]=run;if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run(); diff --git a/docs/assets/mlisp/mlisp.wasm b/docs/assets/mlisp/mlisp.wasm new file mode 100644 index 0000000..a02cfcb Binary files /dev/null and b/docs/assets/mlisp/mlisp.wasm differ diff --git a/docs/feed.xml b/docs/feed.xml new file mode 100644 index 0000000..accc6d1 --- /dev/null +++ b/docs/feed.xml @@ -0,0 +1,4460 @@ +mliezun.github.io +https://mliezun.github.io/ +mliezun.github.io +2024-01-04 +I'm Miguel. Here I write mainly about programming and side projects. +Generating posts using markdown +https://mliezun.github.io/2024/01/04/new-markdown-generator.html +2024-01-04 +

    Reading time: +

    +

    +

    Generating posts using markdown +

    +

    This is pretty standard for Github pages. But in this case, the parser has been written by me. It takes some subset of markdown and compiles it to HTML. +

    Only what you see in this post is what's supported. +

    Code blocks +

    +

    Standard code block: +

    Hello, this is a code block!
    +

    +

    Syntax highlighting: +

    from functools import reduce, partial
    +import operator
    +
    +mul = partial(reduce, operator.mul)
    +print("Factorial of 5:", mul(range(1, 6)))
    +

    +

    Unordered list +

    +

    • This +
    • is an +
    • unordered list +

    Bolds, Italics and Inline code +

    +

    Some text can be bolded, while some other can be in Italics. +

    But the best is to have print("inline code"). +

    Links and images +

    +

    Link to the blog source code where you can see how the parser works (tldr: is awful). +

    Picture of NYC: +

    Picture of NYC +

    HEADINGS +

    +

    There's a thing you haven't noticed so far. There's support for different kinds of headings. You can see them in increasing order here: +

    Microscopic +

    +

    Small +

    +

    Good +

    +

    Pretty good +

    +

    Big +

    +

    Good bye! +

    +

    Thanks for checking out the blog. I've done this to reduce the complexity of creating new blogposts. It was a headache before. +

    Hopefully now I'll write more. Stay tuned. +

    +

    Miguel LiezunCustom Markdown parser and HTML generator using Grotsky, my toy programming language that powers this blog. Up until now I've used a hacky HTML generator that relies on lists. Now Im integrating a simple MD parser that makes easier to write new articles. +
    Day 20. My favourite problem from Advent of Code 2023 +https://mliezun.github.io/2023/12/25/favourite-advent-of-code-2023.html +2023-12-25 +

    Reading time: +

    Day 20 +

    Im gonna briefly describe the problem here, but if you want to see the real thing go check it out https://adventofcode.com/2023/day/20. +

    I like it because it involves some simple electronic devices that are wired together and send pulses/signals to each other. In this problem you have to make sure to correctly propagate the signals and simulate the behaviour of the devices. +

    There are two devices that have a very distinct behaviour: +

  • Flip flops: similar to a T flip-flop electronic device.
  • +
  • Conjunctions: similar to a NAND gate with memory on its inputs.
  • +

    In this problem, Flip flops are initially off and whenever they reiceve a low pulse they toggle between on/off. Each time it toggles state it sends a pulse as an output. When turned off sends a low pulse, when turned on sends a high pulse. +

    Conjunction modules remember the most recent pulse on each input. By default it remembers a low pulse for all inputs. When a pulse is received it updates the memory for that input. Then, if it remembers high pulses for all inputs, it sends a low pulse; otherwise, it sends a high pulse. +

    There is also some "dummy" modules: +

  • Broadcaster: has 1 input and N outputs. It replicates the input in all its outputs.
  • +
  • Button: when pressed sends a low pulse. The button is always connected as the broadcaster input. This is similar to a normally closed switch.
  • +
  • Test module: module that receive and process inputs but has no output.
  • +

    One important thing to have in mind is that modules only send output pulses when they receive a pulse as input. +

    Problem input +

    The example input looks something like this: +
    +broadcaster -> a, b, c
    +%a -> b
    +%b -> c
    +%c -> inv
    +&inv -> a
    +
    +

    There will always be just one Broadcaster module called "broadcaster" that has the Button connected as input. In this case it has module's "a", "b" and "c" connected to its output. +

    The arrow -> indicates what modules are connected to the output of the module to the left. +

    Lines that start with % means the module is a Flip flop, for example: %a -> b indicates that there's a flip flop called "a" whose output is connected to module's "b" input. +

    Lines that start with & means the module is a Conjunction, for example: &inv -> a indicates that there's a conjunction called "inv" whose output is connected to module's "a" input. +

    Let's analyze how this circuit behaves once the button is pushed: +
    +button -0-> broadcaster
    +broadcaster -0-> a
    +broadcaster -0-> b
    +broadcaster -0-> c
    +a -1-> b
    +b -1-> c
    +c -1-> inv
    +inv -0-> a
    +a -0-> b
    +b -0-> c
    +c -0-> inv
    +inv -1-> a
    +
    +In this example 8 low (0) pulses and 4 high (1) pulses are sent. +

    Part 1 +

    To solve the first part we need to calculate the multiplication between high and low pulses sent between devices. +

    In the previous example that would be 8*4=32. +

    But this time we don't push the button only once, but we push it a 1000 times. Each time we push the button we wait until all signals propagate and the circuit settles into a state before pushing the button again. +

    Solution +

    First I started by modelling the devices as objects. Starting with a single base class that has most of the common behaviour. +
    +from abc import ABC
    +measure_pulses = {0: 0, 1: 0}
    +class Module(ABC):
    +    def __init__(self, name: str):
    +        self.name = name
    +        self.outputs = []
    +
    +    def receive_pulse(self, mod: "Module", pulse: int) -> list[tuple["Module", int]]:
    +        measure_pulses[pulse] += 1
    +        print(f"{mod and mod.name} -{pulse}-> {self.name}")
    +        return self.process_pulse(mod, pulse)
    +
    +    def connect_output(self, mod: "Module"):
    +        self.outputs.append(mod)
    +
    +    def propagate_pulse(self, pulse: int):
    +        mods = []
    +        for m in self.outputs:
    +            mods.append((m, pulse))
    +        return mods
    +
    +    def process_pulse(self, mod: "Module", pulse: int):
    +        raise NotImplementedError()
    +
    +    def __str__(self) -> str:
    +        return f"{self.__class__.__name__}(name={self.name})"
    +
    +    def __repr__(self) -> str:
    +        return str(self)
    +
    +

    What we see here is that we expect all modules to have a name and outputs. See __init__(), __str__(), __repr__() and connect_output(). +

    Each module can receive a pulse 0 or 1 from another module. See receive_pulse(). Each time we process a pulse we record it in a global dict called measure_pulses. +

    Also we leave process_pulse() to be defined by each particular module type. +

    We have a method that returns a list of all modules to which signals should be propagated. See propagate_pulse(). +

    Let's start by the easiest module type: +
    +class TestModule(Module):
    +    def process_pulse(self, mod: "Module", pulse: int):
    +        return []
    +
    +Give that it's a dummy module, it doesn't do anything when it receives an input. +
    +class Broadcaster(Module):
    +    def process_pulse(self, mod: "Module", pulse: int):
    +        return super().propagate_pulse(pulse)
    +
    +As expected the Broadcaster always propagates the received input to all its outputs. +
    +class FlipFlop(Module):
    +    def __init__(self, name: str):
    +        super().__init__(name)
    +        self.state = 0
    +
    +    def process_pulse(self, mod: "Module", pulse: int):
    +        if pulse == 0:
    +            self.state = (self.state + 1) % 2
    +            return super().propagate_pulse(self.state)
    +        return []
    +
    +

    The flip flop start initially turned off. See self.state = 0 in __init__(). +

    In process_pulse() we implement the behaviour: +

  • If receives a low pulse, toggles the state and sends a pulse equals to the state to all its outputs.
  • +
  • Otherwise it doesn't do anything.
  • +
    +class Conjunction(Module):
    +    def __init__(self, name: str):
    +        super().__init__(name)
    +        self.memory = {}
    +
    +    def remember_input(self, mod: Module):
    +        self.memory[mod.name] = 0
    +
    +    def process_pulse(self, mod: Module, pulse: int):
    +        self.memory[mod.name] = pulse
    +        if all(self.memory.values()):
    +            return self.propagate_pulse(0)
    +        return self.propagate_pulse(1)
    +
    +

    The conjunction initializes its memory as empty. See __init__(). +

    Each time a module is plugged in as an input it remembers it as OFF (0). See remember_input(). +

    The way it processes pulses is by first recording the pulse for the input in its memory. Then if all inputs are 1s it sends a 0 pulse to all its outputs. +

    Otherwise it sends a 1 pulse to all its outputs. +

    At this point we have all our building blocks for solving this problem. We only need to parse the input and something that pushes the button and makes sure signals are propagated to the end. +

    Parsing modules is straightforward: +
    +def parse_modules(modules: list) -> dict[str, Module]:
    +    modules_by_name = {}
    +    outputs_by_name = {}
    +
    +    # Parse all modules into their correspondig class and store
    +    # them in a dict.
    +    for m in modules:
    +        module_type = m[0]
    +        module_outputs = [o.strip() for o in m[1].split(",") if o.strip()]
    +        if module_type.startswith("broadcaster"):
    +            modules_by_name[module_type] = Broadcaster(module_type)
    +            outputs_by_name[module_type] = module_outputs
    +        elif module_type.startswith("%"):
    +            modules_by_name[module_type[1:]] = FlipFlop(module_type[1:])
    +            outputs_by_name[module_type[1:]] = module_outputs
    +        elif module_type.startswith("&"):
    +            modules_by_name[module_type[1:]] = Conjunction(module_type[1:])
    +            outputs_by_name[module_type[1:]] = module_outputs
    +    # Once all the modules are parsed use connect their outputs.
    +
    +    # If the module doesn't exist at this point is a TestModule.
    +    # If the module is a Conjunction, call remember_input().
    +    for name, outputs in outputs_by_name.items():
    +        for mod_name in outputs:
    +            mod = modules_by_name.get(mod_name, TestModule(mod_name))
    +            modules_by_name[name].connect_output(mod)
    +            if isinstance(mod, Conjunction):
    +                mod.remember_input(modules_by_name[name])
    +
    +    return modules_by_name
    +
    +

    If we parse our example using that function we will receive a dictionary as its output. Keys are module names and values are the objects representing the module. +

    If we parse the example we get something like this: +

    +example = """broadcaster -> a, b, c
    +%a -> b
    +%b -> c
    +%c -> inv
    +&inv -> a"""
    +example_modules = [m.split(" -> ") for m in example.splitlines() if m.strip()]
    +print(parse_modules(example_modules))
    +
    +# Output
    +{
    +    'broadcaster': Broadcaster(name=broadcaster),
    +    'a': FlipFlop(name=a),
    +    'b': FlipFlop(name=b),
    +    'c': FlipFlop(name=c),
    +    'inv': Conjunction(name=inv)
    +}
    +
    +Then we need a function that pushes the button and makes sure all signals are propagated: +
    +def push_button(modules_by_name: dict[str, Module]):
    +    broad = modules_by_name["broadcaster"]
    +    queue = [(broad, broad.receive_pulse(None, 0))]
    +    while queue:
    +        current, signals = queue.pop(0)
    +        for mod, pulse in signals:
    +            queue.append((mod, mod.receive_pulse(current, pulse)))
    +
    +

    Here, we lookup the broadcaster module by name. And send a pulse (note that we pass None as the module because we didn't implement a button class) to the broadcaster. +

    We store the current module (broadcaster) along with all the propagated signals (return value from receive_pulse()) in a queue to be processed. +

    While the signal queue to be processed is not empty we do the following: +

  • Extract the first element of the queue.
  • +
  • Go trough all the signals that this element is sending.
  • +
  • Send the pulses to each corresponding module and store them in the queue to be processed.
  • +

    This process will stop when all responses from receive_pulse() are empty and there are no more signals added to the queue. +

    If we run this for our example: +

    +example_modules = parse_modules(example_modules)
    +push_button(example_modules)
    +
    +# Output
    +None -0-> broadcaster
    +broadcaster -0-> a
    +broadcaster -0-> b
    +broadcaster -0-> c
    +a -1-> b
    +b -1-> c
    +c -1-> inv
    +inv -0-> a
    +a -0-> b
    +b -0-> c
    +c -0-> inv
    +inv -1-> a
    +
    +

    It looks the same as when we analyzed the example above!! +

    We're ready for processing our problems input. (Remeber to comment out the print statement inside receive_pulse()). +

    +modules = open("input.txt", "r").read().strip()
    +modules = [m.split(" -> ") for m in modules.splitlines() if m.strip()]
    +modules = parse_modules(modules)
    +
    +for _ in range(1000):
    +    push_button(modules)
    +
    +print("result:", measure_pulses[0] * measure_pulses[1])
    +
    +# Output
    +result: x
    +
    +Based on your problem input x will be the solution. +

    Part 2 +

    This part as always is much trickier than the first part. It doesn't involve much code changes, just figuring out a way of avoiding large computations. +

    For this part, the problem tells us that there's a module called rx. And we need to find out the lowest amount of button pulses that will make the rx module receive a low pulse. +

    As I have learned troughout this entire challenge, just nahively letting it run and see when the rx module gets a low signal will get me nowhere. It will run forever. +

    So, taking a look at the input and see what the rx module is actually connected to might provide some guidance. +

    Following is for my case (I don't know if all problem inputs are the same). Looking up "rx" in the input I find a single line: +
    +...
    +&lv -> rx
    +...
    +
    +

    That means rx is a TestModule (a dummy module that has nothing connected to its output). And that has only one input: a Conjunction called lv. +

    Ok, that feels like progress. Let's see what lv is connected to: +
    +...
    +&st -> lv
    +&tn -> lv
    +&hh -> lv
    +&dt -> lv
    +...
    +
    +

    Other 4 Conjunctions are connected as inputs of lv. That's interesting. Because lv is a Conjuction it means that to send the low pulse required by rx it should receive all high pulses from its inputs. +

    The solution from here is kind of intuitive at this point. If we figure out how many button pulses does it take for each of the input devices to send a 1 signal we can multiply them together and get the result. +

    I'll explain better. Let's say st sends a 1 on every button push, tn sends a 1 every second button push (this means you have to press the button twice to get tn to send a 1 as an output), hh sends a 1 every fourth button push and dt sends a 1 every eighth button push. +

    So it looks like this: +
    +module | pushes
    +---------------
    +  st   |   1
    +  tn   |   2
    +  hh   |   4
    +  dt   |   8
    +
    +

    In this example, if we push the button 8 times. They are all gonna send a high pulse. Because 8 is divisible by 1, 2, 4 and 8. +

    If the table were different: +
    +module | pushes
    +---------------
    +  st   |   1
    +  tn   |   3
    +  hh   |   5
    +  dt   |   7
    +
    +

    In this case there's no obvious number of times we should push the button. But if we multiply the numbers together we get a number that is divisible by every number in the table. Pushing the button 1 * 3 * 5 * 7 = 105 times will make all the outputs send a 1, and consequently rx will receive a 0. +

    Solution +

    What we need to do then is to figure after out how many button presses we get a 1 on each of those modules. +
    +from collections import defaultdict
    +
    +# Store number of button presses in a global variable
    +ITERATIONS = 0
    +# Store high pulses for target modules
    +OUT_PULSES = defaultdict(list)
    +
    +class Conjunction(Module):
    +    def __init__(self, name: str):
    +        super().__init__(name)
    +        self.memory = {}
    +
    +    def remember_input(self, mod: Module):
    +        self.memory[mod.name] = 0
    +
    +    def start_recording(self):
    +        self.recording = True
    +
    +    def record(self):
    +        if hasattr(self, "recording"):
    +            OUT_PULSES[self.name].append(ITERATIONS)
    +
    +    def process_pulse(self, mod: Module, pulse: int):
    +        self.memory[mod.name] = pulse
    +        if all(self.memory.values()):
    +            return self.propagate_pulse(0)
    +        self.record()
    +        return self.propagate_pulse(1)
    +
    +

    We introduced 2 new methods to the conjunction module: start_recording() and record(). The first just initializes a bool attribute. And the second makes sure to only record high pulses for objects that have been initialized (method start_recording() called). +

    Also introduced 2 global variables: ITERATIONS to keep track of button pushes and OUT_SIGNALS to track each time one of the modules outputs a high pulse. +

    Now we need to make those specific modules record their outputs: +
    +# Get the "lv" module by name
    +lv = modules["lv"]
    +lv_inputs = [modules[k] for k in lv.memory.keys()]
    +for m in lv_inputs:
    +    m.start_recording()
    +
    +I wasn't sure if the cycle was always going to be the same, so just to be sure I did 100_000 button pushes and recorded all the "1" outputs for those modules. +
    +for i in range(100_000):
    +    ITERATIONS += 1
    +    push_button(modules)
    +print(OUT_PULSES)
    +
    +# Output
    +{'hh': [3769, 7538, 11307, 15076, 18845, 22614, 26383, 30152, 33921, 37690, 41459, 45228, 48997, 52766, 56535, 60304, 64073, 67842, 71611, 75380, 79149, 82918, 86687, 90456, 94225, 97994], 'tn': [3863, 7726, 11589, 15452, 19315, 23178, 27041, 30904, 34767, 38630, 42493, 46356, 50219, 54082, 57945, 61808, 65671, 69534, 73397, 77260, 81123, 84986, 88849, 92712, 96575], 'st': [3929, 7858, 11787, 15716, 19645, 23574, 27503, 31432, 35361, 39290, 43219, 47148, 51077, 55006, 58935, 62864, 66793, 70722, 74651, 78580, 82509, 86438, 90367, 94296, 98225], 'dt': [4079, 8158, 12237, 16316, 20395, 24474, 28553, 32632, 36711, 40790, 44869, 48948, 53027, 57106, 61185, 65264, 69343, 73422, 77501, 81580, 85659, 89738, 93817, 97896]}
    +
    +We can observe that for each module we have a periodicity given by: +
    +hh = n*3769
    +tn = n*3863
    +st = n*3929
    +dt = n*4079
    +
    +

    This means we can just multiply the first element of each list for each module and we'll get our result. +

    In my case it was: +
    +accum = 1
    +for name, pulses in OUT_PULSES.items():
    +    accum *= pulses[0]
    +print("result:", accum)
    +
    +# Output
    +result: 233338595643977
    +
    +

    Edit: +As some people have pointed out in the +HN discussion, just multiplying the numbers together only works because the numbers are +coprimes + and the correct solution is to use +LCM. +

    Closing words +

    This problem is my favourite because it has a few characteristics that I personally enjoy: +

  • It's based on real world stuff. In this case electronic devices (which is also a plus because they're fun).
  • +
  • It can be easily translated to an OOP approach which makes it easy to implement and understand.
  • +
  • To solve the second part you need to look at the data and make a solution for your particular input.
  • +
  • It doesn't involve any Graph traversal or specific Math, Calculus or Algebra knowledge. Or any obscure CS algorithm.
  • +

    In the end this is one of my favourites because to solve it you just have to understand the problem and understand the data. +

    Link to my github project with the solutions https://github.com/mliezun/aoc. +

    Miguel LiezunAdvent of code 2023 has gone by, this is my first year participating. It's been fun and I want to share the problem that I enjoyed the most. It's based on simple electronic devices sending signals or pulses to each other. +
    I rewrote my toy language interpreter in Rust +https://mliezun.github.io/2023/11/23/grotsky-rust-part3.html +2023-11-23 +

    Reading time: +

    I rewrote my toy language interpreter in Rust

    Im rewriting Grotsky (my toy programming language) in Rust, the previous implementation +was done in Go. The goal of the rewrite is to improve my Rust skills, and to improve the performance of Grotsky, +by at least 10x. This has been a serious of posts, this one is the latest one. Hopefully the best and most insightful +of them all.

    In previous posts: +

    I've outlined a plan to migrate Grotsky to a Rust based platform. +

    + Originally, my plan was very ambitious and I thought I would be able to finish the transition + in like two months. + In reality it took five months :-) + +

    Performance improvement

    I was aiming at a 10x improvement. In reality is not that much. + I ran various benchmarks and get at most a 4x improvement. + Which is not great, but also not that bad given that I know very little + of how to do high performance Rust. + The interpreter is written in the most dumbest and easiest way I managed to do it. +

    Let's look at some numbers for different programs. +

    Loop program

    let start = io.clock()
    +fn test() {
    +    let a = 1
    +    while a < 10000000 {
    +        a = a + 1
    +    }
    +}
    +test()
    +io.println(io.clock() - start)
    +

    The result is: +

    trial #1
    +build/grotsky   best 5.135s  3.877x time of best
    +build/grotsky-rs   best 1.325s  287.6800% faster
    +trial #2
    +build/grotsky   best 5.052s  3.814x time of best
    +build/grotsky-rs   best 1.325s  281.4182% faster
    +trial #3
    +build/grotsky   best 5.035s  3.802x time of best
    +build/grotsky-rs   best 1.325s  280.1663% faster
    +trial #4
    +build/grotsky   best 5.003s  3.777x time of best
    +build/grotsky-rs   best 1.325s  277.6831% faster
    +trial #5
    +build/grotsky   best 5.003s  3.777x time of best
    +build/grotsky-rs   best 1.325s  277.6831% faster
    +

    Recusive fibonacci

    let start = io.clock()
    +fn fib(n) {
    +	if n <= 2 {
    +		return n
    +	}
    +	return fib(n-2) + fib(n-1)
    +}
    +fib(28)
    +io.println(io.clock() - start)
    +

    The result is: +

    trial #1
    +build/grotsky   best 0.8409s  294.5155% faster
    +build/grotsky-rs   best 3.317s  3.945x time of best
    +trial #2
    +build/grotsky   best 0.8168s  271.3829% faster
    +build/grotsky-rs   best 3.033s  3.714x time of best
    +trial #3
    +build/grotsky   best 0.797s  245.6835% faster
    +build/grotsky-rs   best 2.755s  3.457x time of best
    +trial #4
    +build/grotsky   best 0.7784s  249.9964% faster
    +build/grotsky-rs   best 2.724s  3.5x time of best
    +trial #5
    +build/grotsky   best 0.7784s  249.9964% faster
    +build/grotsky-rs   best 2.724s  3.5x time of best
    +

    In this case is like 3.5x slower. This is due to function calls. + Im not very well versed in Rust, so on each call im copying a lot of + data over and over. In the go implementation everything is just pointers + so there's less copying. +

    Compile to bytecode

    With the Rust implementation, generating and compiling to bytecode was added. + Now it's possible to generate a bytecode file to later read it. + This is a way of distributing files without giving away source code and also + a little bit more performant because you skip parsing and compilation phases. +

    How it works: +

    grotsky compile example.gr  # Compile file
    +grotsky example.grc  # Run compiled file
    +

    Memory model

    Grotsky is a reference-counted language. We're using Rust's Rc and RefCell to keep track of values. +

    pub struct MutValue<T>(pub Rc<RefCell<T>>);
    +
    +impl<T> MutValue<T> {
    +    pub fn new(obj: T) -> Self {
    +        MutValue::<T>(Rc::new(RefCell::new(obj)))
    +    }
    +}
    +
    +pub enum Value {
    +    Class(MutValue<ClassValue>),
    +    Object(MutValue<ObjectValue>),
    +    Dict(MutValue<DictValue>),
    +    List(MutValue<ListValue>),
    +    Fn(MutValue<FnValue>),
    +    Native(NativeValue),
    +    Number(NumberValue),
    +    String(StringValue),
    +    Bytes(BytesValue),
    +    Bool(BoolValue),
    +    Slice(SliceValue),
    +    Nil,
    +}
    +
    +pub enum Record {
    +    Val(Value),
    +    Ref(MutValue<Value>),
    +}
    +
    +pub struct VM {
    +    pub activation_records: Vec<Record>,
    +}
    +

    Most of the simple values are just stored as-is: Native (builtin functions), Number, String, + Bytes, Bool, Slice and Nil. +

    For the other complex values we need to use 'pointers' which in this case are MutValue. +

    Then the Grotsky VM uses Records which can be a plain Value or a reference to a Value. + The records are registers, each function has up to 255 registers. + The reference to values are used to store upvalues. + A register is turned into an upvalue when a variable is closed by another function. +

    This implementation ends up being very slow, but easy to manage. Because Rust stdlib + does all the work. +

    Using Rust in this blogpost

    As you may know, this blog is powered by grotsky. + Im happy to say that I successfully migrated from grotsky to grostky-rs as the backend for the blog. + And what you're reading now is generated by the latest implementation of the language using Rust. +

    Even for local development the Rust version is used. Which means Im using a TCP server and an HTTP + implementation written in Grotsky. +

    Closing remarks

    This has been a great learning, Im happy to have finished because it required a lot of effort. + Im not gonna announce any new work on this interpreter but I would like to keep adding stuff. + Improving it further to make it more performant and more usable. +

    In the end I encourage everyone to try it and also start their own project. Is always cool to see + what everyone else is doing. +

    Thanks for reading and please leave a comment. +

    Miguel LiezunIm rewriting Grotsky (my toy programming language) in Rust, the previous implementation +was done in Go. The goal of the rewrite is to improve my Rust skills, and to improve the performance of Grotsky, +by at least 10x. This has been a serious of posts, this one is the latest one. Hopefully the best and most insightful +of them all. +
    Rewrite my toy language interpreter in Rust, an update +https://mliezun.github.io/2023/09/23/grotsky-rust-part2.html +2023-09-23 +

    Reading time: +

    Rewrite my toy language interpreter in Rust, an update

    Im rewriting Grotsky (my toy programming language) in Rust, the previous implementation +was done in Go. The goal of the rewrite is to improve my Rust skills, and to improve the performance of Grotsky, +by at least 10x.

    In a +previous post + I've outlined a plan to migrate Grotsky to a Rust based platform. +

    I was suposed to finish it more than a month ago :'). +

    Of course, I wasn't able to do it. +

    I think my original estimation was actually very flawed, but I also didn't put enough work. +

    I failed my estimation, because I decided to skip the step of first writing a Tree-based interpreter in Rust. + To go directly to a Register-based virtual machine. +

    The reason that is taking me so long is that I travel a lot. I've been a digital nomad for more than a year now. + And when I finish working I prefer to go outside and explore the place where Im staying. + I go to a lot of interesting places: at this time Im in Turkey and heading to Hong Kong, after that Im going to South Korea. + So, it makes sense to actually experience the places where Im staying than to stay inside writing code. +

    Im here to propose a new roadmap that is more achivable with the time I have to work on this. + And the idea is to finish it before the end of the year. +

    Plus, I recently heard some advice that I think it's worth to try: Work on someone for 15 minutes every day. + I do have 15 minutes everyday where Im probably scrolling through social media or just consuming content. + I can make better use of that time by putting it into this project. +

    Updated Roadmap

    • Sep 30: Publish a blogpost about the memory model of the Rust implementation.
    • Oct 15: Migrate automated tests to the new backend and make sure they pass.
    • Oct 30: Implement stdlib in bytecode interpreter. Share results.
    • Nov 15: Add ability to compile to bytecode and run from bytecode.
    • Dec 30: Finish up everything and publish a blogpost update before end of year.

    This is gonna be rough, because of the traveling and being jetlaged all the time. + But I think the roadmap is easy enough so I can do it. +

    Thanks for reading. Please, leave a comment about your experience and struggle with side projects. +

    Miguel LiezunIm rewriting Grotsky (my toy programming language) in Rust, the previous implementation +was done in Go. The goal of the rewrite is to improve my Rust skills, and to improve the performance of Grotsky, +by at least 10x. +
    The end of a side project +https://mliezun.github.io/2023/07/15/the-end-of-a-side-project.html +2023-07-15 +

    Reading time: +

    The end of a side project

    Cloud Outdated was a personalized digest of updates for cloud services. It's sad to see it go, + but it was a fun project to work on, learn some new stuff and collab with a friend. + There are some takeaways from this that I'd like to share. +

    Building something simple is hard

    From the beginning we were trying to build a simple product to get notified when new versions + of Python were supported in AWS Lambda. But we figured we could support other things too, like + all Lambda runtimes, then also GCP and Azure, then more services of each one. +Features started piling up pretty quickly. +

    When building stuff I try to think of every edge case, even the most improbable ones, and make + the software infalible. Of course, it's impossible to succeed at that, software will always be fallible. +And this premature optimization ends up making the project more complex than it should be. +

    We planned to work on this for a 1 or 2 months, and it ended up taking 6+ months :-). +

    My takeaway here is: start building the dumbest thing possible that gets the job done, then adjust as needed. +

    Getting users is hard

    We're killing this project because nobody uses it. +And nobody except us has used it since it was launched more than a year ago. +Some people subscribed but never even opened an email. +

    We tried to advertise in our social media and post it in different builders communities. +But that will get you so far if you're not an influencer that has the right audience. +

    We thought that we'll get more traffic from organic search or people telling their friends about this. +But in the end I think nobody really needed something like this that much. +

    My takeaway here is: building something that people really want and getting the product to the hands + of the people that want it is very complicated. You should think very deeply about what problem your + product is solving and how your users will find you. +

    The money

    This has costed like $200 (US dollars) since inception. For two people that's like $100 each. +It's not a lot, but for something that has no users that's quite expensive. +

    We used Lambdas to serve this project. +I feel like we were promised that the cloud and serverless are cheap and easy solution. +But in my opinion it doesn't seem to be the case. +It's definitely not easier nor cheaper than having a PHP website in this case. +Im sure there are other cases in which it makes more sense. +

    This is also a reason of why we're killing the project. +Our service started failing because after a deploy dependencies were updated and the code + was to big to fit on a Lambda. +It would have been a lot of work to fix it, so we decided to kill it and save some bucks every month. +

    For personal projects which you're not sure how they're going to scale, + I think serverless is probably not the right choice. +Serverless make sense if you're going to have huge burst of traffics and don't want + to handle a lot of infra. +

    My takeaway here is: beware of the cloud, if you're just building a small side project + or don't have huge infra needs stick with the cheapest providers (Hostinger, PythonAnywhere, Hetzner) + and avoid cloud providers (GCP, Azure, AWS). +

    Final thougths

    If I haven't made this clear enough, building a successful product is *hard*. +There are many things to think about when starting, and the technical stuff hopefully + is the easy part. +I think this are the 3 most important lessons that I've learned working on this: +

    • Build the dumbest thing that does the job, improve as needed.
    • Think deeply about what problem are you solving and how you're going to deliver the solution to the people that need it.
    • Beware of the cloud, if possible use a cheaper provider. It will save you money and headaches.

    Miguel LiezunCloud Outdated was a personalized digest of updates for cloud services. It's sad to see it go, + but it was a fun project to work on, learn some new stuff and collab with a friend. + There are some takeaways from this that I'd like to share. +
    Rewrite my toy language interpreter in Rust +https://mliezun.github.io/2023/06/02/rewrite-grotsky-rust.html +2023-06-02 +

    Reading time: +

    Rewrite my toy language interpreter in Rust

    Im rewriting Grotsky (my toy programming language) in Rust, the previous implementation +was done in Go. The goal of the rewrite is to improve my Rust skills, and to improve the performance of Grotsky, +by at least 10x.

    As of this writting the Rust implementation is 4x faster than the Golang implementation.

    I've rewritten the Lexer, Parser and part of the Runtime. Enough for run this code and measure + how long it takes for each implementation to finish:

    let a = 1
    +while a < 100000000 {
    +    a = a + 1
    +}
    +

    I was inspired by Mitchel Hashimoto's post: +My Approach to Building Large Technical Projects. And I want to create a roadmap of little projects to reach my goal of having a full-fledged interpreter in Rust. +

    Goal

    Until now Grotsky has been running on a Tree-based interpreter. My goal is that at the end of the rewrite I + will have a Bytecode interpreter. +

    First I want to rewrite the Tree-based interpreter in Rust and achieve at least 10x performance improvement. +

    Then figure out if I want to use a Register based or Stack based bytecode interpreter. +

    Also, I would like to have a stable bytecode representation to be able to compile programs to a binary format + that can be shipped as is or packaged into a binary. +

    Finally, it's time Grotsky gets a REPL. +

    Roadmap

    Believe it or not, Grotsky it's pretty big. It has support for reading and writting files, sockets and more + on the stdlib. Which means I have a huge task ahead of me. +

    First thing I want to do is to have some sort of automated testing setup that can compare Rust vs Go implementation. + Right now all test are written as Go unit test, I need to make them agnostic of language backend. +

    • Jun 9: Have a complete setup of automated tests for correctness and performance.
    • Jun 16: Language runtime (without stdlib) rewritten in Rust.
    • Jun 23: Finish migrating stdlib and publish results (new blog post). Use new implementation for blog engine.
    • Jun 30: Decide which kind of bytecode interpreter to use, then make a design and plan for the implementation.
    • Jul 7: Have a working bytecode interpreter that is able to run the program shown in this post ^. Just a simple while loop. Compare performance and share progress.
    • Jul 14: Add support for functions and closures.
    • Jul 21: Finish runtime without stdlib in bytecode interpreter.
    • Jul 28: Implement stdlib in bytecode interpreter. Share results.
    • Aug 5: Add ability to compile to bytecode and run from bytecode.
    • Aug 12: Add REPL and finish up project.

    Im gonna have lots of fun with this project and Im sure next time a post here I'll be a changed man. + Surely gonna learn a lot, Im excited about what lies ahead. +

    Miguel LiezunIm rewriting Grotsky (my toy programming language) in Rust, the previous implementation +was done in Go. The goal of the rewrite is to improve my Rust skills, and to improve the performance of Grotsky, +by at least 10x. +
    Writing a Redis clone in Go from scratch +https://mliezun.github.io/2023/04/08/redis-clone.html +2023-04-08 +

    Reading time: +

    Writing a Redis clone in Go from scratch

    In this post we're going to write a basic Redis clone in Go that implements the most simple commands: GET, +SET, DEL and QUIT. At the end you'll know how to parse a byte stream from a live TCP connection, and hopefully have a working +implementation of Redis.

    What's intersting about this project is that it's production ready (not really). + It's being used in production in an old Web app that I made for a client in 2017. + It has been running for a few months now without issues.

    I mantain that app to this day and I charge like 50 bucks a month for it. I do it because + Im friends with the person that uses the app.

    Long story short, the app's backend is written in PHP and uses Redis for caching, only GET, SET and DEL commands. + I asked my friend if I could replace it with my custom version and said yes, so I decided to give it a go.

    If you're looking for C/C++ implementation, go check out this book.

    What we'll be building

    If you go to the +command list on redis webpage you'll see that there are 463 commands to this day (maybe more if you're in the future). +

    That's a crazy number. Here, we're only implementing 4 commands: GET, SET, DEL, QUIT, + the other 459 commands are left as an exercise to the reader. +

    GET

    GET key
    +

    Returns the value referenced by key. + If the key does not exist then nil is returned. +

    SET

    SET command gains more features on newer versions of Redis. We're going to implement one that has all features + that were realeased up until version 6.0.0. +

    SET key value [NX | XX] [EX seconds | PX milliseconds]
    +

    Stores value as a string that is referenced by key. + Overwrites any data that was previously referenced by the key. +

    Options

    • EX seconds -- Set the specified expire time, in seconds.
    • PX milliseconds -- Set the specified expire time, in seconds.
    • NX -- Only set the key if it does not already exist.
    • XX -- Only set the key if it already exist.

    DEL

    DEL key [key ...]
    +

    Takes 'any' amount of keys as input and removes all of them from storage. If a key doesn't exist it is ignored. + Returns the amount of keys that were deleted. +

    QUIT

    QUIT
    +

    When receiving this command the server closes the connection. It's useful for interactive sessions. + For production environments the client should close the connection without sending any commands. +

    Examples

    Let's start an interactive session of redis to test some commands. + We can install redis-server with docker and run it locally. + Then we can use telnet to connect directly via TCP. + Open a terminal and execute the following instructions: +

    $ docker run -d --name redis-server -p 6379:6379 redis:alpine
    +
    +$ telnet 127.0.0.1 6379
    +Trying 127.0.0.1...
    +Connected to localhost.
    +Escape character is '^]'.
    +

    At this point the prompt should be waiting for you to write something. We're gonna test a couple of commands. + In the code boxes below the first line is the command, following lines are the response. +

    GET a
    +$-1
    +
    ^ That weird $-1 is the special nil value. Which means there's nothing stored here. +

    set a 1
    ++OK
    +
    ^ First thing to notice here is that we can use lowercase version of SET. + Also, when the command is successful returns +OK. +

    set b 2
    ++OK
    +

    SET c 3
    ++OK
    +
    ^ Just storing a couple more values. +

    GET a
    +$1
    +1
    +
    ^ Here the response is returned in two lines. First line is the length of the string. Second line + is the actual string. +

    get b
    +$1
    +2
    +
    ^ We can also use lowercase version of GET, I bet commands are case-insensitive. +

    get C
    +$-1
    +
    ^ Testing with uppercase C gives a nil. Keys seem to be case-sensitive, probably values too. + That makes sense. +

    del a b c
    +:3
    +
    ^ Deleting everything returns the amount of keys deleted. Integers are indicated by ':'. +

    quit
    ++OK
    +Connection closed by foreign host.
    +
    ^ When we send QUIT, the server closes the connection and we're back to our terminal session. +

    With those tests we have enough information to start building. We learned a little bit about the + redis protocol and what the responses look like. +

    Sending commands

    Until now we've been using the inline version of redis command. + There's another kind that follows the +RESP (Redis serialization protocol).

    The RESP protocol is quite similar to what we've seen in the examples above. +The most important addition is arrays. Let's see a Client<>Server interaction + using arrays. +

    Client

    *2
    +$3
    +GET
    +$1
    +a
    +
    Server
    $-1
    +
    The server response looks the same as in the inline version. + But what the client sends looks very different: +

    • In this case, the first thing the client sends is '*' followed by the number of elements in the array, + so '*2' indicates that there are 2 elements in the array and they would be found in the following lines. +
    • After that we have '$3' which means we're expecting the first element to be a string of length 3. + Next line is the actual string, in our case is the command 'GET'. +
    • The next value is also a string and is the key passed to the command. +

    That's almost everything we need to start building a client. There's one last thing: error responses. +

    -Example error message
    +-ERR unknown command 'foo'
    +-WRONGTYPE Operation against a key holding the wrong kind of value
    +
    A response that starts with a '-' is considered an error. The first word is the error type. + We'll only gonna be using 'ERR' as a generic response. +

    RESP protocol is what client libraries use to communicate with Redis. + With all that in our toolbox we're ready to start building. +

    Receiving connections

    A crucial part of our serve is the ability to receive client's information. + The way that this is done is that the server listens on a TCP port and waits + for client connections. Let's start building the basic structure. +

    Create a new go module, open main.go and create a main function as follows. +

    package main
    +
    +import (
    +	"bufio"
    +	"fmt"
    +	"log"
    +	"net"
    +	"strconv"
    +	"strings"
    +	"sync"
    +	"time"
    +)
    +
    +var cache sync.Map
    +
    +func main() {
    +	listener, err := net.Listen("tcp", ":6380")
    +	if err != nil {
    +		log.Fatal(err)
    +	}
    +	log.Println("Listening on tcp://0.0.0.0:6380")
    +
    +	for {
    +		conn, err := listener.Accept()
    +		log.Println("New connection", conn)
    +		if err != nil {
    +			log.Fatal(err)
    +		}
    +
    +		go startSession(conn)
    +	}
    +}
    +

    After declaring the package and imports, we create a global sync.Map that would be our cache. + That's where keys are gonna be stored and retrieved. +

    On the main function we start listening on port 6380. After that we have an infinite loop that accepts + new connections and spawns a goroutine to handle the session. +

    Session handling

    // startSession handles the client's session. Parses and executes commands and writes
    +// responses back to the client.
    +func startSession(conn net.Conn) {
    +	defer func() {
    +		log.Println("Closing connection", conn)
    +		conn.Close()
    +	}()
    +	defer func() {
    +		if err := recover(); err != nil {
    +			log.Println("Recovering from error", err)
    +		}
    +	}()
    +	p := NewParser(conn)
    +	for {
    +		cmd, err := p.command()
    +		if err != nil {
    +			log.Println("Error", err)
    +			conn.Write([]uint8("-ERR " + err.Error() + "\r\n"))
    +			break
    +		}
    +		if !cmd.handle() {
    +			break
    +		}
    +	}
    +}
    +

    It's super important that we close the connection when things are done. That's why we set a deferred function, + to close the connection when the session finishes. +

    After that we handle any panics using recover. We do this mainly because at some point we might be reading from + a connection that was closed by the client. And we don't want the entire server to die in case of an error. +

    Then we create a new parser and start trying to parse commands from the live connection. If we encounter an error + we write the error message back to the client and we finish the session. +

    When cmd.handle() returns false (signaling end of session) we break the loop and the session finishes. +

    Parsing commands

    Basic parser structure: +

    // Parser contains the logic to read from a raw tcp connection and parse commands.
    +type Parser struct {
    +	conn net.Conn
    +	r    *bufio.Reader
    +	// Used for inline parsing
    +	line []byte
    +	pos  int
    +}
    +
    +// NewParser returns a new Parser that reads from the given connection.
    +func NewParser(conn net.Conn) *Parser {
    +	return &Parser{
    +		conn: conn,
    +		r:    bufio.NewReader(conn),
    +		line: make([]byte, 0),
    +		pos:  0,
    +	}
    +}
    +

    This is pretty straight-forward. We store a reference to the connection, a reader and then some + attributes that will help us with parsing. +

    The NewParser() function should be used as a contructor for Parser objects. +

    We need some helper functions that will make parsing easier: +

    func (p *Parser) current() byte {
    +	if p.atEnd() {
    +		return '\r'
    +	}
    +	return p.line[p.pos]
    +}
    +
    +func (p *Parser) advance() {
    +	p.pos++
    +}
    +
    +func (p *Parser) atEnd() bool {
    +	return p.pos >= len(p.line)
    +}
    +
    +func (p *Parser) readLine() ([]byte, error) {
    +	line, err := p.r.ReadBytes('\r')
    +	if err != nil {
    +		return nil, err
    +	}
    +	if _, err := p.r.ReadByte(); err != nil {
    +		return nil, err
    +	}
    +	return line[:len(line)-1], nil
    +}
    +

    Also quite simple. +

    • current(): Returns the character being pointed at by pos inside the line.
    • advance(): Point to the next character in the line.
    • atEnd(): Indicates if we're at the end of the line.
    • readLine(): Reads the input from the connection up to the carriage return char. Skips the '\n' char.

    Parsing strings

    In Redis we can send commands like so: +

    SET text "quoted \"text\" here"
    +

    This means we need a way to handle \, " chars inside a string. +

    For that we need a special parsing function that will handle strings: +

    // consumeString reads a string argument from the current line.
    +func (p *Parser) consumeString() (s []byte, err error) {
    +	for p.current() != '"' && !p.atEnd() {
    +		cur := p.current()
    +		p.advance()
    +		next := p.current()
    +		if cur == '\\' && next == '"' {
    +			s = append(s, '"')
    +			p.advance()
    +		} else {
    +			s = append(s, cur)
    +		}
    +	}
    +	if p.current() != '"' {
    +		return nil, errors.New("unbalanced quotes in request")
    +	}
    +	p.advance()
    +	return
    +}
    +

    From the functions that we've declared up to this point it's pretty clear that our parser + will be reading the input line by line. And the consuming the line one char at a time. +

    The way consumeString() works is quite tricky. + It assumes that the initial " has been consumed before entering the function. + And it consumes all characters in the current line up until it reaches the closing quotes character + or the end of the line. +

    Inside the loop we can see that we're reading the current character and advancing the pointer, then + the next character. + When the user is sending an escaped quote inside the string we detect that by checking the current + and the next characters. + In this special case we end up advancing the pointer twice. Because we consumed two: chars the backslash + and the quote. But we added only one char to the output: ". +

    We append all other characters to the output buffer. +

    When the loop finishes, if we're not pointing to the end quote char, that means that the user + sent an invalid command and we return an error. +

    Otherwise we advance the pointer and return normally. +

    Parsing commands

    // command parses and returns a Command.
    +func (p *Parser) command() (Command, error) {
    +	b, err := p.r.ReadByte()
    +	if err != nil {
    +		return Command{}, err
    +	}
    +	if b == '*' {
    +		log.Println("resp array")
    +		return p.respArray()
    +	} else {
    +		line, err := p.readLine()
    +		if err != nil {
    +			return Command{}, err
    +		}
    +		p.pos = 0
    +		p.line = append([]byte{}, b)
    +		p.line = append(p.line, line...)
    +		return p.inline()
    +	}
    +}
    +

    We read the first character sent by the client. If it's an asterisk we handle it + using the RESP protocol. Otherwise we assume that it's an inline command. +

    Let's start by parsing the inline commands first. +

    // Command implements the behavior of the commands.
    +type Command struct {
    +	args []string
    +	conn net.Conn
    +}
    +
    +// inline parses an inline message and returns a Command. Returns an error when there's
    +// a problem reading from the connection or parsing the command.
    +func (p *Parser) inline() (Command, error) {
    +	// skip initial whitespace if any
    +	for p.current() == ' ' {
    +		p.advance()
    +	}
    +	cmd := Command{conn: p.conn}
    +	for !p.atEnd() {
    +		arg, err := p.consumeArg()
    +		if err != nil {
    +			return cmd, err
    +		}
    +		if arg != "" {
    +			cmd.args = append(cmd.args, arg)
    +		}
    +	}
    +	return cmd, nil
    +}
    +

    This is also quite easy to skim through. We skip any leading whitespace + in case the user sent something like ' GET a'. +

    We create a new Command object with a reference to the session connection. +

    While we're not at the end of the line we consume args and append them to the + arg list of the command object if they are not empty. +

    Consuming arguments

    // consumeArg reads an argument from the current line.
    +func (p *Parser) consumeArg() (s string, err error) {
    +	for p.current() == ' ' {
    +		p.advance()
    +	}
    +	if p.current() == '"' {
    +		p.advance()
    +		buf, err := p.consumeString()
    +		return string(buf), err
    +	}
    +	for !p.atEnd() && p.current() != ' ' && p.current() != '\r' {
    +		s += string(p.current())
    +		p.advance()
    +	}
    +	return
    +}
    +

    Same as before we consume any leading whitespace. +

    If we find a quoted string we call our function from before: consumeString(). +

    We append all characters to the output until we reach a carriage return \r, a whitespace + or the end of the line. +

    Parsing RESP protocol

    // respArray parses a RESP array and returns a Command. Returns an error when there's
    +// a problem reading from the connection.
    +func (p *Parser) respArray() (Command, error) {
    +	cmd := Command{}
    +	elementsStr, err := p.readLine()
    +	if err != nil {
    +		return cmd, err
    +	}
    +	elements, _ := strconv.Atoi(string(elementsStr))
    +	log.Println("Elements", elements)
    +	for i := 0; i < elements; i++ {
    +		tp, err := p.r.ReadByte()
    +		if err != nil {
    +			return cmd, err
    +		}
    +		switch tp {
    +		case ':':
    +			arg, err := p.readLine()
    +			if err != nil {
    +				return cmd, err
    +			}
    +			cmd.args = append(cmd.args, string(arg))
    +		case '$':
    +			arg, err := p.readLine()
    +			if err != nil {
    +				return cmd, err
    +			}
    +			length, _ := strconv.Atoi(string(arg))
    +			text := make([]byte, 0)
    +			for i := 0; len(text) <= length; i++ {
    +				line, err := p.readLine()
    +				if err != nil {
    +					return cmd, err
    +				}
    +				text = append(text, line...)
    +			}
    +			cmd.args = append(cmd.args, string(text[:length]))
    +		case '*':
    +			next, err := p.respArray()
    +			if err != nil {
    +				return cmd, err
    +			}
    +			cmd.args = append(cmd.args, next.args...)
    +		}
    +	}
    +	return cmd, nil
    +}
    +

    As we know, the leading asterisk has already been consumed from the connection input. + So, at this point, the first line contains the number of elements to be consumed. + We read that into an integer. +

    We create a for loop with that will parse all the elements in the array. + We consume the first character to detect which kind of element we need to consume: int, string or array. +

    The int case is quite simple, we just read until the rest of the line. +

    The array case is also quite simple, we call respArray() and append the args of the result, + to the current command object. +

    For strings we read the first line and get the size of the string. + We keep reading lines until we have read the indicated amount of characters. +

    Handling commands

    This is the 'fun' part of the implementation. Were our server becomes alive. + In this section we'll implement the actual functionality of the commands. +

    Let's start with the cmd.handle() function that we saw in handleSession(). +

    // handle Executes the command and writes the response. Returns false when the connection should be closed.
    +func (cmd Command) handle() bool {
    +	switch strings.ToUpper(cmd.args[0]) {
    +	case "GET":
    +		return cmd.get()
    +	case "SET":
    +		return cmd.set()
    +	case "DEL":
    +		return cmd.del()
    +	case "QUIT":
    +		return cmd.quit()
    +	default:
    +		log.Println("Command not supported", cmd.args[0])
    +		cmd.conn.Write([]uint8("-ERR unknown command '" + cmd.args[0] + "'\r\n"))
    +	}
    +	return true
    +}
    +

    Needs no further explanation. Let's implement the easiest command: QUIT. +

    // quit Used in interactive/inline mode, instructs the server to terminate the connection.
    +func (cmd *Command) quit() bool {
    +	if len(cmd.args) != 1 {
    +		cmd.conn.Write([]uint8("-ERR wrong number of arguments for '" + cmd.args[0] + "' command\r\n"))
    +		return true
    +	}
    +	log.Println("Handle QUIT")
    +	cmd.conn.Write([]uint8("+OK\r\n"))
    +	return false
    +}
    +

    If any extra arguments were passed to QUIT, it returns an error. +

    Otherwise write +OK to the client and return false. + Which if you remember handleSession() is the value to indicate that the session has finished. + After that the connection will be automatically closed. +

    The next easieast command is DEL +

    // del Deletes a key from the cache.
    +func (cmd *Command) del() bool {
    +	count := 0
    +	for _, k := range cmd.args[1:] {
    +		if _, ok := cache.LoadAndDelete(k); ok {
    +			count++
    +		}
    +	}
    +	cmd.conn.Write([]uint8(fmt.Sprintf(":%d\r\n", count)))
    +	return true
    +}
    +

    Iterates through all the keys passed, deletes the ones that exists and + writes back to the client the amount of keys deleted. +

    Returns true, which means the connection is kept alive. +

    Handling GET

    // get Fetches a key from the cache if exists.
    +func (cmd Command) get() bool {
    +	if len(cmd.args) != 2 {
    +		cmd.conn.Write([]uint8("-ERR wrong number of arguments for '" + cmd.args[0] + "' command\r\n"))
    +		return true
    +	}
    +	log.Println("Handle GET")
    +	val, _ := cache.Load(cmd.args[1])
    +	if val != nil {
    +		res, _ := val.(string)
    +		if strings.HasPrefix(res, "\"") {
    +			res, _ = strconv.Unquote(res)
    +		}
    +		log.Println("Response length", len(res))
    +		cmd.conn.Write([]uint8(fmt.Sprintf("$%d\r\n", len(res))))
    +		cmd.conn.Write(append([]uint8(res), []uint8("\r\n")...))
    +	} else {
    +		cmd.conn.Write([]uint8("$-1\r\n"))
    +	}
    +	return true
    +}
    +

    As before, we validate that the correct number of arguments were passed to the command. +

    We load the value from the global variable cache. +

    If the value is nil we write back to the client the special $-1. +

    When we have a value we cast it as string and unquote it in case it's quoted. + Then we write the length as the first line of the response and the string as the + second line of the response. +

    Handling SET

    This is the most complicated command that we'll implement. +

    // set Stores a key and value on the cache. Optionally sets expiration on the key.
    +func (cmd Command) set() bool {
    +	if len(cmd.args) < 3 || len(cmd.args) > 6 {
    +		cmd.conn.Write([]uint8("-ERR wrong number of arguments for '" + cmd.args[0] + "' command\r\n"))
    +		return true
    +	}
    +	log.Println("Handle SET")
    +	log.Println("Value length", len(cmd.args[2]))
    +	if len(cmd.args) > 3 {
    +		pos := 3
    +		option := strings.ToUpper(cmd.args[pos])
    +		switch option {
    +		case "NX":
    +			log.Println("Handle NX")
    +			if _, ok := cache.Load(cmd.args[1]); ok {
    +				cmd.conn.Write([]uint8("$-1\r\n"))
    +				return true
    +			}
    +			pos++
    +		case "XX":
    +			log.Println("Handle XX")
    +			if _, ok := cache.Load(cmd.args[1]); !ok {
    +				cmd.conn.Write([]uint8("$-1\r\n"))
    +				return true
    +			}
    +			pos++
    +		}
    +		if len(cmd.args) > pos {
    +			if err := cmd.setExpiration(pos); err != nil {
    +				cmd.conn.Write([]uint8("-ERR " + err.Error() + "\r\n"))
    +				return true
    +			}
    +		}
    +	}
    +	cache.Store(cmd.args[1], cmd.args[2])
    +	cmd.conn.Write([]uint8("+OK\r\n"))
    +	return true
    +}
    +

    As always, first thing we do is validate the number of arguments. + But in this case, SET is more tricky than the others. +

    When more than 3 arguments are passed we check for the NX or XX flags and handle them accordingly. +

    • NX -- Only set the key if it does not already exist.
    • XX -- Only set the key if it already exist.

    Then we parse the expiration flags if any. We'll see how that's done in a second. +

    After handling all those special cases we finally store the key and value in the cache, + write the +OK response and return true to keep the connection alive. +

    Expiration

    // setExpiration Handles expiration when passed as part of the 'set' command.
    +func (cmd Command) setExpiration(pos int) error {
    +	option := strings.ToUpper(cmd.args[pos])
    +	value, _ := strconv.Atoi(cmd.args[pos+1])
    +	var duration time.Duration
    +	switch option {
    +	case "EX":
    +		duration = time.Second * time.Duration(value)
    +	case "PX":
    +		duration = time.Millisecond * time.Duration(value)
    +	default:
    +		return fmt.Errorf("expiration option is not valid")
    +	}
    +	go func() {
    +		log.Printf("Handling '%s', sleeping for %v\n", option, duration)
    +		time.Sleep(duration)
    +		cache.Delete(cmd.args[1])
    +	}()
    +	return nil
    +}
    +

    We read the option and the expiration value, then we compute the duration + for each case and we spawn a new goroutine that sleeps for that amount of + time and the deletes the key from the cache. +

    This is not the most efficient way to do it, but it's simple and it works for us. +

    Working server

    At this point we have an usable implementation of Redis. +

    Let's start the server the server and test it. +

    $ go run main.go
    +2023/04/08 21:09:40 Listening on tcp://0.0.0.0:6380
    +

    On a different terminal connect to the server. +

    $ telnet 127.0.0.1 6380
    +GET a
    +$-1
    +set a "test \"quotes\" are working"
    ++OK
    +get a
    +$25
    +test "quotes" are working
    +

    It's alive!! Go have fun. +

    If you'd like to access the source code of this project there's a public gist + containing all of the code displayed here. +

    Link to source code

    Miguel LiezunIn this post we're going to write a basic Redis clone in Go that implements the most simple commands: GET, +SET, DEL and QUIT. At the end you'll know how to parse a byte stream from a live TCP connection, and hopefully have a working +implementation of Redis. +
    How to write a program that can replicate itself +https://mliezun.github.io/2022/11/26/grotsky-quine.html +2022-11-26 +

    Reading time: +

    How to write a program that can replicate itself

    Grotsky is a toy programming language that I made for fun. Today we're visinting the concept of Quines, +a.k.a. self replicating programs. It's said that any turing-complete language should be able to write a program that replicates + itself. And grotsky is no exception.

    Read more about grotsky in previous blogposts: +

    Quines are very easy to write. The language that you're using needs to be able to do a couple things: +

    • Write to a file or stdout (print)
    • Support for string arrays
    • Translate numbers/integers to character ascii representation
    • Concatenate strings
    • Loop through arrays from arbitrary indexes

    Super simple quine: less than 30 lines of code

    let tabChar = 9
    +let quoteChar = 34
    +let commaChar = 44
    +let code = [
    +	"let tabChar = 9",
    +	"let quoteChar = 34",
    +	"let commaChar = 44",
    +	"let code = [",
    +	"]",
    +	"for let i = 0; i < 4; i = i+1 {",
    +	"	io.println(code[i])",
    +	"}",
    +	"for let i = 0; i < code.length; i = i+1 {",
    +	"	io.println(strings.chr(tabChar) + strings.chr(quoteChar) + code[i] + strings.chr(quoteChar) + strings.chr(commaChar))",
    +	"}",
    +	"for let i = 4; i < code.length; i = i+1 {",
    +	"	io.println(code[i])",
    +	"}",
    +]
    +for let i = 0; i < 4; i = i+1 {
    +	io.println(code[i])
    +}
    +for let i = 0; i < code.length; i = i+1 {
    +	io.println(strings.chr(tabChar) + strings.chr(quoteChar) + code[i] + strings.chr(quoteChar) + strings.chr(commaChar))
    +}
    +for let i = 4; i < code.length; i = i+1 {
    +	io.println(code[i])
    +}
    +
    +

    Now we can use grotksy cli to run the program and compare the output to the original source. +

    Save the original source to a file called +quine.gr then run the following commands: +

    +$ grotsky quine.gr > quine_copy.gr
    +$ cmp quine.gr quine_copy.gr
    +$ echo $?
    +0
    +
    +

    If you see a 0 as the final output that means the files are the same. + Otherwise if you saw an error message or a different output, that means something has gone wrong. +

    How exciting is this?!! +We've just written a program that gives itself as an output. +That sounds impossible when you hear it for the first time. But it was actually pretty easy! +

    Source code available here: +https://gist.github.com/mliezun/c750ba701608850bd86d646a3ebf700f. +

    Grotsky cli binary available here: +https://github.com/mliezun/grotsky/releases/tag/v0.0.6

    Miguel LiezunGrotsky is a toy programming language that I made for fun. Today we're visinting the concept of Quines, +a.k.a. self replicating programs. It's said that any turing-complete language should be able to write a program that replicates + itself. And grotsky is no exception. +
    Migrate from Heroku to Fly.io +https://mliezun.github.io/2022/09/22/heroku-to-fly.html +2022-09-22 +

    Reading time: +

    How to migrate from Heroku to Fly.io

    A couple weeks ago Heroku announced the removal. I have plenty of projects running on free dynos. + I have taken some time to move my code to Fly.io. And also I've written a little tutorial of how to perform the migration.

    I'll use one of my public repos as an example +https://github.com/mliezun/getmyip. It's a simple service that returns the IP from which you're making the request. It's useful when you want to know + your public IP. +

    That project ^ was covered in a previous +blogpost. +

    Migration instructions

    The first thing we need to do is to remove heroku from the remotes. +Inside your project run: +

    git remote remove heroku

    If you have a heroku.yml file, delete it. +

    rm -rf heroku.yml

    Then, we're ready to start using fly. +There are tutorials on the +official fly.io docs for many frameworks and languages. We're going to be following the one for a Docker app, + since it's the most general case. +

    First thing you need to do is create an account in +Fly.io if you don't have one yet. +

    Once you created your account, install the flyctl command line tool. + After that, login by running the following command: +

    flyctl auth login

    After you've logged in to your account, you're ready to launch your application. + Execute the next command and follow the interactive setup. +

    $ flyctl launch
    +Scanning source code
    +Detected a Dockerfile app
    +? App Name (leave blank to use an auto-generated name): 
    +Automatically selected personal organization: Miguel Liezun
    +? Select region: mia (Miami, Florida (US))
    +Created app morning-breeze-4255 in organization personal
    +Wrote config file fly.toml
    +? Would you like to set up a Postgresql database now? No
    +? Would you like to deploy now? Yes
    +Deploying morning-breeze-4255
    +==> Validating app configuration
    +--> Validating app configuration done
    +Services
    +TCP 80/443 -> 8080
    +Remote builder fly-builder-green-pond-8004 ready
    +==> Creating build context
    +--> Creating build context done
    +==> Building image with Docker
    +--> docker host: 20.10.12 linux x86_64
    +...
    +

    Make sure your app listens to port 8080, that's the default for fly apps. + You can change the port inside the file fly.toml if you want +, just search for the internal port and change it. Remember to run launch again if you change the port. +

    # fly.toml file generated for morning-breeze-4255 on 2022-09-21T21:50:20-03:00
    +
    +app = "morning-breeze-4255"
    +kill_signal = "SIGINT"
    +kill_timeout = 5
    +processes = []
    +
    +[env]
    +
    +[experimental]
    +  allowed_public_ports = []
    +  auto_rollback = true
    +
    +[[services]]
    +  http_checks = []
    +  internal_port = 8080 # -< Put your desired port here
    +# ...
    +

    Finally, you only need to open the app and enjoy! +You migrated your first app from heroku to fly :-) +

    $ flyctl open
    +opening http://morning-breeze-4255.fly.dev ...
    +

    Access the newly deployed 'getmyip' service using the link +http://morning-breeze-4255.fly.dev. +

    Miguel LiezunA couple weeks ago Heroku announced the removal. I have plenty of projects running on free dynos. + I have taken some time to move my code to Fly.io. And also I've written a little tutorial of how to perform the migration. +
    Branchable MySQL: Managing multiple dev environments +https://mliezun.github.io/2022/09/20/branchable-mysql.html +2022-09-20 +

    Reading time: +

    Branchable MySQL: Managing multiple dev environments

    When teams start to grow, having a single dev environment becomes an issue. People start stepping on each others toes. +A common problem is that two people want to apply incompatible migrations on the database. That problem is impossible +to fix if folks are working on parallel branches. +If we can have a database for each branch of a project, that will remove much of the pain of having multiple devs applying +changes to the db.

    There are already projects that solve this problem: +PlanetScale and +Neon. +

    A common case where this problem arises is when two devs want to add a column to the same table. +

    Two devs applying changes to the same table in the database.

    We have a +people table in the database. One of the devs wants to add the +last_name column and the other one wants to add the +address. +

    Dev1's code thinks the table will have 3 columns after he applies his operation: +id, name, last_name. +

    Dev2's code also thinks the table will have 3 columns: +id, name, address. +

    In reality the table will have 4 columns. +So neither of them will be able to run their code unless they talk to each other and figure out how to make this work. +

    This is far from ideal. +

    What we want instead, is that each one of them can develop their features independently. +

    Two devs applying changes to the same table in different database branches.

    They both apply to the same table, but each table lives on an instance that was 'replicated' from the original. +

    How can we implement the ideal case?

    MySQL writes data (by default) to the directory +/var/lib/mysql/data. +

    We can use an +Union filesystem. And configure MySQL to use a different directory to read and write data. +

    That way we can have a feature/user-last-name 'branch' read and write data from a directory like +/app/user-last-name/mysql/data. +

    And a feature/user-address 'branch' read and write data from a directory like +/app/user-address/mysql/data. +

    Those branches can be mounted using fuse-overlayfs by executing the following commands: +

    +# Directory /app/base contains data from the original branch
    +
    +fuse-overlayfs -o lowerdir=/app/base,upperdir=/app/user-last-name,workdir=/tmp/user-last-name overlayfs /app/user-last-name
    +
    +fuse-overlayfs -o lowerdir=/app/base,upperdir=/app/user-address,workdir=/tmp/user-address overlayfs /app/user-address
    +
    +

    This means both 'branches' of the database are able to coexist and have different schemas during their lifetime. +

    Experimenting with a use case

    I had this idea in my head for months. I finally convinced myself that it was worth a shot. +

    I decided to do a little implementation using Docker and python FastAPI. +Exposing a simple interface so that it's easy to create and delete branches. +

    The project is live on github +branchable-mysql. +

    The container image is published on Docker Hub +branchable-mysql. +

    To start using the image let's create a docker-compose.yml file. +

    version: "3"
    +
    +services:
    +  mysql:
    +    image: mliezun/branchable-mysql
    +    platform: linux/amd64
    +    privileged: true
    +    restart: always
    +    volumes:
    +      - appdata:/app/
    +
    +volumes:
    +  appdata:
    +
    +

    Then you can execute +docker-compose up and the MySQL server should start running. +

    After that, connect easily to the db +docker compose exec mysql mysql -uroot -h127.0.0.1 --skip-password -P33061. You should enter to an interactive mysql console. +

    Let's create an initial schema, a table and insert some data so that we can see how branching works. +On the console that we just opened execute: +

    +mysql> create schema s1;
    +Query OK, 1 row affected (0.01 sec)
    +
    +mysql> use s1;
    +Database changed
    +mysql> create table people (id int primary key auto_increment, name varchar(255) not null);
    +Query OK, 0 rows affected (0.07 sec)
    +
    +mysql> insert into people select 0, 'Miguel';
    +Query OK, 1 row affected (0.02 sec)
    +Records: 1  Duplicates: 0  Warnings: 0
    +
    +mysql> select * from people;
    ++----+--------+
    +| id | name   |
    ++----+--------+
    +|  1 | Miguel |
    ++----+--------+
    +1 row in set (0.00 sec)
    +
    +

    That's enough for now, we're ready to start creating branches. +

    On a separate terminal, without closing the previous mysql interactive console, execute: +

    +docker compose exec mysql /app/scripts/create_branch.sh base feature/user-last-name
    +
    +{"branch_name":"feature/user-last-name","base_branch":"base","port":33062}
    +
    +

    Now you can login to the new database branch using port 33062 +docker compose exec mysql mysql -uroot -h127.0.0.1 --skip-password -P33062

    +mysql> use s1;
    +Reading table information for completion of table and column names
    +You can turn off this feature to get a quicker startup with -A
    +
    +Database changed
    +mysql> alter table people add column last_name varchar(255) not null;
    +Query OK, 0 rows affected (0.03 sec)
    +Records: 0  Duplicates: 0  Warnings: 0
    +
    +mysql> select * from people;
    ++----+--------+-----------+
    +| id | name   | last_name |
    ++----+--------+-----------+
    +|  1 | Miguel |           |
    ++----+--------+-----------+
    +1 row in set (0.00 sec)
    +
    +

    In a new terminal we can create a another branch: +

    +docker compose exec mysql /app/scripts/create_branch.sh base feature/user-address
    +
    +{"branch_name":"feature/user-address","base_branch":"base","port":33063}
    +
    +

    Then connect using port 33063 +docker compose exec mysql mysql -uroot -h127.0.0.1 --skip-password -P33063

    +mysql> use s1;
    +Reading table information for completion of table and column names
    +You can turn off this feature to get a quicker startup with -A
    +
    +Database changed
    +mysql> alter table people add column last_name varchar(255) not null;
    +Query OK, 0 rows affected (0.03 sec)
    +Records: 0  Duplicates: 0  Warnings: 0
    +
    +mysql> select * from people;
    ++----+--------+
    +| id | name   |
    ++----+--------+
    +|  1 | Miguel |
    ++----+--------+
    +1 row in set (0.00 sec)
    +
    +mysql> alter table people add column address varchar(255) not null;
    +Query OK, 0 rows affected (0.02 sec)
    +Records: 0  Duplicates: 0  Warnings: 0
    +
    +mysql> select * from people;
    ++----+--------+---------+
    +| id | name   | address |
    ++----+--------+---------+
    +|  1 | Miguel |         |
    ++----+--------+---------+
    +1 row in set (0.00 sec)
    +
    +

    As you can see, we have 3 servers running at the same time, each one with different schemas. +

    This is great for local development and for having branch-aware dev environments. +

    Final thoughts

    I hope you find this blogpost useful. +If you want to start using branchable-mysql go ahead. +If you encounter any issues please report them in the github repo or create a pull request. +

    Miguel LiezunWhen teams start to grow, having a single dev environment becomes an issue. People start stepping on each others toes. +A common problem is that two people want to apply incompatible migrations on the database. That problem is impossible +to fix if folks are working on parallel branches. +If we can have a database for each branch of a project, that will remove much of the pain of having multiple devs applying +changes to the db. +
    Webscraping as a side project +https://mliezun.github.io/2022/07/20/cloud-outdated-release.html +2022-07-20 +

    Reading time: +

    Webscraping as a side project

    A friend and I were looking for a side project to work together. We realized we both faced a similar problem. +

    Let's use AWS Lambda Python runtime as an example. +AWS will send out emails when a version is at the end of life making it difficult to stay +on the latest if desired. +Plus, reacting to them usually means you are many many versions behind already. +

    Our journey started. +We made a list of providers for the MVP: AWS, GCP and Azure. +Then a list of the services that have versions (for example S3 doesn't have versions). +After that we realized that we could get some versions using APIs. +Other services exclusively require webscraping. +

    We support 46 services and counting. +Take a look at +Cloud Outdated and subscribe to get notified. +If you are looking for a service that's not there +contact us. +

    Picking a language, framework and platform

    We're both Python programmers. The choice was obvious. +"Let's use Python and Django framework for the job" we said. +We didn't want to spend our innovation tokens on new language/framework. +So we chose Boring Technology. +

    For the db we spent our first innovation token. +We decided to go with the flashy new serverless postgres-compatible +CockroachDB. +

    On the hosting side we're using AWS Lambda. Taking advantage of the free compute time. +Helps mantaining the costs down. +

    Make webscraping reliable

    A webpage that's being scraped can change at any time. First thing we did was account for those edge cases. +We created a custom exception that is triggered when something changed. So that we can react to that downstream. +

    +class ScrapingError(Exception):
    +    pass
    +
    +
    We wanted to keep the implementation simple. Each service is scraped by a single function. +The signature of the function is something like +aws_lambda_python() -> List[Version]. +All the implementations follow a similar pattern: +
    +def aws_lambda_python():
    +    # Read versions from aws docs website:
    +    # https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html
    +
    +    if not found_versions:
    +        raise ScrappingError
    +
    +    # Process and return versions
    +
    +
    That's ^ what we call a poll function. +

    We pass poll functions through a polling class that handles all the errors and results. +When we detect an scraping error we have a special case. We send an email with the details +of what happened. Because the problem is something that requires manual action. We receive +that email in our personal inboxes and fix the problem ASAP. +

    The poll class that handles all the magic behind cloud outdated is actually very simple: +

    +class PollService:
    +    def __init__(self, service: Service, poll_fn: Callable):
    +        self.poll_fn = poll_fn
    +        # Some other attributes...
    +
    +    def poll(self):
    +        try:
    +            results = self.poll_fn()
    +            self.process_results(results)
    +        except ScrapingError as e:
    +            notify_operator(
    +                f"{type(e).__name__} at line {e.__traceback__.tb_lineno} of {__file__}: {e.__str__()}"
    +            )
    +
    +    def process_results(self, results):
    +        # if results contains new versions:
    +        #     save new versions to db
    +        # if results contains deprecated versions:
    +        #     set versions in db as depreacted
    +
    +

    That's the hearth of Cloud Outdated. +After that we have to send notifications to subscribed users. +That part is trivial. +We send an email that contains the difference between what was last sent to a user and what we +have stored in the db at the moment. +

    Last toughts

    Having a side project is usually a good idea. +For us has been a journey were we got to know some new stuff (CockroachDB). +We also learned about how to build a product and keep a MVP mentality. +The most difficult challenge we face is to bring more users to the platform. +

    We'd love to see more people subscribed. +If this blogpost sparked your interest go to +Cloud Outdated and subscribe to start getting emails. +

    See you next time! +

    Miguel LiezunCloud Outdated is a personalized digest of updates for cloud services. Works like a newsletter where you can choose + which services you want to get notified about. For example: Subscribe to AWS Lambda with Python runtime, and you'll get an email + when 3.10 is supported. +
    Blake3 hash plugin for MySQL written in Rust +https://mliezun.github.io/2022/05/05/rust-blake-mysql.html +2022-05-05 +

    Reading time: +

    Implementing a blake3 hash plugin for MySQL in Rust

    It's been long since I've written something, I wanted to bring you some new stuff, so here you have a short blog post. +I encourage you to try this new plugin that uses this +blake3 hash implementation. +Blake3 is secure, unlike MD5 and SHA-1. And secure against length extension, unlike SHA-2. +Start using it and create an issue in the github repo if you would like a feature implemented! +

    How to use

    Download and install MySQL plugin

    $ wget 'https://github.com/mliezun/blake-udf/releases/download/v0.1.0/libblake_udf.so'
    +$ mv libblake_udf.so /usr/lib/mysql/plugin/
    +

    Load UDF in MySQL

    $ mysql -uroot -p -e 'create function blake3_hash returns string soname "libblake_udf.so";'
    +

    Execute function

    $ mysql --binary-as-hex=0 -uroot -p -e 'select blake3_hash("a");'
    +
    Output: 17762fddd969a453925d65717ac3eea21320b66b54342fde15128d6caf21215f +
    Miguel LiezunUsing Rust to create a MySQL plugin that implements blake3 hash. +
    Playing with Javascript Proxies (getters/setters) +https://mliezun.github.io/2021/12/31/playing-with-js.html +2021-12-31 +

    Reading time: +

    Playing with Javascript Proxies (getters/setters)

    Overview

    Happy New Year!

    This is my final post for the 2021. This year I didn't post that much, but a lot of work was put into + the blog to rewrite it using +Grotksy. +I hope everyone has a great 2022 and that next year is much better than the last one. +

    The inspiration for this blog post comes from the idea of building a tiny db that feels more natural to Javscript. +All the databases that I've seen make a heavy use of methods like: +db.get(), +db.put(), +db.scan(), +db.query(). + And many others that Im sure you have seen. +I think it would be great to see something like: +

    const db = getDb("...")
    +// Create new user
    +const u = {username: "jdoe", email: "jdoe@example.com", id: 100}
    +// Store new user in the database
    +db.objects.users[u.username] = u
    +// Commit the changes to the database
    +db.actions.save()
    +

    In this blog post we will be building a much simpler version that stores everything in memory. Each change + made to the objects will be stored in a log (called layers) and the final object will be composed of all + the small layers present in the log. +

    Defining a proxy

    We need to implement some generic getters/setters. +

    const objects = new Proxy({}, {
    +    get: function(obj, prop) {
    +        validate(prop, null)
    +        // Implementation
    +    },
    +    set: function(obj, prop, value) {
    +        validate(prop, value)
    +        // Implementation
    +    }
    +})
    +

    Let's define the validation function. In this case we want the objects to be able to be serialized to JSON. +

    +const validate = (prop, value) => {
    +    // Make sure that the property and value are serializable
    +    // JSON.stringify throws an error if not serializable
    +    const l = {}
    +    l[prop] = value
    +    JSON.stringify(l)
    +    return l
    +}
    +
    +

    This empty proxy will validate that the values and prop are serializable and do nothing else. Now we can start +building on top of it. +

    Building a tree to hold everything together

    We need a root object where we will store all the changes that are applied to an object. +We will have a sort of tree structure to hold everything together. +It will look something like this: +

    +              rootObject({})  -> layers([{users: {jdoe: ...}}, {tokens: {tk1: ...}}])
    +                    |
    +        --------------------------
    +        |                        |
    + child(.users{})          child(.tokens{})
    +        |                        |
    +       ...                      ...
    +
    +

    The root object contains the layers with all the changes made from the beginning of the existence of the object. +Each time a property of the root object is accessed a child is returned that internally holds a reference to the root. +This way we can go through the entire chain of access and be able to reach the stored layers. +By chain of access I mean the following: objects.users.jdoe.metadata.login.ip. +As you can see, we need to traverse through many objects to be able to reach the ip field. But the layer that contains + the information is only stored in the root, so each child needs to mantain a reference to the parent to be able to reach + the root node. +

    Let's define a simple function to be able to create a new rootObject. +

    +const wrapObject = (parent, key, current) => {
    +    const rootObj = {
    +        parent: Object.freeze(parent),
    +        layers: [Object.freeze({'value': current, 'previous': null})],
    +        pushLayer (l) {}, // Push new layer
    +        getLayer (ks) {}, // Get layer where information is stored based on given keys
    +        getValue (k) {} // Get value that matches given key
    +    }
    +
    +    const rootProxy = {
    +        get: function(obj, prop) {
    +            validate(prop, null)
    +            const val = rootObj.getValue(prop)
    +            if (typeof val == 'object') {
    +                // If the value is an object we need to have a child instance
    +                // with a reference to the parent
    +                return wrapObject(rootObj, prop, val).objects
    +            }
    +            // If the value is other kind like a number or string we can safely return that
    +            return val
    +        },
    +        set: function(obj, prop, value) {
    +            const l = validate(prop, value)
    +            // Add new layer to the rootObj
    +            rootObj.pushLayer({'value': l})
    +        }
    +    }
    +
    +    return {
    +        actions: {
    +            revert () {
    +                // Deleting the last layer will revert the changes
    +                const pop = rootObj.layers[rootObj.layers.length-1]
    +                rootObj.layers.splice(rootObj.layers.length-1, rootObj.layers.length)
    +                return pop
    +            }
    +        },
    +        objects: new Proxy({}, rootProxy)
    +    }
    +}
    +
    +

    Handling layers

    The layer format: +

    +const layer = {
    +    value: {status: 'active'},
    +    previous: null // Reference to a previous layer that has the key 'status' in it
    +}
    +
    +
    The layers are stored in an array, each layer holds the value and a reference to the previous layer + that set a value for the same key (in this case the key was 'status'). Also the layers form a simple + linked list through the 'previous' reference. That way we have the entire history of a given key. +

    We would need a function to be able to tell if an object has a list of nested keys. Trust me for now, you'll see. +

    +const nestedExists = (obj, ks) => {
    +    for (let j = 0; j < ks.length; j++) {
    +        let k = ks[j];
    +        if (!(k in obj)) {
    +            return false
    +        }
    +        obj = obj[k]
    +    }
    +    return true
    +}
    +
    +
    In this function we receive an object and a list of keys, we start accessing the first internal object with the first key + and we keep doing the same till we make sure that all the keys are present. +

    Now we're almost done. Let's define the functions for handling the store and retrieval of layers. +

    +const rootObj = {
    +    parent: Object.freeze(parent),
    +    layers: [Object.freeze({'value': current, 'previous': null})],
    +    pushLayer (l) {
    +        // If this is a child object we need to build the entire chain of access
    +        // from the bottom up
    +        if (parent) {
    +            const ll = {}
    +            ll[key] = l['value']
    +            // Search for a previous layer modifying the same key
    +            const previous = parent.getLayer([key])
    +            // Tell the parent object to push the new layer
    +            parent.pushLayer(Object.freeze({'value': ll, previous}))
    +        } else {
    +            // We are in the root object, add the layer to the array
    +            this.layers.push(Object.freeze(l))
    +        }
    +    },
    +    getLayer (ks) {
    +        // Search through the entire list of layers to see if one contains all the keys
    +        // that we are looking for. Start from the end of the array (top of the stack)
    +        for (let i = this.layers.length - 1; i >= 0; i--) {
    +            let v = nestedExists(this.layers[i]['value'], ks)
    +            if (v) {
    +                return this.layers[i]
    +            }
    +        }
    +        if (parent) {
    +            // If we are in a child object, look through all the previous layers
    +            // and see if the key we're looking for is contained in one of them.
    +            let ll = parent.getLayer([key].concat(ks))
    +            while (ll) {
    +                let a = nestedExists(ll['value'][key], ks)
    +                if (a) {
    +                    return Object.freeze({'value': ll['value'][key]})
    +                }
    +                ll = ll.previous
    +            }
    +        }
    +    },
    +    getValue (k) {
    +        // Straightforward, get layer and return value
    +        const l = this.getLayer([k])
    +        if (l) {
    +            return Object.freeze(l['value'][k])
    +        }
    +    }
    +}
    +
    +
    That's all we need. We can create a new object and start adding and modifying properties. Each change will be added + to the end of the log and worked out when a property is accessed. +

    Wrapping Up

    Let's try the final result. The source code is loaded in this page, so you can open a dev console in the browser and + try for yourself. +

    +const store = wrapObject(null, null, {})
    +
    +// Create new user
    +const user = {username: 'jdoe', email: 'jdoe@example.com', name: 'John Doe', id: 100}
    +
    +// Add new user
    +store.objects.users = {}
    +store.objects.users[user.username] = user
    +
    +// Print user email
    +console.log(store.objects.users.jdoe.email)
    +
    +// Change user email and print
    +store.objects.users.jdoe.email = 'jdoe2@example.com'
    +console.log(store.objects.users.jdoe.email)
    +
    +// Revert last change and print email again
    +store.actions.revert()
    +console.log(store.objects.users.jdoe.email)
    +
    +

    That's it for now. We defined a Javascript object that contains the entire history of changes that were made to itself. +And at any point we can revert the changes and go back to a previous state. +Everything is stored in an array and is easily serializable. +If we wanted to take this to the next level, each change could be written to a persistence storage (s3, sqlite, mysql, ...) +

    The full source code is available in a +public gist. +

    Miguel LiezunIn this last post of the year I play with proxies in an attempt to create a Javascript object where changes are appended + to a log and can be reverted by deleting the last element of the log using getters and setters. +
    I created a programming language and this blog is powered by it +https://mliezun.github.io/2021/10/04/new-blog-engine.html +2021-10-04 +

    Reading time: +

    I created a programming language and this blog is powered by it

    Why did I do it?

    Mostly for fun.

    If you follow my blog or take a look at some of the posts that I made, you will see that I was +building a programming language called +Grotksy. Just a toy programming language that I made based on the book +Crafting Interpreters, which I totally recommend buying and reading if you haven't yet. +

    I wanted to build something interesting but simple enough that could be made with Grotsky. +I tought that replacing Jekyll with my own engine was a task worth a try. +There is nothing groundbreaking or innovative being made here, just a little experimentation. +

    I have to give credit to the project +lith, because the 'templating' engine for the blog is inspired by it. +

    How did I do it?

    That's a good question.

    Originally, this blog was powered by Jekyll, that translated markdown to html and hosted + by Github Pages. I decided that I was going to build a templating engine and generate html + to keep things simple. +

    But also, as a challenge I made a simple HTTP server to use as a dev server when trying the blog locally. +

    HTTP Server

    For the purpose of having a custom HTTP Server I had to add support for TCP sockets to the language. +I wrapped the go standard library in some functions and exposed that to the Grotsky language. +In grotsky looks something like this +

    +let socket = net.listenTcp(host + ":" + port)
    +let conn
    +while true {
    +	try {
    +		conn = socket.accept()
    +	} catch err {
    +		io.println("Error accepting new connection", err)
    +		continue
    +	}
    +	try {
    +		# Call function that handles connection
    +		handleConn(conn)
    +	} catch err {
    +		io.println("Error handling connection", err)
    +	}
    +	try {
    +		conn.close()
    +	} catch err {}
    +}
    +				
    +

    This means that the server listens on a socket, accepts connections, writes some text/bytes to the connection + and then closes the connection. +

    Template Engine

    The templating engine is built using the native support Grotsky provide for lists. +A regular page for the blog looks like this: +

    +let base = import("../base.gr")
    +# Create new Post Object
    +let post = base.Post(
    +	"Title",
    +	"Brief description of blog post.",
    +	"Author Name",
    +	"descriptive,tags",
    +	[
    +		[
    +			"h2",
    +			[],
    +			[
    +				"Title"
    +			]
    +		],
    +		[
    +			"div",
    +			["class", "content"],
    +			[
    +				"Content line 1",
    +				"Content line 2",
    +			]
    +		]
    +	]
    +)
    +			

    It's pretty straightforward: the first element of the list is the html tag, the second is +an array of properties for the tag and the last one is a list that contains what will be +the *content* of enclosed by the tags. +

    Resources

    If you want to take a peek, the source code for both projects is available on github: +

    Miguel LiezunI created my own programming language using Go, then I built a blog engine and used that engine to build this blog. +
    Mlisp: My own lisp implementation compiled to WASM +https://mliezun.github.io/2021/04/01/mlisp-wasm.html +2021-04-01 +

    Reading time: +

    + +

    Mlisp, My own lisp implementation +

    +Mlisp a tiny lispy language based on the book Build Your Own Lisp. + +The interpreter is written in C and compiled directly to WASM. You can try it in this page by openning the developer console of your browser and typing Mlisp.interpret("+ 2 2") or using the repl shown below. + + +

    Interface +

    +To be able to access C functions from your browser you have to export them. Let's see how we can define a function that is exported. + +
    +#if __EMSCRIPTEN__
    +EMSCRIPTEN_KEEPALIVE
    +#endif
    +int mlisp_init();
    +
    + +When compilen with emcc the emscripten compiler to wasm, you have to add EMSCRIPTEN_KEEPALIVE macro before your function so it doesn't get optimized away. + +The exported functions in this project are: + +
    +int mlisp_init();
    +char *mlisp_interpret(char *input);
    +void mlisp_cleanup();
    +
    + +The project is then compiled with: + +
    +emcc -std=c99  -Wall -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]'
    +
    + +That means that you would be able to access the exported functions using a cwrap that let's you wrap a C function call from a Javascript function call. + +This compilation generates two files mlisp.js and mlisp.wasm. + +The javascript file defines a Module that provides useful tool to access exported functions. + + +

    Let's start using it +

    +
    +const Mlisp = {
    +    init: Module.cwrap('mlisp_init', 'number', []),
    +    interpret: Module.cwrap('mlisp_interpret', 'string', ['string']),
    +    cleanup: Module.cwrap('mlisp_cleanup', 'void', []),
    +};
    +
    +// Init interpreter
    +Mlisp.init();
    +
    +// Run some commands
    +console.log(Mlisp.interpret("+ 2 2"));
    +
    +// Cleanup interpreter
    +Mlisp.cleanup();
    +
    + + +

    Automated Build & Release from github +

    +I made a github workflow for this project to automatically build and release so you can retrieve them from Github. + + + +

    REPL +

    + + + + +
    +
    + + Input some commands"> +
    +
    + + + + + + +

    Interesting commands to try out +

    +
  • foldl: Fold left (same as reduce left) + - (foldl + 0 {1 2 3 4 5}): Sum of elements +
  • filter + - (filter (\ {e} {> e 3}) {1 2 3 4 5 6}): Elements bigger than 3 +
  • map + - (foldl * 1 (map (\ {e} {* e 2}) {1 1 1 1 1})): Multiply elements by 2 and then multiply all elements + + +
  • Miguel LiezunLisp implementation written in C that compiles to WASM with emscripten. +
    Grotsky Part 4: Writing a service to get your public IP +https://mliezun.github.io/2020/12/17/grotsky-getmyip.html +2020-12-17 +

    Reading time: +

    + +

    Writing a service to get your public IP +

    +Grotsky (my toy programming language) finally can be used to make something useful. + +In this post I want to show you how I made a service that let's your retrieve your public IP as a response to a HTTP Request. + + +

    Show me the code +

    +Let's start by building the http request handler. + +The service will be deployed to heroku. Heroku passes the port that the http server has to listen as an environment variable named PORT. + + +
    Let's get the server up and running +
    +
    +let listen = ":8092"
    +let port = env.get("PORT")
    +if port != "" {
    +    listen = ":" + port
    +}
    +
    +io.println("Listen " + listen)
    +http.listen(listen)
    +
    + +We listen by default at the port 8092 and if the environment variable is given we change it. + +Then we print what is the port and start the server with http.listen. That blocks the execution and starts the server. + +Grotsky interpreter is written in Go, and uses Go's standard http server. Each requests is handled by a goroutine, but because Grotsky is single threaded only one goroutine executes at any given point in time. + +When a request is received the goroutine has to hold the GIL (Global Interrupt Lock) to be able to give control to the interpreter. + + +
    Now lets add some code to handle requests +
    +
    +fn getIP(rq, rs) {
    +    io.println("Request from --> " + rq.address)
    +    rs.write(200, rq.address)
    +}
    +
    +http.handler("/", getIP)
    +
    +let listen = ":8092"
    +let port = env.get("PORT")
    +if port != "" {
    +    listen = ":" + port
    +}
    +
    +io.println("Listen " + listen)
    +http.listen(listen)
    +
    + +Now we have something interesting to try out! + +What we've done is to log and write back as response the address of the device that is doing the request. + +To try it out you need to download grotsky. + +
    +$ go get github.com/mliezun/grotsky/cmd/grotsky
    +
    + +Save the Grotsky code under a filed called getip.g and the execute it using the grotsky interpreter: + +
    +$ go run $(go env GOPATH)/src/github.com/mliezun/grotsky/cmd/grotsky getip.g
    +
    + +Output: +
    +Listen :8092
    +
    + +Now you can make a request to see if it is working + +
    +$ curl localhost:8092
    +
    + +Output: +
    +[::1]:43464
    +
    + +We see that the address contains the port we want to split it and show just the IP. + + + +
    Let's write a couple functions to do that +
    +
    +fn findReversed(string, char) {
    +    for let i = string.length-1; i > -1; i = i - 1 {
    +        if string[i] == char {
    +            return i
    +        }
    +    }
    +    return -1
    +}
    +
    +fn parseIP(address) {
    +    let ix = findReversed(address, ":")
    +    return address[:ix]
    +}
    +
    + +The function findReversed finds the first index where char appears in string starting from the end. + +The function parseIP uses findReversed to obtain the index where ":" splits the IP and the PORT and uses that index to return just the IP address. + + +
    Now we can send just the IP address +
    +
    +fn getIP(rq, rs) {
    +    let address = parseIP(rq.address)
    +    io.println("Request from --> " + address)
    +    rs.write(200, address)
    +}
    +
    + +Add the two functions at the beginning of the file and modify the getIP function. + +Restart the server and now if you make a request you should get just the IP. + +
    +$ curl localhost:8092
    +[::1]
    +
    + +Voila! + + + +
    We have just one last issue: Proxies! +
    +Our service will probably sit behind a proxy, so we need to read the address from a special header X-Forwarded-For. + +Let's implement that! + +
    +fn getIP(rq, rs) {
    +    let address = parseIP(rq.address)
    +    let forwarded = rq.headers["X-Forwarded-For"]
    +    if forwarded != nil {
    +        address = forwarded[0]
    +    }
    +    io.println("Request from --> " + address)
    +    rs.write(200, address)
    +}
    +
    + +We read the header from the request and if X-Forwarded-For is present we sent that as a response to the user. + + +
    Our work is complete. Let's try it! +
    +
    +$ curl localhost:8092 -H 'X-Forwarded-For: 8.8.8.8'
    +8.8.8.8
    +
    +$ curl localhost:8092
    +[::1]
    +
    + +Well done. Now you can deploy it to Heroku (that's up to you) or any other cloud platform. + +I have my own version running under: https://peaceful-lowlands-45821.herokuapp.com/ + + + +

    Deployed to Fly.io after Heroku killed the free plan: +http://morning-breeze-4255.fly.dev +

    Try it from your command line +
    +
    +$ curl http://morning-breeze-4255.fly.dev
    +
    + + +
    Miguel LiezunPart 4 of building my own language series. This time I write and deploy a service to Heroku that let's your retrieve your pulbic IP. +
    Executing python code from MySQL Server +https://mliezun.github.io/2020/04/19/mysql-python.html +2020-04-19 +

    Reading time: +

    + +

    Executing python code from MySQL Server +

    + +

    Trying py_eval +

    + +
    Generate a list of integers from 0 to 10 +
    +
    +> select py_eval('[i for i in range(10)]') list;
    ++--------------------------------+
    +| list                           |
    ++--------------------------------+
    +| [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] |
    ++--------------------------------+
    +
    + + +
    Generate a dictionary (json object) from a list of dicts +
    +
    +> select replace(py_eval('{ str(user["id"]) : user for user in [{"id": 33, "name": "John"}, {"id": 44, "name": "George"}] }'), "'", '"') dict;
    ++------------------------------------------------------------------------+
    +| dict                                                                   |
    ++------------------------------------------------------------------------+
    +| {"33": {"id": 33, "name": "John"}, "44": {"id": 44, "name": "George"}} |
    ++------------------------------------------------------------------------+
    +
    + +Replace is needed here, because python uses single quotes for dictionaries. + + +
    Make a function that receives a json array and a key and sorts the array by key +
    +
    +DROP function IF EXISTS sort_by_key;
    +DELIMITER $$
    +CREATE FUNCTION sort_by_key (arr json, k text)
    +RETURNS json
    +BEGIN
    +    RETURN replace(py_eval(CONCAT("sorted(", arr, ", key=lambda e: e['", k, "'])")), "'", '"');
    +END$$
    +DELIMITER ;
    +
    + +Test + +
    +> select sort_by_key('[{"a":2}, {"a":1}, {"a": 722}, {"a": 0}]', 'a') sorted;
    ++--------------------------------------------+
    +| sorted                                     |
    ++--------------------------------------------+
    +| [{"a": 0}, {"a": 1}, {"a": 2}, {"a": 722}] |
    ++--------------------------------------------+
    +
    + + +

    How to write a MySQL UDF +

    +There is a pretty good guide at the MySQL 8.0 Reference Manual. I'll give you a brief explanation so you can start quickly, but reading the full guide is highly recomended. + +MySQL's UDFs are written in C++ and need to follow certain conventions so they can be recognized as such. + +In our case, we want our MySQL function to be called py_eval, so we have to define the following C++ functions: + +
  • py_eval_init or py_eval_deinit +
  • py_eval + +**py_eval_init**: (Optional) Initializes memory and data structures for the function execution. + +**py_eval**: Executes the actual function, in our case evaluates a python expression. + +**py_eval_deinit**: (Optional) If any memory was allocated in the init function, this is the place where we free it. + +For py_eval we only need **py_eval_init** and **py_eval**. + + +
  • Functions signatures +

    +
    +bool py_eval_init(UDF_INIT *initid, UDF_ARGS *args,
    +                             char *message);
    +
    +char *py_eval(UDF_INIT *, UDF_ARGS *args, char *result,
    +                         unsigned long *res_length, unsigned char *null_value,
    +                         unsigned char *);
    +
    + +These are the standard definitions for MySQL functions that return string, as is the case of py_eval. To be able to declare this functions, you need to have the definition of UDF_INIT and UDF_ARGS, you can find that at the source code of mysql server -> right here. + + +

    Evaluating python expression +

    +For evaluating python expression, we'll be using pybind11. That gives us the ability to directly access the python interpreter and execute code. + + +
    Example +
    +Make sure you have g++ installed. Try executing: g++ --help. And some version of python running of your system, for this tutorial I'll be using version _3.8_. + +
    +$ mkdir py_eval && cd py_eval
    +$ git clone https://github.com/pybind/pybind11
    +
    + +Create a new file called main.cpp with the following content: + +
    +#include "pybind11/include/pybind11/embed.h"
    +#include "pybind11/include/pybind11/eval.h"
    +#include 
    +
    +namespace py = pybind11;
    +
    +py::scoped_interpreter guard{}; // We need this to keep the interpreter alive
    +
    +int main(void) {
    +    auto obj = py::eval("[i for i in range(10)]");
    +    std::cout << std::string(py::str(obj)) << std::endl;
    +}
    +
    + +To run the example we have to compile the file. + +First, we need the compilation flags. + +
    +$ pkg-config python-3.8 --libs --cflags
    +-I/usr/include/python3.8
    +
    + +Then, we can compile and run our code with the following. + +
    +$ g++ main.cpp -I/usr/include/python3.8 -lpython3.8
    +$ ./a.out
    +[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    +
    + + +

    Putting all together +

    +Download udf types to the project folder. + +
    +$ wget https://raw.githubusercontent.com/mysql/mysql-server/8.0/include/mysql/udf_registration_types.h
    +
    + +Create a new file called py_eval.cpp, with the following content: + +
    c++
    +#include "pybind11/include/pybind11/embed.h"
    +#include "pybind11/include/pybind11/eval.h"
    +#include "udf_registration_types.h"
    +#include 
    +
    +namespace py = pybind11;
    +
    +py::scoped_interpreter guard{}; // We need this to keep the interpreter alive
    +
    +extern "C" bool py_eval_init(UDF_INIT *initid, UDF_ARGS *args,
    +                             char *message)
    +{
    +    // Here we can check if we received one argument
    +    if (args->arg_count != 1)
    +    {
    +        // The function returns true if there is an error,
    +        // the error message is copied to the message arg.
    +        strcpy(message, "py_eval must have one argument");
    +        return true;
    +    }
    +    // Cast the passed argument to string
    +    args->arg_type[0] = STRING_RESULT;
    +    initid->maybe_null = true; /* The result may be null */
    +    return false;
    +}
    +
    +extern "C" char *py_eval(UDF_INIT *, UDF_ARGS *args, char *result,
    +                         unsigned long *res_length, unsigned char *null_value,
    +                         unsigned char *)
    +{
    +    // Evaluate the argument as a python expression
    +    auto obj = py::eval(args->args[0]);
    +    // Cast the result to std::string
    +    std::string res_str = std::string(py::str(obj));
    +
    +    // Copy the output string from py::eval to the result argument
    +    strcpy(result, res_str.c_str());
    +
    +    // Set the length of the result string
    +    *res_length = res_str.length();
    +
    +    return result;
    +}
    +
    + +Then, we have to compile the project as a shared library, and move it to the plugin folder of mysql (in your case, it could be located in some other directory). + +
    +$ g++ -I/usr/include/python3.8 -lpython3.8 -shared -fPIC -o py_eval.so py_eval.cpp
    +$ sudo cp py_eval.so /usr/lib/mysql/plugin/
    +
    + +Now, it's time to try it from mysql. + +First, connect to your server as root. + +
    +$ sudo mysql -uroot
    +
    + +Create and test the function. + +
    +> create function py_eval returns string soname 'py_eval.so';
    +Query OK, 0 rows affected (0.029 sec)
    +
    +> select py_eval('[i for i in range(10)]') list;
    ++--------------------------------+
    +| list                           |
    ++--------------------------------+
    +| [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] |
    ++--------------------------------+
    +1 row in set (0.001 sec)
    +
    + + +

    Future +

    +There is a lot to do, for example there is no error control on the function execution. The python expression that we are trying to evaluate could fail causing a server reboot. Also, there is some extra work to do to be able to use import. And there are many concerns regarding concurrency issues. + +If you want to contribute to improve execution of python code on mysql server, please go to my github project and make a PR. + +I hope you enjoyed this tutorial and come back soon for more. + +
    Miguel LiezunEvaluate python expressions inside MySQL using a UDF that binds to python interpreter. +
    Writing your own C malloc and free +https://mliezun.github.io/2020/04/11/custom-malloc.html +2020-04-11 +

    Reading time: +

    + +

    Writing your own C malloc and free +

    + +

    Challenge +

    +This challenge comes from the book Crafting Interpreters by Bob Nystrom. And can be found in Chapter 14 - Challenge 3. + +The challenge goes: + +> You are allowed to call malloc() once, at the beginning of the interpreters execution, to allocate a single big block of memory which your reallocate() function has access to. It parcels out blobs of memory from that single region, your own personal heap. Its your job to define how it does that. + + +

    Check out this +comparison of malloc() to S3.

    Solution +

    +As stated in the challenge I'll be using a big chunk of _contiguous_ memory. The main idea of my solution is to store the blocks of memory in the array prepending a header with metadata. + +
    + _______________________________________________
    +|head_0|block_0 ... |head_1|block_1    ...      |
    + 
    +
    + +The structure of the header is pretty similar to that of a linked list. + +
    +struct block_meta
    +{
    +    size_t size;
    +    struct block_meta *next;
    +    int free;
    +};
    +
    +#define META_SIZE sizeof(struct block_meta)
    +
    + +It stores the size of the block, a pointer to the next block and a flag to mark wether it's free or not. + +Then, a function to traverse the list of blocks and find if there is any freed block is needed: + +
    +void *first_block = NULL;
    +
    +struct block_meta *find_free_block(struct block_meta **last, size_t size)
    +{
    +    struct block_meta *current = first_block;
    +    while (current && !(current->free && current->size >= size))
    +    {
    +        *last = current;
    +        current = current->next;
    +    }
    +    return current;
    +}
    +
    + +This function receives a double pointer to a block_meta struct called last that at the end of the execution should be pointing to the last node of the list and a size_t variable that indicates the minimum size that the block needs to be. + + +
    Memory initialization +
    +Two functions are needed to handle the big chunk of memory, one to initialize and the other to free it. + +
    +void initMemory();
    +void freeMemory();
    +
    + +To implement initMemory I've decided that I would ask for the maximum amount of memory that I could get from the OS. + +
    +#define MINREQ 0x20000
    +
    +// Big block of memory
    +void *memory = NULL;
    +// Position where the last block ends
    +size_t endpos = 0;
    +
    +void initMemory()
    +{
    +    size_t required = PTRDIFF_MAX;
    +    while (memory == NULL)
    +    {
    +        memory = malloc(required);
    +        if (required < MINREQ)
    +        {
    +            if (memory)
    +            {
    +                free(memory);
    +            }
    +            printf("Cannot allocate enough memory\n");
    +            exit(ENOMEM);
    +        }
    +        required >>= 1;
    +    }
    +}
    +
    +void freeMemory()
    +{
    +    free(memory);
    +}
    +
    + +As you can see, initMemory starts trying to allocate the maximum amount a memory allowed, and starts to divide that amount by 2 every time the allocation fails. If there isn't at least 128KB of memory available the program crashes with ENOMEM. + +Now that we have our chunk of memory ready to go, we can start to start giving blocks away. + +
    +struct block_meta *request_block(size_t size)
    +{
    +    struct block_meta *last = NULL;
    +    struct block_meta *block = find_free_block(&last, size);
    +    if (block)
    +    {
    +        block->free = 0;
    +        return block;
    +    }
    +    // Append new block to list
    +    block = memory + endpos;
    +    endpos += META_SIZE + size;
    +    if (last)
    +    {
    +        last->next = block;
    +    }
    +    else
    +    {
    +        first_block = block;
    +    }
    +    block->free = 0;
    +    block->next = NULL;
    +    block->size = size;
    +    return block;
    +}
    +
    + +How request_block works: + +1. Tries to find a free block with enough space. If there is one, it is set as occupied and returns that block. +2. If there isn't a free block available. It adds a new block with enough space at the end of memory (the big chunk). +3. If this is the first call, points the head of the list to the recently created block, else point the last node to the block. +4. Set the new block as occupied, set the size and next to null. Then return the new block. + +With this function, implementing malloc and free is pretty easy: + +
    +void *my_malloc(size_t size)
    +{
    +    struct block_meta *block = request_block(size);
    +    return block + 1;
    +}
    +
    +void my_free(void *ptr)
    +{
    +    struct block_meta *block = ptr - META_SIZE;
    +    block->free = 1;
    +}
    +
    + +To finish the challenge, I have to implement realloc, that is a little bit more tricky. + +
    +void *my_realloc(void *ptr, size_t size)
    +{
    +    if (!ptr)
    +    {
    +        return my_malloc(size);
    +    }
    +    struct block_meta *block = ptr - META_SIZE;
    +    if (block->size >= size)
    +    {
    +        return block + 1;
    +    }
    +    uint8_t *newptr = my_malloc(size);
    +    size_t i;
    +    for (i = 0; i < (block->size < size ? block->size : size); i++)
    +    {
    +        newptr[i] = ((uint8_t *)ptr)[i];
    +    }
    +    block->free = 1;
    +    return newptr;
    +}
    +
    + +How realloc works: + +
  • If the pointer to reallocate is null, works just like malloc. +
  • If the given size is bigger than the prior size, it allocates a bigger block and copies all data from the original block to the new block. +
  • If the given size is smaller than the prior size, it allocates a smaller block and copies just the data that fits into the smaller block. + + +
  • New challenge +

    +In my implementation I used a linked list where each node holds a pointer to the next, but given that I have control over the _entire_ memory this actualy isn't necessary. + +My challenge to you is that you remove the pointer to next from the block_meta struct. + + +

    Resources +

    +
  • https://danluu.com/malloc-tutorial/ +
  • http://www.craftinginterpreters.com/chunks-of-bytecode.html + +
  • Miguel LiezunChallenge for writing your own implementation of malloc and free. +
    Grotsky Part 3: Interpreting +https://mliezun.github.io/2020/04/01/grotsky-part3.html +2020-04-01 +

    Reading time: +

    + +

    Grotsky Part 3: Interpreting +

    + +

    It's slow! +

    +My interpreter it's really, really, wait for it... _Really slow_. + +An example of a bad performing grotsky code: + +
    +# fib: calculates the n-th fibonacci number recursively
    +fn fib(n) begin
    +    if n < 2 return n
    +    return fib(n-2) + fib(n-1)
    +end
    +println(fib(30))
    +
    + + +
    Running the code +
    +
    +$ time ./grotsky examples/fib.g
    +
    + +Gives a wooping result of: + +
    +832040
    +
    +real    0m11,154s
    +user    0m11,806s
    +sys     0m0,272s
    +
    + +Almost twelve seconds!!! + +Comparing with a similar python code + +
    +def fib(n):
    +    if n < 2: return n
    +    return fib(n-2) + fib(n-1)
    +print(fib(30))
    +
    + +Gives a result of: + +
    +832040
    +
    +real    0m0,423s
    +user    0m0,387s
    +sys     0m0,021s
    +
    + +That means, my interpreter is at least 20 times slower than Cpython. + + +
    Why is it so slow? +
    +Here is an explanation. + +As the person from the first comment states, go garbage collector is not well suited for this kind of scenario with heavy allocation of objects. + +> Go's GC is not generational, so allocation requires (comparatively speaking) much more work. It's also tuned for low latency (smallest pause when GC has to stop the program) at the expense of throughput (i.e. total speed). This is the right trade-off for most programs but doesn't perform optimally on micro-benchmarks that measure throughtput. + +Setting the gc percent at 800 (100 by default) more than halves the time that the function takes to compute: + +
    +$ time GOGC=800 ./grotsky examples/fib.g
    +832040
    +
    +real    0m5,110s
    +user    0m5,182s
    +sys     0m0,061s
    +
    + + +

    Interpreting functions +

    +Callable interface + +
    +type callable interface {
    +	arity() int
    +	call(exec *exec, arguments []interface{}) interface{}
    +}
    +
    + +_All grotsky functions must be an object that implements the callable interface._ + +For that I defined two kind of structs: + +
    +type function struct {
    +	declaration   *fnStmt
    +	closure       *env
    +	isInitializer bool
    +}
    +
    +type nativeFn struct {
    +	arityValue int
    +	callFn  func(exec *exec, arguments []interface{}) interface{}
    +}
    +
    + + +
    nativeFn +
    +Let's you define standard functions available on all grotsky interpreters. Line println. + +
    +func (n *nativeFn) arity() int {
    +	return n.arityValue
    +}
    +
    +func (n *nativeFn) call(exec *exec, arguments []interface{}) interface{} {
    +	return n.callFn(exec, arguments)
    +}
    +
    + +From that, println would be pretty straight forward: + +
    +...
    +
    +var println nativeFn
    +println.arityValue = 1
    +println.callFn = func(exec *exec, arguments []interface{}) interface{} {
    +    fmt.Println(arguments[0])
    +    return nil
    +}
    +...
    +
    + + +
    Ordinary grotsky functions +
    +For ordinary grotsky functions the things are a little bit messier. + +First I got to introduce the environment that is an object that holds map[string]interface{} as a dictionary for variables in the local scope and a pointer to another environment that contains variables for the outer scope. + +
    +type env struct {
    +	state *state
    +
    +	enclosing *env
    +	values    map[string]interface{}
    +}
    +
    +func newEnv(state *state, enclosing *env) *env {
    +	return &env{
    +		state:     state,
    +		enclosing: enclosing,
    +		values:    make(map[string]interface{}),
    +	}
    +}
    +
    +func (e *env) get(name *token) interface{} {
    +	if value, ok := e.values[name.lexeme]; ok {
    +		return value
    +	}
    +	if e.enclosing != nil {
    +		return e.enclosing.get(name)
    +	}
    +	e.state.runtimeErr(errUndefinedVar, name)
    +	return nil
    +}
    +
    +func (e *env) define(name string, value interface{}) {
    +	e.values[name] = value
    +}
    +
    + +As you can see, the define method creates a variable on the local scope, and the get methods tries to retrieve a variable first from the local scope and then from the outer scope. + +Let's see how functions are implemented. + +
    +func (f *function) arity() int {
    +	return len(f.declaration.params)
    +}
    +
    +func (f *function) call(exec *exec, arguments []interface{}) (result interface{}) {
    +	env := newEnv(exec.state, f.closure)
    +	for i := range f.declaration.params {
    +		env.define(f.declaration.params[i].lexeme, arguments[i])
    +	}
    +
    +	defer func() {
    +		if r := recover(); r != nil {
    +			if returnVal, isReturn := r.(returnValue); isReturn {
    +				result = returnVal
    +			} else {
    +				panic(r)
    +			}
    +		}
    +	}()
    +
    +	exec.executeBlock(f.declaration.body, env)
    +
    +	return nil
    +}
    +
    + +Function arity is pretty simple. + +The function call takes an exec object, that is no more than an instance of the interpreter, and the arguments to the function as an array of objects. Then creates a new environment the is surrounded by the environment local to the function definition and defines all the function parameters. Then comes the tricky part, first there is a deferred call to an anonymous function, let's ignore that for a moment, in the end, the function executeBlock gets called. Let's see what that function does: + +
    +func (e *exec) executeBlock(stmts []stmt, env *env) {
    +	previous := e.env
    +	defer func() {
    +		e.env = previous
    +	}()
    +	e.env = env
    +	for _, s := range stmts {
    +		e.execute(s)
    +	}
    +}
    +
    + +What's happening here is that the interpreter steps into the new environment, saving the previous environment in a variable, and execute all given statements, after that it restores the environment to the previous one. Exactly as a function does. + + +
    What happens when you hit a return +
    +
    +type returnValue interface{}
    +
    +...
    +
    +func (e *exec) visitReturnStmt(stmt *returnStmt) R {
    +	if stmt.value != nil {
    +		panic(returnValue(stmt.value.accept(e)))
    +	}
    +	return nil
    +}
    +
    + +When you get to a return node in the ast, the nodes panics with a return value. This has to do with the fact that you need to go up the call stack and finish the execution of the function, otherwise the function will keep it's execution. + +That's the reason of the deferred function we forgot a couple seconds ago: + +
    +func (f *function) call(exec *exec, arguments []interface{}) (result interface{}) {
    +    ...
    +
    +    defer func() {
    +		if r := recover(); r != nil {
    +			if returnVal, isReturn := r.(returnValue); isReturn {
    +				result = returnVal
    +			} else {
    +				panic(r)
    +			}
    +		}
    +    }()
    +
    +    ...
    +}
    +
    + +This function recovers from a panic. If the value recovered is of type returnValue it recovers successfully and sets the result value of the function call to the return value, else it panics again. + + +

    Hasta la vista, baby +

    +That's it for now. There are a lot of nifty stuff to keep talking about. But I think it's enough for now. + +Remember to check out the source code. And stay tuned for more. + +
    Miguel LiezunPart 3 of building my own language series. Interpreting expressions and statement, traversing the Abstract Syntax Tree. +
    Grotsky Part 2: Parsing expressions +https://mliezun.github.io/2020/03/15/grotsky-part2.html +2020-03-15 +

    Reading time: +

    + +

    Grotsky Part 2: Parsing expressions +

    + +

    Expressions +

    +Parsing an expression like 1+2*3 requires a complex representation on memory. Just looking at it we think that it's pretty simple, but there is some hidden hierarchy that we have to pay attention to, like the fact that first we have to compute 2*3 and then add 1 to it. + +To represent that in a data structure the best thing we can come up to is a tree, as seen in the next figure: + +![image](/assets/images/grotsky-part2/AST.png) + +As you can see the leaves of the tree are literals and the root and intermediate nodes are operations that have to be applied from the bottom up. That means that we traverse the tree until we reach the bottom and start computing the results by going up. + + +

    Defining node types +

    +> Not all operations are created equal. + +We have to define how each node fits into the tree. + +I'll use the following syntax: Binary -> left expr, operator token, right expr. Which means that a binary operation (as we have seen in the image before) links to 2 expressions (left and right) and stores 1 value (operator). + + +
    Let's define all posible operations on literals +
    +
    +Literal -> value object
    +# 1, "asd", 5.2, true, false
    +
    +Binary -> left expr, operator token, right expr
    +# 1+2, 3*3, 4^2+1
    +
    +Grouping -> expression expr
    +# (1+2)
    +
    +Logical -> left expr, operator token, right expr
    +# true or false, false and true
    +
    +Unary: operator token, right expr
    +# not true, -5
    +
    +List -> elements []expr
    +# [1, 2, 3, [4], "asd"]
    +
    +Dictionary -> elements []expr
    +# {"a": 1, "b": 2, 3: 4}
    +
    +Access -> object expr, slice expr
    +# [1, 2, 3][0], {"a":1}["a"]
    +
    +Slice -> first expr, second expr, third expr
    +# [1, 2, 3, 4, 5, 6][1:4:2]
    +
    + + +

    Traversing the abstract syntax tree +

    +To traverse the syntax tree we need a pattern that's uniform and easily scalable when we have to add other types of expressions and statements. + +For that we'll use the Visitor Pattern. + + +

    Visitor Pattern +

    +First we need an interface for the expression that allows a visitor to visit it. + +
    +type expr interface {
    +    accept(exprVisitor) interface{}
    +}
    +
    + +An expression visitor should have a method for each kind of expression it has to visit. + +
    +type exprVisitor interface {
    +    visitLiteralExpr(expr expr) interface{}
    +    visitBinaryExpr(expr expr) interface{}
    +    visitGroupingExpr(expr expr) interface{}
    +    visitLogicalExpr(expr expr) interface{}
    +    visitUnaryExpr(expr expr) interface{}
    +    visitListExpr(expr expr) interface{}
    +    visitDictionaryExpr(expr expr) interface{}
    +    visitAccessExpr(expr expr) interface{}
    +    visitSliceExpr(expr expr) interface{}
    +}
    +
    + +Then we have to define a type for each kind of expression that implements expr interface. For example, this is the implementation for a binary expression: + +
    +type binaryExpr struct {
    +    left expr
    +    operator *token
    +    right expr
    +}
    +
    +func (s *binaryExpr) accept(visitor exprVisitor) R {
    +    return visitor.visitBinaryExpr(s)
    +}
    +
    + +For all other expressions the definition is practically the same. + + +

    String Visitor +

    +To finish this chapter, let's define a visitor that allows you to print the syntax tree in a lisp-like syntax, ex: (+ 1 2). + +Here is the implementation of the string visitor for a binary expression: + +
    +type stringVisitor struct{}
    +
    +func (v stringVisitor) visitBinaryExpr(expr expr) R {
    +    binary := expr.(*binaryExpr)
    +    return fmt.Sprintf("(%s %v %v)", binary.operator.lexeme, binary.left.accept(v), binary.right.accept(v))
    +}
    +
    + + +

    Grotsky expression +

    +You can check out the state of the Grotsky project right here: https://github.com/mliezun/grotsky. + +Grotsky it's able to parse and print all types of expressions defined in this article right now. + + +

    Expressions +

    +Examples of operations supported: + +
    +# Math operations
    +1+2*3^2-(4+123)/2.6
    +=> (- (+ 1 (* 2 (^ 3 2))) (/ (+ 4 123) 2.6))
    +
    +# Logical operations
    +true or false
    +=> (or true false)
    +
    +# Comparisons
    +1 == 1 and (1 > 3 or 11/5.5 <= 3+2^2 and 1 != 2)
    +=> (and (== 1 1) (or (> 1 3) (and (<= (/ 11 5.5) (+ 3 (^ 2 2))) (!= 1 2))))
    +
    +# Lists
    +[1, 2, [3], "asd"]
    +=> (list 1 2 (list 3) "asd")
    +
    +# List slicing
    +[1,2,3,4][1:3][::2][0]
    +=> (#0 (#::2 (#1:3 (list 1 2 3 4))))
    +
    +# Dictionary
    +{
    +    1: 2,
    +    3: 4,
    +    "asd": 3.14
    +}
    +=> (dict 1=>2 3=>4 "asd"=>3.14)
    +
    +# Dictionary key lookup
    +{"key":0.6}["key"]
    +=> (#"key" (dict "key"=>0.6))
    +
    + +That's it for now. In the next chapter we'll traverse the tree but instead of printing we'll execute the operations listed before. + +If you have questions or suggestions please get in touch. + +
    Miguel LiezunPart 2 of building my own language series. Parsing expressions, traversing and printing the Abstract Syntax Tree. +
    Grotsky Part 1: Syntax +https://mliezun.github.io/2020/02/21/grotsky-part1.html +2020-02-21 +

    Reading time: +

    + +

    Grotsky Part 1: Syntax +

    + +

    Syntax Restrictions +

    +
  • No use of semicolon ; +
  • Block statements delimited by begin and end +
  • Function definition using fn keyword +
  • Logic operators in plain english or, and, not +
  • Conditional statements use the following keywords: if, elif, else +
  • There is no switch statement +
  • Class definition with class keyword +
  • Arithmetic operations: *, /, -, +, ^ +
  • Grouping with parentheses () +
  • Native support for python-like lists and dictionaries: [], {} +
  • Support for enhanced for loop: for i, el in array +
  • Keywords and identifiers can only use alphabethic characters + + +
  • Primitives +

    +
  • nil +
  • Integers +
  • Floats +
  • Booleans +
  • Strings +
  • Lists +
  • Dictionaries + + +
  • Example of functions and operations +

    +
    +## Arithmethic
    +print(2^10 - 2323*3)
    +# Output: -5945
    +print(2^(12*3+400/-4+10*5/2))
    +# Output: 1.8189894035458565e-12
    +
    +## Logic
    +print(true or false)
    +# Output: true (short circuit)
    +print(false and true)
    +# Output: false (short circuit)
    +
    +## Conditionals
    +if 3 > 2 or (1 < 3 and 2 == 2) begin
    +    print('Condition is true')
    +end
    +elif 3 == 4 begin
    +    print('Condition 2 is true')
    +end
    +else begin
    +    print('Conditions are false')
    +end
    +
    +## Lists
    +for i in [1, 2, 3, 4] begin
    +    print(i)
    +end
    +
    +let lst = [1, 2, 3, 4]
    +lst[0] = -1
    +print(lst) # Output: [-1, 2, 3, 4]
    +print(lst[1:3]) # Output: [2, 3]
    +
    +## Dictionaries
    +# (dictionaries and lists not allowed as keys)
    +let dct = {
    +    "Key1": "Val1",
    +    2: "Val2",
    +    true: false
    +}
    +for key, val in dct begin
    +    print(key, val)
    +end
    +
    +## Functions
    +fn square(x)
    +begin
    +    return x^2
    +end
    +
    +fn operate(x, operation)
    +begin
    +    return operation(x)
    +end
    +
    +## Clojure
    +fn makeCounter()
    +begin
    +    let n = 0
    +    return fn() begin
    +        n = n+1
    +        return n
    +    end
    +end
    +
    +## Classes
    +class Counter
    +begin
    +    init(start) begin
    +        self.start = start
    +    end
    +    count() begin
    +        self.start = self.start+1
    +        return self.start
    +    end
    +end
    +
    +class CounterTwo
    +begin
    +    count() begin
    +        return super.count()*2
    +    end
    +end
    +
    + + +

    Syntax definition +

    +Let's build a syntax definition in backus naur format that will be easy to parse with a recursive descent parser. + + +
    Expresions +
    +
    +expression       assignment;
    +list             "[" arguments? "]";
    +dictionary       "{" dict_elements? "}";
    +dict_elements    keyval ("," keyval)*;
    +keyval           expression ":" expression;
    +assignment       (call ".")? IDENTIFIER "=" assignment | access;
    +access           logic_or ("[" slice "]")*;
    +logic_or         logic_and ("or" logic_and)*;
    +logic_and        equality ("and" equality)*;
    +equality         comparison (("!=" | "==") comparison)*;
    +comparison       addition ((">" | ">=" | "<" | "<=") addition)*;
    +addition         multiplication (("-" | "+") multiplication)*;
    +multiplication   power (("/" | "*") power)*;
    +power            unary ("^" unary)*;
    +unary            ("not" | "-") unary | call;
    +call             primary ("(" arguments? ")" | "." IDENTIFIER)*;
    +arguments        expression ("," expression)*;
    +slice            (":" expression)
    +                | (":" expression ":" expression)
    +                | (":" ":" expression)
    +                | expression
    +                | (expression ":")
    +                | (expression ":" expression)
    +                | (expression ":" ":" expression)
    +                | (expression ":" expression ":" expression);
    +primary          NUMBER
    +                | STRING
    +                | "false"
    +                | "true"
    +                | "nil"
    +                | IDENTIFIER
    +                | "(" expression ")"
    +                | fnAnon
    +                | list
    +                | dictionary;
    +fnAnon           "fn" "(" parameters? ")" block;
    +
    + + +
    Statements +
    +
    +program         declaration* EOF;
    +declaration     classDecl | funDecl | varDecl | statement;
    +classDecl       "class" IDENTIFIER ( "<" IDENTIFIER )? "begin" methodDecl* "end" NEWLINE;
    +methodDecl      "class"? function;
    +funDecl         "fn" function ;
    +function        IDENTIFIER "(" parameters? ")" block ;
    +parameters      IDENTIFIER ( "," IDENTIFIER )* ;
    +varDecl         "let" IDENTIFIER ("=" expression)? NEWLINE;
    +statement       forStmt
    +                | ifStmt
    +                | returnStmt
    +                | whileStmt
    +                | exprStmt
    +                | block;
    +exprStmt        expression NEWLINE;
    +forStmt         "for"  (classicFor | newFor) statement;
    +classicFor      (varDecl | exprStmt | ",") expression? "," expression?;
    +newFor          IDENTIFIER ("," IDENTIFIER)? "in" expression;
    +ifStmt          "if" expression statement ("elif" expression statement)* ("else" statement)?;
    +returnStmt      "return" expression? NEWLINE;
    +whileStmt       "while" expression statement;
    +block           "begin" NEWLINE declaration* "end" NEWLINE;
    +
    + +That's it! The next step is to build a lexer and a parser. + +
    Miguel LiezunPart 1 of building my own language series. Defining the syntax of grotsky toy language. +
    Sudoku Solver +https://mliezun.github.io/2020/02/18/sudoku-solver.html +2020-02-18 +

    Reading time: +

    + +

    Sudoku Solver +

    +I wanted to make my own sudoku solver to challenge myself. + +Im not a sudoku player so my approach is a brute force scan of possible combinations sort-of. + +I just know the basic rules: + +
  • Numbers 1-9 are allowed. +
  • Numbers in the same row cannot be repeated. +
  • Numbers in the same column cannot be repeated. +
  • Numbers in the 3x3 square cannot be repeated. + +The first thing i did was to build a some classes that calculates the possible values a cell can have if it's empty, based on the constraints. + +I came up with 3 classes: + +
  • Board that stores the entire board. +
  • BoardSlice that stores a slice of a board. An object of this type is returned when a Board is sliced (method __getitem__). +
  • Cell that stores the value of a single cell and calculates all possible values a cell can take. + +The class Cell receives a board, the coordinates on the board, and the value that holds. Also has the method options that uses python set data structure to calculate the posibilites. + +If you look at the following snippet you can see that the method options +generates the sets: options that contains all possible options (1-9), row that contains all the numbers that are in the same row, column that contains all the numbers that are in the same column and square that contains all the numbers that are in the same 3x3 square. The return value is options without all the used values. + +
    +class Cell:
    +    def __init__(self, b, i, j, value):
    +        self.b = b
    +        self.value = value
    +        self.i = i
    +        self.j = j
    +
    +    def options(self):
    +        if self.value != 0:
    +            return {self.value}
    +        options = set(range(1, 10))
    +        row = set(map(lambda x: x.value, self.b[self.i]))
    +        column = set(map(lambda x: x.value, self.b[:][self.j]))
    +        def to_square(k): return slice((k // 3) * 3, (k // 3) * 3 + 3)
    +        square = set(
    +            map(lambda x: x.value,
    +                self.b[to_square(self.i)][to_square(self.j)]))
    +        return options - row - column - square - {0}
    +
    + +To make easier the implementation of the square I used the class BoardSlice that contains a slice of a board and implements the magic method __getitem__. + +
    +class BoardSlice:
    +    def __init__(self, board_slice):
    +        self.board_slice = board_slice
    +
    +    def __getitem__(self, items):
    +        if type(items) == slice:
    +            return (el for row in self.board_slice for el in row[items])
    +        if type(items) == int:
    +            return (row[items] for row in self.board_slice)
    +        raise KeyError
    +
    + +The base class: Board contains the board and a copy method that copies all the values and creates a new Board object. This is necessary to avoid messing with object references and have a clean object when needed. + +
    +class Board:
    +    def __init__(self, board):
    +        self.board = [[Cell(self, i, j, value)
    +                       for (j, value) in enumerate(row)] for (i, row) in enumerate(board)]
    +
    +    def copy(self):
    +        return Board(((cell.value for cell in row) for row in self.board))
    +
    +    def __getitem__(self, items):
    +        if type(items) == int:
    +            return self.board[items]
    +        if type(items) == slice:
    +            return BoardSlice(self.board[items])
    +        raise KeyError
    +
    +    def __repr__(self):
    +        return repr(self.board)
    +
    + +With these tools the next step is to solve the problem! + +My idea was to generate a mixed iterative-recursive algorithm. + +The first pass will be iterative, and if needed, the second pass will be recursive. + + +
  • Iterative pass +
    +Iterates over the whole board and calculates the options that each cell can have. If a cell has only one option set that option on the cell and set a flag to repeat the iterative pass, if has 0 options return None meaning that the board has no solutions, and if has more than one option store the options for the recursive pass. + +If the loop ends and we found that no cell has more than one option then we solved the board! + +The idea of this first step is to solve an _easy_ board quickly. + + +
    Recursive pass +
    +If the iterative pass ends and we found that a cell has more than one option then we try all that options and call solve again! + +If solve returns a board that means we've found the solution! + +If solve returns None (back at the iterative passs) we have to try with another options. + + +
    BoardSolver +
    +The class is pretty straightforward. + +
    +class SudokuSolver:
    +    @staticmethod
    +    def solve(board):
    +        b = board.copy()
    +        # First pass: Iterative
    +        board_map = {}
    +        exhaust = False
    +        while not exhaust:
    +            exhaust = True
    +            for i in range(9):
    +                for j in range(9):
    +                    cell = b[i][j]
    +                    if cell.value == 0:
    +                        options = cell.options()
    +                        if len(options) == 1:
    +                            cell.value = options.pop()
    +                            exhaust = False
    +                        elif len(options) == 0:
    +                            return None
    +                        elif len(board_map) == 0:
    +                            board_map[(i, j)] = options
    +
    +        # Second pass: Recursive
    +        for ((i, j), options) in board_map.items():
    +            for op in options:
    +                b[i][j].value = op
    +                solved = SudokuSolver.solve(b)
    +                if solved:
    +                    return solved
    +            return None
    +
    +        return b
    +
    + + +
    Conclusions +
    +Actually my implementation is not a brute force algorithm, is a search algorithm, that searches the path to solving a board. Because it doesn't try all values on all cells nonsensically, it rather tries _some_ options for a given cell and advances to the next option as _soon_ as it detects that it's not the correct path. + + +

    Source +

    +Take a look at the source code. + +
    Miguel LiezunIterative + recursive sudoku solver using python magic methods. +
    Crafting interpreters +https://mliezun.github.io/2020/02/12/crafting-interpreters.html +2020-02-12 +

    Reading time: +

    + +

    Crafting interpreters +

    +I've just finished the section 2 of the book _Crafting Interpreters_, and I wanted to upload it to github right away. + +Take a look at the source code. + +Beside the lox specification I've added: + +
  • The keyword until that is a variation of while loops (as in ruby). also +
  • print is a function instead of a statement. +
  • eval function that let's you eval source code in runtime. +
  • Class methods. + +I'll implement another language interpreter, this time using golang and with a syntax similar to ruby. + +
  • Miguel LiezunLox language interpreter based on the book craftinginterpreters.com by Bob Nystrom. +
    Reinventing the Wheel: PHP Generators +https://mliezun.github.io/2020/01/24/php-generator.html +2020-01-24 +

    Reading time: +

    + +

    Reinventing the Wheel: PHP Generators +

    + +

    First thing first. How a generator works? +

    + +

    Starting back at C +

    +Let's create a function that each time we call it we get the next number of the fibonacci sequence. + +
    +int fibonacci()
    +{
    +    static int a = 0;
    +    static int b = 1;
    +    int aux = b;
    +    b = a + b;
    +    a = aux;
    +    return a;
    +}
    +
    + +If we call fibonacci(), the first time we'll get 1, the second time 1, the third 2, the fourth 3, and so on... + +This happens because we declared variables a, b to be static. This means that they mantain the value after the function returns. Normally, what happens (if we don't declare a variable as static) is that the variables inside the function don't mantain the values of the last execution. + + +

    First generator for PHP +

    +The equivalent function in PHP is pretty similar to C's approach. + +
    +
    +
    +function fibonacci()
    +{
    +    static $a = 0;
    +    static $b = 1;
    +    $aux = $b;
    +    $b = $a + $b;
    +    $a = $aux;
    +    return $a;
    +}
    +
    +$out = [];
    +
    +for ($i = 1; $i <= 10; $i++) {
    +    $out[] = fibonacci();
    +}
    +
    +echo implode(', ', $out) . "\n";
    +
    +/*
    +Output: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
    +*/
    +
    + +Let's compare this to the real PHP version using yield. + +
    +
    +
    +function fibonacci($N)
    +{
    +    $a = 0;
    +    $b = 1;
    +    for ($i = 0; $i < $N; $i++) {
    +        $aux = $b;
    +        $b = $a + $b;
    +        $a = $aux;
    +        yield $a;
    +    }
    +}
    +
    +$out = [];
    +
    +foreach (fibonacci(10) as $fib) {
    +    $out[] = $fib;
    +}
    +
    +echo implode(', ', $out) . "\n";
    +
    +/*
    +Output: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
    +*/
    +
    + + +

    Creating a custom version of PHP yield +

    +This is my own version using the library parallel and channels (probably uses yield internally). + +
    +
    +
    +class MyGenerator implements Iterator
    +{
    +    private $chan;
    +    private $current;
    +    private $iteratorFn;
    +    private $runtime;
    +    private $key = -1;
    +    private $valid = true;
    +
    +    public function __construct($iteratorFn)
    +    {
    +        $this->iteratorFn = $iteratorFn;
    +        $this->runtime = new \parallel\Runtime();
    +        $channel = new \parallel\Channel();
    +
    +        $this->runtime->run(function() use ($iteratorFn, $channel) {
    +            $iteratorFn(function ($val) use ($channel) {
    +                $channel->send($val);
    +            });
    +            $channel->close();
    +        });
    +
    +        $this->chan = $channel;
    +        $this->next();
    +    }
    +
    +    public function current()
    +    {
    +        return $this->current;
    +    }
    +
    +    public function next()
    +    {
    +        try {
    +            ++$this->key;
    +            $val = $this->chan->recv();
    +            $this->current = $val;
    +        } catch (\parallel\Channel\Error\Closed $e) {
    +            $this->valid = false;
    +        }
    +        return $this->current;
    +    }
    +
    +    public function key() {return $this->key;}
    +    public function valid() {return $this->valid;}
    +    public function rewind() {}
    +}
    +
    +
    +function fibonacci($N)
    +{
    +    return new MyGenerator(function ($yield) use ($N) {
    +        $a = 0;
    +        $b = 1;
    +        for ($i = 0; $i < $N; $i++) {
    +            $aux = $b;
    +            $b = $a + $b;
    +            $a = $aux;
    +            $yield($a);
    +        }
    +    });
    +}
    +
    +$out = [];
    +
    +foreach (fibonacci(10) as $fib) {
    +    $out[] = $fib;
    +}
    +
    +echo implode(', ', $out) . "\n";
    +
    + + +

    Performance comparison: PHP vs Custom +

    + +
    Tested code +
    +
    +for ($i = 0; $i < 1000; ++$i) {
    +    foreach (fibonacci(100) as $fib) {
    +        $out[] = $fib;
    +    }
    +}
    +
    + + +
    yield version +
    +
    +real    0m0,083s
    +user    0m0,059s
    +sys     0m0,023s
    +
    + + +
    MyGenerator version +
    +
    +real    0m2,138s
    +user    0m1,426s
    +sys     0m1,363s
    +
    + +So, it's aproximately 26 times slower :-) + +
    Miguel LiezunAttempt of a lunatic to recreate functionalities that a language already has using the same language, and failing. +
    \ No newline at end of file diff --git a/docs/googlee7507b09f10f12d1.html b/docs/googlee7507b09f10f12d1.html new file mode 100644 index 0000000..e474a5f --- /dev/null +++ b/docs/googlee7507b09f10f12d1.html @@ -0,0 +1 @@ +google-site-verification: googlee7507b09f10f12d1.html \ No newline at end of file diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..1aac893 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,45 @@ +Blog | mliezun.github.io + +

    Blog

    Hi folks!

    I'm Miguel. Here I write mainly about programming and side projects.

    I've written my own programming language called Grotsky, and im +rewriting it in Rust. +

    Generating posts using markdown

    2024-01-04
    Custom Markdown parser and HTML generator using Grotsky, my toy programming language that powers this blog. Up until now I've used a hacky HTML generator that relies on lists. Now Im integrating a simple MD parser that makes easier to write new articles.

    Day 20. My favourite problem from Advent of Code 2023

    2023-12-25
    Advent of code 2023 has gone by, this is my first year participating. It's been fun and I want to share the problem that I enjoyed the most. It's based on simple electronic devices sending signals or pulses to each other.

    I rewrote my toy language interpreter in Rust

    2023-11-23
    Im rewriting Grotsky (my toy programming language) in Rust, the previous implementation +was done in Go. The goal of the rewrite is to improve my Rust skills, and to improve the performance of Grotsky, +by at least 10x. This has been a serious of posts, this one is the latest one. Hopefully the best and most insightful +of them all.

    Rewrite my toy language interpreter in Rust, an update

    2023-09-23
    Im rewriting Grotsky (my toy programming language) in Rust, the previous implementation +was done in Go. The goal of the rewrite is to improve my Rust skills, and to improve the performance of Grotsky, +by at least 10x.

    The end of a side project

    2023-07-15
    Cloud Outdated was a personalized digest of updates for cloud services. It's sad to see it go, + but it was a fun project to work on, learn some new stuff and collab with a friend. + There are some takeaways from this that I'd like to share.

    Rewrite my toy language interpreter in Rust

    2023-06-02
    Im rewriting Grotsky (my toy programming language) in Rust, the previous implementation +was done in Go. The goal of the rewrite is to improve my Rust skills, and to improve the performance of Grotsky, +by at least 10x.

    Writing a Redis clone in Go from scratch

    2023-04-08
    In this post we're going to write a basic Redis clone in Go that implements the most simple commands: GET, +SET, DEL and QUIT. At the end you'll know how to parse a byte stream from a live TCP connection, and hopefully have a working +implementation of Redis.

    How to write a program that can replicate itself

    2022-11-26
    Grotsky is a toy programming language that I made for fun. Today we're visinting the concept of Quines, +a.k.a. self replicating programs. It's said that any turing-complete language should be able to write a program that replicates + itself. And grotsky is no exception.

    Migrate from Heroku to Fly.io

    2022-09-22
    A couple weeks ago Heroku announced the removal. I have plenty of projects running on free dynos. + I have taken some time to move my code to Fly.io. And also I've written a little tutorial of how to perform the migration.

    Branchable MySQL: Managing multiple dev environments

    2022-09-20
    When teams start to grow, having a single dev environment becomes an issue. People start stepping on each others toes. +A common problem is that two people want to apply incompatible migrations on the database. That problem is impossible +to fix if folks are working on parallel branches. +If we can have a database for each branch of a project, that will remove much of the pain of having multiple devs applying +changes to the db.

    Webscraping as a side project

    2022-07-20
    Cloud Outdated is a personalized digest of updates for cloud services. Works like a newsletter where you can choose + which services you want to get notified about. For example: Subscribe to AWS Lambda with Python runtime, and you'll get an email + when 3.10 is supported.

    Playing with Javascript Proxies (getters/setters)

    2021-12-31
    In this last post of the year I play with proxies in an attempt to create a Javascript object where changes are appended + to a log and can be reverted by deleting the last element of the log using getters and setters.

    Grotsky Part 3: Interpreting

    2020-04-01
    Part 3 of building my own language series. Interpreting expressions and statement, traversing the Abstract Syntax Tree.

    Grotsky Part 1: Syntax

    2020-02-21
    Part 1 of building my own language series. Defining the syntax of grotsky toy language.

    Sudoku Solver

    2020-02-18
    Iterative + recursive sudoku solver using python magic methods.

    Crafting interpreters

    2020-02-12
    Lox language interpreter based on the book craftinginterpreters.com by Bob Nystrom.
    \ No newline at end of file