diff options
Diffstat (limited to 'users/tazjin')
214 files changed, 16221 insertions, 0 deletions
diff --git a/users/tazjin/OWNERS b/users/tazjin/OWNERS new file mode 100644 index 000000000000..c86f6eaa6adb --- /dev/null +++ b/users/tazjin/OWNERS @@ -0,0 +1,3 @@ +inherited: false +owners: + - tazjin diff --git a/users/tazjin/aoc2019/default.nix b/users/tazjin/aoc2019/default.nix new file mode 100644 index 000000000000..a1798f400174 --- /dev/null +++ b/users/tazjin/aoc2019/default.nix @@ -0,0 +1,26 @@ +# Solutions for Advent of Code 2019, written in Emacs Lisp. +# +# For each day a new file is created as "solution-day$n.el". +{ depot, ... }: + +let + inherit (builtins) attrNames filter head listToAttrs match readDir; + dir = readDir ./.; + matchSolution = match "solution-(.*)\.el"; + isSolution = f: (matchSolution f) != null; + getDay = f: head (matchSolution f); + + solutionFiles = filter (e: dir."${e}" == "regular" && isSolution e) (attrNames dir); + solutions = map + (f: + let day = getDay f; in { + name = day; + value = depot.nix.writeElispBin { + name = "aoc2019"; + deps = p: with p; [ dash s ht ]; + src = ./. + ("/" + f); + }; + }) + solutionFiles; +in +listToAttrs solutions diff --git a/users/tazjin/aoc2019/solution-day1.el b/users/tazjin/aoc2019/solution-day1.el new file mode 100644 index 000000000000..d805c22ec870 --- /dev/null +++ b/users/tazjin/aoc2019/solution-day1.el @@ -0,0 +1,28 @@ +;; Advent of Code 2019 - Day 1 +(require 'dash) + +;; Puzzle 1: + +(defvar day-1/input + '(83285 96868 121640 51455 128067 128390 141809 52325 68310 140707 124520 149678 + 87961 52040 133133 52203 117483 85643 84414 86558 65402 122692 88565 61895 + 126271 128802 140363 109764 53600 114391 98973 124467 99574 69140 144856 + 56809 149944 138738 128823 82776 77557 51994 74322 64716 114506 124074 + 73096 97066 96731 149307 135626 121413 69575 98581 50570 60754 94843 72165 + 146504 53290 63491 50936 79644 119081 70218 85849 133228 114550 131943 + 67288 68499 80512 148872 99264 119723 68295 90348 146534 52661 99146 95993 + 130363 78956 126736 82065 77227 129950 97946 132345 107137 79623 148477 + 88928 118911 75277 97162 80664 149742 88983 74518)) + +(defun calculate-fuel (mass) + (- (/ mass 3) 2)) + +(message "Solution to day1/1: %d" (apply #'+ (-map #'calculate-fuel day-1/input))) + +;; Puzzle 2: +(defun calculate-recursive-fuel (mass) + (let ((fuel (calculate-fuel mass))) + (if (< fuel 0) 0 + (+ fuel (calculate-recursive-fuel fuel))))) + +(message "Solution to day1/2: %d" (apply #'+ (-map #'calculate-recursive-fuel day-1/input))) diff --git a/users/tazjin/aoc2019/solution-day2.el b/users/tazjin/aoc2019/solution-day2.el new file mode 100644 index 000000000000..6ecac1e2016c --- /dev/null +++ b/users/tazjin/aoc2019/solution-day2.el @@ -0,0 +1,53 @@ +;; -*- lexical-binding: t; -*- +;; Advent of Code 2019 - Day 2 +(require 'dash) +(require 'ht) + +(defvar day2/input + [1 0 0 3 1 1 2 3 1 3 4 3 1 5 0 3 2 1 9 19 1 19 5 23 1 13 23 27 1 27 6 31 + 2 31 6 35 2 6 35 39 1 39 5 43 1 13 43 47 1 6 47 51 2 13 51 55 1 10 55 + 59 1 59 5 63 1 10 63 67 1 67 5 71 1 71 10 75 1 9 75 79 2 13 79 83 1 9 + 83 87 2 87 13 91 1 10 91 95 1 95 9 99 1 13 99 103 2 103 13 107 1 107 10 + 111 2 10 111 115 1 115 9 119 2 119 6 123 1 5 123 127 1 5 127 131 1 10 + 131 135 1 135 6 139 1 10 139 143 1 143 6 147 2 147 13 151 1 5 151 155 1 + 155 5 159 1 159 2 163 1 163 9 0 99 2 14 0 0]) + +;; Puzzle 1 + +(defun day2/single-op (f state idx) + (let* ((a (aref state (aref state (+ 1 idx)))) + (b (aref state (aref state (+ 2 idx)))) + (p (aref state (+ 3 idx))) + (result (funcall f a b))) + (aset state p (funcall f a b)))) + +(defun day2/operate (state idx) + (pcase (aref state idx) + (99 (aref state 0)) + (1 (day2/single-op #'+ state idx) + (day2/operate state (+ 4 idx))) + (2 (day2/single-op #'* state idx) + (day2/operate state (+ 4 idx))) + (other (error "Unknown opcode: %s" other)))) + +(defun day2/program-with-inputs (noun verb) + (let* ((input (copy-tree day2/input t))) + (aset input 1 noun) + (aset input 2 verb) + (day2/operate input 0))) + +(message "Solution to day2/1: %s" (day2/program-with-inputs 12 2)) + +;; Puzzle 2 +(let* ((used (ht)) + (noun 0) + (verb 0) + (result (day2/program-with-inputs noun verb))) + (while (/= 19690720 result) + (setq noun (random 100)) + (setq verb (random 100)) + (unless (ht-get used (format "%d%d" noun verb)) + (ht-set used (format "%d%d" noun verb) t) + (setq result (day2/program-with-inputs noun verb)))) + + (message "Solution to day2/2: %s%s" noun verb)) diff --git a/users/tazjin/aoc2019/solution-day3.el b/users/tazjin/aoc2019/solution-day3.el new file mode 100644 index 000000000000..b7dfdd245fb1 --- /dev/null +++ b/users/tazjin/aoc2019/solution-day3.el @@ -0,0 +1,64 @@ +;; -*- lexical-binding: t; -*- +;; Advent of Code 2019 - Day 3 + +(require 'cl-lib) +(require 'dash) +(require 'ht) +(require 's) + +(defvar day3/input/wire1 + "R1010,D422,L354,U494,L686,U894,R212,U777,L216,U9,L374,U77,R947,U385,L170,U916,R492,D553,L992,D890,L531,U360,R128,U653,L362,U522,R817,U198,L126,D629,L569,U300,L241,U145,R889,D196,L450,D576,L319,D147,R985,U889,L941,U837,L608,D77,L864,U911,L270,D869,R771,U132,L249,U603,L36,D328,L597,U992,L733,D370,L947,D595,L308,U536,L145,U318,R55,D773,R175,D505,R483,D13,R780,U778,R445,D107,R490,U245,L587,U502,R446,U639,R150,U35,L455,D522,R866,U858,R394,D975,R513,D378,R58,D646,L374,D675,R209,U228,R530,U543,L480,U677,L912,D164,L573,U587,L784,D626,L994,U250,L215,U985,R684,D79,L877,U811,L766,U617,L665,D246,L408,U800,L360,D272,L436,U138,R240,U735,L681,U68,L608,D59,R532,D808,L104,U968,R887,U819,R346,U698,L317,U582,R516,U55,L303,U607,L457,U479,L510,D366,L583,U519,R878,D195,R970,D267,R842,U784,R9,D946,R833,D238,L232,D94,L860,D47,L346,U951,R491,D745,R849,U273,R263,U392,L341,D808,R696,U326,R886,D296,L865,U833,R241,U644,R729,D216,R661,D712,L466,D699,L738,U5,L556,D693,R912,D13,R48,U63,L877,U628,L689,D929,R74,U924,R612,U153,R417,U425,L879,D378,R79,D248,L3,U519,R366,U281,R439,D823,R149,D668,R326,D342,L213,D735,R504,U265,L718,D842,L565,U105,L214,U963,R518,D681,R642,U170,L111,U6,R697,U572,R18,U331,L618,D255,R534,D322,L399,U595,L246,U651,L836,U757,R417,D795,R291,U759,L568,U965,R828,D570,R350,U317,R338,D173,L74,D833,L650,D844,L70,U913,R594,U407,R674,D684,L481,D564,L128,D277,R851,D274,L435,D582,R469,U729,R387,D818,R443,U504,R414,U8,L842,U845,R275,U986,R53,U660,R661,D225,R614,U159,R477") + +(defvar day3/input/wire2 + "L1010,D698,R442,U660,L719,U702,L456,D86,R938,D177,L835,D639,R166,D285,L694,U468,L569,D104,L234,D574,L669,U299,L124,D275,L179,D519,R617,U72,L985,D248,R257,D276,L759,D834,R490,U864,L406,U181,R911,U873,R261,D864,R260,U759,R648,U158,R308,D386,L835,D27,L745,U91,R840,U707,R275,U543,L663,U736,L617,D699,R924,U103,R225,U455,R708,U319,R569,U38,R315,D432,L179,D975,R519,D546,L295,U680,L685,U603,R262,D250,R7,U171,R261,U519,L832,U534,L471,U431,L474,U886,R10,D179,L79,D555,R452,U452,L832,U863,L367,U538,L237,D160,R441,U605,R942,U259,L811,D552,R646,D353,L225,D94,L35,D307,R752,U23,R698,U610,L379,D932,R698,D751,R178,D347,R325,D156,R471,D555,R558,D593,R773,U2,L955,U764,L735,U438,R364,D640,L757,U534,R919,U409,R361,U407,R336,D808,R877,D648,R610,U198,R340,U94,R795,D667,R811,U975,L965,D224,R565,D681,L64,U567,R621,U922,L665,U329,R242,U592,L727,D481,L339,U402,R213,D280,R656,U169,R976,D962,L294,D505,L251,D689,L497,U133,R230,D441,L90,D220,L896,D657,L500,U331,R502,U723,R762,D613,L447,D256,L226,U309,L935,U384,L740,D459,R309,D707,R952,D747,L304,D105,R977,D539,R941,D21,R291,U216,R132,D543,R515,U453,L854,D42,R982,U102,L469,D639,R559,D68,R302,U734,R980,D214,R107,D191,L730,D793,L63,U17,R807,U196,R412,D592,R330,D941,L87,D291,L44,D94,L272,D780,R968,U837,L712,D704,R163,U981,R537,U778,R220,D303,L196,D951,R163,D446,R11,D623,L72,D778,L158,U660,L189,D510,L247,D716,L89,U887,L115,U114,L36,U81,R927,U293,L265,U183,R331,D267,R745,D298,L561,D918,R299,U810,L322,U679,L739,D854,L581,U34,L862,D779,R23") + +;; Puzzle 1 + +(defun wire-from (raw) + (-map (lambda (s) + (cons (substring s 0 1) (string-to-number (substring s 1)))) + (s-split "," raw))) + +(defun day3/move (x y next) + (cl-flet ((steps (by op) + (-map op (reverse (number-sequence 1 by))))) + (pcase next + (`("L" . ,by) (steps by (lambda (n) (cons (- x n) y)))) + (`("R" . ,by) (steps by (lambda (n) (cons (+ x n) y)))) + (`("U" . ,by) (steps by (lambda (n) (cons x (+ y n))))) + (`("D" . ,by) (steps by (lambda (n) (cons x (- y n)))))))) + +(defun day3/wire-points (wire) + (let ((points (ht)) + (point-list (-reduce-from + (lambda (acc point) + (-let* (((x . y) (car acc)) + (next (day3/move x y point))) + (-concat next acc))) + '((0 . 0)) wire))) + (-map (-lambda ((s . p)) (ht-set! points p s)) + (-zip (reverse (number-sequence 0 (- (length point-list) 1))) point-list)) + (ht-remove! points '(0 . 0)) + points)) + +(defun day3/closest-intersection (crossed-points) + (car (-sort #'< + (-map (-lambda ((x . y)) + (+ (abs x) (abs y))) + crossed-points)))) + +(defun day3/minimum-steps (wire1 wire2 crossed) + (car (-sort #'< + (-map (-lambda (p) + (+ (ht-get wire1 p) (ht-get wire2 p))) + crossed)))) + +;; Example: +(let* ((wire1-points (day3/wire-points (wire-from day3/input/wire1))) + (wire2-points (day3/wire-points (wire-from day3/input/wire2))) + (crossed-points (-filter (lambda (p) (ht-contains? wire1-points p)) + (ht-keys wire2-points)))) + (message "Solution for day3/1: %d" (day3/closest-intersection crossed-points)) + (message "Solution for day3/2: %d" (day3/minimum-steps wire1-points + wire2-points + crossed-points))) diff --git a/users/tazjin/aoc2019/solution-day4.el b/users/tazjin/aoc2019/solution-day4.el new file mode 100644 index 000000000000..2805f3f4e9cd --- /dev/null +++ b/users/tazjin/aoc2019/solution-day4.el @@ -0,0 +1,73 @@ +;; -*- lexical-binding: t; -*- +;; Advent of Code 2019 - Day 4 + +(require 'cl-lib) +(require 'dash) + +;; Puzzle 1 + +(defun day4/to-digits (num) + "Convert NUM to a list of its digits." + (cl-labels ((steps (n digits) + (if (= n 0) digits + (steps (/ n 10) (cons (% n 10) digits))))) + (steps num '()))) + +(defvar day4/input (-map #'day4/to-digits (number-sequence 128392 643281))) + +(defun day4/filter-password (digits) + "Determines whether the given rules match the supplied + number." + + (and + ;; It is a six digit number + (= 6 (length digits)) + + ;; Value is within the range given in puzzle input + ;; (noop because the range is generated from the input) + + ;; Two adjacent digits are the same (like 22 in 122345). + (car (-reduce-from (-lambda ((acc . prev) next) + (cons (or acc (= prev next)) next)) + '(nil . 0) digits)) + + ;; Going from left to right, the digits never decrease; they only + ;; ever increase or stay the same (like 111123 or 135679). + (car (-reduce-from (-lambda ((acc . prev) next) + (cons (and acc (>= next prev)) next)) + '(t . 0) digits)))) + +;; Puzzle 2 +;; +;; Additional criteria: If there's matching digits, they're not in a group. + +(cl-defstruct day4/acc state prev count) + +(defun day4/filter-longer-groups (digits) + (let ((res (-reduce-from + (lambda (acc next) + (cond ;; sequence is broken and count was at 1 -> + ;; match! + ((and (= (day4/acc-count acc) 2) + (/= (day4/acc-prev acc) next)) + (setf (day4/acc-state acc) t)) + + ;; sequence continues, counter increment! + ((= (day4/acc-prev acc) next) + (setf (day4/acc-count acc) (+ 1 (day4/acc-count acc)))) + + ;; sequence broken, reset counter + ((/= (day4/acc-prev acc) next) + (setf (day4/acc-count acc) 1))) + + (setf (day4/acc-prev acc) next) + acc) + (make-day4/acc :prev 0 :count 0) digits))) + (or (day4/acc-state res) + (= 2 (day4/acc-count res))))) + +(let* ((simple (-filter #'day4/filter-password day4/input)) + (complex (-filter #'day4/filter-longer-groups simple))) + (message "Solution to day4/1: %d" (length simple)) + (message "Solution to day4/2: %d" (length complex))) + diff --git a/users/tazjin/aoc2020/default.nix b/users/tazjin/aoc2020/default.nix new file mode 100644 index 000000000000..cd89da7de412 --- /dev/null +++ b/users/tazjin/aoc2020/default.nix @@ -0,0 +1,26 @@ +# Solutions for Advent of Code 2020, written in Emacs Lisp. +# +# For each day a new file is created as "solution-day$n.el". +{ depot, pkgs, ... }: + +let + inherit (builtins) attrNames filter head listToAttrs match readDir; + dir = readDir ./.; + matchSolution = match "solution-(.*)\.el"; + isSolution = f: (matchSolution f) != null; + getDay = f: head (matchSolution f); + + solutionFiles = filter (e: dir."${e}" == "regular" && isSolution e) (attrNames dir); + solutions = map + (f: + let day = getDay f; in depot.nix.writeElispBin { + name = day; + deps = p: with p; [ dash s ht p.f ]; + src = ./. + ("/" + f); + }) + solutionFiles; +in +pkgs.symlinkJoin { + name = "aoc2020"; + paths = solutions; +} diff --git a/users/tazjin/aoc2020/solution-day1.el b/users/tazjin/aoc2020/solution-day1.el new file mode 100644 index 000000000000..a04f43d15197 --- /dev/null +++ b/users/tazjin/aoc2020/solution-day1.el @@ -0,0 +1,44 @@ +;; Advent of Code 2020 - Day 1 +(require 'cl) +(require 'ht) +(require 'dash) + +(defmacro hash-set (&rest elements) + "Define a hash-table with empty values, for use as a set." + (cons 'ht (-map (lambda (x) (list x nil)) elements))) + +;; Puzzle 1: + +(defvar day1/input + (hash-set 1645 1995 1658 1062 1472 1710 1424 1823 1518 1656 1811 1511 1320 1521 1395 + 1996 1724 1666 1637 1504 1766 534 1738 1791 1372 1225 1690 1949 1495 1436 1166 + 1686 1861 1889 1887 997 1202 1478 833 1497 1459 1717 1272 1047 1751 1549 1204 + 1230 1260 1611 1506 1648 1354 1415 1615 1327 1622 1592 1807 1601 1026 1757 1376 + 1707 1514 1905 1660 1578 1963 1292 390 1898 1019 1580 1499 1830 1801 1881 1764 + 1442 1838 1088 1087 1040 1349 1644 1908 1697 1115 1178 1224 1810 1445 1594 1894 + 1287 1676 1435 1294 1796 1350 1685 1118 1488 1726 1696 1190 1538 1780 1806 1207 + 1346 1705 983 1249 1455 2002 1466 1723 1227 1390 1281 1715 1603 1862 1744 1774 + 1385 1312 1654 1872 1142 1273 1508 1639 1827 1461 1795 1533 1304 1417 1984 28 + 1693 1951 1391 1931 1179 1278 1400 1361 1369 1343 1416 1426 314 1510 1933 1239 + 1218 1918 1797 1255 1399 1229 723 1992 1595 1191 1916 1525 1605 1524 1869 1652 + 1874 1756 1246 1310 1219 1482 1429 1244 1554 1575 1123 1194 1408 1917 1613 1773 + 1809 1987 1733 1844 1423 1718 1714 1923 1503)) + +(message "Solution to day1/1: %s" + (cl-loop for first being the hash-keys of day1/input + for second = (- 2020 first) + when (ht-contains? day1/input second) + return (* first second))) + +;; Puzzle 2: + +(message "Solution to day1/1: %s" + (cl-loop for first being the hash-keys of day1/input + for result = + (cl-loop + for second being the elements of (-drop 1 (ht-keys day1/input)) + for third = (- 2020 first second) + when (ht-contains? day1/input third) + return (* first second third)) + + when result return result)) diff --git a/users/tazjin/aoc2020/solution-day2.el b/users/tazjin/aoc2020/solution-day2.el new file mode 100644 index 000000000000..5993bf3407e4 --- /dev/null +++ b/users/tazjin/aoc2020/solution-day2.el @@ -0,0 +1,54 @@ +;; Advent of Code 2020 - Day 2 + +(require 'cl-lib) +(require 'f) +(require 'ht) +(require 's) +(require 'seq) + +(defvar day2/input + ;; This one was too large to inline. + (s-lines (f-read "/tmp/aoc/day2.txt"))) + +(defun day2/count-letters (password) + (let ((table (ht-create))) + (cl-loop for char across password + for current = (ht-get table char) + do (ht-set table char + (if current (+ 1 current) 1))) + table)) + +(defun day2/parse (input) + (let* ((split (s-split " " input)) + (range (s-split "-" (car split)))) + (list (string-to-number (car range)) + (string-to-number (cadr range)) + (string-to-char (cadr split)) + (caddr split)))) + +(defun day2/count-with-validation (func) + (length (-filter + (lambda (password) + (and (not (seq-empty-p password)) + (apply func (day2/parse password)))) + day2/input))) + +;; Puzzle 1 + +(defun day2/validate-oldjob (min max char password) + (let ((count (ht-get (day2/count-letters password) char))) + (when count + (and (>= count min) + (<= count max))))) + +(message "Solution to day2/1: %s" + (day2/count-with-validation #'day2/validate-oldjob)) + +;; Puzzle 2 + +(defun day2/validate-toboggan (pos1 pos2 char password) + (xor (= char (aref password (- pos1 1))) + (= char (aref password (- pos2 1))))) + +(message "Solution to day2/2: %s" + (day2/count-with-validation #'day2/validate-toboggan)) diff --git a/users/tazjin/aoc2020/solution-day3.el b/users/tazjin/aoc2020/solution-day3.el new file mode 100644 index 000000000000..80ea4a226405 --- /dev/null +++ b/users/tazjin/aoc2020/solution-day3.el @@ -0,0 +1,43 @@ +;; Advent of Code 2020 - Day 3 + +(require 'cl-lib) +(require 'dash) +(require 'f) +(require 's) +(require 'seq) + +(setq day3/input + (-filter (lambda (s) (not (seq-empty-p s))) + (s-lines (f-read "/tmp/aoc/day3.txt")))) + +(setq day3/input-width (length (elt day3/input 0))) +(setq day3/input-height (length day3/input)) + +(defun day3/thing-at-point (x y) + "Pun intentional." + (when (>= day3/input-height y) + (let ((x-repeated (mod (- x 1) day3/input-width))) + (elt (elt day3/input (- y 1)) x-repeated)))) + +(defun day3/slope (x-steps y-steps) + "Produce the objects encountered through this slope until the + bottom of the map." + (cl-loop for x from 1 by x-steps + for y from 1 to day3/input-height by y-steps + collect (day3/thing-at-point x y))) + +;; Puzzle 1 + +(defun day3/count-trees (x-steps y-steps) + (cl-loop for thing being the elements of (day3/slope x-steps y-steps) + count (= thing ?#))) + +(message "Solution to day3/1: One encounters %s trees" (day3/count-trees 3 1)) + +;; Puzzle 2 + +(message "Solution to day3/2 %s" (* (day3/count-trees 1 1) + (day3/count-trees 3 1) + (day3/count-trees 5 1) + (day3/count-trees 7 1) + (day3/count-trees 1 2))) diff --git a/users/tazjin/aoc2020/solution-day4.el b/users/tazjin/aoc2020/solution-day4.el new file mode 100644 index 000000000000..034a40a9558d --- /dev/null +++ b/users/tazjin/aoc2020/solution-day4.el @@ -0,0 +1,98 @@ +;; Advent of Code 2020 - Day 4 + +(require 'cl-lib) +(require 's) +(require 'dash) +(require 'f) + +(cl-defstruct day4/passport + byr ;; Birth Year + iyr ;; Issue Year + eyr ;; Expiration Year + hgt ;; Height + hcl ;; Hair Color + ecl ;; Eye Color + pid ;; Passport ID + cid ;; Country ID + ) + +(defun day4/parse-passport (input) + (let* ((pairs (s-split " " (s-replace "\n" " " input) t)) + (slots + (-map + (lambda (pair) + (pcase-let ((`(,key ,value) (s-split ":" (s-trim pair)))) + (list (intern (format ":%s" key)) value))) + pairs))) + (apply #'make-day4/passport (-flatten slots)))) + +(defun day4/parse-passports (input) + (-map #'day4/parse-passport (s-split "\n\n" input t))) + +(setq day4/input (day4/parse-passports (f-read "/tmp/aoc/day4.txt"))) + +;; Puzzle 1 + +(defun day4/validate (passport) + "Check that all fields except CID are present." + (cl-check-type passport day4/passport) + (and (day4/passport-byr passport) + (day4/passport-iyr passport) + (day4/passport-eyr passport) + (day4/passport-hgt passport) + (day4/passport-hcl passport) + (day4/passport-ecl passport) + (day4/passport-pid passport))) + +(message "Solution to day4/1: %s" (cl-loop for passport being the elements of day4/input + count (day4/validate passport))) + +;; Puzzle 2 + +(defun day4/year-bound (min max value) + (and + (s-matches? (rx (= 4 digit)) value) + (<= min (string-to-number value) max))) + +(defun day4/check-unit (unit min max value) + (and + (string-match (rx (group (+? digit)) (literal unit)) value) + (<= min (string-to-number (match-string 1 value)) max))) + +(defun day4/properly-validate (passport) + "Opting for readable rather than clever here." + (and + (day4/validate passport) + + ;; byr (Birth Year) - four digits; at least 1920 and at most 2002. + (day4/year-bound 1920 2002 (day4/passport-byr passport)) + + ;; iyr (Issue Year) - four digits; at least 2010 and at most 2020. + (day4/year-bound 2010 2020 (day4/passport-iyr passport)) + + ;; eyr (Expiration Year) - four digits; at least 2020 and at most 2030. + (day4/year-bound 2020 2030 (day4/passport-eyr passport)) + + ;; hgt (Height) - a number followed by either cm or in: + ;; If cm, the number must be at least 150 and at most 193. + ;; If in, the number must be at least 59 and at most 76. + (or (day4/check-unit "cm" 150 193 (day4/passport-hgt passport)) + (day4/check-unit "in" 59 76 (day4/passport-hgt passport))) + + ;; hcl (Hair Color) - a # followed by exactly six characters 0-9 or a-f. + (s-matches? (rx ?# (= 6 hex)) (day4/passport-hcl passport)) + + ;; ecl (Eye Color) - exactly one of: amb blu brn gry grn hzl oth. + (-contains? '("amb" "blu" "brn" "gry" "grn" "hzl" "oth") + (day4/passport-ecl passport)) + + ;; pid (Passport ID) - a nine-digit number, including leading zeroes. + (s-matches? (rx line-start (= 9 digit) line-end) + (day4/passport-pid passport)) + + ;; cid (Country ID) - ignored, missing or not. + )) + +(message "Solution to day4/2: %s" + (cl-loop for passport being the elements of day4/input + count (day4/properly-validate passport))) diff --git a/users/tazjin/aoc2020/solution-day5.el b/users/tazjin/aoc2020/solution-day5.el new file mode 100644 index 000000000000..9bba322902b0 --- /dev/null +++ b/users/tazjin/aoc2020/solution-day5.el @@ -0,0 +1,61 @@ +;; Advent of Code 2020 - Day 5 + +(require 'cl-lib) +(require 'dash) +(require 'f) +(require 'ht) +(require 's) +(require 'seq) + +(defvar day5/input + (-filter (lambda (s) (not (seq-empty-p s))) + (s-lines (f-read "/tmp/aoc/day5.txt")))) + +(defun day5/lower (sequence) + (seq-subseq sequence 0 (/ (length sequence) 2))) + +(defun day5/upper (sequence) + (seq-subseq sequence (/ (length sequence) 2))) + +(defun day5/seat-id (column row) + (+ column (* 8 row))) + +(defun day5/find-seat (boarding-pass) + (let ((rows (number-sequence 0 127)) + (columns (number-sequence 0 7))) + (cl-loop for char across boarding-pass + do (pcase char + (?F (setq rows (day5/lower rows))) + (?B (setq rows (day5/upper rows))) + (?R (setq columns (day5/upper columns))) + (?L (setq columns (day5/lower columns)))) + finally return (day5/seat-id (car columns) (car rows))))) + +;; Puzzle 1 + +(message "Solution to day5/1: %s" + (cl-loop for boarding-pass in day5/input + maximize (day5/find-seat boarding-pass))) + +;; Puzzle 2 + +(defun day5/all-seats-in (row) + (-map (lambda (column) (day5/seat-id column row)) + (number-sequence 0 7))) + +(message "Solution to day5/2: %s" + (let ((all-seats (ht-create))) + (-each (-mapcat #'day5/all-seats-in (number-sequence 1 126)) + (lambda (seat) (ht-set all-seats seat nil))) + + (cl-loop for boarding-pass in day5/input + do (ht-remove all-seats (day5/find-seat boarding-pass)) + + ;; Remove seats that lack adjacent entries, those + ;; are missing on the plane. + finally return + (car + (-filter (lambda (seat) + (and (not (ht-contains? all-seats (- seat 1))) + (not (ht-contains? all-seats (+ seat 1))))) + (ht-keys all-seats)))))) diff --git a/users/tazjin/aoc2020/solution-day6.el b/users/tazjin/aoc2020/solution-day6.el new file mode 100644 index 000000000000..8179c79af2bd --- /dev/null +++ b/users/tazjin/aoc2020/solution-day6.el @@ -0,0 +1,40 @@ +;; Advent of Code 2020 - Day 6 + +(require 'cl-lib) +(require 'dash) +(require 'f) +(require 'ht) +(require 's) + +(defvar day6/input (s-split "\n\n" (f-read "/tmp/aoc/day6.txt") t) + "Input, split into groups (with people in each group still distinct)") + +;; Puzzle 1 + +(defun day6/count-answers (group-answers) + "I suspect doing it this way will be useful in puzzle 2." + (let ((table (ht-create))) + (-each group-answers + (lambda (answer) + (cl-loop for char across answer + do (ht-set table char (+ 1 (or (ht-get table char) + 0)))))) + table)) + +(message "Solution to day6/1: %s" + (cl-loop for group being the elements of day6/input + sum (length + (ht-keys + (day6/count-answers (s-lines group)))))) + +;; Puzzle 2 + +(defun day6/count-unanimous-answers (answers) + (ht-reject (lambda (_key value) (not (= value (length answers)))) + (day6/count-answers answers))) + +(message "Solution to day6/2: %s" + (cl-loop for group being the elements of day6/input + sum (length + (ht-keys + (day6/count-unanimous-answers (s-split "\n" group t)))))) diff --git a/users/tazjin/aoc2020/solution-day7.el b/users/tazjin/aoc2020/solution-day7.el new file mode 100644 index 000000000000..251a85fede02 --- /dev/null +++ b/users/tazjin/aoc2020/solution-day7.el @@ -0,0 +1,92 @@ +;; Advent of Code 2020 - Day 7 + +(require 'cl-lib) +(require 'dash) +(require 'f) +(require 's) +(require 'ht) + +(defvar day7/input + (s-lines (s-chomp (f-read "/tmp/aoc/day7.txt")))) + +(defun day7/parse-bag (input) + (string-match (rx line-start + (group (one-or-more (or letter space))) + "s contain " + (group (one-or-more anything)) + "." line-end) + input) + (cons (match-string 1 input) + (-map + (lambda (content) + (unless (equal content "no other bags") + (progn + (string-match + (rx (group (one-or-more digit)) + space + (group (one-or-more anything) "bag")) + content) + (cons (match-string 2 content) + (string-to-number (match-string 1 content)))))) + (s-split ", " (match-string 2 input))))) + +(defun day7/id-or-next (table bag-type) + (unless (ht-contains? table bag-type) + (ht-set table bag-type (length (ht-keys table)))) + (ht-get table bag-type)) + +(defun day7/build-graph (input &optional flip) + "Represent graph mappings directionally using an adjacency + matrix, because that's probably easiest. + + By default an edge means 'contains', with optional argument + FLIP edges are inverted and mean 'contained by'." + + (let ((bag-mapping (ht-create)) + (graph (let ((length (length input))) + (apply #'vector + (-map (lambda (_) (make-vector length 0)) input))))) + (cl-loop for bag in (-map #'day7/parse-bag input) + for bag-id = (day7/id-or-next bag-mapping (car bag)) + do (-each (-filter #'identity (cdr bag)) + (pcase-lambda (`(,contained-type . ,count)) + (let ((contained-id (day7/id-or-next bag-mapping contained-type))) + (if flip + (aset (aref graph contained-id) bag-id count) + (aset (aref graph bag-id) contained-id count)))))) + (cons bag-mapping graph))) + +;; Puzzle 1 + +(defun day7/find-ancestors (visited graph start) + (ht-set visited start t) + (cl-loop for bag-count being the elements of (aref graph start) + using (index bag-id) + when (and (> bag-count 0) + (not (ht-contains? visited bag-id))) + do (day7/find-ancestors visited graph bag-id))) + +(message + "Solution to day7/1: %s" + (pcase-let* ((`(,mapping . ,graph) (day7/build-graph day7/input t)) + (shiny-gold-id (ht-get mapping "shiny gold bag")) + (visited (ht-create))) + (day7/find-ancestors visited graph shiny-gold-id) + (- (length (ht-keys visited)) 1))) + +;; Puzzle 2 + +(defun ht-find-by-value (table value) + (ht-find (lambda (_key item-value) (equal item-value value)) table)) + +(defun day7/count-contained-bags (mapping graph start) + (cl-loop for bag-count being the elements of (aref graph start) + using (index bag-id) + when (> bag-count 0) + sum (+ bag-count + (* bag-count (day7/count-contained-bags mapping graph bag-id))))) + +(message "Solution to day7/2: %s" + (pcase-let* ((`(,mapping . ,graph) (day7/build-graph day7/input)) + (shiny-gold-id (ht-get mapping "shiny gold bag"))) + (day7/count-contained-bags mapping graph shiny-gold-id))) diff --git a/users/tazjin/aoc2020/solution-day8.el b/users/tazjin/aoc2020/solution-day8.el new file mode 100644 index 000000000000..591a07fbf3a0 --- /dev/null +++ b/users/tazjin/aoc2020/solution-day8.el @@ -0,0 +1,63 @@ +;; Advent of Code 2020 - Day + +(require 'cl-lib) +(require 'dash) +(require 'f) +(require 's) + +(setq day8/input + (apply #'vector + (-map (lambda (s) + (pcase-let ((`(,op ,val) (s-split " " s t))) + (cons (intern op) (string-to-number val)))) + (s-lines (s-chomp (f-read "/tmp/aoc/day8.txt")))))) + +(defun day8/step (code position acc) + (if (>= position (length code)) + (cons 'final acc) + + (let ((current (aref code position))) + (aset code position :done) + (pcase current + (:done (cons 'loop acc)) + (`(nop . ,val) (cons (+ position 1) acc)) + (`(acc . ,val) (cons (+ position 1) (+ acc val))) + (`(jmp . ,val) (cons (+ position val) acc)))))) + +;; Puzzle 1 + +(message "Solution to day8/1: %s" + (let ((code (copy-sequence day8/input)) + (position 0) + (acc 0)) + (cl-loop for next = (day8/step code position acc) + when (equal 'loop (car next)) return (cdr next) + do (setq position (car next)) + do (setq acc (cdr next))))) + +;; Puzzle 2 + +(defun day8/flip-at (code pos) + (pcase (aref code pos) + (`(nop . ,val) (aset code pos `(jmp . ,val))) + (`(jmp . ,val) (aset code pos `(nop . ,val))) + (other (error "Unexpected flip op: %s" other)))) + +(defun day8/try-flip (flip-at code position acc) + (day8/flip-at code flip-at) + (cl-loop for next = (day8/step code position acc) + when (equal 'loop (car next)) return nil + when (equal 'final (car next)) return (cdr next) + do (setq position (car next)) + do (setq acc (cdr next)))) + +(message "Solution to day8/2: %s" + (let ((flip-options (cl-loop for op being the elements of day8/input + using (index idx) + for opcode = (car op) + when (or (equal 'nop opcode) + (equal 'jmp opcode)) + collect idx))) + (cl-loop for flip-at in flip-options + for result = (day8/try-flip flip-at (copy-sequence day8/input) 0 0) + when result return result))) diff --git a/users/tazjin/avatar.jpeg b/users/tazjin/avatar.jpeg new file mode 100644 index 000000000000..f6888e01c7dc --- /dev/null +++ b/users/tazjin/avatar.jpeg Binary files differdiff --git a/users/tazjin/blog/.skip-subtree b/users/tazjin/blog/.skip-subtree new file mode 100644 index 000000000000..e7fa50d49bdd --- /dev/null +++ b/users/tazjin/blog/.skip-subtree @@ -0,0 +1 @@ +Subdirectories contain blog posts and static assets only diff --git a/users/tazjin/blog/default.nix b/users/tazjin/blog/default.nix new file mode 100644 index 000000000000..c8b3c318995b --- /dev/null +++ b/users/tazjin/blog/default.nix @@ -0,0 +1,46 @@ +{ depot, lib, pkgs, ... }: + +with depot.nix.yants; + +let + inherit (builtins) hasAttr filter; + + config = { + name = "tazjin's blog"; + baseUrl = "https://tazj.in/blog"; + + footer = '' + <p class="footer"> + <a class="uncoloured-link" href="https://tazj.in">homepage</a> + | + <a class="uncoloured-link" href="https://cs.tvl.fyi/">code</a> + </p> + <p class="lod">เฒ _เฒ </p> + ''; + }; + + inherit (depot.web.blog) post includePost renderPost; + + posts = filter includePost (list post (import ./posts.nix)); + + rendered = pkgs.runCommandNoCC "tazjins-blog" { } '' + mkdir -p $out + + ${lib.concatStringsSep "\n" (map (post: + "cp ${renderPost config post} $out/${post.key}.html" + ) posts)} + ''; + +in +{ + inherit posts rendered config; + + # Generate embeddable nginx configuration for redirects from old post URLs + oldRedirects = lib.concatStringsSep "\n" (map + (post: '' + location ~* ^(/en)?/${post.oldKey} { + return 301 https://tazj.in/blog/${post.key}; + } + '') + (filter (hasAttr "oldKey") posts)); +} diff --git a/users/tazjin/blog/posts.nix b/users/tazjin/blog/posts.nix new file mode 100644 index 000000000000..eeba600286db --- /dev/null +++ b/users/tazjin/blog/posts.nix @@ -0,0 +1,57 @@ +# This file defines all the blog posts. +[ + { + key = "emacs-is-underrated"; + title = "Emacs is the most underrated tool"; + date = 1581286656; + content = ./posts/emacs-is-underrated.md; + draft = true; + } + { + key = "best-tools"; + title = "tazjin's best tools"; + date = 1576800001; + content = ./posts/best-tools.md; + } + { + key = "nixery-layers"; + title = "Nixery: Improved Layering Design"; + date = 1565391600; + content = ./posts/nixery-layers.md; + } + { + key = "reversing-watchguard-vpn"; + title = "Reverse-engineering WatchGuard Mobile VPN"; + date = 1486830338; + content = ./posts/reversing-watchguard-vpn.md; + oldKey = "1486830338"; + } + { + key = "make-object-t-again"; + title = "Make Object <T> Again!"; + date = 1476807384; + content = ./posts/make-object-t-again.md; + oldKey = "1476807384"; + } + { + key = "the-smu-problem"; + title = "The SMU-problem of messaging apps"; + date = 1450354078; + content = ./posts/the-smu-problem.md; + oldKey = "1450354078"; + } + { + key = "sick-in-sweden"; + title = "Being sick in Sweden"; + date = 1423995834; + content = ./posts/sick-in-sweden.md; + oldKey = "1423995834"; + } + { + key = "nsa-zettabytes"; + title = "The NSA's 5 zettabytes of data"; + date = 1375310627; + content = ./posts/nsa-zettabytes.md; + oldKey = "1375310627"; + } +] diff --git a/users/tazjin/blog/posts/best-tools.md b/users/tazjin/blog/posts/best-tools.md new file mode 100644 index 000000000000..314d0a4419bd --- /dev/null +++ b/users/tazjin/blog/posts/best-tools.md @@ -0,0 +1,183 @@ +In the spirit of various other "Which X do you use?"-pages I thought it would be +fun to have a little post here that describes which tools I've found to work +well for myself. + +When I say "tools" here, it's not about software - it's about real, physical +tools! + +If something goes on this list that's because I think it's seriously a +best-in-class type of product. + +<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc --> +- [Media & Tech](#media--tech) + - [Keyboard](#keyboard) + - [Speakers](#speakers) + - [Headphones](#headphones) + - [Earphones](#earphones) + - [Phone](#phone) +- [Other stuff](#other-stuff) + - [Toothbrush](#toothbrush) + - [Shavers](#shavers) + - [Shoulder bag](#shoulder-bag) + - [Wallet](#wallet) +<!-- markdown-toc end --> + +--------- + +# Media & Tech + +## Keyboard + +The best keyboard that money will buy you at the moment is the [Kinesis +Advantage][advantage]. There's a variety of contoured & similarly shaped +keyboards on the market, but the Kinesis is the only one I've tried that has +properly implemented the keywell concept. + +I struggle with RSI issues and the Kinesis actually makes it possible for me to +type for longer periods of time, which always leads to extra discomfort on +laptop keyboards and such. + +Honestly, the Kinesis is probably the best piece of equipment on this entire +list. I own several of them and there will probably be more in the future. They +last forever and your wrists will thank you in the future, even if you do not +suffer from RSI yet. + +Kinesis have announced a split version of the Advantage. Once that is +easily available, I will buy one and evaluate it. + +[advantage]: https://kinesis-ergo.com/shop/advantage2/ + +## Speakers + +There are two sets of speakers I use, unfortunately one pair has been in storage +since I left the UK. + +My original favourite speakers are the [Teufel Motiv 2][motiv-2], usually hooked +up to a Chromecast and a record player. I've had these for over a decade and +they're incredibly good, but unfortunately Teufel no longer makes them. Mine are +currently in a warehouse somewhere in London, and I don't know when I will see +them again ... + +It's possible to grab a pair on eBay occasionally, so keep an eye out if you're +interested! + +In my Moscow flat, I have a pair of [Wharfedale Diamond 12][diamond-12] +connected to a Philips amplifier older than myself. These provide an excellent, +balanced, "Wharfedale-sound". Some people find it needs some getting used to, +but don't want to go back after that initial phase. + +[motiv-2]: https://www.teufelaudio.com/uk/pc/motiv-2-p167.html +[diamond-12]: https://www.wharfedaleusa.com/collections/diamond-12 + +## Headphones + +I use the [Bose QC35][qc35] (note: link goes to a newer generation than the one +I own) for their outstanding noise cancelling functionality and decent sound. + +When I first bought them I didn't expect them to end up on this list as the +firmware had issues that made them only barely usable, but Bose has managed to +iron these problems out over time. + +I avoid using Bluetooth when outside and fortunately the QC35 come with an +optional cable that you can plug into any good old 3.5mm jack. + +[qc35]: https://www.bose.co.uk/en_gb/products/headphones/over_ear_headphones/quietcomfort-35-wireless-ii.html + +### Earphones + +Actually, to follow up on the above - most of the time I'm not using (over-ear) +headphones, but (in-ear) earphones - specifically the (**wired!!!**) [Apple +EarPods][earpods]. + +Apple will probably stop selling these soon because they've gotten into the +habit of cancelling all of their good products, so I have a stash of these +around. You will usually find no fewer than 3-4 of them lying around in my +flat. + +[earpods]: https://www.apple.com/uk/shop/product/MNHF2ZM/A/earpods-with-35mm-headphone-plug + +## Phone + +My current phone is the [Unihertz Atom L][atom-l]. It is a rugged, small-screen +smartphone with a 3.5mm heapdhone jack. This phone is *weird* looking, getting +one of these means you will constantly be asked what kind of device you have +there. It does everything I need, and survives being thrown against a wall when +the software misbehaves. As the Swedes would say, this phone is solidly *lagom*. + +The ruggedness is an additional bonus as it survives things like sauna / banya +trips just fine. + +Up until a few years ago I used the original [iPhone SE][se]. This was the last +truly good phone anyone made, but unfortunately it was discontinued and is iOS +only. + +[atom-l]: https://www.unihertz.com/products/atom-l +[se]: https://en.wikipedia.org/wiki/IPhone_SE + +# Other stuff + +## Toothbrush + +The [Philips Sonicare][sonicare] is excellent and well worth its price. + +I've had it for a few years and whereas I occasionally had minor teeth issues +before, they seem to be mostly gone now. According to my dentist the state of my +teeth is now usually pretty good and I draw a direct correlation back to this +thing. + +It has an app and stuff, but I just ignore that. + +I first got one of these in about 2014, and it lasted until 2020, at which point +I upgraded to whatever the current model was. + +[sonicare]: https://www.philips.co.uk/c-m-pe/electric-toothbrushes + +## Shavers + +The [Philipps SensoTouch 3D][sensotouch] is excellent. Super-comfortable close +face shave in no time and leaves absolutely no mess around, as far as I can +tell! I've had this for ~7 years and it's not showing any serious signs of aging +yet. + +Another bonus is that its battery time is effectively infinite (in the order of +months of use per charge). I've never had to worry when bringing it on a longer +trip! + +[sensotouch]: https://www.philips.co.uk/c-p/1250X_40/norelco-sensotouch-3d-wet-and-dry-electric-razor-with-precision-trimmer + +## Shoulder bag + +When I moved to London I wanted to stop using backpacks most of the time, as +those are just annoying to deal with when commuting on the tube. + +To work around this I wanted a good shoulder bag with a vertical format (to save +space), but it turned out that there's very few of those around that reach any +kind of quality standard. + +The one I settled on is the [Waterfield Muzetto][muzetto] leather bag. It's one +of those things that comes with a bit of a price tag attached, but it's well +worth it! + +**Unfortunately**, just like my speakers, this bag is now in storage somewhere +in the UK since I left the country. + +After moving to Moscow I quickly ran into the same problem as in London when +using the metro, but getting another Muzetto was kind of impractical. + +I couldn't find any other vertical messenger bags that I liked, and ended up +going for a more traditional one: The [Brialdi Ostin][ostin]. + +[muzetto]: https://www.sfbags.com/collections/shoulder-messenger-bags/products/muzetto-leather-bag +[ostin]: https://www.brialdi.ru/shop/handbags/brialdi_ostin_brown/ + +## Wallet + +My wallet is the [Bellroy Slim Sleeve][slim-sleeve]. It's near indestructible, +looks great, is very slim and fits a ton of cards, business cards, receipts and +whatever else you want to be lugging around with you! + +However, now that I'm spending a lot of time in Egypt (where cash is still +king), not having a place to stash cash is a bit of an issue. So far, no +solution! + +[slim-sleeve]: https://bellroy.com/products/slim-sleeve-wallet/default/charcoal diff --git a/users/tazjin/blog/posts/emacs-is-underrated.md b/users/tazjin/blog/posts/emacs-is-underrated.md new file mode 100644 index 000000000000..afb8dc889e53 --- /dev/null +++ b/users/tazjin/blog/posts/emacs-is-underrated.md @@ -0,0 +1,233 @@ +TIP: Hello, and thanks for offering to review my draft! This post +intends to convey to people what the point of Emacs is. Not to convert +them to use it, but at least with opening their minds to the +possibility that it might contain valuable things. I don't know if I'm +on track in the right direction, and your input will help me figure it +out. Thanks! + +TODO(tazjin): Restructure sections: Intro -> Introspectability (and +story) -> text-based UIs (which lead to fluidity, muscle memory across +programs and "translatability" of workflows) -> Outro. It needs more +flow! + +TODO(tazjin): Highlight more that it's not about editing: People can +derive useful things from Emacs by just using magit/org/notmuch/etc.! + +TODO(tazjin): Note that there's value in trying Emacs even if people +don't end up using it, similar to how learning languages like Lisp or +Haskell helps grow as a programmer even without using them day-to-day. + +*Real post starts below!* + +--------- + +There are two kinds of people: Those who use Emacs, and those who +think it is a text editor. This post is aimed at those in the second +category. + +Emacs is the most critical piece of software I run. My [Emacs +configuration][emacs-config] has steadily evolved for almost a decade. +Emacs is my window manager, mail client, terminal, git client, +information management system and - perhaps unsurprisingly - text +editor. + +Before going into why I chose to invest so much into this program, +follow me along on a little thought experiment: + +---------- + +Lets say you use a proprietary spreadsheet program. You find that +there are features in it that *almost, but not quite* do what you +want. + +What can you do? You can file a feature request to the company that +makes it and hope they listen, but for the likes of Apple and +Microsoft chances are they won't and there is nothing you can do. + +Let's say you are also running an open-source program for image +manipulation. You again find that some of its features are subtly +different from what you would want them to do. + +Things look a bit different this time - after all, the program is +open-source! You can go and fetch its source code, figure out its +internal structure and wrangle various layers of code into submission +until you find the piece that implements the functionality you want to +change. If you know the language it is written in; you can modify the +feature. + +Now all that's left is figuring out its build system[^1], building and +installing it and moving over to the new version. + +Realistically you are not going to do this much in the real world. The +friction to contributing to projects, especially complex ones, is +often quite high. For minor inconveniences, you might often find +yourself just shrugging and working around them. + +What if it didn't have to be this way? + +------------- + +One of the core properties of Emacs is that it is *introspective* and +*self-documenting*. + +For example: A few years ago, I had just switched over to using +[EXWM][], the Emacs X Window Manager. To launch applications I was +using an Emacs program called Helm that let me select installed +programs interactively and press <kbd>RET</kbd> to execute them. + +This was very useful - until I discovered that if I tried to open a +second terminal window, it would display an error: + + Error: urxvt is already running + +Had this been dmenu, I might have had to go through the whole process +described above to fix the issue. But it wasn't dmenu - it was an +Emacs program, and I did the following things: + +1. I pressed <kbd>C-h k</kbd>[^2] (which means "please tell me what + the following key does"), followed by <kbd>s-d</kbd> (which was my + keybinding for launching programs). + +2. Emacs displayed a new buffer saying, roughly: + + ``` + s-d runs the command helm-run-external-command (found in global-map), + which is an interactive autoloaded compiled Lisp function in + โ.../helm-external.elโ. + + It is bound to s-d. + ``` + + I clicked on the filename. + +3. Emacs opened the file and jumped to the definition of + `helm-run-external-command`. After a few seconds of reading through + the code, I found this snippet: + + ```lisp + (if (get-process proc) + (if helm-raise-command + (shell-command (format helm-raise-command real-com)) + (error "Error: %s is already running" real-com)) + ;; ... the actual code to launch programs followed below ... + ) + ``` + +4. I deleted the outer if-expression which implemented the behaviour I + didn't want, pressed <kbd>C-M-x</kbd> to reload the code and saved + the file. + +The whole process took maybe a minute, and the problem was now gone. + +Emacs isn't just "open-source", it actively encourages the user to +modify it, discover what to modify and experiment while it is running. + +In some sense it is like the experience of the old Lisp machines, a +paradigm that we have completely forgotten. + +--------------- + +Circling back to my opening statement: If Emacs is not a text editor, +then what *is* it? + +The Emacs website says this: + +> [Emacs] is an interpreter for Emacs Lisp, a dialect of the Lisp +> programming language with extensions to support text editing + +The core of Emacs implements the language and the functionality needed +to evaluate and run it, as well as various primitives for user +interface construction such as buffers, windows and frames. + +Every other feature of Emacs is implemented *in Emacs Lisp*. + +The Emacs distribution ships with rudimentary text editing +functionality (and some language-specific support for the most popular +languages), but it also brings with it two IRC clients, a Tetris +implementation, a text-mode web browser, [org-mode][] and many other +tools. + +Outside of the core distribution there is a myriad of available +programs for Emacs: [magit][] (the famous git porcelain), text-based +[HTTP clients][], even interactive [Kubernetes frontends][k8s]. + +What all of these tools have in common is that they use text-based +user interfaces (UI elements like images are used only sparingly in +Emacs), and that they can be introspected and composed like everything +else in Emacs. + +If magit does not expose a git flag I need, it's trivial to add. If I +want a keybinding to jump from a buffer showing me a Kubernetes pod to +a magit buffer for the source code of the container, it only takes a +few lines of Emacs Lisp to implement. + +As proficiency with Emacs Lisp ramps up, the environment becomes +malleable like clay and evolves along with the user's taste and needs. +Muscle memory learned for one program translates seamlessly to others, +and the overall effect is an improvement in *workflow fluidity* that +is difficult to overstate. + +Also, workflows based on Emacs are *stable*. Moving my window +management to Emacs has meant that I'm not subject to the whim of some +third-party developer changing my window layouting features (as they +often do on MacOS). + +To illustrate this: Emacs has development history back to the 1970s, +continuous git history that survived multiple VCS migrations [since +1985][first-commit] (that's 22 years before git itself was released!) +and there is code[^3] implementing interactive functionality that has +survived unmodified in Emacs *since then*. + +--------------- + +Now, what is the point of this post? + +I decided to write this after a recent [tweet][] by @IanColdwater (in +the context of todo-management apps): + +> The fact that it's 2020 and the most viable answer to this appears +> to be Emacs might be the saddest thing I've ever heard + +What bothers me is that people see this as *sad*. Emacs being around +for this long and still being unparalleled for many of the UX +paradigms implemented by its programs is, in my book, incredible - and +not sad. + +How many other paradigms have survived this long? How many other tools +still have fervent followers, amazing [developer tooling][] and a +[vibrant ecosystem][] at this age? + +Steve Yegge [said it best][babel][^5]: Emacs has the Quality Without a +Name. + +What I wish you, the reader, should take away from this post is the +following: + +TODO(tazjin): Figure out what people should take away from this post. +I need to sleep on it. It's something about not dismissing tools just +because of their age, urging them to explore paradigms that might seem +unfamiliar and so on. Ideas welcome. + +--------------- + +[^1]: Wouldn't it be a joy if every project just used Nix? I digress ... +[^2]: These are keyboard shortcuts written in [Emacs Key Notation][ekn]. +[^3]: For example, [functionality for online memes][studly] that + wouldn't be invented for decades to come! +[^4]: ... and some things wrong, but that is an issue for a separate post! +[^5]: And I really *do* urge you to read that post's section on Emacs. + +[emacs-config]: https://git.tazj.in/tree/tools/emacs +[EXWM]: https://github.com/ch11ng/exwm +[helm]: https://github.com/emacs-helm/helm +[ekn]: https://www.gnu.org/software/emacs/manual/html_node/efaq/Basic-keys.html +[org-mode]: https://orgmode.org/ +[magit]: https://magit.vc +[HTTP clients]: https://github.com/pashky/restclient.el +[k8s]: https://github.com/jypma/kubectl +[first-commit]: http://git.savannah.gnu.org/cgit/emacs.git/commit/?id=ce5584125c44a1a2fbb46e810459c50b227a95e2 +[studly]: http://git.savannah.gnu.org/cgit/emacs.git/commit/?id=47bdd84a0a9d20aab934482a64b84d0db63e7532 +[tweet]: https://twitter.com/IanColdwater/status/1220824466525229056 +[developer tooling]: https://github.com/alphapapa/emacs-package-dev-handbook +[vibrant ecosystem]: https://github.com/emacs-tw/awesome-emacs +[babel]: https://sites.google.com/site/steveyegge2/tour-de-babel#TOC-Lisp diff --git a/users/tazjin/blog/posts/make-object-t-again.md b/users/tazjin/blog/posts/make-object-t-again.md new file mode 100644 index 000000000000..420b57c0fde9 --- /dev/null +++ b/users/tazjin/blog/posts/make-object-t-again.md @@ -0,0 +1,98 @@ +A few minutes ago I found myself debugging a strange Java issue related +to Jackson, one of the most common Java JSON serialization libraries. + +The gist of the issue was that a short wrapper using some types from +[Javaslang](http://www.javaslang.io/) was causing unexpected problems: + +```java +public <T> Try<T> readValue(String json, TypeReference type) { + return Try.of(() -> objectMapper.readValue(json, type)); +} +``` + +The signature of this function was based on the original Jackson +`readValue` type signature: + +```java +public <T> T readValue(String content, TypeReference valueTypeRef) +``` + +While happily using my wrapper function I suddenly got an unexpected +error telling me that `Object` is incompatible with the type I was +asking Jackson to de-serialize, which got me to re-evaluate the above +type signature again. + +Lets look for a second at some code that will *happily compile* if you +are using Jackson\'s own `readValue`: + +```java +// This shouldn't compile! +Long l = objectMapper.readValue("\"foo\"", new TypeReference<String>(){}); +``` + +As you can see there we ask Jackson to decode the JSON into a `String` +as enclosed in the `TypeReference`, but assign the result to a `Long`. +And it compiles. And it failes at runtime with +`java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Long`. +Huh? + +Looking at the Jackson `readValue` implementation it becomes clear +what\'s going on here: + +```java +@SuppressWarnings({ "unchecked", "rawtypes" }) +public <T> T readValue(String content, TypeReference valueTypeRef) + throws IOException, JsonParseException, JsonMappingException +{ + return (T) _readMapAndClose(/* whatever */); +} +``` + +The function is parameterised over the type `T`, however the only place +where `T` occurs in the signature is in the parameter declaration and +the function return type. Java will happily let you use generic +functions and types without specifying type parameters: + +```java +// Compiles fine! +final List myList = List.of(1,2,3); + +// Type is now myList : List<Object> +``` + +Meaning that those parameters default to `Object`. Now in the code above +Jackson also explicitly casts the return value of its inner function +call to `T`. + +What ends up happening is that Java infers the expected return type from +the context of the `readValue` and then happily uses the unchecked cast +to fit that return type. If the type hints of the context aren\'t strong +enough we simply get `Object` back. + +So what\'s the fix for this? It\'s quite simple: + +```java +public <T> T readValue(String content, TypeReference<T> valueTypeRef) +``` + +By also making the parameter appear in the `TypeReference` we \"bind\" +`T` to the type enclosed in the type reference. The cast can then also +safely be removed. + +The cherries on top of this are: + +1. `@SuppressWarnings({ "rawtypes" })` explicitly disables a + warning that would\'ve caught this + +2. the `readValue` implementation using the less powerful `Class` + class to carry the type parameter does this correctly: `public <T> + T readValue(String content, Class<T> valueType)` + +The big question I have about this is *why* does Jackson do it this way? +Obviously the warning did not just appear there by chance, so somebody +must have thought about this? + +If anyone knows what the reason is, I\'d be happy to hear from you. + +PS: Shoutout to David & Lucia for helping me not lose my sanity over +this. diff --git a/users/tazjin/blog/posts/nixery-layers.md b/users/tazjin/blog/posts/nixery-layers.md new file mode 100644 index 000000000000..38ca2294a88a --- /dev/null +++ b/users/tazjin/blog/posts/nixery-layers.md @@ -0,0 +1,272 @@ +TIP: This blog post was originally published as a design document for +[Nixery][] and is not written in the same style +as other blog posts. + +Thanks to my colleagues at Google and various people from the Nix community for +reviewing this. + +------ + +# Nixery: Improved Layering + +**Authors**: tazjin@ + +**Reviewers**: so...@, en...@, pe...@ + +**Status**: Implemented + +**Last Updated**: 2019-08-10 + +## Introduction + +This document describes a design for an improved image layering method for use +in Nixery. The algorithm [currently used][grhmc] is designed for a slightly +different use-case and we can improve upon it by making use of more of the +available data. + +## Background / Motivation + +Nixery is a service that uses the [Nix package manager][nix] to build container +images (for runtimes such as Docker), that are served on-demand via the +container [registry protocols][]. A demo instance is available at +[nixery.dev][]. + +In practice this means users can simply issue a command such as `docker pull +nixery.dev/shell/git` and receive an image that was built ad-hoc containing a +shell environment and git. + +One of the major advantages of building container images via Nix (as described +for `buildLayeredImage` in [this blog post][grhmc]) is that the +content-addressable nature of container image layers can be used to provide more +efficient caching characteristics (caching based on layer content) than what is +common with Dockerfiles and other image creation methods (caching based on layer +creation method). + +However, this is constrained by the maximum number of layers supported in an +image (125). A naive approach such as putting each included package (any +library, binary, etc.) in its own layer quickly runs into this limitation due to +the large number of dependencies more complex systems tend to have. In addition, +users wanting to extend images created by Nixery (e.g. via `FROM nixery.dev/โฆ`) +share this layer maximum with the created image - limiting extensibility if all +layers are used up by Nixery. + +In theory the layering strategy of `buildLayeredImage` should already provide +good caching characteristics, but in practice we are seeing many images with +significantly more packages than the number of layers configured, leading to +more frequent cache-misses than desired. + +The current implementation of `buildLayeredImage` inspects a graph of image +dependencies and determines the total number of references (direct & indirect) +to any node in the graph. It then sorts all dependencies by this popularity +metric and puts the first `n - 2` (for `n` being the maximum number of layers) +packages in their own layers, all remaining packages in one layer and the image +configuration in the final layer. + +## Design / Proposal + +## (Close-to) ideal layer-layout using more data + +We start out by considering what a close to ideal layout of layers would look +like for a simple use-case. + +![Ideal layout](/static/img/nixery/ideal_layout.webp) + +In this example, counting the total number of references to each node in the +graph yields the following result: + +| pkg | refs | +|-------|------| +| E | 3 | +| D | 2 | +| F | 2 | +| A,B,C | 1 | + +Assuming we are constrained to 4 layers, the current algorithm would yield these layers: + +``` +L1: E +L2: D +L3: F +L4: A, B, C +``` + +The initial proposal for this design is that additional data should be +considered in addition to the total number of references, in particular a +distinction should be made between direct and indirect references. Packages that +are only referenced indirectly should be merged with their parents. + +This yields the following table: + +| pkg | direct | indirect | +|-------|--------|----------| +| E | 3 | 3 | +| D | 2 | 2 | +| F | *1* | 2 | +| A,B,C | 1 | 1 | + +Despite having two indirect references, F is in fact only being referred to +once. Assuming that we have no other data available outside of this graph, we +have no reason to assume that F has any popularity outside of the scope of D. +This might yield the following layers: + +``` +L1: E +L2: D, F +L3: A +L4: B, C +``` + +D and F were grouped, while the top-level references (i.e. the packages +explicitly requested by the user) were split up. + +An assumption is introduced here to justify this split: The top-level packages +is what the user is modifying directly, and those groupings are likely +unpredictable. Thus it is opportune to not group top-level packages in the same +layer. + +This raises a new question: Can we make better decisions about where to split +the top-level? + +## (Even closer to) ideal layering using (even) more data + +So far when deciding layer layouts, only information immediately available in +the build graph of the image has been considered. We do however have much more +information available, as we have both the entire nixpkgs-tree and potentially +other information (such as download statistics). + +We can calculate the total number of references to any derivation in nixpkgs and +use that to rank the popularity of each package. Packages within some percentile +can then be singled out as good candidates for a separate layer. + +When faced with a splitting decision such as in the last section, this data can +aid the decision. Assume for example that package B in the above is actually +`openssl`, which is a very popular package. Taking this into account would +instead yield the following layers: + +``` +L1: E, +L2: D, F +L3: B, +L4: A, C +``` + +## Layer budgets and download size considerations + +As described in the introduction, there is a finite amount of layers available +for each image (the โlayer budgetโ). When calculating the layer distribution, we +might end up with the โidealโ list of layers that we would like to create. Using +our previous example: + +``` +L1: E, +L2: D, F +L3: A +L4: B +L5: C +``` + +If we only have a layer budget of 4 available, something needs to be merged into +the same layer. To make a decision here we could consider only the package +popularity, but there is in fact another piece of information that has not come +up yet: The actual size of the package. + +Presumably a user would not mind downloading a library that is a few kilobytes +in size repeatedly, but they would if it was a 200 megabyte binary instead. + +Conversely if a large binary was successfully cached, but an extremely popular +small library is not, the total download size might also grow to irritating +levels. + +To avoid this we can calculate a merge rating: + + merge_rating(pkg) = popularity_percentile(pkg) ร size(pkg.subtree) + +Packages with a low merge rating would be merged together before packages with +higher merge ratings. + +## Implementation + +There are two primary components of the implementation: + +1. The layering component which, given an image specification, decides the image + layers. + +2. The popularity component which, given the entire nixpkgs-tree, calculates the + popularity of packages. + +## Layering component + +It turns out that graph theoryโs concept of [dominator trees][] maps reasonably +well onto the proposed idea of separating direct and indirect dependencies. This +becomes visible when creating the dominator tree of a simple example: + +![Example without extra edges](/static/img/nixery/example_plain.webp) + +Before calculating the dominator tree, we inspect each node and insert extra +edges from the root for packages that match a certain popularity or size +threshold. In this example, G is popular and an extra edge is inserted: + +![Example with extra edges](/static/img/nixery/example_extra.webp) + +Calculating the dominator tree of this graph now yields our ideal layer +distribution: + +![Dominator tree of example](/static/img/nixery/dominator.webp) + +The nodes immediately dominated by the root node can now be โharvestedโ as image +layers, and merging can be performed as described above until the result fits +into the layer budget. + +To implement this, the layering component uses the [gonum/graph][] library which +supports calculating dominator trees. The program is fed with Nixโs +`exportReferencesGraph` (which contains the runtime dependency graph and runtime +closure size) as well as the popularity data and layer budget. It returns a list +of layers, each specifying the paths it should contain. + +Nix invokes this program and uses the output to create a derivation for each +layer, which is then built and returned to Nixery as usual. + +TIP: This is implemented in [`layers.go`][layers.go] in Nixery. The file starts +with an explanatory comment that talks through the process in detail. + +## Popularity component + +The primary issue in calculating the popularity of each package in the tree is +that we are interested in the runtime dependencies of a derivation, not its +build dependencies. + +To access information about the runtime dependency, the derivation actually +needs to be built by Nix - it can not be inferred because Nix does not know +which store paths will still be referenced by the build output. + +However for packages that are cached in the NixOS cache, we can simply inspect +the `narinfo`-files and use those to determine popularity. + +Not every package in nixpkgs is cached, but we can expect all *popular* packages +to be cached. Relying on the cache should therefore be reasonable and avoids us +having to rebuild/download all packages. + +The implementation will read the `narinfo` for each store path in the cache at a +given commit and create a JSON-file containing the total reference count per +package. + +For the public Nixery instance, these popularity files will be distributed via a +GCS bucket. + +TIP: This is implemented in [popcount][] in Nixery. + +-------- + +Hopefully this detailed design review was useful to you. You can also watch [my +NixCon talk][talk] about Nixery for a review of some of this, and some demos. + +[Nixery]: https://cs.tvl.fyi/depot/-/tree/tools/nixery +[grhmc]: https://grahamc.com/blog/nix-and-layered-docker-images +[Nix]: https://nixos.org/nix +[registry protocols]: https://github.com/opencontainers/distribution-spec/blob/master/spec.md +[nixery.dev]: https://nixery.dev +[dominator trees]: https://en.wikipedia.org/wiki/Dominator_(graph_theory) +[gonum/graph]: https://godoc.org/gonum.org/v1/gonum/graph +[layers.go]: https://cs.tvl.fyi/depot/-/blob/tools/nixery/builder/layers.go +[popcount]: https://cs.tvl.fyi/depot/-/tree/tools/nixery/popcount +[talk]: https://www.youtube.com/watch?v=pOI9H4oeXqA diff --git a/users/tazjin/blog/posts/nsa-zettabytes.md b/users/tazjin/blog/posts/nsa-zettabytes.md new file mode 100644 index 000000000000..f8b326f2fb42 --- /dev/null +++ b/users/tazjin/blog/posts/nsa-zettabytes.md @@ -0,0 +1,93 @@ +I've been reading a few discussions on Reddit about the new NSA data +centre that is being built and stumbled upon [this +post](http://www.reddit.com/r/restorethefourth/comments/1jf6cx/the_guardian_releases_another_leaked_document_nsa/cbe5hnc), +putting its alleged storage capacity at *5 zettabytes*. + +That seems to be a bit much which I tried to explain to that guy, but I +was quickly blocked by the common conspiracy argument that government +technology is somehow far beyond the wildest dreams of us mere mortals - +thus I wrote a very long reply that will most likely never be seen by +anybody. Therefore I've decided to repost it here. + +------------------------------------------------------------------------ + +I feel like I've entered /r/conspiracy. Please have some facts (and do +read them!) + +A one terabyte SSD (I assume that\'s what you meant by flash-drive) +would require 5000000000 of those. That is *five billion* of those flash +drives. Can you visualise how much five billion flash-drives are? + +A single SSD is roughly 2cm\*13cm\*13cm with an approximate weight of +80g. That would make 400 000 metric tons of SSDs, a weight equivalent to +*over one thousand Boeing 747 airplanes*. Even if we assume that they +solder the flash chips directly onto some kind of controller (which also +weighs something), the raw material for that would be completely insane. + +Another visualization: If you stacked 5 billion SSDs on top of each +other you would get an SSD tower that is a hundred thousand kilometres +high, that is equivalent to 2,5 x the equatorial circumference of +*Earth* or 62000 miles. + +The volume of those SSDs would be clocking in at 1690000000 cubic +metres, more than the Empire State building. Are you still with me? + +Lets speak cost. The Samsung SSD that I assume you are referring to will +clock in at \$600, lets assume that the NSA gets a discount when buying +*five billion* of those and gets them at the cheap price of \$250. That +makes 1.25 trillion dollars. That would be a significant chunk of the +current US national debt. + +And all of this is just SSDs to stick into servers and storage units, +which need a whole bunch of other equipment as well to support them - +the cost would probably shoot up to something like 8 trillion dollars if +they were to build this. It would with very high certainty be more than +the annual production of SSDs (I can\'t find numbers on that +unfortunately) and take up *slightly* more space than they have in the +Utah data centre (assuming you\'re not going to tell me that it is in +fact attached to an underground base that goes down to the core of the +Earth). + +Lets look at the \"But the government has better technologies!\" idea. + +Putting aside the fact that the military *most likely* does not have a +secret base on Mars that deals with advanced science that the rest of us +can only dream of, and doing this under the assumption that they do have +this base, lets assume that they build a storage chip that stores 100TB. +This reduces the amount of needed chips to \"just\" 50 million, lets say +they get 10 of those into a server / some kind of specialized storage +unit and we only need 5 million of those specially engineered servers, +with custom connectors, software, chips, storage, most likely also power +sources and whatever - 10 million completely custom units built with +technology that is not available to the market. Google is estimated to +have about a million servers in total, I don\'t know exactly in how many +data centres those are placed but numbers I heard recently said that +it\'s about 40. When Apple assembles a new iPhone model they need +massive factories with thousands of workers and supplies from many +different countries, over several months, to assemble just a few million +units for their launch month. + +You are seriously proposing that the NSA is better than Google and Apple +and the rest of the tech industry, world-wide, combined at designing +*everything* in tech, manufacturing *everything* in tech, without *any* +information about that leaking and without *any* of the science behind +it being known? That\'s not just insane, that\'s outright impossible. + +And we haven\'t even touched upon how they would route the necessary +amounts of bandwidth (crazy insane) to save *the entire internet* into +that data center. + +------------------------------------------------------------------------ + +I\'m not saying that the NSA is not building a data center to store +surveillance information, to have more capacity to spy on people and all +that - I\'m merely making the point that the extent in which conspiracy +sites say they do this vastly overestimates their actual abilities. They +don\'t have magic available to them! Instead of making up insane figures +like that you should focus on what we actually know about their +operations, because using those figures in a debate with somebody who is +responsible for this (and knows what they\'re talking about) will end +with you being destroyed - nobody will listen to the rest of what +you\'re saying when that happens. + +\"Stick to the facts\" is valid for our side as well. diff --git a/users/tazjin/blog/posts/reversing-watchguard-vpn.md b/users/tazjin/blog/posts/reversing-watchguard-vpn.md new file mode 100644 index 000000000000..8968dc864590 --- /dev/null +++ b/users/tazjin/blog/posts/reversing-watchguard-vpn.md @@ -0,0 +1,158 @@ +TIP: WatchGuard has +[responded](https://www.reddit.com/r/netsec/comments/5tg0f9/reverseengineering_watchguard_mobile_vpn/dds6knx/) +to this post on Reddit. If you haven\'t read the post yet I\'d recommend +doing that first before reading the response to have the proper context. + +------------------------------------------------------------------------ + +One of my current clients makes use of +[WatchGuard](http://www.watchguard.com/help/docs/fireware/11/en-US/Content/en-US/mvpn/ssl/mvpn_ssl_client-install_c.html) +Mobile VPN software to provide access to the internal network. + +Currently WatchGuard only provides clients for OS X and Windows, neither +of which I am very fond of. In addition an OpenVPN configuration file is +provided, but it quickly turned out that this was only a piece of the +puzzle. + +The problem is that this VPN setup is secured using 2-factor +authentication (good!), but it does not use OpenVPN's default +[challenge/response](https://openvpn.net/index.php/open-source/documentation/miscellaneous/79-management-interface.html) +functionality to negotiate the credentials. + +Connecting with the OpenVPN config that the website supplied caused the +VPN server to send me a token to my phone, but I simply couldn't figure +out how to supply it back to the server. In a normal challenge/response +setting the token would be supplied as the password on the second +authentication round, but the VPN server kept rejecting that. + +Other possibilities were various combinations of username&password +(I've seen a lot of those around) so I tried a whole bunch, for example +`$password:$token` or even a `sha1(password, token)` - to no avail. + +At this point it was time to crank out +[Hopper](https://www.hopperapp.com/) and see what's actually going on +in the official OS X client - which uses OpenVPN under the hood! + +Diving into the client +---------------------- + +The first surprise came up right after opening the executable: It had +debug symbols in it - and was written in Objective-C! + +![Debug symbols](/static/img/watchblob_1.webp) + +A good first step when looking at an application binary is going through +the strings that are included in it, and the WatchGuard client had a lot +to offer. Among the most interesting were a bunch of URIs that looked +important: + +![Some URIs](/static/img/watchblob_2.webp) + +I started with the first one + + %@?action=sslvpn_download&filename=%@&fw_password=%@&fw_username=%@ + +and just curled it on the VPN host, replacing the username and +password fields with bogus data and the filename field with +`client.wgssl` - another string in the executable that looked like a +filename. + +To my surprise this endpoint immediately responded with a GZIPed file +containing the OpenVPN config, CA certificate, and the client +*certificate and key*, which I previously thought was only accessible +after logging in to the web UI - oh well. + +The next endpoint I tried ended up being a bit more interesting still: + + /?action=sslvpn_logon&fw_username=%@&fw_password=%@&style=fw_logon_progress.xsl&fw_logon_type=logon&fw_domain=Firebox-DB + +Inserting the correct username and password into the query parameters +actually triggered the process that sent a token to my phone. The +response was a simple XML blob: + +```xml +<?xml version="1.0" encoding="UTF-8"?> +<resp> + <action>sslvpn_logon</action> + <logon_status>4</logon_status> + <auth-domain-list> + <auth-domain> + <name>RADIUS</name> + </auth-domain> + </auth-domain-list> + <logon_id>441</logon_id> + <chaStr>Enter Your 6 Digit Passcode </chaStr> +</resp> +``` + +Somewhat unsurprisingly that `chaStr` field is actually the challenge +string displayed in the client when logging in. + +This was obviously going in the right direction so I proceeded to the +procedures making use of this string. The first step was a relatively +uninteresting function called `-[VPNController sslvpnLogon]` which +formatted the URL, opened it and checked whether the `logon_status` was +`4` before proceeding with the `logon_id` and `chaStr` contained in the +response. + +*(Code snippets from here on are Hopper's pseudo-Objective-C)* + +![sslvpnLogon](/static/img/watchblob_3.webp) + +It proceeded to the function `-[VPNController processTokenPrompt]` which +showed the dialog window into which the user enters the token, sent it +off to the next URL and checked the `logon_status` again: + +(`r12` is the reference to the `VPNController` instance, i.e. `self`). + +![processTokenPrompt](/static/img/watchblob_4.webp) + +If the `logon_status` was `1` (apparently \"success\" here) it proceeded +to do something quite interesting: + +![processTokenPrompt2](/static/img/watchblob_5.webp) + +The user's password was overwritten with the (verified) OTP token - +before OpenVPN had even been started! + +Reading a bit more of the code in the subsequent +`-[VPNController doLogin]` method revealed that it shelled out to +`openvpn` and enabled the management socket, which makes it possible to +remotely control an `openvpn` process by sending it commands over TCP. + +It then simply sent the username and the OTP token as the credentials +after configuring OpenVPN with the correct config file: + +![doLogin](/static/img/watchblob_6.webp) + +... and the OpenVPN connection then succeeds. + +TL;DR +----- + +Rather than using OpenVPN's built-in challenge/response mechanism, the +WatchGuard client validates user credentials *outside* of the VPN +connection protocol and then passes on the OTP token, which seems to be +temporarily in a 'blessed' state after verification, as the user's +password. + +I didn't check to see how much verification of this token is performed +(does it check the source IP against the IP that performed the challenge +validation?), but this certainly seems like a bit of a security issue - +considering that an attacker on the same network would, if they time the +attack right, only need your username and 6-digit OTP token to +authenticate. + +Don't roll your own security, folks! + +Bonus +----- + +The whole reason why I set out to do this is so I could connect to this +VPN from Linux, so this blog post wouldn't be complete without a +solution for that. + +To make this process really easy I've written a [little +tool](https://github.com/tazjin/watchblob) that performs the steps +mentioned above from the CLI and lets users know when they can +authenticate using their OTP token. diff --git a/users/tazjin/blog/posts/sick-in-sweden.md b/users/tazjin/blog/posts/sick-in-sweden.md new file mode 100644 index 000000000000..0c43c5832d73 --- /dev/null +++ b/users/tazjin/blog/posts/sick-in-sweden.md @@ -0,0 +1,26 @@ +I\'ve been sick more in the two years in Sweden than in the ten years +before that. + +Why? I have a theory about it and after briefly discussing it with one +of my roommates (who is experiencing the same thing) I\'d like to share +it with you: + +Normally when people get sick, are coughing, have a fever and so on they +take a few days off from work and stay at home. The reasons are twofold: +You want to rest a bit in order to get rid of the disease and you want +to *avoid infecting your co-workers*. + +In Sweden people will drag themselves into work anyways, because of a +concept called the +[karensdag](https://www.forsakringskassan.se/wps/portal/sjukvard/sjukskrivning_och_sjukpenning/karensdag_och_forstadagsintyg). +The TL;DR of this is \'if you take days off sick you won\'t get paid for +the first day, and only 80% of your salary on the remaining days\'. + +Many people are not willing to take that financial hit. In combination +with Sweden\'s rather mediocre healthcare system you end up constantly +being surrounded by sick people, not just in your own office but also on +public transport and basically all other public places. + +Oh and the best thing about this? Swedish politicians [often ignore +this](https://www.aftonbladet.se/nyheter/article10506886.ab) rule and +just don\'t report their sick days. Nice. diff --git a/users/tazjin/blog/posts/the-smu-problem.md b/users/tazjin/blog/posts/the-smu-problem.md new file mode 100644 index 000000000000..f411e3116046 --- /dev/null +++ b/users/tazjin/blog/posts/the-smu-problem.md @@ -0,0 +1,151 @@ +After having tested countless messaging apps over the years, being +unsatisfied with most of them and finally getting stuck with +[Telegram](https://telegram.org/) I have developed a little theory about +messaging apps. + +SMU stands for *Security*, *Multi-Device* and *Usability*. Quite like +the [CAP-theorem](https://en.wikipedia.org/wiki/CAP_theorem) I believe +that you can - using current models - only solve two out of three things +on this list. Let me elaborate what I mean by the individual points: + +**Security**: This is mainly about encryption of messages, not so much +about hiding identities to third-parties. Commonly some kind of +asymmetric encryption scheme. Verification of keys used must be possible +for the user. + +**Multi-Device**: Messaging-app clients for multiple devices, with +devices being linked to the same identifier, receiving the same messages +and being independent of each other. A nice bonus is also an open +protocol (like Telegram\'s) that would let people write new clients. + +**Usability**: Usability is a bit of a broad term, but what I mean by it +here is handling contacts and identities. It should be easy to create +accounts, give contact information to people and have everything just +work in a somewhat automated fashion. + +Some categorisation of popular messaging apps: + +**SU**: Threema + +**MU**: Telegram, Google Hangouts, iMessage, Facebook Messenger + +**SM**: +[Signal](https://gist.github.com/TheBlueMatt/d2fcfb78d29faca117f5) + +*Side note: The most popular messaging app - WhatsApp - only scores a +single letter (U). This makes it completely uninteresting to me.* + +Let\'s talk about **SM** - which might contain the key to solving SMU. +Two approaches are interesting here. + +The single key model +-------------------- + +In Signal there is a single identity key which can be used to register a +device on the server. There exists a process for sharing this identity +key from a primary device to a secondary one, so that the secondary +device can register itself (see the link above for a description). + +This *almost* breaks M because there is still a dependence on a primary +device and newly onboarded devices can not be used to onboard further +devices. However, for lack of a better SM example I\'ll give it a pass. + +The other thing it obviously breaks is U as the process for setting it +up is annoying and having to rely on the primary device is a SPOF (there +might be a way to recover from a lost primary device, but I didn\'t find +any information so far). + +The multiple key model +---------------------- + +In iMessage every device that a user logs into creates a new key pair +and submits its public key to a per-account key pool. Senders fetch all +available public keys for a recipient and encrypt to all of the keys. + +Devices that join can catch up on history by receiving it from other +devices that use its public key. + +This *almost* solves all of SMU, but its compliance with S breaks due to +the fact that the key pool is not auditable, and controlled by a +third-party (Apple). How can you verify that they don\'t go and add +another key to your pool? + +A possible solution +------------------- + +Out of these two approaches I believe the multiple key one looks more +promising. If there was a third-party handling the key pool but in a way +that is verifiable, transparent and auditable that model could be used +to solve SMU. + +The technology I have been thinking about for this is some kind of +blockchain model and here\'s how I think it could work: + +1. Bob installs the app and begins onboarding. The first device + generates its keypair, submits the public key and an account + creation request. + +2. Bob\'s account is created on the messaging apps\' servers and a + unique identifier plus the fingerprint of the first device\'s public + key is written to the chain. + +3. Alice sends a message to Bob, her device asks the messaging service + for Bob\'s account\'s identity and public keys. Her device verifies + the public key fingerprint against the one in the blockchain before + encrypting to it and sending the message. + +4. Bob receives Alice\'s message on his first device. + +5. Bob logs in to his account on a second device. The device generates + a key pair and sends the public key to the service, the service + writes it to the blockchain using its identifier. + +6. The messaging service requests that Bob\'s first device signs the + second device\'s key and triggers a simple confirmation popup. + +7. Bob confirms the second device on his first device. It signs the key + and writes the signature to the chain. + +8. Alice sends another message, her device requests Bob\'s current keys + and receives the new key. It verifies that both the messaging + service and one of Bob\'s older devices have confirmed this key in + the chain. It encrypts the message to both keys and sends it on. + +9. Bob receives Alice\'s message on both devices. + +After this the second device can request conversation history from the +first one to synchronise old messages. + +Further devices added to an account can be confirmed by any of the +devices already in the account. + +The messaging service could not add new keys for an account on its own +because it does not control any of the private keys confirmed by the +chain. + +In case all devices were lost, the messaging service could associate the +account with a fresh identity in the block chain. Message history +synchronisation would of course be impossible. + +Feedback welcome +---------------- + +I would love to hear some input on this idea, especially if anyone knows +of an attempt to implement a similar model already. Possible attack +vectors would also be really interesting. + +Until something like this comes to fruition, I\'ll continue using +Telegram with GPG as the security layer when needed. + +**Update:** WhatsApp has launched an integration with the Signal guys +and added their protocol to the official WhatsApp app. This means +WhatsApp now firmly sits in the SU-category, but it still does not solve +this problem. + +**Update 2:** Facebook Messenger has also integrated with Signal, but +their secret chats do not support multi-device well (it is Signal +afterall). This means it scores either SU or MU depending on which mode +you use it in. + +An interesting service I have not yet evaluated properly is +[Matrix](http://matrix.org/). diff --git a/users/tazjin/default.nix b/users/tazjin/default.nix new file mode 100644 index 000000000000..1b68b7127a72 --- /dev/null +++ b/users/tazjin/default.nix @@ -0,0 +1,30 @@ +# //users/tazjin-specific CI configuration. +{ depot, pkgs, ... }: + +let + rustfmt = pkgs.writeShellScript "rustfmt-tazjin" '' + ${pkgs.fd}/bin/fd -e rs | \ + ${pkgs.ripgrep}/bin/rg 'users/tazjin' | \ + xargs ${pkgs.rustfmt}/bin/rustfmt --check --config-path users/tazjin + ''; + +in +depot.nix.readTree.drvTargets { + rustfmt = rustfmt.overrideAttrs (_: { + # rustfmt not respecting config atm, disable + meta.ci.skip = true; + + meta.ci.extraSteps.rustfmt = { + command = rustfmt; + }; + }); + + # Use a screen lock command that resets the keyboard layout + # before locking, to avoid locking me out when the layout is + # in Russian. + screenLock = pkgs.writeShellScriptBin "tazjin-screen-lock" '' + ${pkgs.xorg.setxkbmap}/bin/setxkbmap us + ${pkgs.xorg.setxkbmap}/bin/setxkbmap -option caps:super + exec ${pkgs.xsecurelock}/bin/xsecurelock + ''; +} diff --git a/users/tazjin/dns/default.nix b/users/tazjin/dns/default.nix new file mode 100644 index 000000000000..6c51cb5de4f5 --- /dev/null +++ b/users/tazjin/dns/default.nix @@ -0,0 +1,13 @@ +# Performs simple (local-only) validity checks on DNS zones. +{ depot, pkgs, ... }: + +let + checkZone = zone: file: pkgs.runCommandNoCC "${zone}-check" { } '' + ${pkgs.bind}/bin/named-checkzone -i local ${zone} ${file} | tee $out + ''; + +in +depot.nix.readTree.drvTargets { + kontemplate-works = checkZone "kontemplate.works" ./kontemplate.works.zone; + tazj-in = checkZone "tazj.in" ./tazj.in.zone; +} diff --git a/users/tazjin/dns/import b/users/tazjin/dns/import new file mode 100755 index 000000000000..8ea1d694c9a1 --- /dev/null +++ b/users/tazjin/dns/import @@ -0,0 +1,12 @@ +#!/bin/sh +set -ue + +# Imports a zone file into Google Cloud DNS +readonly ZONE="${1}" +readonly FILE="${2}" + +gcloud dns record-sets import "${FILE}" \ + --project composite-watch-759 \ + --zone-file-format \ + --delete-all-existing \ + --zone "${ZONE}" diff --git a/users/tazjin/dns/kontemplate.works.zone b/users/tazjin/dns/kontemplate.works.zone new file mode 100644 index 000000000000..326a129d2105 --- /dev/null +++ b/users/tazjin/dns/kontemplate.works.zone @@ -0,0 +1,15 @@ +;; -*- mode: zone; -*- +;; Do not delete these +kontemplate.works. 21600 IN NS ns-cloud-d1.googledomains.com. +kontemplate.works. 21600 IN NS ns-cloud-d2.googledomains.com. +kontemplate.works. 21600 IN NS ns-cloud-d3.googledomains.com. +kontemplate.works. 21600 IN NS ns-cloud-d4.googledomains.com. +kontemplate.works. 21600 IN SOA ns-cloud-d1.googledomains.com. cloud-dns-hostmaster.google.com. 4 21600 3600 259200 300 + +;; Github site setup +kontemplate.works. 60 IN A 185.199.108.153 +kontemplate.works. 60 IN A 185.199.109.153 +kontemplate.works. 60 IN A 185.199.110.153 +kontemplate.works. 60 IN A 185.199.111.153 + +www.kontemplate.works. 60 IN CNAME tazjin.github.io. diff --git a/users/tazjin/dns/tazj.in.zone b/users/tazjin/dns/tazj.in.zone new file mode 100644 index 000000000000..43db5834a0ca --- /dev/null +++ b/users/tazjin/dns/tazj.in.zone @@ -0,0 +1,33 @@ +;; -*- mode: zone; -*- +;; Do not delete these +tazj.in. 21600 IN NS ns-cloud-a1.googledomains.com. +tazj.in. 21600 IN NS ns-cloud-a2.googledomains.com. +tazj.in. 21600 IN NS ns-cloud-a3.googledomains.com. +tazj.in. 21600 IN NS ns-cloud-a4.googledomains.com. +tazj.in. 21600 IN SOA ns-cloud-a1.googledomains.com. cloud-dns-hostmaster.google.com. 123 21600 3600 1209600 300 + +;; Email setup +tazj.in. 300 IN MX 1 aspmx.l.google.com. +tazj.in. 300 IN MX 5 alt1.aspmx.l.google.com. +tazj.in. 300 IN MX 5 alt2.aspmx.l.google.com. +tazj.in. 300 IN MX 10 alt3.aspmx.l.google.com. +tazj.in. 300 IN MX 10 alt4.aspmx.l.google.com. +tazj.in. 300 IN TXT "v=spf1 include:_spf.google.com ~all" +google._domainkey.tazj.in. 21600 IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9AphX/WJf8zVXQB5Jk0Ry1MI6ARa6vEyAoJtpjpt9Nbm7XU4qVWFRJm+L0VFd5EZ5YDPJTIZ90lJE3/B8vae2ipnoGbJbj8LaVSzzIPMbWmhPhX3fkLJFdkv7xRDMDn730iYXRlfkgv6GsqbS8vZt7mzxx4mpnePTI323yjRVkwRW8nGVbsmB25ZoG1/0985" "kg4mSYxzWeJ2ozCPFhT4sfMtZMXe/4QEkJz/zkod29KZfFJmLgEaf73WLdBX8kdwbhuh2PYXt/PwzUrRzF5ujVCsSaTZwdRVPErcf+yo4NvedelTjjs8rFVfoJiaDD1q2bQ3w0gDEBWPdC2VP7k9zwIDAQAB" + +;; Site verifications +tazj.in. 3600 IN TXT "keybase-site-verification=gC4kzEmnLzY7F669PjN-pw2Cf__xHqcxQ08Gb-W9dhE" +tazj.in. 300 IN TXT "google-site-verification=d3_MI1OwD6q2OT42Vvh0I9w2u3Q5KFBu-PieNUE1Fig" +www.tazj.in. 3600 IN TXT "keybase-site-verification=ER8m_byyqAhzeIy9TyzkAU1H2p2yHtpvImuB_XrRF2U" + +;; Blog "storage engine" +blog.tazj.in. 21600 IN NS ns-cloud-c1.googledomains.com. +blog.tazj.in. 21600 IN NS ns-cloud-c2.googledomains.com. +blog.tazj.in. 21600 IN NS ns-cloud-c3.googledomains.com. +blog.tazj.in. 21600 IN NS ns-cloud-c4.googledomains.com. + +;; Webpage records setup +tazj.in. 300 IN A 34.98.120.189 +www.tazj.in. 300 IN A 34.98.120.189 +git.tazj.in. 300 IN A 34.98.120.189 +files.tazj.in. 300 IN CNAME c.storage.googleapis.com. diff --git a/users/tazjin/docs/install-zfs.md b/users/tazjin/docs/install-zfs.md new file mode 100644 index 000000000000..415af30fd41d --- /dev/null +++ b/users/tazjin/docs/install-zfs.md @@ -0,0 +1,116 @@ +Current steps for my NixOS-on-ZFS installs with impermanence. + +## Target layout (example from tverskoy): + +Partitioning: + +``` +nvme0n1 259:0 0 238.5G 0 disk +โโnvme0n1p1 259:1 0 128M 0 part /boot (type: EFI system) +โโnvme0n1p2 259:2 0 238.3G 0 part (type: Solaris root) +``` + +ZFS layout: + +``` +NAME USED AVAIL REFER MOUNTPOINT +zpool 212G 19.0G 248K /zpool +zpool/ephemeral 668M 19.0G 192K /zpool/ephemeral +zpool/ephemeral/home 667M 19.0G 667M legacy +zpool/local 71.3G 19.0G 192K /zpool/local +zpool/local/nix 71.3G 19.0G 71.3G legacy +zpool/safe 140G 19.0G 192K /zpool/safe +zpool/safe/depot 414M 19.0G 414M legacy +zpool/safe/persist 139G 19.0G 139G legacy +``` + +With reset-snapshots: + +``` +NAME USED AVAIL REFER MOUNTPOINT +zpool/ephemeral/home@blank 144K - 192K - +zpool/ephemeral/home@tazjin-clean 144K - 200K - +``` + +Legacy mountpoints are used because the NixOS wiki advises that using +ZFS own mountpoints might lead to issues with the mount order during +boot. + +## Install steps + +1. First, get internet. + +2. Use `fdisk` to set up the partition layout above (fwiw, EFI type + should be `1`, Solaris root should be `66`). + +3. Format the first partition for EFI: `mkfs.fat -F32 -n EFI $part1` + +4. Init ZFS stuff: + + ``` + zpool create \ + # 2 SSD only settings + -o ashift=12 \ + -o autotrim=on \ + -R /mnt \ + -O canmount=off \ + -O mountpoint=none \ + -O acltype=posixacl \ + -O compression=lz4 \ + -O atime=off \ + -O xattr=sa \ + -O encryption=aes-256-gcm \ + -O keylocation=prompt \ + -O keyformat=passphrase \ + zpool $part2 + ``` + + Reserve some space for deletions: + + ``` + zfs create -o refreservation=1G -o mountpoint=none zpool/reserved + ``` + + Create the datasets as per the target layout: + + ``` + # Throwaway datasets + zfs create -o canmount=off -o mountpoint=none zpool/ephemeral + zfs create -o mountpoint=legacy zpool/ephemeral/root + zfs create -o mountpoint=legacy zpool/ephemeral/home + + # Persistent datasets + zfs create -o canmount=off -o mountpoint=none zpool/persistent + zfs create -o mountpoint=legacy zpool/persistent/nix + zfs create -o mountpoint=legacy zpool/persistent/depot + zfs create -o mountpoint=legacy zpool/persistent/data + ``` + + Create completely blank snapshots of the ephemeral datasets: + + ``` + zfs snapshot zpool/ephemeral/root@blank + zfs snapshot zpool/ephemeral/home@blank + ``` + + The ephemeral home volume needs the user folder already set up with + permissions. Mount it and create the folder there: + + ``` + mount -t zfs zpool/ephemeral/root /mnt + mkdir /mnt/home + mount -t zfs zpool/ephemeral/home /mnt/home + mkdir /mnt/home/tazjin + chmod 1000:100 /mnt/home/tazjin + zfs snapshot zpool/ephemeral/home@tazjin-clean + ``` + + Now the persistent Nix store volume can be mounted and installation + can begin. + + ``` + mkdir /mnt/nix + mount -t zfs zpool/persistent/nix /mnt/nix + ``` + +4. Configure & install NixOS as usual. diff --git a/users/tazjin/dotfiles/config.fish b/users/tazjin/dotfiles/config.fish new file mode 100644 index 000000000000..de2c99ae6007 --- /dev/null +++ b/users/tazjin/dotfiles/config.fish @@ -0,0 +1,40 @@ +# Configure classic prompt +set fish_color_user --bold blue +set fish_color_cwd --bold white + +# Enable colour hints in VCS prompt: +set __fish_git_prompt_showcolorhints yes +set __fish_git_prompt_color_prefix purple +set __fish_git_prompt_color_suffix purple + +# Fish configuration +set fish_greeting "" +set PATH $HOME/.local/bin $HOME/.cargo/bin $PATH + +# Editor configuration +set -gx EDITOR "emacsclient" +set -gx ALTERNATE_EDITOR "emacs -q -nw" +set -gx VISUAL "emacsclient" + +# Miscellaneous +eval (direnv hook fish) + +# Useful command aliases +alias gpr 'git pull --rebase' +alias gco 'git checkout' +alias gf 'git fetch' +alias gap 'git add -p' +alias pbcopy 'xclip -selection clipboard' +alias edit 'emacsclient -n' +alias servedir 'nix-shell -p haskellPackages.wai-app-static --run warp' + +# Old habits die hard (also ls is just easier to type): +alias ls 'exa' + +# Fix up nix-env & friends for Nix 2.0 +export NIX_REMOTE=daemon + +# Fix display of fish in emacs' term-mode: +function fish_title + true +end diff --git a/users/tazjin/dotfiles/default.nix b/users/tazjin/dotfiles/default.nix new file mode 100644 index 000000000000..9b783a9c857c --- /dev/null +++ b/users/tazjin/dotfiles/default.nix @@ -0,0 +1,3 @@ +_: { + dunstrc = ./dunstrc; +} diff --git a/users/tazjin/dotfiles/dunstrc b/users/tazjin/dotfiles/dunstrc new file mode 100644 index 000000000000..2aa1141b6ec2 --- /dev/null +++ b/users/tazjin/dotfiles/dunstrc @@ -0,0 +1,54 @@ +[global] +font = Iosevka Term 11 +origin = top-left +markup = yes +plain_text = no +format = "<b>%s</b>\n%b" +sort = no +indicate_hidden = yes +alignment = center +bounce_freq = 0 +show_age_threshold = -1 +word_wrap = yes +ignore_newline = no +stack_duplicates = yes +hide_duplicate_count = yes +geometry = "300x50-15+49" +shrink = no +transparency = 5 +idle_threshold = 0 +monitor = 0 +follow = keyboard +sticky_history = yes +history_length = 15 +show_indicators = no +line_height = 3 +separator_height = 2 +padding = 6 +horizontal_padding = 6 +separator_color = frame +startup_notification = false +dmenu = /usr/bin/dmenu -p dunst: +browser = /usr/bin/firefox -new-tab +icon_position = off +max_icon_size = 80 +frame_width = 3 +frame_color = "#8EC07C" + +[urgency_low] +frame_color = "#3B7C87" +foreground = "#3B7C87" +background = "#191311" +timeout = 4 + +[urgency_normal] +frame_color = "#5B8234" +foreground = "#5B8234" +background = "#191311" +timeout = 6 + +[urgency_critical] +frame_color = "#B7472A" +foreground = "#B7472A" +background = "#191311" +timeout = 8 diff --git a/users/tazjin/dotfiles/msmtprc b/users/tazjin/dotfiles/msmtprc new file mode 100644 index 000000000000..2af3b9433a6d --- /dev/null +++ b/users/tazjin/dotfiles/msmtprc @@ -0,0 +1,15 @@ +defaults +port 587 +tls on +tls_trust_file /etc/ssl/certs/ca-certificates.crt + +# GSuite for tazj.in +account tazjin +host smtp.gmail.com +port 587 +from mail@tazj.in +auth oauthbearer +user mail@tazj.in +passwordeval "cat ~/mail/account.tazjin/.credentials.gmailieer.json | jq -r '.access_token'" + +account default : tazjin diff --git a/users/tazjin/dotfiles/notmuch-config b/users/tazjin/dotfiles/notmuch-config new file mode 100644 index 000000000000..a490774e635f --- /dev/null +++ b/users/tazjin/dotfiles/notmuch-config @@ -0,0 +1,21 @@ +# .notmuch-config - Configuration file for the notmuch mail system +# +# For more information about notmuch, see https://notmuchmail.org + +[database] +path=/home/vincent/mail + +[user] +name=Vincent Ambo +primary_email=mail@tazj.in +other_email=tazjin@gmail.com; + +[new] +tags=unread;inbox; +ignore= + +[search] +exclude_tags=deleted;spam;draft; + +[maildir] +synchronize_flags=true diff --git a/users/tazjin/emacs/.gitignore b/users/tazjin/emacs/.gitignore new file mode 100644 index 000000000000..7b666905f847 --- /dev/null +++ b/users/tazjin/emacs/.gitignore @@ -0,0 +1,11 @@ +.smex-items +*token* +auto-save-list/ +clones/ +elpa/ +irc.el +local.el +other/ +scripts/ +themes/ +*.elc diff --git a/users/tazjin/emacs/README.md b/users/tazjin/emacs/README.md new file mode 100644 index 000000000000..5c667333962e --- /dev/null +++ b/users/tazjin/emacs/README.md @@ -0,0 +1,7 @@ +tools/emacs +=========== + +This sub-folder builds my Emacs configuration, supplying packages from +Nix and configuration from this folder. + +I use Emacs for many things (including as my desktop environment). diff --git a/users/tazjin/emacs/config/bindings.el b/users/tazjin/emacs/config/bindings.el new file mode 100644 index 000000000000..916d9477568c --- /dev/null +++ b/users/tazjin/emacs/config/bindings.el @@ -0,0 +1,65 @@ +;; Font size +(define-key global-map (kbd "C-=") 'increase-default-text-scale) ;; '=' because there lies '+' +(define-key global-map (kbd "C--") 'decrease-default-text-scale) +(define-key global-map (kbd "C-x C-0") 'set-default-text-scale) + +;; What does <tab> do? Well, it depends ... +(define-key prog-mode-map (kbd "<tab>") #'company-indent-or-complete-common) + +;; imenu instead of insert-file +(global-set-key (kbd "C-x i") 'imenu) + +;; Window switching. (C-x o goes to the next window) +(windmove-default-keybindings) ;; Shift+direction + +;; Start eshell or switch to it if it's active. +(global-set-key (kbd "C-x m") 'eshell) + +(global-set-key (kbd "C-x C-p") 'browse-repositories) +(global-set-key (kbd "M-g M-g") 'goto-line-with-feedback) + +;; Miscellaneous editing commands +(global-set-key (kbd "C-c w") 'whitespace-cleanup) +(global-set-key (kbd "C-c a") 'align-regexp) +(global-set-key (kbd "C-c m") 'mc/mark-dwim) + +;; Browse URLs (very useful for Gitlab's SSH output!) +(global-set-key (kbd "C-c b p") 'browse-url-at-point) +(global-set-key (kbd "C-c b b") 'browse-url) + +;; C-x REALLY QUIT (idea by @magnars) +(global-set-key (kbd "C-x r q") 'save-buffers-kill-terminal) +(global-set-key (kbd "C-x C-c") 'ignore) + +;; Open a file in project: +(global-set-key (kbd "C-c f") 'project-find-file) + +;; Search in a project +(global-set-key (kbd "C-c r g") 'rg-in-project) + +;; Open a file via magit: +(global-set-key (kbd "C-c C-f") #'magit-find-file-worktree) + +;; Insert TODO comments +(global-set-key (kbd "C-c t") 'insert-todo-comment) + +;; Make sharing music easier +(global-set-key (kbd "s-s w") #'songwhip-lookup-url) + +;; Open the depot +(global-set-key (kbd "s-s d") #'tvl-depot-status) + +;; Open any repo through zoxide +(global-set-key (kbd "s-s r") #'zoxide-open-magit) + +;; Add subthread collapsing to notmuch-show. +;; +;; C-, closes a thread, C-. opens a thread. This mirrors stepping +;; in/out of definitions. +(define-key notmuch-show-mode-map (kbd "C-,") 'notmuch-show-open-or-close-subthread) +(define-key notmuch-show-mode-map (kbd "C-.") + (lambda () + (interactive) + (notmuch-show-open-or-close-subthread t))) ;; open + +(provide 'bindings) diff --git a/users/tazjin/emacs/config/custom.el b/users/tazjin/emacs/config/custom.el new file mode 100644 index 000000000000..91eaf69ae59b --- /dev/null +++ b/users/tazjin/emacs/config/custom.el @@ -0,0 +1,27 @@ +(custom-set-variables + ;; custom-set-variables was added by Custom. + ;; If you edit it by hand, you could mess it up, so be careful. + ;; Your init file should contain only one such instance. + ;; If there is more than one, they won't work right. + '(ac-auto-show-menu 0.8) + '(ac-delay 0.2) + '(avy-background t) + '(cargo-process--enable-rust-backtrace 1) + '(company-auto-complete (quote (quote company-explicit-action-p))) + '(company-idle-delay 0.5) + '(custom-safe-themes + (quote + ("d61fc0e6409f0c2a22e97162d7d151dee9e192a90fa623f8d6a071dbf49229c6" "3c83b3676d796422704082049fc38b6966bcad960f896669dfc21a7a37a748fa" "89336ca71dae5068c165d932418a368a394848c3b8881b2f96807405d8c6b5b6" default))) + '(display-time-default-load-average nil) + '(display-time-interval 30) + '(elnode-send-file-program "/run/current-system/sw/bin/cat") + '(frame-brackground-mode (quote dark)) + '(global-auto-complete-mode t) + '(kubernetes-commands-display-buffer-function (quote display-buffer)) + '(lsp-gopls-server-path "/home/tazjin/go/bin/gopls") + '(magit-log-show-gpg-status t) + '(ns-alternate-modifier (quote none)) + '(ns-command-modifier (quote control)) + '(ns-right-command-modifier (quote meta)) + '(require-final-newline (quote visit-save)) + '(tls-program (quote ("gnutls-cli --x509cafile %t -p %p %h")))) diff --git a/users/tazjin/emacs/config/desktop.el b/users/tazjin/emacs/config/desktop.el new file mode 100644 index 000000000000..15c0b1f5e107 --- /dev/null +++ b/users/tazjin/emacs/config/desktop.el @@ -0,0 +1,372 @@ +;; -*- lexical-binding: t; -*- +;; +;; Configure desktop environment settings, including both +;; window-management (EXWM) as well as additional system-wide +;; commands. + +(require 'dash) +(require 'exwm) +(require 'exwm-config) +(require 'exwm-randr) +(require 'exwm-systemtray) +(require 'exwm-xim ) +(require 'f) +(require 's) + +(defcustom tazjin--screen-lock-command "tazjin-screen-lock" + "Command to execute for locking the screen." + :group 'tazjin) + +(defcustom tazjin--backlight-increase-command "light -A 4" + "Command to increase screen brightness." + :group 'tazjin) + +(defcustom tazjin--backlight-decrease-command "light -U 4" + "Command to decrease screen brightness." + :group 'tazjin) + +(defun pactl (cmd) + (shell-command (concat "pactl " cmd)) + (message "Volume command: %s" cmd)) + +(defun volume-mute () (interactive) (pactl "set-sink-mute @DEFAULT_SINK@ toggle")) +(defun volume-up () (interactive) (pactl "set-sink-volume @DEFAULT_SINK@ +5%")) +(defun volume-down () (interactive) (pactl "set-sink-volume @DEFAULT_SINK@ -5%")) + +(defun brightness-up () + (interactive) + (shell-command tazjin--backlight-increase-command) + (message "Brightness increased")) + +(defun brightness-down () + (interactive) + (shell-command tazjin--backlight-decrease-command) + (message "Brightness decreased")) + +(defun set-xkb-layout (layout) + "Set the current X keyboard layout." + + (shell-command (format "setxkbmap %s" layout)) + (shell-command "setxkbmap -option caps:super") + (message "Set X11 keyboard layout to '%s'" layout)) + +(defun lock-screen () + (interactive) + (set-xkb-layout "us") + (deactivate-input-method) + (shell-command tazjin--screen-lock-command)) + +(defun create-window-name () + "Construct window names to be used for EXWM buffers by + inspecting the window's X11 class and title. + + A lot of commonly used applications either create titles that + are too long by default, or in the case of web + applications (such as Cider) end up being constructed in + awkward ways. + + To avoid this issue, some rewrite rules are applied for more + human-accessible titles." + + (pcase (list (or exwm-class-name "unknown") (or exwm-title "unknown")) + ;; In Cider windows, rename the class and keep the workspace/file + ;; as the title. + (`("Google-chrome" ,(and (pred (lambda (title) (s-ends-with? " - Cider" title))) title)) + (format "Cider<%s>" (s-chop-suffix " - Cider" title))) + (`("Google-chrome" ,(and (pred (lambda (title) (s-ends-with? " - Cider V" title))) title)) + (format "Cider V<%s>" (s-chop-suffix " - Cider V" title))) + + ;; Attempt to detect IRCCloud windows via their title, which is a + ;; combination of the channel name and network. + ;; + ;; This is what would often be referred to as a "hack". The regexp + ;; will not work if a network connection buffer is selected in + ;; IRCCloud, but since the title contains no other indication that + ;; we're dealing with an IRCCloud window + (`("Google-chrome" + ,(and (pred (lambda (title) + (s-matches? "^[\*\+]\s#[a-zA-Z0-9/\-]+\s\|\s[a-zA-Z\.]+$" title))) + title)) + (format "IRCCloud<%s>" title)) + + ;; For other Chrome windows, make the title shorter. + (`("Google-chrome" ,title) + (format "Chrome<%s>" (s-truncate 42 (s-chop-suffix " - Google Chrome" title)))) + + ;; Gnome-terminal -> Term + (`("Gnome-terminal" ,title) + ;; fish-shell buffers contain some unnecessary whitespace and + ;; such before the current working directory. This can be + ;; stripped since most of my terminals are fish shells anyways. + (format "Term<%s>" (s-trim-left (s-chop-prefix "fish" title)))) + + ;; Quassel buffers + ;; + ;; These have a title format that looks like: + ;; "Quassel IRC - #tvl (hackint) โ Quassel IRC" + (`("quassel" ,title) + (progn + (if (string-match + (rx "Quassel IRC - " + (group (one-or-more (any alnum "[" "]" "&" "-" "#"))) ;; <-- channel name + " (" (group (one-or-more (any ascii space))) ")" ;; <-- network name + " โ Quassel IRC") + title) + (format "Quassel<%s>" (match-string 2 title)) + title))) + + ;; For any other application, a name is constructed from the + ;; window's class and name. + (`(,class ,title) (format "%s<%s>" class (s-truncate 12 title))))) + +;; EXWM launch configuration +;; +;; This used to use use-package, but when something breaks use-package +;; it doesn't exactly make debugging any easier. + +(let ((titlef (lambda () + (exwm-workspace-rename-buffer (create-window-name))))) + (add-hook 'exwm-update-class-hook titlef) + (add-hook 'exwm-update-title-hook titlef)) + +(fringe-mode 3) +(exwm-enable) + +;; Create 10 EXWM workspaces +(setq exwm-workspace-number 10) + +;; 's-N': Switch to certain workspace, but switch back to the previous +;; one when tapping twice (emulates i3's `back_and_forth' feature) +(defvar *exwm-workspace-from-to* '(-1 . -1)) +(defun exwm-workspace-switch-back-and-forth (target-idx) + ;; If the current workspace is the one we last jumped to, and we are + ;; asked to jump to it again, set the target back to the previous + ;; one. + (when (and (eq exwm-workspace-current-index (cdr *exwm-workspace-from-to*)) + (eq target-idx exwm-workspace-current-index)) + (setq target-idx (car *exwm-workspace-from-to*))) + + (setq *exwm-workspace-from-to* + (cons exwm-workspace-current-index target-idx)) + + (exwm-workspace-switch-create target-idx)) + +(dotimes (i 10) + (exwm-input-set-key (kbd (format "s-%d" i)) + `(lambda () + (interactive) + (exwm-workspace-switch-back-and-forth ,i)))) + +;; Implement MRU functionality for EXWM workspaces, making it possible +;; to jump to the previous/next workspace very easily. +(defvar *recent-workspaces* nil + "List of the most recently used EXWM workspaces.") + +(defvar *workspace-jumping-to* nil + "What offset in the workspace history are we jumping to?") + +(defvar *workspace-history-position* 0 + "Where in the workspace history are we right now?") + +(defun update-recent-workspaces () + "Hook to run on every workspace switch which will prepend the new +workspace to the MRU list, unless we are already on that +workspace. Does not affect the MRU list if a jump is +in-progress." + + (if *workspace-jumping-to* + (setq *workspace-history-position* *workspace-jumping-to* + *workspace-jumping-to* nil) + + ;; reset the history position to the front on a normal jump + (setq *workspace-history-position* 0) + + (unless (eq exwm-workspace-current-index (car *recent-workspaces*)) + (setq *recent-workspaces* (cons exwm-workspace-current-index + (-take 9 *recent-workspaces*)))))) + +(add-to-list 'exwm-workspace-switch-hook #'update-recent-workspaces) + +(defun switch-to-previous-workspace () + "Switch to the previous workspace in the MRU workspace list." + (interactive) + + (let* (;; the previous workspace is one position further down in the + ;; workspace history + (position (+ *workspace-history-position* 1)) + (target-idx (elt *recent-workspaces* position))) + (if (not target-idx) + (message "No previous workspace in history!") + + (setq *workspace-jumping-to* position) + (exwm-workspace-switch target-idx)))) + +(exwm-input-set-key (kbd "s-b") #'switch-to-previous-workspace) + +(defun switch-to-next-workspace () + "Switch to the next workspace in the MRU workspace list." + (interactive) + + (if (= *workspace-history-position* 0) + (message "No next workspace in history!") + (let* (;; The next workspace is one position further up in the + ;; history. This always exists unless someone messed with + ;; it. + (position (- *workspace-history-position* 1)) + (target-idx (elt *recent-workspaces* position))) + (setq *workspace-jumping-to* position) + (exwm-workspace-switch target-idx)))) + +(exwm-input-set-key (kbd "s-f") #'switch-to-next-workspace) + +;; Provide a binding for jumping to a buffer on a workspace. +(defun exwm-jump-to-buffer () + "Jump to a workspace on which the target buffer is displayed." + (interactive) + (let ((exwm-layout-show-all-buffers nil) + (initial exwm-workspace-current-index)) + (call-interactively #'exwm-workspace-switch-to-buffer) + ;; After jumping, update the back-and-forth list like on a direct + ;; index jump. + (when (not (eq initial exwm-workspace-current-index)) + (setq *exwm-workspace-from-to* + (cons initial exwm-workspace-current-index))))) + +(exwm-input-set-key (kbd "C-c j") #'exwm-jump-to-buffer) + +;; Launch applications / any command with completion (dmenu style!) +(exwm-input-set-key (kbd "s-d") #'counsel-linux-app) +(exwm-input-set-key (kbd "s-x") #'run-external-command) +(exwm-input-set-key (kbd "s-p") #'password-store-lookup) + +;; Add X11 terminal selector to a key +(exwm-input-set-key (kbd "C-x t") #'ts/switch-to-terminal) + +;; Toggle between line-mode / char-mode +(exwm-input-set-key (kbd "C-c C-t C-t") #'exwm-input-toggle-keyboard) + +;; Volume keys +(exwm-input-set-key (kbd "<XF86AudioMute>") #'volume-mute) +(exwm-input-set-key (kbd "<XF86AudioRaiseVolume>") #'volume-up) +(exwm-input-set-key (kbd "<XF86AudioLowerVolume>") #'volume-down) + +;; Brightness keys +(exwm-input-set-key (kbd "<XF86MonBrightnessDown>") #'brightness-down) +(exwm-input-set-key (kbd "<XF86MonBrightnessUp>") #'brightness-up) +(exwm-input-set-key (kbd "<XF86Display>") #'lock-screen) + +;; Shortcuts for switching between keyboard layouts +(defmacro bind-xkb (lang key) + `(exwm-input-set-key (kbd (format "s-%s" ,key)) + (lambda () + (interactive) + (set-xkb-layout ,lang)))) + +(bind-xkb "us" "k u") +(bind-xkb "de" "k d") +(bind-xkb "no" "k n") +(bind-xkb "ru" "k r") +(bind-xkb "se" "k s") + +;; These are commented out because Emacs no longer starts (??) if +;; they're set at launch. +;; +(bind-xkb "us" "ะป ะณ") +(bind-xkb "de" "ะป ะฒ") +(bind-xkb "no" "ะป ั") +(bind-xkb "ru" "ะป ะบ") + +;; Configuration of EXWM input method handling for X applications +(exwm-xim-enable) +(setq default-input-method "russian-computer") +(push ?\C-\\ exwm-input-prefix-keys) + +;; Line-editing shortcuts +(exwm-input-set-simulation-keys + '(([?\C-d] . delete) + ([?\C-w] . ?\C-c))) + +;; Show time & battery status in the mode line +(display-time-mode) +(display-battery-mode) + +;; enable display of X11 system tray within Emacs +(exwm-systemtray-enable) + +;; Configure xrandr (multi-monitor setup). + +(defun set-randr-config (screens) + (setq exwm-randr-workspace-monitor-plist + (-flatten (-map (lambda (screen) + (-map (lambda (screen-id) (list screen-id (car screen))) (cdr screen))) + screens)))) + +;; Layouts for Tverskoy (X13 AMD laptop) +(defun randr-tverskoy-layout-single () + "Laptop screen only!" + (interactive) + (set-randr-config '(("eDP" (number-sequence 0 9)))) + (shell-command "xrandr --output eDP --auto --primary") + (shell-command "xrandr --output HDMI-A-0 --off") + (exwm-randr-refresh)) + +(defun randr-tverskoy-split-workspace () + "Split the workspace across two screens, assuming external to the left." + (interactive) + (set-randr-config + '(("HDMI-A-0" 1 2 3 4 5 6 7) + ("eDP" 8 9 0))) + + (shell-command "xrandr --output HDMI-A-0 --left-of eDP --auto") + (exwm-randr-refresh)) + +(defun randr-tverskoy-tv () + "Split off a workspace to the TV over HDMI." + (interactive) + (set-randr-config + '(("eDP" 1 2 3 4 5 6 7 8 9) + ("HDMI-A-0" 0))) + + (shell-command "xrandr --output HDMI-A-0 --left-of eDP --mode 1920x1080") + (exwm-randr-refresh)) + +;; Layouts for frog (desktop) + +(defun randr-frog-layout-right-only () + "Use only the right screen on frog." + (interactive) + (set-randr-config `(("DisplayPort-0" ,(number-sequence 0 9)))) + (shell-command "xrandr --output DisplayPort-0 --off") + (shell-command "xrandr --output DisplayPort-1 --auto --primary")) + +(defun randr-frog-layout-both () + "Use the left and right screen on frog." + (interactive) + (set-randr-config `(("DisplayPort-0" 1 2 3 4 5) + ("DisplayPort-1" 6 7 8 9 0))) + + (shell-command "xrandr --output DisplayPort-0 --auto --primary --left-of DisplayPort-1") + (shell-command "xrandr --output DisplayPort-1 --auto --right-of DisplayPort-0 --rotate left")) + +(pcase (s-trim (shell-command-to-string "hostname")) + ("tverskoy" + (exwm-input-set-key (kbd "s-m s") #'randr-tverskoy-layout-single) + (exwm-input-set-key (kbd "s-m 2") #'randr-tverskoy-split-workspace)) + + ("frog" + (exwm-input-set-key (kbd "s-m b") #'randr-frog-layout-both) + (exwm-input-set-key (kbd "s-m r") #'randr-frog-layout-right-only))) + +;; Notmuch shortcuts as EXWM globals +;; (g m => gmail) +(exwm-input-set-key (kbd "s-g m") #'notmuch) +(exwm-input-set-key (kbd "s-g M") #'counsel-notmuch) + +(exwm-randr-enable) + +;; Let buffers move seamlessly between workspaces by making them +;; accessible in selectors on all frames. +(setq exwm-workspace-show-all-buffers t) +(setq exwm-layout-show-all-buffers t) + +(provide 'desktop) diff --git a/users/tazjin/emacs/config/eshell-setup.el b/users/tazjin/emacs/config/eshell-setup.el new file mode 100644 index 000000000000..0b23c5a2d1bc --- /dev/null +++ b/users/tazjin/emacs/config/eshell-setup.el @@ -0,0 +1,68 @@ +;; EShell configuration + +(require 'eshell) + +;; Generic settings +;; Hide banner message ... +(setq eshell-banner-message "") + +;; Prompt configuration +(defun clean-pwd (path) + "Turns a path of the form /foo/bar/baz into /f/b/baz + (inspired by fish shell)" + (let* ((hpath (replace-regexp-in-string home-dir + "~" + path)) + (current-dir (split-string hpath "/")) + (cdir (last current-dir)) + (head (butlast current-dir))) + (concat (mapconcat (lambda (s) + (if (string= "" s) nil + (substring s 0 1))) + head + "/") + (if head "/" nil) + (car cdir)))) + +(defun vcprompt (&optional args) + "Call the external vcprompt command with optional arguments. + VCPrompt" + (replace-regexp-in-string + "\n" "" + (shell-command-to-string (concat "vcprompt" args)))) + +(defmacro with-face (str &rest properties) + `(propertize ,str 'face (list ,@properties))) + +(defun prompt-f () + "EShell prompt displaying VC info and such" + (concat + (with-face (concat (clean-pwd (eshell/pwd)) " ") :foreground "#96a6c8") + (if (= 0 (user-uid)) + (with-face "#" :foreground "#f43841") + (with-face "$" :foreground "#73c936")) + (with-face " " :foreground "#95a99f"))) + + +(setq eshell-prompt-function 'prompt-f) +(setq eshell-highlight-prompt nil) +(setq eshell-prompt-regexp "^.+? \\((\\(git\\|svn\\|hg\\|darcs\\|cvs\\|bzr\\):.+?) \\)?[$#] ") + +;; Ignore version control folders in autocompletion +(setq eshell-cmpl-cycle-completions nil + eshell-save-history-on-exit t + eshell-cmpl-dir-ignore "\\`\\(\\.\\.?\\|CVS\\|\\.svn\\|\\.git\\)/\\'") + +;; Load some EShell extensions +(eval-after-load 'esh-opt + '(progn + (require 'em-term) + (require 'em-cmpl) + ;; More visual commands! + (add-to-list 'eshell-visual-commands "ssh") + (add-to-list 'eshell-visual-commands "tail") + (add-to-list 'eshell-visual-commands "sl"))) + +(setq eshell-directory-name "~/.config/eshell/") + +(provide 'eshell-setup) diff --git a/users/tazjin/emacs/config/functions.el b/users/tazjin/emacs/config/functions.el new file mode 100644 index 000000000000..602809138eef --- /dev/null +++ b/users/tazjin/emacs/config/functions.el @@ -0,0 +1,343 @@ +(require 'chart) +(require 'dash) +(require 'map) + +(defun load-file-if-exists (filename) + (if (file-exists-p filename) + (load filename))) + +(defun goto-line-with-feedback () + "Show line numbers temporarily, while prompting for the line number input" + (interactive) + (unwind-protect + (progn + (setq-local display-line-numbers t) + (let ((target (read-number "Goto line: "))) + (avy-push-mark) + (goto-line target))) + (setq-local display-line-numbers nil))) + +;; These come from the emacs starter kit + +(defun esk-add-watchwords () + (font-lock-add-keywords + nil '(("\\<\\(FIX\\(ME\\)?\\|TODO\\|DEBUG\\|HACK\\|REFACTOR\\|NOCOMMIT\\)" + 1 font-lock-warning-face t)))) + +(defun esk-sudo-edit (&optional arg) + (interactive "p") + (if (or arg (not buffer-file-name)) + (find-file (concat "/sudo:root@localhost:" (read-file-name "File: "))) + (find-alternate-file (concat "/sudo:root@localhost:" buffer-file-name)))) + +;; Open the NixOS man page +(defun nixos-man () + (interactive) + (man "configuration.nix")) + +;; Get the nix store path for a given derivation. +;; If the derivation has not been built before, this will trigger a build. +(defun nix-store-path (derivation) + (let ((expr (concat "with import <nixos> {}; " derivation))) + (s-chomp (shell-command-to-string (concat "nix-build -E '" expr "'"))))) + +(defun insert-nix-store-path () + (interactive) + (let ((derivation (read-string "Derivation name (in <nixos>): "))) + (insert (nix-store-path derivation)))) + +(defun toggle-force-newline () + "Buffer-local toggle for enforcing final newline on save." + (interactive) + (setq-local require-final-newline (not require-final-newline)) + (message "require-final-newline in buffer %s is now %s" + (buffer-name) + require-final-newline)) + +(defun list-external-commands () + "Creates a list of all external commands available on $PATH + while filtering NixOS wrappers." + (cl-loop + for dir in (split-string (getenv "PATH") path-separator) + when (and (file-exists-p dir) (file-accessible-directory-p dir)) + for lsdir = (cl-loop for i in (directory-files dir t) + for bn = (file-name-nondirectory i) + when (and (not (s-contains? "-wrapped" i)) + (not (member bn completions)) + (not (file-directory-p i)) + (file-executable-p i)) + collect bn) + append lsdir into completions + finally return (sort completions 'string-lessp))) + +(defvar external-command-flag-overrides + '(("google-chrome" . "--force-device-scale-factor=1.4")) + + "This setting lets me add additional flags to specific commands + that are run interactively via `run-external-command'.") + +(defun run-external-command--handler (cmd) + "Execute the specified command and notify the user when it + finishes." + (let* ((extra-flags (cdr (assoc cmd external-command-flag-overrides))) + (cmd (if extra-flags (s-join " " (list cmd extra-flags)) cmd))) + (message "Starting %s..." cmd) + (set-process-sentinel + (start-process-shell-command cmd nil cmd) + (lambda (process event) + (when (string= event "finished\n") + (message "%s process finished." process)))))) + +(defun run-external-command () + "Prompts the user with a list of all installed applications and + lets them select one to launch." + + (interactive) + (let ((external-commands-list (list-external-commands))) + (run-external-command--handler + (completing-read "Command: " external-commands-list + nil ;; predicate + t ;; require-match + nil ;; initial-input + ;; hist + 'external-commands-history)))) + +(defun password-store-lookup (&optional password-store-dir) + "Interactive password-store lookup function that actually uses +the GPG agent correctly." + + (interactive) + + (let* ((entry (completing-read "Copy password of entry: " + (password-store-list (or password-store-dir + (password-store-dir))) + nil ;; predicate + t ;; require-match + )) + (password (auth-source-pass-get 'secret entry))) + (password-store-clear) + (kill-new password) + (setq password-store-kill-ring-pointer kill-ring-yank-pointer) + (message "Copied %s to the kill ring. Will clear in %s seconds." + entry (password-store-timeout)) + (setq password-store-timeout-timer + (run-at-time (password-store-timeout) + nil 'password-store-clear)))) + +(defun browse-repositories () + "Select a git repository and open its associated magit buffer." + + (interactive) + (magit-status + (completing-read "Repository: " (magit-list-repos)))) + +(defun bottom-right-window-p () + "Determines whether the last (i.e. bottom-right) window of the + active frame is showing the buffer in which this function is + executed." + (let* ((frame (selected-frame)) + (right-windows (window-at-side-list frame 'right)) + (bottom-windows (window-at-side-list frame 'bottom)) + (last-window (car (seq-intersection right-windows bottom-windows)))) + (eq (current-buffer) (window-buffer last-window)))) + +(defhydra mc/mark-more-hydra (:color pink) + ("<up>" mc/mmlte--up "Mark previous like this") + ("<down>" mc/mmlte--down "Mark next like this") + ("<left>" mc/mmlte--left (if (eq mc/mark-more-like-this-extended-direction 'up) + "Skip past the cursor furthest up" + "Remove the cursor furthest down")) + ("<right>" mc/mmlte--right (if (eq mc/mark-more-like-this-extended-direction 'up) + "Remove the cursor furthest up" + "Skip past the cursor furthest down")) + ("f" nil "Finish selecting")) + +;; Mute the message that mc/mmlte wants to print on its own +(advice-add 'mc/mmlte--message :around (lambda (&rest args) (ignore))) + +(defun mc/mark-dwim (arg) + "Select multiple things, but do what I mean." + + (interactive "p") + (if (not (region-active-p)) (mc/mark-next-lines arg) + (if (< 1 (count-lines (region-beginning) + (region-end))) + (mc/edit-lines arg) + ;; The following is almost identical to `mc/mark-more-like-this-extended', + ;; but uses a hydra (`mc/mark-more-hydra') instead of a transient key map. + (mc/mmlte--down) + (mc/mark-more-hydra/body)))) + +(setq mc/cmds-to-run-for-all '(kill-region paredit-newline)) + +(setq mc/cmds-to-run-once '(mc/mark-dwim + mc/mark-more-hydra/mc/mmlte--down + mc/mark-more-hydra/mc/mmlte--left + mc/mark-more-hydra/mc/mmlte--right + mc/mark-more-hydra/mc/mmlte--up + mc/mark-more-hydra/mmlte--up + mc/mark-more-hydra/nil)) + +(defun memespace-region () + "Make a meme out of it." + + (interactive) + (let* ((start (region-beginning)) + (end (region-end)) + (memed + (message + (s-trim-right + (apply #'string + (-flatten + (nreverse + (-reduce-from (lambda (acc x) + (cons (cons x (-repeat (+ 1 (length acc)) 32)) acc)) + '() + (string-to-list (buffer-substring-no-properties start end)))))))))) + + (save-excursion (delete-region start end) + (goto-char start) + (insert memed)))) + +(defun insert-todo-comment (prefix todo) + "Insert a comment at point with something for me to do." + + (interactive "P\nsWhat needs doing? ") + (save-excursion + (move-end-of-line nil) + (insert (format " %s TODO(%s): %s" + (s-trim-right comment-start) + (if prefix (read-string "Who needs to do this? ") + (getenv "USER")) + todo)))) + +;; Custom text scale adjustment functions that operate on the entire instance +(defun modify-text-scale (factor) + (set-face-attribute 'default nil + :height (+ (* factor 5) (face-attribute 'default :height)))) + +(defun increase-default-text-scale (prefix) + "Increase default text scale in all Emacs frames, or just the + current frame if PREFIX is set." + + (interactive "P") + (if prefix (text-scale-increase 1) + (modify-text-scale 1))) + +(defun decrease-default-text-scale (prefix) + "Increase default text scale in all Emacs frames, or just the + current frame if PREFIX is set." + + (interactive "P") + (if prefix (text-scale-decrease 1) + (modify-text-scale -1))) + +(defun set-default-text-scale (prefix &optional to) + "Set the default text scale to the specified value, or the + default. Restores current frame's text scale only, if PREFIX is + set." + + (interactive "P") + (if prefix (text-scale-adjust 0) + (set-face-attribute 'default nil :height (or to 120)))) + +(defun scrot-select () + "Take a screenshot based on a mouse-selection and save it to + ~/screenshots." + (interactive) + (shell-command "scrot '$a_%Y-%m-%d_%s.png' -s -e 'mv $f ~/screenshots/'")) + +(defun graph-unread-mails () + "Create a bar chart of unread mails based on notmuch tags. + Certain tags are excluded from the overview." + + (interactive) + (let ((tag-counts + (-keep (-lambda ((name . search)) + (let ((count + (string-to-number + (s-trim + (notmuch-command-to-string "count" search "and" "tag:unread"))))) + (when (>= count 1) (cons name count)))) + (notmuch-hello-generate-tag-alist '("unread" "signed" "attachment" "important"))))) + + (chart-bar-quickie + (if (< (length tag-counts) 6) + 'vertical 'horizontal) + "Unread emails" + (-map #'car tag-counts) "Tag:" + (-map #'cdr tag-counts) "Count:"))) + +(defun notmuch-show-open-or-close-subthread (&optional prefix) + "Open or close the subthread from (and including) the message at point." + (interactive "P") + (save-excursion + (let ((current-depth (map-elt (notmuch-show-get-message-properties) :depth 0))) + (loop do (notmuch-show-message-visible (notmuch-show-get-message-properties) prefix) + until (or (not (notmuch-show-goto-message-next)) + (= (map-elt (notmuch-show-get-message-properties) :depth) current-depth))))) + (force-window-update)) + +(defun vterm-send-ctrl-x () + "Sends `C-x' to the libvterm." + (interactive) + (vterm-send-key "x" nil nil t)) + +(defun find-depot-project (dir) + "Function used in the `project-find-functions' hook list to + determine the current project root of a depot project." + (when (s-starts-with? "/depot" dir) + (if (f-exists-p (f-join dir "default.nix")) + (cons 'transient dir) + (find-depot-project (f-parent dir))))) + +(add-to-list 'project-find-functions #'find-depot-project) + +(defun magit-find-file-worktree () + (interactive) + "Find a file in the current (ma)git worktree." + (magit-find-file--internal "{worktree}" + (magit-read-file-from-rev "HEAD" "Find file") + #'pop-to-buffer-same-window)) + +(defun songwhip--handle-result (status &optional cbargs) + ;; TODO(tazjin): Inspect status, which looks different in practice + ;; than the manual claims. + (if-let* ((response (json-parse-string + (buffer-substring url-http-end-of-headers (point-max)))) + (sw-path (ht-get* response "data" "path")) + (link (format "https://songwhip.com/%s" sw-path)) + (select-enable-clipboard t)) + (progn + (kill-new link) + (message "Copied Songwhip link (%s)" link)) + (warn "Something went wrong while retrieving Songwhip link!") + ;; For debug purposes, the buffer is persisted in this case. + (setq songwhip--debug-buffer (current-buffer)))) + +(defun songwhip-lookup-url (url) + "Look up URL on Songwhip and copy the resulting link to the clipboard." + (interactive "sEnter source URL: ") + (let ((songwhip-url "https://songwhip.com/api/") + (url-request-method "POST") + (url-request-extra-headers '(("Content-Type" . "application/json"))) + (url-request-data + (json-serialize `((country . "GB") + (url . ,url))))) + (url-retrieve "https://songwhip.com/api/" #'songwhip--handle-result nil t t) + (message "Requesting Songwhip URL ... please hold the line."))) + +(defun rg-in-project (&optional prefix) + "Interactively call ripgrep in the current project, or fall + back to ripgrep default behaviour if prefix is set." + (interactive "P") + (counsel-rg nil (unless prefix + (if-let ((pr (project-current))) + (project-root pr))))) + +(defun zoxide-open-magit () + "Query Zoxide for paths and open magit in the result." + (interactive) + (zoxide-open-with nil #'magit-status-setup-buffer)) + +(provide 'functions) diff --git a/users/tazjin/emacs/config/init.el b/users/tazjin/emacs/config/init.el new file mode 100644 index 000000000000..b0e80998d63b --- /dev/null +++ b/users/tazjin/emacs/config/init.el @@ -0,0 +1,308 @@ +;;; init.el --- Package bootstrapping. -*- lexical-binding: t; -*- + +;; Disable annoying warnings from native compilation. +(setq native-comp-async-report-warnings-errors nil + warning-suppress-log-types '((comp))) + +;; Packages are installed via Nix configuration, this file only +;; initialises the newly loaded packages. + +(require 'use-package) +(require 'seq) + +;; TODO(tazjin): Figure out what's up with vc. +;; +;; Leaving vc enabled breaks all find-file operations with messages +;; about .git folders being absent, but in random places. +(require 'vc) +(setq vc-handled-backends nil) + +(package-initialize) + +;; Initialise all packages installed via Nix. +;; +;; TODO: Generate this section in Nix for all packages that do not +;; require special configuration. + +;; +;; Packages providing generic functionality. +;; + +(use-package ace-window + :bind (("C-x o" . ace-window)) + :config + (setq aw-keys '(?f ?j ?d ?k ?s ?l ?a) + aw-scope 'frame)) + +(use-package auth-source-pass :config (auth-source-pass-enable)) + +(use-package avy + :bind (("M-j" . avy-goto-char) + ("M-p" . avy-pop-mark) + ("M-g g" . avy-goto-line))) + +(use-package browse-kill-ring) + +(use-package company + :hook ((prog-mode . company-mode)) + :config (setq company-tooltip-align-annotations t)) + +(use-package counsel + :after (ivy) + :config (counsel-mode 1)) + +(use-package dash) +(use-package gruber-darker-theme) + +(use-package eglot + :custom + (eglot-autoshutdown t) + (eglot-send-changes-idle-time 0.3)) + +(use-package elfeed + :config + (setq elfeed-feeds + '("https://lobste.rs/rss" + "https://www.anti-spiegel.ru/feed/" + "https://www.reddit.com/r/lockdownskepticism/.rss" + "https://www.reddit.com/r/rust/.rss" + "https://news.ycombinator.com/rss" + ("https://xkcd.com/atom.xml" media) + + ;; vlogcreations + ("https://www.youtube.com/feeds/videos.xml?channel_id=UCR0VLWitB2xM4q7tjkoJUPw" media) + ))) + +(use-package ht) + +(use-package hydra) +(use-package idle-highlight-mode :hook ((prog-mode . idle-highlight-mode))) + +(use-package ivy + :config + (ivy-mode 1) + (setq enable-recursive-minibuffers t) + (setq ivy-use-virtual-buffers t)) + +(use-package ivy-prescient + :after (ivy prescient) + :config + (ivy-prescient-mode) + ;; Fixes an issue with how regexes are passed to ripgrep from counsel, + ;; see raxod502/prescient.el#43 + (setf (alist-get 'counsel-rg ivy-re-builders-alist) #'ivy--regex-plus)) + +(use-package multiple-cursors) + +(use-package notmuch + :custom + (notmuch-search-oldest-first nil) + (notmuch-show-all-tags-list t) + (notmuch-hello-tag-list-make-query "tag:unread")) + +(use-package paredit :hook ((lisp-mode . paredit-mode) + (emacs-lisp-mode . paredit-mode))) + +(use-package pinentry + :config + (setq epa-pinentry-mode 'loopback) + (pinentry-start)) + +(use-package prescient + :after (ivy counsel) + :config (prescient-persist-mode)) + +(use-package rainbow-delimiters :hook (prog-mode . rainbow-delimiters-mode)) +(use-package rainbow-mode) +(use-package s) +(use-package string-edit) + +(use-package swiper + :after (counsel ivy) + :bind (("C-s" . swiper))) + +(use-package telephone-line) ;; configuration happens outside of use-package +(use-package term-switcher) + +(use-package undo-tree + :config (global-undo-tree-mode) + :custom (undo-tree-auto-save-history nil)) + +(use-package uuidgen) +(use-package which-key :config (which-key-mode t)) + +;; +;; Applications in emacs +;; + +(use-package magit + :bind ("C-c g" . magit-status) + :config (setq magit-repository-directories '(("/home/tazjin/projects" . 2) + ("/home/tazjin" . 1)))) + +(use-package password-store) +(use-package restclient) + +(use-package vterm + :config (progn + (setq vterm-shell "fish") + (setq vterm-exit-functions + (lambda (&rest _) (kill-buffer (current-buffer)))) + (setq vterm-kill-buffer-on-exit t))) + +;; vterm removed the ability to set a custom title generator function +;; via the public API, so this overrides its private title generation +;; function instead +(defun vterm--set-title (title) + (rename-buffer + (generate-new-buffer-name + (format "vterm<%s>" + (s-trim-left + (s-chop-prefix "fish" title)))))) + +;; +;; Packages providing language-specific functionality +;; + +(use-package cargo + :hook ((rust-mode . cargo-minor-mode) + (cargo-process-mode . visual-line-mode)) + :bind (:map cargo-mode-map ("C-c C-c C-l" . ignore))) + +(use-package dockerfile-mode) + +(use-package erlang + :hook ((erlang-mode . (lambda () + ;; Don't indent after '>' while I'm writing + (local-set-key ">" 'self-insert-command))))) + +(use-package f) + +(use-package go-mode + :bind (:map go-mode-map ("C-c C-r" . recompile)) + :hook ((go-mode . (lambda () + (setq tab-width 2) + (setq-local compile-command + (concat "go build " buffer-file-name)))))) + +(use-package haskell-mode) + +(use-package ielm + :hook ((inferior-emacs-lisp-mode . (lambda () + (paredit-mode) + (rainbow-delimiters-mode-enable) + (company-mode))))) + +(use-package jq-mode + :config (add-to-list 'auto-mode-alist '("\\.jq\\'" . jq-mode))) + +(use-package kotlin-mode + :hook ((kotlin-mode . (lambda () + (setq indent-line-function #'indent-relative))))) + +(use-package lsp-mode) + +(use-package markdown-mode + :config + (add-to-list 'auto-mode-alist '("\\.markdown\\'" . markdown-mode)) + (add-to-list 'auto-mode-alist '("\\.md\\'" . markdown-mode))) + +(use-package markdown-toc) + +(use-package nix-mode + :hook ((nix-mode . (lambda () + (setq indent-line-function #'nix-indent-line))))) + +(use-package nix-util) +(use-package nginx-mode) +(use-package rust-mode) + +(use-package sly + :hook ((sly-mrepl-mode . (lambda () + (paredit-mode) + (rainbow-delimiters-mode-enable) + (company-mode)))) + :config + (setq common-lisp-hyperspec-root "file:///home/tazjin/docs/lisp/")) + +(use-package telega + :bind (:map global-map ("s-t" . telega)) + :config (telega-mode-line-mode 1)) + +(use-package terraform-mode) +(use-package toml-mode) + +(use-package tvl) + +(use-package web-mode) +(use-package yaml-mode) +(use-package zoxide) + +(use-package passively + :custom + (passively-store-state "/persist/tazjin/known-russian-words.el")) + +;; Note taking configuration for deft. +(use-package deft + :custom + (deft-directory "/persist/tazjin/deft/") + (deft-extensions '("md" "org" "txt")) + (deft-default-extension "md")) + +(use-package zetteldeft + :custom + ;; Configure for Markdown + (zetteldeft-link-indicator "[[") + (zetteldeft-link-suffix "]]") + (zetteldeft-title-prefix "# ") + (zetteldeft-list-prefix "* ")) + +;; Initialise midnight.el, which by default automatically cleans up +;; unused buffers at midnight. +(require 'midnight) + +(defgroup tazjin nil + "Settings related to my configuration") + +(defcustom depot-path "/depot" + "Local path to the depot checkout" + :group 'tazjin) + +;; Configuration changes in `customize` can not actually be persisted +;; to the customise file that Emacs is currently using (since it comes +;; from the Nix store). +;; +;; The way this will work for now is that Emacs will *write* +;; configuration to the file tracked in my repository, while not +;; actually *reading* it from there (unless Emacs is rebuilt). +(setq custom-file (expand-file-name "~/depot/tools/emacs/config/custom.el")) +(load-library "custom") + +(defvar home-dir (expand-file-name "~")) + +;; Seed RNG +(random t) + +;; Load all other Emacs configuration. These configurations are +;; added to `load-path' by Nix. +(mapc 'require '(desktop + mail-setup + look-and-feel + functions + settings + modes + bindings + eshell-setup)) +(telephone-line-setup) +(ace-window-display-mode) + +;; If a local configuration library exists, it should be loaded. +;; +;; This can be provided by calling my Emacs derivation with +;; `withLocalConfig'. +(if-let (local-file (locate-library "local")) + (load local-file)) + +(require 'dottime) + +(provide 'init) diff --git a/users/tazjin/emacs/config/look-and-feel.el b/users/tazjin/emacs/config/look-and-feel.el new file mode 100644 index 000000000000..72665d00c67f --- /dev/null +++ b/users/tazjin/emacs/config/look-and-feel.el @@ -0,0 +1,131 @@ +;;; -*- lexical-binding: t; -*- + +;; Hide those ugly tool bars: +(tool-bar-mode 0) +(scroll-bar-mode 0) +(menu-bar-mode 0) +(add-hook 'after-make-frame-functions + (lambda (frame) (scroll-bar-mode 0))) + +;; Don't do any annoying things: +(setq ring-bell-function 'ignore) +(setq initial-scratch-message "") + +;; Remember layout changes +(winner-mode 1) + +;; Usually emacs will run as a proper GUI application, in which case a few +;; extra settings are nice-to-have: +(when window-system + (setq frame-title-format '(buffer-file-name "%f" ("%b"))) + (mouse-wheel-mode t) + (blink-cursor-mode -1)) + +;; Configure Emacs fonts. +(let ((font (if (equal "frog" (s-trim (shell-command-to-string "hostname"))) + ;; For unclear reasons, frog refuses to render the + ;; regular font weight - everything ends up bold, + ;; which makes it hard to distinguish e.g. read/unread + ;; emails. + ;; + ;; Semi-bold looks a little different than on vauxhall + ;; and other machines, but it's alright. + (format "JetBrains Mono Semi Light-%d" 12) + (format "JetBrains Mono-%d" 12)))) + (setq default-frame-alist `((font . ,font))) + (set-frame-font font t t)) + +;; Configure telephone-line +(defun telephone-misc-if-last-window () + "Renders the mode-line-misc-info string for display in the + mode-line if the currently active window is the last one in the + frame. + + The idea is to not display information like the current time, + load, battery levels on all buffers." + + (when (bottom-right-window-p) + (telephone-line-raw mode-line-misc-info t))) + +(defun telephone-line-setup () + (telephone-line-defsegment telephone-line-last-window-segment () + (telephone-misc-if-last-window)) + + ;; Display the current EXWM workspace index in the mode-line + (telephone-line-defsegment telephone-line-exwm-workspace-index () + (when (bottom-right-window-p) + (format "[%s]" exwm-workspace-current-index))) + + ;; Define a highlight font for ~ important ~ information in the last + ;; window. + (defface special-highlight '((t (:foreground "white" :background "#5f627f"))) "") + (add-to-list 'telephone-line-faces + '(highlight . (special-highlight . special-highlight))) + + (setq telephone-line-lhs + '((nil . (telephone-line-position-segment)) + (accent . (telephone-line-buffer-segment)))) + + (setq telephone-line-rhs + '((accent . (telephone-line-major-mode-segment)) + (nil . (telephone-line-last-window-segment + telephone-line-exwm-workspace-index)) + + ;; TODO(tazjin): lets not do this particular thing while I + ;; don't actually run notmuch, there are too many things + ;; that have a dependency on the modeline drawing correctly + ;; (including randr operations!) + ;; + ;; (highlight . (telephone-line-notmuch-counts)) + )) + + (setq telephone-line-primary-left-separator 'telephone-line-tan-left + telephone-line-primary-right-separator 'telephone-line-tan-right + telephone-line-secondary-left-separator 'telephone-line-tan-hollow-left + telephone-line-secondary-right-separator 'telephone-line-tan-hollow-right) + + (telephone-line-mode 1)) + +;; Auto refresh buffers +(global-auto-revert-mode 1) + +;; Use clipboard properly +(setq select-enable-clipboard t) + +;; Show in-progress chords in minibuffer +(setq echo-keystrokes 0.1) + +;; Show column numbers in all buffers +(column-number-mode t) + +(defalias 'yes-or-no-p 'y-or-n-p) +(defalias 'auto-tail-revert-mode 'tail-mode) + +;; Style line numbers (shown with M-g g) +(setq linum-format + (lambda (line) + (propertize + (format (concat " %" + (number-to-string + (length (number-to-string + (line-number-at-pos (point-max))))) + "d ") + line) + 'face 'linum))) + +;; Display tabs as 2 spaces +(setq tab-width 2) + +;; Don't wrap around when moving between buffers +(setq windmove-wrap-around nil) + +;; Don't show me all emacs warnings immediately. Unfortunately this is +;; not very granular, as emacs displays most of its warnings in the +;; `emacs' "category", but without it every time I +;; fullscreen/unfullscreen the warning buffer destroys my layout. +;; +;; Warnings suppressed by this are still logged to the warnings +;; buffer. +(setq warning-suppress-types '((emacs))) + +(provide 'look-and-feel) diff --git a/users/tazjin/emacs/config/mail-setup.el b/users/tazjin/emacs/config/mail-setup.el new file mode 100644 index 000000000000..7fbece1b102a --- /dev/null +++ b/users/tazjin/emacs/config/mail-setup.el @@ -0,0 +1,85 @@ +(require 'notmuch) +(require 'counsel-notmuch) + +;; (global-set-key (kbd "C-c m") 'notmuch-hello) +;; (global-set-key (kbd "C-c C-m") 'counsel-notmuch) +;; (global-set-key (kbd "C-c C-e n") 'notmuch-mua-new-mail) + +(setq notmuch-cache-dir (format "%s/.cache/notmuch" (getenv "HOME"))) +(make-directory notmuch-cache-dir t) + +;; Cache addresses for completion: +(setq notmuch-address-save-filename (concat notmuch-cache-dir "/addresses")) + +;; Don't spam my home folder with drafts: +(setq notmuch-draft-folder "drafts") ;; relative to notmuch database + +;; Mark things as read when archiving them: +(setq notmuch-archive-tags '("-inbox" "-unread" "+archive")) + +;; Show me saved searches that I care about: +(setq notmuch-saved-searches + '((:name "inbox" :query "tag:inbox" :count-query "tag:inbox AND tag:unread" :key "i") + (:name "sent" :query "tag:sent" :key "t") + (:name "drafts" :query "tag:draft"))) +(setq notmuch-show-empty-saved-searches t) + +;; Mail sending configuration +(setq sendmail-program "gmi") ;; lieer binary supports sendmail emulation +(setq message-sendmail-extra-arguments + '("send" "--quiet" "-t" "-C" "~/mail/account.tazjin")) +(setq send-mail-function 'sendmail-send-it) +(setq notmuch-mua-user-agent-function + (lambda () (format "Emacs %s; notmuch.el %s" emacs-version notmuch-emacs-version))) +(setq mail-host-address (system-name)) +(setq notmuch-mua-cite-function #'message-cite-original-without-signature) +(setq notmuch-fcc-dirs nil) ;; Gmail does this server-side +(setq message-signature nil) ;; Insert message signature manually with C-c C-w + +;; Close mail buffers after sending mail +(setq message-kill-buffer-on-exit t) + +;; Ensure sender is correctly passed to msmtp +(setq mail-specify-envelope-from t + message-sendmail-envelope-from 'header + mail-envelope-from 'header) + +;; Store sent mail in the correct folder per account +(setq notmuch-maildir-use-notmuch-insert nil) + +;; I don't use drafts but I instinctively hit C-x C-s constantly, lets +;; handle that gracefully. +(define-key notmuch-message-mode-map (kbd "C-x C-s") #'ignore) + +;; Define a telephone-line segment for displaying the count of unread, +;; important mails in the last window's mode-line: +(defvar *last-notmuch-count-redraw* 0) +(defvar *current-notmuch-count* nil) + +(defun update-display-notmuch-counts () + "Update and render the current state of the notmuch unread + count for display in the mode-line. + + The offlineimap-timer runs every 2 minutes, so it does not make + sense to refresh this much more often than that." + + (when (> (- (float-time) *last-notmuch-count-redraw*) 30) + (setq *last-notmuch-count-redraw* (float-time)) + (let* ((inbox-unread (notmuch-saved-search-count "tag:inbox and tag:unread")) + (notmuch-count (format "I: %s; D: %s" inbox-unread))) + (setq *current-notmuch-count* notmuch-count))) + + (when (and (bottom-right-window-p) + ;; Only render if the initial update is done and there + ;; are unread mails: + *current-notmuch-count* + (not (equal *current-notmuch-count* "I: 0; D: 0"))) + *current-notmuch-count*)) + +(telephone-line-defsegment telephone-line-notmuch-counts () + "This segment displays the count of unread notmuch messages in + the last window's mode-line (if unread messages are present)." + + (update-display-notmuch-counts)) + +(provide 'mail-setup) diff --git a/users/tazjin/emacs/config/modes.el b/users/tazjin/emacs/config/modes.el new file mode 100644 index 000000000000..69fb523d0d91 --- /dev/null +++ b/users/tazjin/emacs/config/modes.el @@ -0,0 +1,37 @@ +;; Initializes modes I use. + +(add-hook 'prog-mode-hook 'esk-add-watchwords) +(add-hook 'prog-mode-hook 'hl-line-mode) + +;; Use auto-complete as completion at point +(defun set-auto-complete-as-completion-at-point-function () + (setq completion-at-point-functions '(auto-complete))) + +(add-hook 'auto-complete-mode-hook + 'set-auto-complete-as-completion-at-point-function) + +;; Enable rainbow-delimiters for all things programming +(add-hook 'prog-mode-hook 'rainbow-delimiters-mode) + +;; Enable Paredit & Company in Emacs Lisp mode +(add-hook 'emacs-lisp-mode-hook 'company-mode) + +;; Always highlight matching brackets +(show-paren-mode 1) + +;; Always auto-close parantheses and other pairs +(electric-pair-mode) + +;; Keep track of recent files +(recentf-mode) + +;; Easily navigate sillycased words +(global-subword-mode 1) + +;; Transparently open compressed files +(auto-compression-mode t) + +;; Configure go-mode for Go2 Alpha +(add-to-list 'auto-mode-alist '("\\.go2$" . go-mode)) + +(provide 'modes) diff --git a/users/tazjin/emacs/config/settings.el b/users/tazjin/emacs/config/settings.el new file mode 100644 index 000000000000..8b15b6cda183 --- /dev/null +++ b/users/tazjin/emacs/config/settings.el @@ -0,0 +1,48 @@ +(require 'uniquify) + +;; We don't live in the 80s, but we're also not a shitty web app. +(setq gc-cons-threshold 20000000) + +(setq uniquify-buffer-name-style 'forward) + +; Fix some defaults +(setq visible-bell nil + inhibit-startup-message t + color-theme-is-global t + sentence-end-double-space nil + shift-select-mode nil + uniquify-buffer-name-style 'forward + whitespace-style '(face trailing lines-tail tabs) + whitespace-line-column 80 + default-directory "~" + fill-column 80 + ediff-split-window-function 'split-window-horizontally + initial-major-mode 'emacs-lisp-mode) + +(add-to-list 'safe-local-variable-values '(lexical-binding . t)) +(add-to-list 'safe-local-variable-values '(whitespace-line-column . 80)) + +(set-default 'indent-tabs-mode nil) + +;; UTF-8 please +(setq locale-coding-system 'utf-8) ; pretty +(set-terminal-coding-system 'utf-8) ; pretty +(set-keyboard-coding-system 'utf-8) ; pretty +(set-selection-coding-system 'utf-8) ; please +(prefer-coding-system 'utf-8) ; with sugar on top + +;; Make emacs behave sanely (overwrite selected text) +(delete-selection-mode 1) + +;; Keep your temporary files in tmp, emacs! +(setq auto-save-file-name-transforms + `((".*" ,temporary-file-directory t))) +(setq backup-directory-alist + `((".*" . ,temporary-file-directory))) + +(remove-hook 'kill-buffer-query-functions 'server-kill-buffer-query-function) + +;; Show time in 24h format +(setq display-time-24hr-format t) + +(provide 'settings) diff --git a/users/tazjin/emacs/default.nix b/users/tazjin/emacs/default.nix new file mode 100644 index 000000000000..5b778be1d9b6 --- /dev/null +++ b/users/tazjin/emacs/default.nix @@ -0,0 +1,179 @@ +# This file builds an Emacs pre-configured with the packages I need +# and my personal Emacs configuration. +{ lib, pkgs, ... }: + +pkgs.makeOverridable + ({ emacs ? pkgs.emacsNativeComp }: + let + emacsWithPackages = (pkgs.emacsPackagesFor emacs).emacsWithPackages; + + # If switching telega versions, use this variable because it will + # keep the version check, binary path and so on in sync. + currentTelega = epkgs: epkgs.melpaPackages.telega; + + # $PATH for binaries that need to be available to Emacs + emacsBinPath = lib.makeBinPath [ + (currentTelega pkgs.emacsPackages) + pkgs.libwebp # for dwebp, required by telega + ]; + + identity = x: x; + + tazjinsEmacs = pkgfun: (emacsWithPackages (epkgs: pkgfun (with epkgs; [ + ace-link + ace-window + avy + bazel + browse-kill-ring + cargo + clojure-mode + cmake-mode + company + counsel + counsel-notmuch + d-mode + deft + direnv + dockerfile-mode + eglot + elfeed + elixir-mode + elm-mode + erlang + exwm + flymake + go-mode + google-c-style + gruber-darker-theme + haskell-mode + ht + hydra + idle-highlight-mode + ivy + ivy-prescient + jq-mode + kotlin-mode + lsp-mode + magit + markdown-toc + meson-mode + multi-term + multiple-cursors + nginx-mode + nix-mode + notmuch + paredit + password-store + pinentry + polymode + prescient + protobuf-mode + rainbow-delimiters + rainbow-mode + refine + request + restclient + rust-mode + sly + string-edit + swiper + telephone-line + terraform-mode + toml-mode + transient + undo-tree + use-package + uuidgen + vterm + web-mode + websocket + which-key + xelb + yaml-mode + yasnippet + zetteldeft + zoxide + + # Wonky stuff + (currentTelega epkgs) + + # Custom depot packages (either ours, or overridden ones) + tvlPackages.dottime + tvlPackages.nix-util + tvlPackages.passively + tvlPackages.rcirc + tvlPackages.term-switcher + tvlPackages.tvl + ]))); + + # Tired of telega.el runtime breakages through tdlib + # incompatibility. Target to make that a build failure instead. + tdlibCheck = + let + tgEmacs = emacsWithPackages (epkgs: [ (currentTelega epkgs) ]); + verifyTdlibVersion = builtins.toFile "verify-tdlib-version.el" '' + (require 'telega) + (defvar tdlib-version "${pkgs.tdlib.version}") + (when (or (version< tdlib-version + telega-tdlib-min-version) + (and telega-tdlib-max-version + (version< telega-tdlib-max-version + tdlib-version))) + (message "Found TDLib version %s, but require %s to %s" + tdlib-version telega-tdlib-min-version telega-tdlib-max-version) + (kill-emacs 1)) + ''; + in + pkgs.runCommandNoCC "tdlibCheck" { } '' + export PATH="${emacsBinPath}:$PATH" + ${tgEmacs}/bin/emacs --script ${verifyTdlibVersion} && touch $out + ''; + in + lib.fix + (self: l: f: (pkgs.writeShellScriptBin "tazjins-emacs" '' + export PATH="${emacsBinPath}:$PATH" + exec ${tazjinsEmacs f}/bin/emacs \ + --debug-init \ + --no-site-file \ + --no-site-lisp \ + --no-init-file \ + --directory ${./config} ${if l != null then "--directory ${l}" else ""} \ + --eval "(require 'init)" $@ + '').overrideAttrs + (_: { + passthru = { + # Call overrideEmacs with a function (pkgs -> pkgs) to modify the + # packages that should be included in this Emacs distribution. + overrideEmacs = f': self l f'; + + # Call withLocalConfig with the path to a *folder* containing a + # `local.el` which provides local system configuration. + withLocalConfig = confDir: self confDir f; + + # Build a derivation that uses the specified local Emacs (i.e. + # built outside of Nix) instead + withLocalEmacs = emacsBin: pkgs.writeShellScriptBin "tazjins-emacs" '' + export PATH="${emacsBinPath}:$PATH" + export EMACSLOADPATH="${(tazjinsEmacs f).deps}/share/emacs/site-lisp:" + exec ${emacsBin} \ + --debug-init \ + --no-site-file \ + --no-site-lisp \ + --no-init-file \ + --directory ${./config} \ + ${if l != null then "--directory ${l}" else ""} \ + --eval "(require 'init)" $@ + ''; + + # Expose telega/tdlib version check as a target that is built in + # CI. + # + # TODO(tazjin): uncomment when telega works again + inherit tdlibCheck; + meta.ci.targets = [ "tdlibCheck" ]; + }; + })) + null + identity + ) +{ } diff --git a/users/tazjin/finito/.gitignore b/users/tazjin/finito/.gitignore new file mode 100644 index 000000000000..548206b0b297 --- /dev/null +++ b/users/tazjin/finito/.gitignore @@ -0,0 +1,3 @@ +.envrc +/target/ +**/*.rs.bk diff --git a/users/tazjin/finito/Cargo.lock b/users/tazjin/finito/Cargo.lock new file mode 100644 index 000000000000..7427a6b11c42 --- /dev/null +++ b/users/tazjin/finito/Cargo.lock @@ -0,0 +1,773 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "addr2line" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602d785912f476e480434627e8732e6766b760c045bbf897d9dfaa9f4fbd399c" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler32" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b077b825e468cc974f0020d4082ee6e03132512f207ef1a02fd5d00d1f32d" + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "autocfg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" + +[[package]] +name = "backtrace" +version = "0.3.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05100821de9e028f12ae3d189176b41ee198341eb8f369956407fea2f5cc666c" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96434f987501f0ed4eb336a411e0631ecd1afa11574fe148587adc4ff96143c9" +dependencies = [ + "byteorder", + "safemem", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "block-buffer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a076c298b9ecdb530ed9d967e74a6027d6a7478924520acddcddc24c1c8ab3ab" +dependencies = [ + "arrayref", + "byte-tools", +] + +[[package]] +name = "byte-tools" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "560c32574a12a89ecd91f5e742165893f86e3ab98d21f8ea548658eb9eef5f40" + +[[package]] +name = "byteorder" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" + +[[package]] +name = "bytes" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" +dependencies = [ + "byteorder", + "iovec", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "chrono" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" +dependencies = [ + "num-integer", + "num-traits", + "time", +] + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "crypto-mac" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0999b4ff4d3446d4ddb19a63e9e00c1876e75cd7000d20e57a693b4b3f08d958" +dependencies = [ + "constant_time_eq", + "generic-array", +] + +[[package]] +name = "digest" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b072242a8cbaf9c145665af9d250c59af3b958f83ed6824e13533cf76d5b90" +dependencies = [ + "generic-array", +] + +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2 1.0.18", + "quote 1.0.7", + "syn 1.0.33", + "synstructure", +] + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + +[[package]] +name = "fallible-iterator" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7217124812dc5672b7476d0c2d20cfe9f7c0f1ba0904b674a9762a0212f72e" + +[[package]] +name = "finito" +version = "0.1.0" +dependencies = [ + "serde", +] + +[[package]] +name = "finito-door" +version = "0.1.0" +dependencies = [ + "failure", + "finito", + "serde", + "serde_derive", +] + +[[package]] +name = "finito-postgres" +version = "0.1.0" +dependencies = [ + "chrono", + "finito", + "finito-door", + "postgres", + "postgres-derive", + "r2d2_postgres", + "serde", + "serde_json", + "uuid", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "generic-array" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25c5683767570c2bbd7deba372926a55eaae9982d7726ee2a1050239d45b9d" +dependencies = [ + "typenum", +] + +[[package]] +name = "gimli" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc8e0c9bce37868955864dbecd2b1ab2bdf967e6f28066d65aaac620444b65c" + +[[package]] +name = "hex" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6a22814455d41612f41161581c2883c0c6a1c41852729b17d5ed88f01e153aa" + +[[package]] +name = "hmac" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f3bdb08579d99d7dc761c0e266f13b5f2ab8c8c703b9fc9ef333cd8f48f55e" +dependencies = [ + "crypto-mac", + "digest", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "itoa" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" + +[[package]] +name = "libc" +version = "0.2.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49" + +[[package]] +name = "lock_api" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + +[[package]] +name = "md5" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79c56d6a0b07f9e19282511c83fc5b086364cbae4ba8c7d5f190c3d9b0425a48" + +[[package]] +name = "memchr" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a" +dependencies = [ + "libc", +] + +[[package]] +name = "miniz_oxide" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +dependencies = [ + "adler32", +] + +[[package]] +name = "num-integer" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab52be62400ca80aa00285d25253d7f7c437b7375c4de678f5405d3afe82ca5" + +[[package]] +name = "parking_lot" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" +dependencies = [ + "cfg-if", + "cloudabi", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "phf" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" +dependencies = [ + "siphasher", +] + +[[package]] +name = "postgres" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115dde90ef51af573580c035857badbece2aa5cde3de1dfb3c932969ca92a6c5" +dependencies = [ + "bytes", + "fallible-iterator", + "log", + "postgres-protocol", + "postgres-shared", + "socket2", +] + +[[package]] +name = "postgres-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44ef42ae50f1547dde36aa78d5e44189cbf21f4e77ce6ddc2bbaa068337fc221" +dependencies = [ + "quote 0.5.2", + "syn 0.13.11", +] + +[[package]] +name = "postgres-protocol" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2487e66455bf88a1b247bf08a3ce7fe5197ac6d67228d920b0ee6a0e97fd7312" +dependencies = [ + "base64", + "byteorder", + "bytes", + "fallible-iterator", + "generic-array", + "hmac", + "md5", + "memchr", + "rand 0.3.23", + "sha2", + "stringprep", +] + +[[package]] +name = "postgres-shared" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffac35b3e0029b404c24a3b82149b4e904f293e8ca4a327eefa24d3ca50df36f" +dependencies = [ + "chrono", + "fallible-iterator", + "hex", + "phf", + "postgres-protocol", + "serde_json", + "uuid", +] + +[[package]] +name = "proc-macro2" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b06e2f335f48d24442b35a19df506a835fb3547bc3c06ef27340da9acf5cae7" +dependencies = [ + "unicode-xid 0.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa" +dependencies = [ + "unicode-xid 0.2.1", +] + +[[package]] +name = "quote" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9949cfe66888ffe1d53e6ec9d9f3b70714083854be20fd5e271b232a017401e8" +dependencies = [ + "proc-macro2 0.3.8", +] + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2 1.0.18", +] + +[[package]] +name = "r2d2" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1497e40855348e4a8a40767d8e55174bce1e445a3ac9254ad44ad468ee0485af" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + +[[package]] +name = "r2d2_postgres" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c7fe9c0c3d2c298cf262bc3ce4b89cdf0eab620fd9fe759f65b34a1a00fb93" +dependencies = [ + "postgres", + "postgres-shared", + "r2d2", +] + +[[package]] +name = "rand" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" +dependencies = [ + "libc", + "rand 0.4.6", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" + +[[package]] +name = "rustc-demangle" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "safemem" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f" + +[[package]] +name = "scheduled-thread-pool" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0988d7fdf88d5e5fcf5923a0f1e8ab345f3e98ab4bc6bc45a2d5ff7f7458fbf6" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3" + +[[package]] +name = "serde_derive" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0be94b04690fbaed37cddffc5c134bf537c8e3329d53e982fe04c374978f8e" +dependencies = [ + "proc-macro2 1.0.18", + "quote 1.0.7", + "syn 1.0.33", +] + +[[package]] +name = "serde_json" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3433e879a558dde8b5e8feb2a04899cf34fdde1fafb894687e52105fc1162ac3" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eb6be24e4c23a84d7184280d2722f7f2731fcdd4a9d886efbfe4413e4847ea0" +dependencies = [ + "block-buffer", + "byte-tools", + "digest", + "fake-simd", +] + +[[package]] +name = "siphasher" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" + +[[package]] +name = "smallvec" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7cb5678e1615754284ec264d9bb5b4c27d2018577fd90ac0ceb578591ed5ee4" + +[[package]] +name = "socket2" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03088793f677dce356f3ccc2edb1b314ad191ab702a5de3faf49304f7e104918" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "winapi", +] + +[[package]] +name = "stringprep" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "syn" +version = "0.13.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f9bf6292f3a61d2c716723fdb789a41bbe104168e6f496dc6497e531ea1b9b" +dependencies = [ + "proc-macro2 0.3.8", + "quote 0.5.2", + "unicode-xid 0.1.0", +] + +[[package]] +name = "syn" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d5d96e8cbb005d6959f119f773bfaebb5684296108fb32600c00cde305b2cd" +dependencies = [ + "proc-macro2 1.0.18", + "quote 1.0.7", + "unicode-xid 0.2.1", +] + +[[package]] +name = "synstructure" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" +dependencies = [ + "proc-macro2 1.0.18", + "quote 1.0.7", + "syn 1.0.33", + "unicode-xid 0.2.1", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "tinyvec" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53953d2d3a5ad81d9f844a32f14ebb121f50b650cd59d0ee2a07cf13c617efed" + +[[package]] +name = "typenum" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +dependencies = [ + "matches", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "uuid" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc7e3b898aa6f6c08e5295b6c89258d1331e9ac578cc992fb818759951bdc22" +dependencies = [ + "rand 0.3.23", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/users/tazjin/finito/Cargo.toml b/users/tazjin/finito/Cargo.toml new file mode 100644 index 000000000000..310133abeebb --- /dev/null +++ b/users/tazjin/finito/Cargo.toml @@ -0,0 +1,6 @@ +[workspace] +members = [ + "finito-core", + "finito-door", + "finito-postgres" +] diff --git a/users/tazjin/finito/README.md b/users/tazjin/finito/README.md new file mode 100644 index 000000000000..5acd67d3bea7 --- /dev/null +++ b/users/tazjin/finito/README.md @@ -0,0 +1,27 @@ +Finito +====== + +This is a Rust port of the Haskell state-machine library Finito. It is +slightly less featureful because it loses the ability to ensure that +side-effects are contained and because of a slight reduction in +expressivity, which makes it a bit more restrictive. + +However, it still implements the FSM model well enough. + +# Components + +Finito is split up into multiple independent components (note: not all +of these exist yet), separating functionality related to FSM +persistence from other things. + +* `finito`: Core abstraction implemented by Finito +* `finito-door`: Example implementation of a simple, lockable door +* `finito-postgres`: Persistent state-machines using Postgres + +**Note**: The `finito` core library does not contain any tests. Its +coverage is instead provided by the `finito-door` library, which +actually implements an example FSM. + +These are split out because the documentation for `finito-door` is +interesting regardless and because other Finito packages also need an +example implementation. diff --git a/users/tazjin/finito/default.nix b/users/tazjin/finito/default.nix new file mode 100644 index 000000000000..e50ac32be452 --- /dev/null +++ b/users/tazjin/finito/default.nix @@ -0,0 +1,5 @@ +{ depot, ... }: + +depot.third_party.naersk.buildPackage { + src = ./.; +} diff --git a/users/tazjin/finito/finito-core/Cargo.toml b/users/tazjin/finito/finito-core/Cargo.toml new file mode 100644 index 000000000000..1d7bdb8b01fe --- /dev/null +++ b/users/tazjin/finito/finito-core/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "finito" +version = "0.1.0" +authors = ["Vincent Ambo <mail@tazj.in>"] + +[dependencies] +serde = "1.0" diff --git a/users/tazjin/finito/finito-core/src/lib.rs b/users/tazjin/finito/finito-core/src/lib.rs new file mode 100644 index 000000000000..aaec03a77b42 --- /dev/null +++ b/users/tazjin/finito/finito-core/src/lib.rs @@ -0,0 +1,248 @@ +//! Finito's core finite-state machine abstraction. +//! +//! # What & why? +//! +//! Most processes that occur in software applications can be modeled +//! as finite-state machines (FSMs), however the actual states, the +//! transitions between them and the model's interaction with the +//! external world is often implicit. +//! +//! Making the states of a process explicit using a simple language +//! that works for both software developers and other people who may +//! have opinions on processes makes it easier to synchronise thoughts, +//! extend software and keep a good level of control over what is going +//! on. +//! +//! This library aims to provide functionality for implementing +//! finite-state machines in a way that balances expressivity and +//! safety. +//! +//! Finito does not aim to prevent every possible incorrect +//! transition, but aims for somewhere "safe-enough" (please don't +//! lynch me) that is still easily understood. +//! +//! # Conceptual overview +//! +//! The core idea behind Finito can be expressed in a single line and +//! will potentially look familiar if you have used Erlang in a +//! previous life. The syntax used here is the type-signature notation +//! of Haskell. +//! +//! ```text +//! advance :: state -> event -> (state, [action]) +//! ``` +//! +//! In short, every FSM is made up of three distinct types: +//! +//! * a state type representing all possible states of the machine +//! +//! * an event type representing all possible events in the machine +//! +//! * an action type representing a description of all possible side-effects +//! of the machine +//! +//! Using the definition above we can now say that a transition in a +//! state-machine, involving these three types, takes an initial state +//! and an event to apply it to and returns a new state and a list of +//! actions to execute. +//! +//! With this definition most processes can already be modeled quite +//! well. Two additional functions are required to make it all work: +//! +//! ```text +//! -- | The ability to cause additional side-effects after entering +//! -- a new state. +//! > enter :: state -> [action] +//! ``` +//! +//! as well as +//! +//! ```text +//! -- | An interpreter for side-effects +//! act :: action -> m [event] +//! ``` +//! +//! **Note**: This library is based on an original Haskell library. In +//! Haskell, side-effects can be controlled via the type system which +//! is impossible in Rust. +//! +//! Some parts of Finito make assumptions about the programmer not +//! making certain kinds of mistakes, which are pointed out in the +//! documentation. Unfortunately those assumptions are not +//! automatically verifiable in Rust. +//! +//! ## Example +//! +//! Please consult `finito-door` for an example representing a simple, +//! lockable door as a finite-state machine. This gives an overview +//! over Finito's primary features. +//! +//! If you happen to be the kind of person who likes to learn about +//! libraries by reading code, you should familiarise yourself with the +//! door as it shows up as the example in other finito-related +//! libraries, too. +//! +//! # Persistence, side-effects and mud +//! +//! These three things are inescapable in the fateful realm of +//! computers, but Finito separates them out into separate libraries +//! that you can drag in as you need them. +//! +//! Currently, those libraries include: +//! +//! * `finito`: Core components and classes of Finito +//! +//! * `finito-in-mem`: In-memory implementation of state machines that do not +//! need to live longer than an application using standard library +//! concurrency primitives. +//! +//! * `finito-postgres`: Postgres-backed, persistent implementation of state +//! machines that, well, do need to live longer. Uses Postgres for +//! concurrency synchronisation, so keep that in mind. +//! +//! Which should cover most use-cases. Okay, enough prose, lets dive +//! in. +//! +//! # Does Finito make you want to scream? +//! +//! Please reach out! I want to know why! + +extern crate serde; + +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::fmt::Debug; +use std::mem; + +/// Primary trait that needs to be implemented for every state type +/// representing the states of an FSM. +/// +/// This trait is used to implement transition logic and to "tie the +/// room together", with the room being our triplet of types. +pub trait FSM +where + Self: Sized, +{ + /// A human-readable string uniquely describing what this FSM + /// models. This is used in log messages, database tables and + /// various other things throughout Finito. + const FSM_NAME: &'static str; + + /// The associated event type of an FSM represents all possible + /// events that can occur in the state-machine. + type Event; + + /// The associated action type of an FSM represents all possible + /// actions that can occur in the state-machine. + type Action; + + /// The associated error type of an FSM represents failures that + /// can occur during action processing. + type Error: Debug; + + /// The associated state type of an FSM describes the state that + /// is made available to the implementation of action + /// interpretations. + type State; + + /// `handle` deals with any incoming events to cause state + /// transitions and emit actions. This function is the core logic + /// of any state machine. + /// + /// Implementations of this function **must not** cause any + /// side-effects to avoid breaking the guarantees of Finitos + /// conceptual model. + fn handle(self, event: Self::Event) -> (Self, Vec<Self::Action>); + + /// `enter` is called when a new state is entered, allowing a + /// state to produce additional side-effects. + /// + /// This is useful for side-effects that event handlers do not + /// need to know about and for resting assured that a certain + /// action has been caused when a state is entered. + /// + /// FSM state types are expected to be enum (i.e. sum) types. A + /// state is considered "new" and enter calls are run if is of a + /// different enum variant. + fn enter(&self) -> Vec<Self::Action>; + + /// `act` interprets and executes FSM actions. This is the only + /// part of an FSM in which side-effects are allowed. + fn act(action: Self::Action, state: &Self::State) -> Result<Vec<Self::Event>, Self::Error>; +} + +/// This function is the primary function used to advance a state +/// machine. It takes care of both running the event handler as well +/// as possible state-enter calls and returning the result. +/// +/// Users of Finito should basically always use this function when +/// advancing state-machines manually, and never call FSM-trait +/// methods directly. +pub fn advance<S: FSM>(state: S, event: S::Event) -> (S, Vec<S::Action>) { + // Determine the enum variant of the initial state (used to + // trigger enter calls). + let old_discriminant = mem::discriminant(&state); + + let (new_state, mut actions) = state.handle(event); + + // Compare the enum variant of the resulting state to the old one + // and run `enter` if they differ. + let new_discriminant = mem::discriminant(&new_state); + let mut enter_actions = if old_discriminant != new_discriminant { + new_state.enter() + } else { + vec![] + }; + + actions.append(&mut enter_actions); + + (new_state, actions) +} + +/// This trait is implemented by Finito backends. Backends are +/// expected to be able to keep track of the current state of an FSM +/// and retrieve it / apply updates transactionally. +/// +/// See the `finito-postgres` and `finito-in-mem` crates for example +/// implementations of this trait. +/// +/// Backends must be parameterised over an additional (user-supplied) +/// state type which can be used to track application state that must +/// be made available to action handlers, for example to pass along +/// database connections. +pub trait FSMBackend<S: 'static> { + /// Key type used to identify individual state machines in this + /// backend. + /// + /// TODO: Should be parameterised over FSM type after rustc + /// #44265. + type Key; + + /// Error type for all potential failures that can occur when + /// interacting with this backend. + type Error: Debug; + + /// Insert a new state-machine into the backend's storage and + /// return its newly allocated key. + fn insert_machine<F>(&self, initial: F) -> Result<Self::Key, Self::Error> + where + F: FSM + Serialize + DeserializeOwned; + + /// Retrieve the current state of an FSM by its key. + fn get_machine<F: FSM>(&self, key: Self::Key) -> Result<F, Self::Error> + where + F: FSM + Serialize + DeserializeOwned; + + /// Advance a state machine by applying an event and persisting it + /// as well as any resulting actions. + /// + /// **Note**: Whether actions are automatically executed depends + /// on the backend used. Please consult the backend's + /// documentation for details. + fn advance<'a, F: FSM>(&'a self, key: Self::Key, event: F::Event) -> Result<F, Self::Error> + where + F: FSM + Serialize + DeserializeOwned, + F::State: From<&'a S>, + F::Event: Serialize + DeserializeOwned, + F::Action: Serialize + DeserializeOwned; +} diff --git a/users/tazjin/finito/finito-door/Cargo.toml b/users/tazjin/finito/finito-door/Cargo.toml new file mode 100644 index 000000000000..32c0a5a7c4ef --- /dev/null +++ b/users/tazjin/finito/finito-door/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "finito-door" +version = "0.1.0" +authors = ["Vincent Ambo <mail@tazj.in>"] + +[dependencies] +failure = "0.1" +serde = "1.0" +serde_derive = "1.0" + +[dependencies.finito] +path = "../finito-core" diff --git a/users/tazjin/finito/finito-door/src/lib.rs b/users/tazjin/finito/finito-door/src/lib.rs new file mode 100644 index 000000000000..441ab0e3d2cf --- /dev/null +++ b/users/tazjin/finito/finito-door/src/lib.rs @@ -0,0 +1,333 @@ +//! Example implementation of a lockable door in Finito +//! +//! # What & why? +//! +//! This module serves as a (hopefully simple) example of how to +//! implement finite-state machines using Finito. Note that the +//! concepts of Finito itself won't be explained in detail here, +//! consult its library documentation for that. +//! +//! Reading through this module should give you a rough idea of how to +//! work with Finito and get you up and running modeling things +//! *quickly*. +//! +//! Note: The generated documentation for this module will display the +//! various components of the door, but it will not inform you about +//! the actual transition logic and all that stuff. Read the source, +//! too! +//! +//! # The Door +//! +//! My favourite example when explaining these state-machines +//! conceptually has been to use a simple, lockable door. Our door has +//! a keypad next to it which can be used to lock the door by entering +//! a code, after which the same code must be entered to unlock it +//! again. +//! +//! The door can only be locked if it is closed. Oh, and it has a few +//! extra features: +//! +//! * whenever the door's state changes, an IRC channel receives a message about +//! that +//! +//! * the door calls the police if the code is intered incorrectly more than a +//! specified number of times (mhm, lets say, three) +//! +//! * if the police is called the door can not be interacted with anymore (and +//! honestly, for the sake of this example, we don't care how its +//! functionality is restored) +//! +//! ## The Door - Visualized +//! +//! Here's a rough attempt at drawing a state diagram in ASCII. The +//! bracketed words denote states, the arrows denote events: +//! +//! ```text +//! <--Open--- <--Unlock-- correct code? --Unlock--> +//! [Opened] [Closed] [Locked] [Disabled] +//! --Close--> ----Lock--> +//! ``` +//! +//! I'm so sorry for that drawing. +//! +//! ## The Door - Usage example +//! +//! An interaction session with our final door could look like this: +//! +//! ```rust,ignore +//! use finito_postgres::{insert_machine, advance}; +//! +//! let door = insert_machine(&conn, &DoorState::Opened)?; +//! +//! advance(&conn, &door, DoorEvent::Close)?; +//! advance(&conn, &door, DoorEvent::Lock(1337))?; +//! +//! format!("Door is now: {}", get_machine(&conn, &door)?); +//! ``` +//! +//! Here we have created, closed and then locked a door and inspected +//! its state. We will see that it is locked, has the locking code we +//! gave it and three remaining attempts to open it. +//! +//! Alright, enough foreplay, lets dive in! + +#[macro_use] +extern crate serde_derive; + +extern crate failure; +extern crate finito; + +use finito::FSM; + +/// Type synonym to represent the code with which the door is locked. This +/// exists only for clarity in the signatures below and please do not email me +/// about the fact that an integer is not actually a good representation of +/// numerical digits. Thanks! +type Code = usize; + +/// Type synonym to represent the remaining number of unlock attempts. +type Attempts = usize; + +/// This type represents the possible door states and the data that they carry. +/// We can infer this from the "diagram" in the documentation above. +/// +/// This type is the one for which `finito::FSM` will be implemented, making it +/// the wooden (?) heart of our door. +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub enum DoorState { + /// In `Opened` state, the door is wide open and anyone who fits through can + /// go through. + Opened, + + /// In `Closed` state, the door is shut but does not prevent anyone from + /// opening it. + Closed, + + /// In `Locked` state, the door is locked and waiting for someone to enter + /// its locking code on the keypad. + /// + /// This state contains the code that the door is locked with, as well as + /// the remaining number of attempts before the door calls the police and + /// becomes unusable. + Locked { code: Code, attempts: Attempts }, + + /// This state represents a disabled door after the police has been called. + /// The police will need to unlock it manually! + Disabled, +} + +/// This type represents the events that can occur in our door, i.e. the input +/// and interactions it receives. +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub enum DoorEvent { + /// `Open` means someone is opening the door! + Open, + + /// `Close` means, you guessed it, the exact opposite. + Close, + + /// `Lock` means somebody has entered a locking code on the + /// keypad. + Lock(Code), + + /// `Unlock` means someone has attempted to unlock the door. + Unlock(Code), +} + +/// This type represents the possible actions, a.k.a. everything our door "does" +/// that does not just impact itself, a.k.a. side-effects. +/// +/// **Note**: This type by itself *is not* a collection of side-effects, it +/// merely describes the side-effects we want to occur (which are then +/// interpreted by the machinery later). +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub enum DoorAction { + /// `NotifyIRC` is used to display some kind of message on the + /// aforementioned IRC channel that is, for some reason, very interested in + /// the state of the door. + NotifyIRC(String), + + /// `CallThePolice` does what you think it does. + /// + /// **Note**: For safety reasons, causing this action is not recommended for + /// users inside the US! + CallThePolice, +} + +/// This trait implementation turns our 'DoorState' into a type actually +/// representing a finite-state machine. To implement it, we need to do three +/// main things: +/// +/// * Define what our associated `Event` and `Action` type should be +/// +/// * Define the event-handling and state-entering logic (i.e. the meat of the +/// ... door) +/// +/// * Implement the interpretation of our actions, i.e. implement actual +/// side-effects +impl FSM for DoorState { + const FSM_NAME: &'static str = "door"; + + // As you might expect, our `Event` type is 'DoorEvent' and our `Action` + // type is 'DoorAction'. + type Event = DoorEvent; + type Action = DoorAction; + type State = (); + + // For error handling, the door simply uses `failure` which provides a + // generic, chainable error type. In real-world implementations you may want + // to use a custom error type or similar. + type Error = failure::Error; + + // The implementation of `handle` provides us with the actual transition + // logic of the door. + // + // The door is conceptually not that complicated so it is relatively short. + fn handle(self, event: DoorEvent) -> (Self, Vec<DoorAction>) { + match (self, event) { + // An opened door can be closed: + (DoorState::Opened, DoorEvent::Close) => return (DoorState::Closed, vec![]), + + // A closed door can be opened: + (DoorState::Closed, DoorEvent::Open) => return (DoorState::Opened, vec![]), + + // A closed door can also be locked, in which case the locking code + // is stored with the next state and the unlock attempts default to + // three: + (DoorState::Closed, DoorEvent::Lock(code)) => { + return (DoorState::Locked { code, attempts: 3 }, vec![]) + } + + // A locked door receiving an `Unlock`-event can do several + // different things ... + (DoorState::Locked { code, attempts }, DoorEvent::Unlock(unlock_code)) => { + // In the happy case, entry of a correct code leads to the door + // becoming unlocked (i.e. transitioning back to `Closed`). + if code == unlock_code { + return (DoorState::Closed, vec![]); + } + + // If the code wasn't correct and the fraudulent unlocker ran + // out of attempts (i.e. there was only one attempt remaining), + // it's time for some consequences. + if attempts == 1 { + return (DoorState::Disabled, vec![DoorAction::CallThePolice]); + } + + // If the code wasn't correct, but there are still some + // remaining attempts, the user doesn't have to face the police + // quite yet but IRC gets to laugh about it. + return ( + DoorState::Locked { + code, + attempts: attempts - 1, + }, + vec![DoorAction::NotifyIRC("invalid code entered".into())], + ); + } + + // This actually already concludes our event-handling logic. Our + // uncaring door does absolutely nothing if you attempt to do + // something with it that it doesn't support, so the last handler is + // a simple fallback. + // + // In a real-world state machine, especially one that receives + // events from external sources, you may want fallback handlers to + // actually do something. One example could be creating an action + // that logs information about unexpected events, alerts a + // monitoring service, or whatever else. + (current, _) => (current, vec![]), + } + } + + // The implementation of `enter` lets door states cause additional actions + // they are transitioned to. In the door example we use this only to notify + // IRC about what is going on. + fn enter(&self) -> Vec<DoorAction> { + let msg = match self { + DoorState::Opened => "door was opened", + DoorState::Closed => "door was closed", + DoorState::Locked { .. } => "door was locked", + DoorState::Disabled => "door was disabled", + }; + + vec![DoorAction::NotifyIRC(msg.into())] + } + + // The implementation of `act` lets us perform actual side-effects. + // + // Again, for the sake of educational simplicity, this does not deal with + // all potential (or in fact any) error cases that can occur during this toy + // implementation of actions. + // + // Additionally the `act` function can return new events. This is useful for + // a sort of "callback-like" pattern (cause an action to fetch some data, + // receive it as an event) but is not used in this example. + fn act(action: DoorAction, _state: &()) -> Result<Vec<DoorEvent>, failure::Error> { + match action { + DoorAction::NotifyIRC(msg) => { + use std::fs::OpenOptions; + use std::io::Write; + + let mut file = OpenOptions::new() + .append(true) + .create(true) + .open("/tmp/door-irc.log")?; + + write!(file, "<doorbot> {}\n", msg)?; + Ok(vec![]) + } + + DoorAction::CallThePolice => { + // TODO: call the police + println!("The police was called! For real!"); + Ok(vec![]) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use finito::advance; + + fn test_fsm<S: FSM>(initial: S, events: Vec<S::Event>) -> (S, Vec<S::Action>) { + events + .into_iter() + .fold((initial, vec![]), |(state, mut actions), event| { + let (new_state, mut new_actions) = advance(state, event); + actions.append(&mut new_actions); + (new_state, actions) + }) + } + + #[test] + fn test_door() { + let initial = DoorState::Opened; + let events = vec![ + DoorEvent::Close, + DoorEvent::Open, + DoorEvent::Close, + DoorEvent::Lock(1234), + DoorEvent::Unlock(1234), + DoorEvent::Lock(4567), + DoorEvent::Unlock(1234), + ]; + let (final_state, actions) = test_fsm(initial, events); + + assert_eq!(final_state, DoorState::Locked { + code: 4567, + attempts: 2 + }); + assert_eq!(actions, vec![ + DoorAction::NotifyIRC("door was closed".into()), + DoorAction::NotifyIRC("door was opened".into()), + DoorAction::NotifyIRC("door was closed".into()), + DoorAction::NotifyIRC("door was locked".into()), + DoorAction::NotifyIRC("door was closed".into()), + DoorAction::NotifyIRC("door was locked".into()), + DoorAction::NotifyIRC("invalid code entered".into()), + ]); + } +} diff --git a/users/tazjin/finito/finito-postgres/Cargo.toml b/users/tazjin/finito/finito-postgres/Cargo.toml new file mode 100644 index 000000000000..dd8d1d000304 --- /dev/null +++ b/users/tazjin/finito/finito-postgres/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "finito-postgres" +version = "0.1.0" +authors = ["Vincent Ambo <mail@tazj.in>"] + +[dependencies] +chrono = "0.4" +postgres-derive = "0.3" +serde = "1.0" +serde_json = "1.0" +r2d2_postgres = "0.14" + +[dependencies.postgres] +version = "0.15" +features = [ "with-uuid", "with-chrono", "with-serde_json" ] + +[dependencies.uuid] +version = "0.5" +features = [ "v4" ] + +[dependencies.finito] +path = "../finito-core" + +[dev-dependencies.finito-door] +path = "../finito-door" diff --git a/users/tazjin/finito/finito-postgres/migrations/2018-09-26-160621_bootstrap_finito_schema/down.sql b/users/tazjin/finito/finito-postgres/migrations/2018-09-26-160621_bootstrap_finito_schema/down.sql new file mode 100644 index 000000000000..9b56f9d35abe --- /dev/null +++ b/users/tazjin/finito/finito-postgres/migrations/2018-09-26-160621_bootstrap_finito_schema/down.sql @@ -0,0 +1,4 @@ +DROP TABLE actions; +DROP TYPE ActionStatus; +DROP TABLE events; +DROP TABLE machines; diff --git a/users/tazjin/finito/finito-postgres/migrations/2018-09-26-160621_bootstrap_finito_schema/up.sql b/users/tazjin/finito/finito-postgres/migrations/2018-09-26-160621_bootstrap_finito_schema/up.sql new file mode 100644 index 000000000000..18ace393b8d9 --- /dev/null +++ b/users/tazjin/finito/finito-postgres/migrations/2018-09-26-160621_bootstrap_finito_schema/up.sql @@ -0,0 +1,37 @@ +-- Creates the initial schema required by finito-postgres. + +CREATE TABLE machines ( + id UUID PRIMARY KEY, + created TIMESTAMPTZ NOT NULL DEFAULT NOW(), + fsm TEXT NOT NULL, + state JSONB NOT NULL +); + +CREATE TABLE events ( + id UUID PRIMARY KEY, + created TIMESTAMPTZ NOT NULL DEFAULT NOW(), + fsm TEXT NOT NULL, + fsm_id UUID NOT NULL REFERENCES machines(id), + event JSONB NOT NULL +); +CREATE INDEX idx_events_machines ON events(fsm_id); + +CREATE TYPE ActionStatus AS ENUM ( + 'Pending', + 'Completed', + 'Failed' +); + +CREATE TABLE actions ( + id UUID PRIMARY KEY, + created TIMESTAMPTZ NOT NULL DEFAULT NOW(), + fsm TEXT NOT NULL, + fsm_id UUID NOT NULL REFERENCES machines(id), + event_id UUID NOT NULL REFERENCES events(id), + content JSONB NOT NULL, + status ActionStatus NOT NULL, + error TEXT +); + +CREATE INDEX idx_actions_machines ON actions(fsm_id); +CREATE INDEX idx_actions_events ON actions(event_id); diff --git a/users/tazjin/finito/finito-postgres/src/error.rs b/users/tazjin/finito/finito-postgres/src/error.rs new file mode 100644 index 000000000000..ed33775cd70e --- /dev/null +++ b/users/tazjin/finito/finito-postgres/src/error.rs @@ -0,0 +1,103 @@ +//! This module defines error types and conversions for issue that can +//! occur while dealing with persisted state machines. + +use std::error::Error as StdError; +use std::{fmt, result}; +use uuid::Uuid; + +// errors to chain: +use postgres::Error as PgError; +use r2d2_postgres::r2d2::Error as PoolError; +use serde_json::Error as JsonError; + +pub type Result<T> = result::Result<T, Error>; + +#[derive(Debug)] +pub struct Error { + pub kind: ErrorKind, + pub context: Option<String>, +} + +#[derive(Debug)] +pub enum ErrorKind { + /// Errors occuring during JSON serialization of FSM types. + Serialization(String), + + /// Errors occuring during communication with the database. + Database(String), + + /// Errors with the database connection pool. + DBPool(String), + + /// State machine could not be found. + FSMNotFound(Uuid), + + /// Action could not be found. + ActionNotFound(Uuid), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use ErrorKind::*; + let msg = match &self.kind { + Serialization(err) => format!("JSON serialization error: {}", err), + + Database(err) => format!("PostgreSQL error: {}", err), + + DBPool(err) => format!("Database connection pool error: {}", err), + + FSMNotFound(id) => format!("FSM with ID {} not found", id), + + ActionNotFound(id) => format!("Action with ID {} not found", id), + }; + + match &self.context { + None => write!(f, "{}", msg), + Some(ctx) => write!(f, "{}: {}", ctx, msg), + } + } +} + +impl StdError for Error {} + +impl<E: Into<ErrorKind>> From<E> for Error { + fn from(err: E) -> Error { + Error { + kind: err.into(), + context: None, + } + } +} + +impl From<JsonError> for ErrorKind { + fn from(err: JsonError) -> ErrorKind { + ErrorKind::Serialization(err.to_string()) + } +} + +impl From<PgError> for ErrorKind { + fn from(err: PgError) -> ErrorKind { + ErrorKind::Database(err.to_string()) + } +} + +impl From<PoolError> for ErrorKind { + fn from(err: PoolError) -> ErrorKind { + ErrorKind::DBPool(err.to_string()) + } +} + +/// Helper trait that makes it possible to supply contextual +/// information with an error. +pub trait ResultExt<T> { + fn context<C: fmt::Display>(self, ctx: C) -> Result<T>; +} + +impl<T, E: Into<Error>> ResultExt<T> for result::Result<T, E> { + fn context<C: fmt::Display>(self, ctx: C) -> Result<T> { + self.map_err(|err| Error { + context: Some(format!("{}", ctx)), + ..err.into() + }) + } +} diff --git a/users/tazjin/finito/finito-postgres/src/lib.rs b/users/tazjin/finito/finito-postgres/src/lib.rs new file mode 100644 index 000000000000..ea63cc9dfdfd --- /dev/null +++ b/users/tazjin/finito/finito-postgres/src/lib.rs @@ -0,0 +1,456 @@ +//! PostgreSQL-backed persistence for Finito state machines +//! +//! This module implements ... TODO when I can write again. +//! +//! TODO: events & actions should have `SERIAL` keys + +#[macro_use] +extern crate postgres; +#[macro_use] +extern crate postgres_derive; + +extern crate chrono; +extern crate finito; +extern crate r2d2_postgres; +extern crate serde; +extern crate serde_json; +extern crate uuid; + +#[cfg(test)] +mod tests; +#[cfg(test)] +extern crate finito_door; + +mod error; +pub use error::{Error, ErrorKind, Result}; + +use chrono::prelude::{DateTime, Utc}; +use error::ResultExt; +use finito::{FSMBackend, FSM}; +use postgres::transaction::Transaction; +use postgres::GenericConnection; +use r2d2_postgres::{r2d2, PostgresConnectionManager}; +use serde::de::DeserializeOwned; +use serde::Serialize; +use serde_json::Value; +use std::marker::PhantomData; +use uuid::Uuid; + +type DBPool = r2d2::Pool<PostgresConnectionManager>; +type DBConn = r2d2::PooledConnection<PostgresConnectionManager>; + +/// This struct represents rows in the database table in which events +/// are persisted. +#[derive(Debug, ToSql, FromSql)] +struct EventT { + /// ID of the persisted event. + id: Uuid, + + /// Timestamp at which the event was stored. + created: DateTime<Utc>, + + /// Name of the type of FSM that this state belongs to. + fsm: String, + + /// ID of the state machine belonging to this event. + fsm_id: Uuid, + + /// Serialised content of the event. + event: Value, +} + +/// This enum represents the possible statuses an action can be in. +#[derive(Debug, PartialEq, ToSql, FromSql)] +#[postgres(name = "actionstatus")] +enum ActionStatus { + /// The action was requested but has not run yet. + Pending, + + /// The action completed successfully. + Completed, + + /// The action failed to run. Information about the error will + /// have been persisted in Postgres. + Failed, +} + +/// This struct represents rows in the database table in which actions +/// are persisted. +#[derive(Debug, ToSql, FromSql)] +struct ActionT { + /// ID of the persisted event. + id: Uuid, + + /// Timestamp at which the event was stored. + created: DateTime<Utc>, + + /// Name of the type of FSM that this state belongs to. + fsm: String, + + /// ID of the state machine belonging to this event. + fsm_id: Uuid, + + /// ID of the event that resulted in this action. + event_id: Uuid, + + /// Serialised content of the action. + #[postgres(name = "content")] // renamed because 'action' is a keyword in PG + action: Value, + + /// Current status of the action. + status: ActionStatus, + + /// Detailed (i.e. Debug-trait formatted) error message, if an + /// error occured during action processing. + error: Option<String>, +} + +// The following functions implement the public interface of +// `finito-postgres`. + +/// TODO: Write docs for this type, brain does not want to do it right +/// now. +pub struct FinitoPostgres<S> { + state: S, + + db_pool: DBPool, +} + +impl<S> FinitoPostgres<S> { + pub fn new(state: S, db_pool: DBPool, _pool_size: usize) -> Self { + FinitoPostgres { state, db_pool } + } +} + +impl<State: 'static> FSMBackend<State> for FinitoPostgres<State> { + type Key = Uuid; + type Error = Error; + + fn insert_machine<S: FSM + Serialize>(&self, initial: S) -> Result<Uuid> { + let query = r#" + INSERT INTO machines (id, fsm, state) + VALUES ($1, $2, $3) + "#; + + let id = Uuid::new_v4(); + let fsm = S::FSM_NAME.to_string(); + let state = serde_json::to_value(initial).context("failed to serialise FSM")?; + + self.conn()? + .execute(query, &[&id, &fsm, &state]) + .context("failed to insert FSM")?; + + return Ok(id); + } + + fn get_machine<S: FSM + DeserializeOwned>(&self, key: Uuid) -> Result<S> { + get_machine_internal(&*self.conn()?, key, false) + } + + /// Advance a persisted state machine by applying an event, and + /// storing the event as well as all resulting actions. + /// + /// This function holds a database-lock on the state's row while + /// advancing the machine. + /// + /// **Note**: This function returns the new state of the machine + /// immediately after applying the event, however this does not + /// necessarily equate to the state of the machine after all related + /// processing is finished as running actions may result in additional + /// transitions. + fn advance<'a, S>(&'a self, key: Uuid, event: S::Event) -> Result<S> + where + S: FSM + Serialize + DeserializeOwned, + S::State: From<&'a State>, + S::Event: Serialize + DeserializeOwned, + S::Action: Serialize + DeserializeOwned, + { + let conn = self.conn()?; + let tx = conn.transaction().context("could not begin transaction")?; + let state = get_machine_internal(&tx, key, true)?; + + // Advancing the FSM consumes the event, so it is persisted first: + let event_id = insert_event::<_, S>(&tx, key, &event)?; + + // Core advancing logic is run: + let (new_state, actions) = finito::advance(state, event); + + // Resulting actions are persisted (TODO: and interpreted) + let mut action_ids = vec![]; + for action in actions { + let action_id = insert_action::<_, S>(&tx, key, event_id, &action)?; + action_ids.push(action_id); + } + + // And finally the state is updated: + update_state(&tx, key, &new_state)?; + tx.commit().context("could not commit transaction")?; + + self.run_actions::<S>(key, action_ids); + + Ok(new_state) + } +} + +impl<State: 'static> FinitoPostgres<State> { + /// Execute several actions at the same time, each in a separate + /// thread. Note that actions returning further events, causing + /// further transitions, returning further actions and so on will + /// potentially cause multiple threads to get created. + fn run_actions<'a, S>(&'a self, fsm_id: Uuid, action_ids: Vec<Uuid>) + where + S: FSM + Serialize + DeserializeOwned, + S::Event: Serialize + DeserializeOwned, + S::Action: Serialize + DeserializeOwned, + S::State: From<&'a State>, + { + let state: S::State = (&self.state).into(); + let conn = self.conn().expect("TODO"); + + for action_id in action_ids { + let tx = conn.transaction().expect("TODO"); + + // TODO: Determine which concurrency setup we actually want. + if let Ok(events) = run_action(tx, action_id, &state, PhantomData::<S>) { + for event in events { + self.advance::<S>(fsm_id, event).expect("TODO"); + } + } + } + } + + /// Retrieve a single connection from the database connection pool. + fn conn(&self) -> Result<DBConn> { + self.db_pool + .get() + .context("failed to retrieve connection from pool") + } +} + +/// Insert a single state-machine into the database and return its +/// newly allocated, random UUID. +pub fn insert_machine<C, S>(conn: &C, initial: S) -> Result<Uuid> +where + C: GenericConnection, + S: FSM + Serialize, +{ + let query = r#" + INSERT INTO machines (id, fsm, state) + VALUES ($1, $2, $3) + "#; + + let id = Uuid::new_v4(); + let fsm = S::FSM_NAME.to_string(); + let state = serde_json::to_value(initial).context("failed to serialize FSM")?; + + conn.execute(query, &[&id, &fsm, &state])?; + + return Ok(id); +} + +/// Insert a single event into the database and return its UUID. +fn insert_event<C, S>(conn: &C, fsm_id: Uuid, event: &S::Event) -> Result<Uuid> +where + C: GenericConnection, + S: FSM, + S::Event: Serialize, +{ + let query = r#" + INSERT INTO events (id, fsm, fsm_id, event) + VALUES ($1, $2, $3, $4) + "#; + + let id = Uuid::new_v4(); + let fsm = S::FSM_NAME.to_string(); + let event_value = serde_json::to_value(event).context("failed to serialize event")?; + + conn.execute(query, &[&id, &fsm, &fsm_id, &event_value])?; + return Ok(id); +} + +/// Insert a single action into the database and return its UUID. +fn insert_action<C, S>(conn: &C, fsm_id: Uuid, event_id: Uuid, action: &S::Action) -> Result<Uuid> +where + C: GenericConnection, + S: FSM, + S::Action: Serialize, +{ + let query = r#" + INSERT INTO actions (id, fsm, fsm_id, event_id, content, status) + VALUES ($1, $2, $3, $4, $5, $6) + "#; + + let id = Uuid::new_v4(); + let fsm = S::FSM_NAME.to_string(); + let action_value = serde_json::to_value(action).context("failed to serialize action")?; + + conn.execute(query, &[ + &id, + &fsm, + &fsm_id, + &event_id, + &action_value, + &ActionStatus::Pending, + ])?; + + return Ok(id); +} + +/// Update the state of a specified machine. +fn update_state<C, S>(conn: &C, fsm_id: Uuid, state: &S) -> Result<()> +where + C: GenericConnection, + S: FSM + Serialize, +{ + let query = r#" + UPDATE machines SET state = $1 WHERE id = $2 + "#; + + let state_value = serde_json::to_value(state).context("failed to serialize FSM")?; + let res_count = conn.execute(query, &[&state_value, &fsm_id])?; + + if res_count != 1 { + Err(ErrorKind::FSMNotFound(fsm_id).into()) + } else { + Ok(()) + } +} + +/// Conditionally alter SQL statement to append locking clause inside +/// of a transaction. +fn alter_for_update(alter: bool, query: &str) -> String { + match alter { + false => query.to_string(), + true => format!("{} FOR UPDATE", query), + } +} + +/// Retrieve the current state of a state machine from the database, +/// optionally locking the machine state for the duration of some +/// enclosing transaction. +fn get_machine_internal<C, S>(conn: &C, id: Uuid, for_update: bool) -> Result<S> +where + C: GenericConnection, + S: FSM + DeserializeOwned, +{ + let query = alter_for_update( + for_update, + r#" + SELECT state FROM machines WHERE id = $1 + "#, + ); + + let rows = conn + .query(&query, &[&id]) + .context("failed to retrieve FSM")?; + + if let Some(row) = rows.into_iter().next() { + Ok(serde_json::from_value(row.get(0)).context("failed to deserialize FSM")?) + } else { + Err(ErrorKind::FSMNotFound(id).into()) + } +} + +/// Retrieve an action from the database, optionally locking it for +/// the duration of some enclosing transaction. +fn get_action<C, S>(conn: &C, id: Uuid) -> Result<(ActionStatus, S::Action)> +where + C: GenericConnection, + S: FSM, + S::Action: DeserializeOwned, +{ + let query = alter_for_update( + true, + r#" + SELECT status, content FROM actions + WHERE id = $1 AND fsm = $2 + "#, + ); + + let rows = conn.query(&query, &[&id, &S::FSM_NAME])?; + + if let Some(row) = rows.into_iter().next() { + let action = + serde_json::from_value(row.get(1)).context("failed to deserialize FSM action")?; + Ok((row.get(0), action)) + } else { + Err(ErrorKind::ActionNotFound(id).into()) + } +} + +/// Update the status of an action after an attempt to run it. +fn update_action_status<C, S>( + conn: &C, + id: Uuid, + status: ActionStatus, + error: Option<String>, + _fsm: PhantomData<S>, +) -> Result<()> +where + C: GenericConnection, + S: FSM, +{ + let query = r#" + UPDATE actions SET status = $1, error = $2 + WHERE id = $3 AND fsm = $4 + "#; + + let result = conn.execute(&query, &[&status, &error, &id, &S::FSM_NAME])?; + + if result != 1 { + Err(ErrorKind::ActionNotFound(id).into()) + } else { + Ok(()) + } +} + +/// Execute a single action in case it is pending or retryable. Holds +/// a lock on the action's database row while performing the action +/// and writes back the status afterwards. +/// +/// Should the execution of an action fail cleanly (i.e. without a +/// panic), the error will be persisted. Should it fail by panicking +/// (which developers should never do explicitly in action +/// interpreters) its status will not be changed. +fn run_action<S>( + tx: Transaction, + id: Uuid, + state: &S::State, + _fsm: PhantomData<S>, +) -> Result<Vec<S::Event>> +where + S: FSM, + S::Action: DeserializeOwned, +{ + let (status, action) = get_action::<Transaction, S>(&tx, id)?; + + let result = match status { + ActionStatus::Pending => { + match S::act(action, state) { + // If the action succeeded, update its status to + // completed and return the created events. + Ok(events) => { + update_action_status(&tx, id, ActionStatus::Completed, None, PhantomData::<S>)?; + events + } + + // If the action failed, persist the debug message and + // return nothing. + Err(err) => { + let msg = Some(format!("{:?}", err)); + update_action_status(&tx, id, ActionStatus::Failed, msg, PhantomData::<S>)?; + vec![] + } + } + } + + _ => { + // TODO: Currently only pending actions are run because + // retryable actions are not yet implemented. + vec![] + } + }; + + tx.commit().context("failed to commit transaction")?; + Ok(result) +} diff --git a/users/tazjin/finito/finito-postgres/src/tests.rs b/users/tazjin/finito/finito-postgres/src/tests.rs new file mode 100644 index 000000000000..dd270c38759f --- /dev/null +++ b/users/tazjin/finito/finito-postgres/src/tests.rs @@ -0,0 +1,54 @@ +use super::*; + +use finito_door::*; +use postgres::{Connection, TlsMode}; + +// TODO: read config from environment +fn open_test_connection() -> Connection { + Connection::connect("postgres://finito:finito@localhost/finito", TlsMode::None) + .expect("Failed to connect to test database") +} + +#[test] +fn test_insert_machine() { + let conn = open_test_connection(); + let initial = DoorState::Opened; + let door = insert_machine(&conn, initial).expect("Failed to insert door"); + let result = get_machine(&conn, &door, false).expect("Failed to fetch door"); + + assert_eq!( + result, + DoorState::Opened, + "Inserted door state should match" + ); +} + +#[test] +fn test_advance() { + let conn = open_test_connection(); + + let initial = DoorState::Opened; + let events = vec![ + DoorEvent::Close, + DoorEvent::Open, + DoorEvent::Close, + DoorEvent::Lock(1234), + DoorEvent::Unlock(1234), + DoorEvent::Lock(4567), + DoorEvent::Unlock(1234), + ]; + + let door = insert_machine(&conn, initial).expect("Failed to insert door"); + + for event in events { + advance(&conn, &door, event).expect("Failed to advance door FSM"); + } + + let result = get_machine(&conn, &door, false).expect("Failed to fetch door"); + let expected = DoorState::Locked { + code: 4567, + attempts: 2, + }; + + assert_eq!(result, expected, "Advanced door state should match"); +} diff --git a/users/tazjin/gruber-darker.qss b/users/tazjin/gruber-darker.qss new file mode 100644 index 000000000000..16f4c2f32991 --- /dev/null +++ b/users/tazjin/gruber-darker.qss @@ -0,0 +1,508 @@ +/** +** Gruber Darker theme for Quassel. +** +** This theme derives from multiple different things: +** +** - Quassel DarkSolarized (https://gist.github.com/Zren/e91ad5197f9d6b6d410f) +** - Quassel Dracula (https://github.com/dracula/quassel) +** - gruber-darker for Emacs (https://github.com/rexim/gruber-darker-theme) +** - Original Gruber theme for BBEdit (https://daringfireball.net/projects/bbcolors/schemes/) +** +** This is a work-in-progress as I haven't figured out the point of +** all of the colours yet, and what I want them to be instead. +** +**/ + +/** +** Helpful Links: +** - QT: +** http://qt-project.org/doc/qt-4.8/stylesheet-syntax.html +** http://doc.qt.nokia.com/4.7-snapshot/stylesheet-reference.html +** http://doc.qt.nokia.com/4.7-snapshot/stylesheet-examples.html +** - Plastique Client Style: +** https://qt.gitorious.org/qt/qt/source/src/gui/styles/qplastiquestyle.cpp +** https://github.com/mirror/qt/blob/4.8/src/gui/styles/qplastiquestyle.cpp +** - Quassel Stylesheet Gallery: +** http://bugs.quassel-irc.org/projects/1/wiki/Stylesheet_Gallery +** http://bugs.quassel-irc.org/projects/1/wiki/Stylesheet_Gallery#DarkMonokaiqss +*/ + +/** +** - QSS Notes: +** Quassel stylesheets also support Palette { role: color; } for setting the system +** palette. See the QPalette docs for available roles, and convert them into qss-style +** attributes, so ButtonText would become button-text or see qssparser.cpp In fact, +** qssparser.cpp is the authorative source for Quassel's qss syntax that contains all +** the extensions over standard Qt qss syntax. +** See: +** http://qt-project.org/doc/qt-4.8/qpalette.html#ColorRole-enum +** https://github.com/quassel/quassel/blob/master/src/uisupport/qssparser.cpp +** +*/ + +Palette { + /* Window colors */ + window: #282828; + background: #181818; + foreground: #f4f4f4; + + base: #181818; + alternate-base: #282828; + + /* Just setting palette(tooltip-base) doesn't work as intended so we set it in + ** a QTooltip{} rule as well. + */ + tooltip-base: #282a36; // palette(base) TODO + tooltip-text: white; // palette(text) TODO + + /* The following attributes should be done in a scale */ + light: #444444; // Tab Borders, Scrollbar handle grips, Titled Panel border (Settings) + midlight: #333333; // ? + button: #292929; // Menu BG, Scrollbar and Button base. + mid: #252525; // Titled Panel border (Settings) + dark: #202020; // TreeView [-] and ... color (Also various borders in Windows Client Style) + shadow: #1d1d1d; // ? + + + /* Text colors */ + text: white; + button-text: #f8f8f2; + + highlight: #44475a; + + /* Link colors */ + link: #ff79c6; + link-visited: #bd93f9; + + /* Color of the marker line in the chat view. BG Node that is overlayed on the first new ChatLine. */ + // 0 -> 0.1 (sharp line) + marker-line: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #586e75, stop: 0.1 #586e75, stop: 0.1 transparent); +} + +/* +** Base Object Colors +*/ + +/* Tables */ +// QTreeView#settingsTree -> Tree in the Settings popup. + +QTreeView, QTableView { + alternate-background-color: #282a36; + // background-color: palette(shadow); + border: 0px; +} + +QTreeView { + selection-background-color: transparent; +} + +QTreeView::item { + border-left: 2px solid palette(base); +} + +QTreeView::item:focus { + border-width: 0 0 0 2px; + outline: none; +} + +QTreeView::item:selected { + border-width: 0 0 0 2px; + color: palette(button-text); +} + +QTreeView::item:hover { + background: palette(dark); +} + + +QTreeView::item:selected:active{ + color: palette(button-text); + background: palette(dark); + border-color: palette(highlight); +} + +QTreeView::item:selected:!active { + color: palette(button-text); + background: palette(dark); + border-color: palette(highlight); +} + +/* Scrollbar */ +/* From Quassel Wiki: http://sprunge.us/iZGB */ +QScrollBar { + //background: transparent; + background: palette(base); + margin: 0; +} +QScrollBar:hover { + /* Optional: Subtle accent of scrolling area on hover */ + background: #161616; /* base +2 */ +} +QScrollBar:vertical { + width: 8px; +} +QScrollBar:horizontal { + height: 8px; +} + +QScrollBar::handle { + padding: 0; + margin: 2px; + border-radius: 2px; + border: 2px solid palette(midlight); + background: palette(midlight); +} + +QScrollBar::handle:vertical { + min-height: 20px; + min-width: 0px; +} + +QScrollBar::handle:horizontal { + min-width: 20px; + min-height: 0px; +} +QScrollBar::handle:hover { + border-color: palette(light); + background: palette(light); +} +QScrollBar::handle:pressed { + background: palette(highlight); + border-color: palette(highlight); +} + +QScrollBar::add-line , QScrollBar::sub-line { + height: 0px; + border: 0px; +} +QScrollBar::up-arrow, QScrollBar::down-arrow { + border: 0px; + width: 0px; + height: 0px; +} + +QScrollBar::add-page, QScrollBar::sub-page { + background: none; +} + +/* Input Box */ +MultiLineEdit { + //background: palette(base); + //color: palette(foreground); +} + +/* Widgets */ +/* http://doc.qt.nokia.com/4.7-snapshot/qdockwidget.html */ +//QMainWindow, +QMainWindow QAbstractScrollArea { + //border: 0; // Remove borders. + border: 1px solid palette(shadow); +} + +QMainWindow { + //background: palette(mid); // Main window trim +} + +/* Splitter */ +/* The splits between QDockWidgets and QMainWindow is a different element. */ +QSplitter::handle, +QMainWindow::separator { + background: palette(dark); +} +QSplitter::handle:horizontal:hover, +QMainWindow::separator:vertical:hover { + background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 palette(window), stop: 0.5 palette(light), stop: 1 palette(window)); +} + +QSplitter::handle:vertical:hover, +QMainWindow::separator:horizontal:hover { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 palette(window), stop: 0.5 palette(light), stop: 1 palette(window)); +} + +/* Menu Bar / Context Menues */ +QMenu { + margin: 5px; // A bit of nice padding around menu items. +} + +/* ToolTip */ +/* Note: You cannot create transparent sections in the popup box without a mask set. Thus the black edges outside the rounded borders. */ +QToolTip { + border: 2px solid #202020; // palette(dark) + border-radius: 2px; + background: #282a36; // palette(base) + color: white; // palette(text) +} + +/* Tabs */ +/* + The palette is designed for the selected one to be darker. So we need to change it. Decided to do a simple line. + tab:bottom and tab:top reverse y1 and y2 on the linear gradients. + + Tab Shadow: #444444 (light) + Tab Hover: #666 + Tab Selected: palette(highlight) +*/ + +QTabWidget::tab-bar { + alignment: center; +} + +QTabBar::tab { + min-width: 30px; + height: 20px; +} + +QTabBar::tab:bottom:selected { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 palette(highlight), stop: 0.2 palette(highlight), stop: 0.2 transparent); +} + +QTabBar::tab:top:selected { + background: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, stop: 0 palette(highlight), stop: 0.2 palette(highlight), stop: 0.2 transparent); +} + +QTabBar::tab:!selected { + color: #888; +} + +QTabBar::tab:bottom:!selected { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 palette(light), stop: 0.2 palette(light), stop: 0.2 transparent); +} + +QTabBar::tab:top:!selected { + background: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, stop: 0 palette(light), stop: 0.2 palette(light), stop: 0.2 transparent); +} + +QTabBar::tab:!selected:hover { + color: #aaa; +} + +QTabBar::tab:bottom:!selected:hover { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #666, stop: 0.2 #666, stop: 0.2 transparent); +} + +QTabBar::tab:top:!selected:hover { + background: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, stop: 0 #666, stop: 0.2 #666, stop: 0.2 transparent); +} + +/* +** Quassel CSS +*/ + +/* Main Chat Background Override */ +ChatView { + background: #181818; +} +ChatView QScrollBar { + background: #282a36; +} +ChatView QScrollBar:hover { + background: #282a36; +} + +ChatView QScrollBar::handle { + border-color: #44475a; + background: #44475a; +} + +ChatView QScrollBar::handle:hover { + border-color: #44475a; + background: #44475a; +} + +/**/ +QStatusBar {} +QStatusBar::item { + border: none; +} +QStatusBar QLabel { + color: #888; +} + +/* https://github.com/quassel/quassel/blob/master/src/qtui/ui/msgprocessorstatuswidget.ui */ +QStatusBar MsgProcessorStatusWidget {} +QStatusBar MsgProcessorStatusWidget QLabel#label {} +QStatusBar MsgProcessorStatusWidget QProgressBar#progressBar {} + +/* https://github.com/quassel/quassel/blob/master/src/qtui/ui/coreconnectionstatuswidget.ui */ +QStatusBar CoreConnectionStatusWidget {} +QStatusBar CoreConnectionStatusWidget QLabel#messageLabel {} +QStatusBar CoreConnectionStatusWidget QProgressBar#progressBar {} +QStatusBar CoreConnectionStatusWidget QLabel#lagLabel {} +QStatusBar CoreConnectionStatusWidget QLabel#sslLabel { + qproperty-pixmap: none; /* Hide the SSL status icon */ +} + + +/* Font */ +// Will not override if selectors are doubled up eg: "ChatLine, MultiLineEdit {}" +// These will override anything set in Quassel's Settings. +/** + * Don't bold or style MultiLineEdit text in any way otherwise you will be + * prone to get weird behaviour in submitting from the Input box. + * It will randomly bold your input if you do. + */ +ChatLine { + //font-family: "MingLiU_HKSCS-ExtB", "Courier New", Courier, Monotype; + + //font-size: 13pt; + //font-weight: bold; + } +MultiLineEdit { + //font-family: "MingLiU_HKSCS-ExtB", "Courier New", Courier, Monotype; + + //font-size: 20px; + //font-weight: normal; + } +ChatLine#plain { + //font-weight: bold; + } + +/* Font: UI Global Font */ +QWidget { + //font-family: consolas; + } +ChatListItem { + font-family: consolas; + } +NickListItem { + font-family: consolas; + } +StyledLabel#topicLabel { + font-family: consolas; + font-size: 14px; + } + + +/* Topic Box */ +StyledLabel#topicLabel { background: palette(base); font-family: consolas; } + +/* Buffer / Channel List */ +/** + state: inactive, channel-event, unread-message, highlighted + type: query, channel, network +**/ +ChatListItem { foreground: #f8f8f2; } +ChatListItem[state="inactive"] { foreground: #44475a; } +ChatListItem[state="channel-event"] { foreground: #6272a4; } /* palette(button-text) */ +ChatListItem[state="unread-message"] { foreground: #f8f8f2; } +ChatListItem[state="highlighted"] { foreground: #44475a; } + +ChatListItem[type="network", state="unread-message"] { foreground: #44475a; } +ChatListItem[type="network", state="highlighted"] { foreground: #44475a; } +ChatListItem[type="query", state="unread-message"] { foreground: #44475a; } + + +/* Nick List */ +/** + state: away + type: user, category +**/ +NickListItem[type="category"] { foreground: #6272a4; } +NickListItem[type="user"] { foreground: #f8f8f2 } +NickListItem[type="user", state="away"] { foreground: #44475a; } + + + +/* Chatbox Line Formatting */ +ChatLine[label="highlight"] { + foreground: #f5f5f5; + background: #282828; +} + +/* +** Option: Bold highlighted text, but not the timestamp. +*/ +/* +ChatLine[label="highlight"] { font-weight: bold; } +ChatLine::timestamp[label="highlight"]{ font-weight: normal; } +*/ + +ChatLine::timestamp[label="highlight"] { foreground: #44475a; } + +ChatLine::timestamp { } + +/* ::contents == Message */ +ChatLine::contents { + /* Can only set background */ +} + +ChatLine#plain { foreground: #f8f8f2; } +ChatLine#notice { foreground: #44475a; } +ChatLine#action { foreground: #565f73; font-style: italic; font-weight: bold; } +ChatLine#nick { foreground: #6272a4; } +ChatLine#mode { foreground: #6272a4; } +ChatLine#join { foreground: #6272a4; } +ChatLine#part { foreground: #6272a4; } +ChatLine#quit { foreground: #6272a4; } +ChatLine#kick { foreground: #6272a4; } +ChatLine#kill { foreground: #6272a4; } +ChatLine#server { foreground: #44475a; } +ChatLine#info { foreground: #44475a; } +ChatLine#error { foreground: #ff5555; } +ChatLine#daychange { foreground: #44475a; } +ChatLine#topic { foreground: #f1fa8c; } +ChatLine#netsplit-join { foreground: #44475a; } +ChatLine#netsplit-quit { foreground: #44475a; } + +ChatLine::timestamp { + foreground: #586e75; + // Resets the timestemp font during #action and other possible formatting. + font-style: normal; + font-weight: normal; +} + +ChatLine::url { + foreground: palette(link); + //font-style: underline; // Uncomment if you always want an underline on links. +} + +/* Sender Colors */ +ChatLine::sender#plain[sender="self"] { foreground: #586e75; } + +/** + * The following are the sixteen colours used for the senders. + * The names are calculated by taking the hash of the nickname. + * Then take the modulo (the remainder) when divided by 16. + * Preview: http://i.imgur.com/xeRKI4H.png + */ +ChatLine::sender#plain[sender="0"] { foreground: #96a6c8; } +ChatLine::sender#plain[sender="1"] { foreground: #73c936; } +ChatLine::sender#plain[sender="2"] { foreground: #ffdd33; } +ChatLine::sender#plain[sender="3"] { foreground: #cc8c3c; } +ChatLine::sender#plain[sender="4"] { foreground: #ff4f58; } +ChatLine::sender#plain[sender="5"] { foreground: #9e95c7; } +ChatLine::sender#plain[sender="6"] { foreground: #95a99f; } +ChatLine::sender#plain[sender="7"] { foreground: #8be9fd; } + +/* +32 */ +ChatLine::sender#plain[sender="8"] { foreground: #96a6c8; } +ChatLine::sender#plain[sender="9"] { foreground: #73c936; } +ChatLine::sender#plain[sender="a"] { foreground: #ffdd33; } +ChatLine::sender#plain[sender="b"] { foreground: #cc8c3c; } +ChatLine::sender#plain[sender="c"] { foreground: #ff4f58; } +ChatLine::sender#plain[sender="d"] { foreground: #9e95c7; } +ChatLine::sender#plain[sender="e"] { foreground: #95a99f; } +ChatLine::sender#plain[sender="f"] { foreground: #8be9fd; } + +/* +** mIRC formats +*/ +ChatLine[format="bold"] { font-weight: bold;} +ChatLine[format="italic"] { font-style: italic; } +ChatLine[format="underline"] { font-style: underline; } + +/* Blues are hard to read. */ +ChatLine[fg-color="2"] { foreground: #15a; } +ChatLine[bg-color="2"] { background: #15a; } +ChatLine[fg-color="c"] { foreground: #15f; } +ChatLine[bg-color="c"] { background: #15f; } + +/* +** Experimental +*/ +BufferViewDock[active=true] { + /* The circle is hardcoded into the title. */ + /* Color only changes on a refresh (F5) (so it's pointless). */ + /* Also colors the border in Breeze. */ + //color: palette(highlight); +} diff --git a/users/tazjin/hanebuschtag.txt b/users/tazjin/hanebuschtag.txt new file mode 100644 index 000000000000..daeb41c9aabb --- /dev/null +++ b/users/tazjin/hanebuschtag.txt @@ -0,0 +1,66 @@ +bazurschnaburkini +buchweizengrรผtze +burkischnurkischnurzelwutz +burwurgurken +burwurka +gaschnurzel +gezwurkel +grunzelgewunzel +gurzelschnurzelgurke +hanemazurka +hanemazurkelgurkel +haneschlawitzka +haneschnaburkeln +haneschnawurkagurka +haneschnawurkel +haneschnuren +haneschnurkissima +hanewurka +hanewurkini +hanewurzeln +ronzelschlawonzel +ronzelwonzel +schabernackel +schabernackensteak +schlagurkelwini +schlaraffenwurburzel +schlawiburschnurschlakini +schlawonzel +schlawurkinischnagurka +schlawurzelgegurkel +schlawurzeltrollurzel +schlunzelgarfunzel +schmonzelgafonzel +schmotzrotzel +schnaburka +schnaburkel +schnaburkini +schnackel +schnarkelbarkel +schnarwurzelka +schnawurkeln +schnawurzelgackschnurschnacksschnicks +schnawurzini +schniepel +schnirkelschini +schnรถckel +schnockelgockel +schnorchel +schnรถrk +schnorkelbusch +schnรถrkelknรถrkel +schnorkelorgel +schnรถrks +schnotzelgekrotzel +schnudelwurkini +schnurburka +schnurkini +schnurkinihanfini +schnurzelgawurzel +schnurzelwurzelwutz +schnurzelwutz +strazurkeln +wazurka +wurkelgurkel +wurkelschnurrini +wurzelchakramahurka diff --git a/users/tazjin/home/shared.nix b/users/tazjin/home/shared.nix new file mode 100644 index 000000000000..03c7ee10b796 --- /dev/null +++ b/users/tazjin/home/shared.nix @@ -0,0 +1,87 @@ +# Shared home configuration for all machines. + +{ depot, pkgs, ... }: # readTree +{ config, lib, ... }: # home-manager + +{ + imports = [ "${depot.third_party.impermanence}/home-manager.nix" ]; + + home.persistence."/persist/tazjin/home" = { + allowOther = true; + + directories = [ + ".cargo" + ".config/audacity" + ".config/google-chrome" + ".config/quassel-irc.org" + ".config/unity3d" + ".electrum" + ".gnupg" + ".local/share/audacity" + ".local/share/direnv" + ".local/share/fish" + ".local/share/keyrings" + ".local/share/zoxide" + ".mozilla/firefox" + ".password-store" + ".rustup" + ".ssh" + ".steam" + ".telega" + "go" + "mail" + ]; + + files = [ + ".notmuch-config" + ]; + }; + + home.activation.screenshots = lib.hm.dag.entryAnywhere '' + $DRY_RUN_CMD mkdir -p $HOME/screenshots + ''; + + programs.git = { + enable = true; + userName = "Vincent Ambo"; + userEmail = "mail@tazj.in"; + extraConfig = { + pull.rebase = true; + init.defaultBranch = "canon"; + safe.directory = [ "/depot" ]; + }; + }; + + programs.fish = { + enable = true; + interactiveShellInit = '' + ${pkgs.zoxide}/bin/zoxide init fish | source + ''; + }; + + services.screen-locker = { + enable = true; + enableDetectSleep = true; + inactiveInterval = 10; # minutes + lockCmd = "${depot.users.tazjin.screenLock}/bin/tazjin-screen-lock"; + }; + + services.picom = { + enable = true; + vSync = true; + backend = "glx"; + }; + + # Enable the dunst notification daemon, but force the + # configuration file separately instead of going via the strange + # Nix->dunstrc encoding route. + services.dunst.enable = true; + xdg.configFile."dunst/dunstrc" = { + source = depot.users.tazjin.dotfiles.dunstrc; + onChange = '' + ${pkgs.procps}/bin/pkill -u "$USER" ''${VERBOSE+-e} dunst || true + ''; + }; + + systemd.user.startServices = true; +} diff --git a/users/tazjin/home/tverskoy.nix b/users/tazjin/home/tverskoy.nix new file mode 100644 index 000000000000..d72734920ea6 --- /dev/null +++ b/users/tazjin/home/tverskoy.nix @@ -0,0 +1,17 @@ +# Home manage configuration for tverskoy. + +{ depot, pkgs, ... }: # readTree +{ config, lib, ... }: # home-manager + +{ + imports = [ + depot.users.tazjin.home.shared + ]; + + home.persistence."/persist/tazjin/home" = { + directories = [ + ".config/spotify" + ".local/share/Steam" + ]; + }; +} diff --git a/users/tazjin/home/zamalek.nix b/users/tazjin/home/zamalek.nix new file mode 100644 index 000000000000..6bac67eb1c4d --- /dev/null +++ b/users/tazjin/home/zamalek.nix @@ -0,0 +1,10 @@ +# Home manage configuration for zamalek. + +{ depot, pkgs, ... }: # readTree +{ config, lib, ... }: # home-manager + +{ + imports = [ + depot.users.tazjin.home.shared + ]; +} diff --git a/users/tazjin/homepage/default.nix b/users/tazjin/homepage/default.nix new file mode 100644 index 000000000000..0edb75d60933 --- /dev/null +++ b/users/tazjin/homepage/default.nix @@ -0,0 +1,78 @@ +# Assembles the website index and configures an nginx instance to +# serve it. +# +# The website is made up of a simple header&footer and content +# elements for things such as blog posts and projects. +# +# Content for the blog is in //users/tazjin/blog instead of here. +{ depot, lib, pkgs, ... }@args: + +with depot; +with nix.yants; + +let + inherit (builtins) readFile replaceStrings sort; + inherit (pkgs) writeFile runCommandNoCC; + + # The different types of entries on the homepage. + entryClass = enum "entryClass" [ "blog" "project" "misc" ]; + + # The definition of a single entry. + entry = struct "entry" { + class = entryClass; + title = string; + url = string; + date = int; # epoch + description = option string; + }; + + escape = replaceStrings [ "<" ">" "&" "'" ] [ "<" ">" "&" "'" ]; + + postToEntry = defun [ web.blog.post entry ] (post: { + class = "blog"; + title = post.title; + url = "/blog/${post.key}"; + date = post.date; + }); + + formatDate = defun [ int string ] (date: readFile (runCommandNoCC "date" { } '' + date --date='@${toString date}' '+%Y-%m-%d' > $out + '')); + + formatEntryDate = defun [ entry string ] (entry: entryClass.match entry.class { + blog = "Blog post from ${formatDate entry.date}"; + project = "Project from ${formatDate entry.date}"; + misc = "Posted on ${formatDate entry.date}"; + }); + + entryToDiv = defun [ entry string ] (entry: '' + <a href="${entry.url}" class="entry ${entry.class}"> + <div> + <p class="entry-title">${escape entry.title}</p> + ${ + lib.optionalString ((entry ? description) && (entry.description != null)) + "<p class=\"entry-description\">${escape entry.description}</p>" + } + <p class="entry-date">${formatEntryDate entry}</p> + </div> + </a> + ''); + + index = entries: pkgs.writeText "index.html" (lib.concatStrings ( + [ (builtins.readFile ./header.html) ] + ++ (map entryToDiv (sort (a: b: a.date > b.date) entries)) + ++ [ (builtins.readFile ./footer.html) ] + )); + + pageEntries = import ./entries.nix; + homepage = index ((map postToEntry users.tazjin.blog.posts) ++ pageEntries); + atomFeed = import ./feed.nix (args // { inherit entry pageEntries; }); +in +runCommandNoCC "website" { } '' + mkdir $out + cp ${homepage} $out/index.html + cp ${atomFeed} $out/feed.atom + mkdir $out/static + cp -r ${depot.web.static}/* $out/static + cp -rf ${./static}/* $out/static +'' diff --git a/users/tazjin/homepage/entries.nix b/users/tazjin/homepage/entries.nix new file mode 100644 index 000000000000..9e43516e5358 --- /dev/null +++ b/users/tazjin/homepage/entries.nix @@ -0,0 +1,103 @@ +[ + { + class = "misc"; + title = "@tazlog on Telegram"; + url = "https://t.me/tazlog"; + date = 1643321164; + description = '' + My new channel on Telegram, for occasional updates smaller (and + more frequent) than what ends up being posted here. + ''; + } + { + class = "project"; + title = "Ship It! #37"; + url = "https://changelog.com/shipit/37"; + date = 1641819600; + description = '' + Episode #37 of Ship It!, a podcast about systems, featuring me. + We talk about TVL, Nix, monorepos and related things. + ''; + } + { + class = "project"; + title = "Tvix"; + url = "https://tvl.fyi/blog/rewriting-nix"; + date = 1638381387; + description = '' + TVL is rewriting Nix with funding from NLNet. + ''; + } + { + class = "misc"; + title = "Interview with Joscha Bach"; + url = "https://www.youtube.com/watch?v=P-2P3MSZrBM"; + date = 1594594800; + description = '' + A fascinating, mind-bending interview by Lex Fridman with Joscha + Bach about the Nature of the Universe. + ''; + } + { + class = "misc"; + title = "The Virus Lounge"; + url = "https://tvl.fyi"; + date = 1587435629; + description = "A community around Nix, monorepos, build tooling and the like!"; + } + { + class = "project"; + title = "depot"; + url = "https://code.tvl.fyi/about"; + date = 1576800000; + description = "Merging all of my projects into a single, Nix-based monorepo"; + } + { + class = "project"; + title = "Nixery"; + url = "https://github.com/google/nixery"; + date = 1565132400; + description = "A Nix-backed container registry that builds container images on demand"; + } + { + class = "project"; + title = "kontemplate"; + url = "https://code.tvl.fyi/about/ops/kontemplate"; + date = 1486550940; + description = "Simple file templating tool built for Kubernetes resources"; + } + { + class = "misc"; + title = "dottime"; + url = "https://dotti.me/"; + date = 1560898800; + description = "A universal convention for conveying time (by edef <3)"; + } + { + class = "project"; + title = "journaldriver"; + url = "https://code.tvl.fyi/about/ops/journaldriver"; + date = 1527375600; + description = "Small daemon to forward logs from journald to Stackdriver Logging"; + } + { + class = "misc"; + title = "Principia Discordia"; + url = "https://principiadiscordia.com/book/1.php"; + date = 1495494000; + description = '' + The Principia is a short book I read as a child, and didn't + understand until much later. It shaped much of my world view. + ''; + } + { + class = "misc"; + title = "This Week in Virology"; + url = "http://www.microbe.tv/twiv/"; + date = 1585517557; + description = '' + Podcast with high-quality information about virology, + epidemiology and so on. Highly relevant to COVID19. + ''; + } +] diff --git a/users/tazjin/homepage/feed.nix b/users/tazjin/homepage/feed.nix new file mode 100644 index 000000000000..09bc36341401 --- /dev/null +++ b/users/tazjin/homepage/feed.nix @@ -0,0 +1,43 @@ +# Creates the Atom feed for my homepage. +{ depot, lib, pkgs, entry, pageEntries, ... }: + +with depot.nix.yants; + +let + inherit (builtins) map readFile; + inherit (lib) max singleton; + inherit (pkgs) writeText; + inherit (depot.web) blog atom-feed; + + pageEntryToEntry = defun [ entry atom-feed.entry ] (e: { + id = "tazjin:${e.class}:${toString e.date}"; + updated = e.date; + published = e.date; + title = e.title; + summary = e.description; + + links = singleton { + rel = "alternate"; + href = e.url; + }; + }); + + allEntries = (with depot.users.tazjin.blog; map (blog.toFeedEntry config) posts) + ++ (map pageEntryToEntry pageEntries); + + feed = { + id = "https://tazj.in/"; + title = "tazjin's interblag"; + subtitle = "my posts, projects and other interesting things"; + rights = "ยฉ 2020 tazjin"; + authors = [ "tazjin" ]; + + links = singleton { + rel = "self"; + href = "https://tazjin/feed.atom"; + }; + + entries = allEntries; + }; +in +writeText "feed.atom" (atom-feed.renderFeed feed) diff --git a/users/tazjin/homepage/footer.html b/users/tazjin/homepage/footer.html new file mode 100644 index 000000000000..2f17135066e8 --- /dev/null +++ b/users/tazjin/homepage/footer.html @@ -0,0 +1,2 @@ + </div> +</body> diff --git a/users/tazjin/homepage/header.html b/users/tazjin/homepage/header.html new file mode 100644 index 000000000000..5a22d9eb7b45 --- /dev/null +++ b/users/tazjin/homepage/header.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<head><meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="description" content="tazjin's blog"> + <link rel="stylesheet" type="text/css" href="static/tvl.css" media="all"> + <link rel="icon" type="image/webp" href="/static/favicon.webp"> + <link rel="alternate" type="application/atom+xml" href="/feed.atom"> + <title>tazjin's interblag</title> +</head> +<body class="dark"> + <header> + <h1> + <a class="interblag-title" href="/">tazjin's interblag</a> + </h1> + <hr> + </header> + <div class="introduction"> + <p>Hi, I'm tazjin.</p> + <p> + I spend a lot of my time hacking on the + <a class="dark-link" href="https://tvl.fyi">TVL</a> monorepo and + doing other computer-related things. Follow me + on <a class="dark-link" href="https://t.me/tazlog">Telegram</a>, + via the feed here or (occasionally) catch me in-person + at <a href="https://undef.club/#about" class="dark-link"> + undef.club</a>. + </p> + <p> + Below is a collection of + my <span class="project">projects</span>, <span class="blog">blog + posts</span> and some <span class="misc">random things</span> by + me or others. If you'd like to get in touch about anything, send + me a mail at mail@[this domain] or ping me on IRC. + </p> + </div> + <div class="entry-container"> diff --git a/users/tazjin/homepage/static/favicon.webp b/users/tazjin/homepage/static/favicon.webp new file mode 100644 index 000000000000..f99c9085340b --- /dev/null +++ b/users/tazjin/homepage/static/favicon.webp Binary files differdiff --git a/users/tazjin/homepage/static/img/nixery/dominator.webp b/users/tazjin/homepage/static/img/nixery/dominator.webp new file mode 100644 index 000000000000..2d8569a6ca21 --- /dev/null +++ b/users/tazjin/homepage/static/img/nixery/dominator.webp Binary files differdiff --git a/users/tazjin/homepage/static/img/nixery/example_extra.webp b/users/tazjin/homepage/static/img/nixery/example_extra.webp new file mode 100644 index 000000000000..101f0f633aef --- /dev/null +++ b/users/tazjin/homepage/static/img/nixery/example_extra.webp Binary files differdiff --git a/users/tazjin/homepage/static/img/nixery/example_plain.webp b/users/tazjin/homepage/static/img/nixery/example_plain.webp new file mode 100644 index 000000000000..a2b90b3e21d5 --- /dev/null +++ b/users/tazjin/homepage/static/img/nixery/example_plain.webp Binary files differdiff --git a/users/tazjin/homepage/static/img/nixery/ideal_layout.webp b/users/tazjin/homepage/static/img/nixery/ideal_layout.webp new file mode 100644 index 000000000000..0e9f74556682 --- /dev/null +++ b/users/tazjin/homepage/static/img/nixery/ideal_layout.webp Binary files differdiff --git a/users/tazjin/homepage/static/img/watchblob_1.webp b/users/tazjin/homepage/static/img/watchblob_1.webp new file mode 100644 index 000000000000..27e588e1a145 --- /dev/null +++ b/users/tazjin/homepage/static/img/watchblob_1.webp Binary files differdiff --git a/users/tazjin/homepage/static/img/watchblob_2.webp b/users/tazjin/homepage/static/img/watchblob_2.webp new file mode 100644 index 000000000000..b2dea98b4fb4 --- /dev/null +++ b/users/tazjin/homepage/static/img/watchblob_2.webp Binary files differdiff --git a/users/tazjin/homepage/static/img/watchblob_3.webp b/users/tazjin/homepage/static/img/watchblob_3.webp new file mode 100644 index 000000000000..99b49373b5b4 --- /dev/null +++ b/users/tazjin/homepage/static/img/watchblob_3.webp Binary files differdiff --git a/users/tazjin/homepage/static/img/watchblob_4.webp b/users/tazjin/homepage/static/img/watchblob_4.webp new file mode 100644 index 000000000000..41dbdb6be1cf --- /dev/null +++ b/users/tazjin/homepage/static/img/watchblob_4.webp Binary files differdiff --git a/users/tazjin/homepage/static/img/watchblob_5.webp b/users/tazjin/homepage/static/img/watchblob_5.webp new file mode 100644 index 000000000000..c42a4ce1bc0f --- /dev/null +++ b/users/tazjin/homepage/static/img/watchblob_5.webp Binary files differdiff --git a/users/tazjin/homepage/static/img/watchblob_6.webp b/users/tazjin/homepage/static/img/watchblob_6.webp new file mode 100644 index 000000000000..1440761859dd --- /dev/null +++ b/users/tazjin/homepage/static/img/watchblob_6.webp Binary files differdiff --git a/users/tazjin/keys/default.nix b/users/tazjin/keys/default.nix new file mode 100644 index 000000000000..1212c37d36cf --- /dev/null +++ b/users/tazjin/keys/default.nix @@ -0,0 +1,11 @@ +# My SSH public keys +{ ... }: + +let withAll = keys: keys // { all = builtins.attrValues keys; }; +in withAll { + # frog = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKMZzRdcrHTuCPoaFy36MPr5IW/hnImlse/OBOn6udL/ tazjin@frog"; + tverskoy = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBAWvA3RpXpMAqruUbB+eVgvvHCzhs5R9khFRza3YSLeFiIqOxVVgyhzW/BnCSD9t/5JrqRdJIGQLnkQU9m4REhUAAAAEc3NoOg== tazjin@tverskoy"; + tverskoy_ed25519 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM1fGWz/gsq+ZeZXjvUrV+pBlanw1c3zJ9kLTax9FWQy tazjin@tverskoy"; + unihertz_sk = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMgLwumRAp7u9sgaFR5aneNbQo07ZXXAFWV45pwzUiP2 unihertz-sk"; + zamalek_sk = "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIOAw3OaPAjnC6hArGYEmBoXhPf7aZdRGlDZcSqm6gbB8AAAABHNzaDo= tazjin@zamalek"; +} diff --git a/users/tazjin/nisp/transform.el b/users/tazjin/nisp/transform.el new file mode 100644 index 000000000000..89b2bb104d27 --- /dev/null +++ b/users/tazjin/nisp/transform.el @@ -0,0 +1,137 @@ +;; Nix as a Lisp + +(require 'cl-lib) +(require 'json) +(require 's) +(require 'dash) + +(defun nisp/expr (form) + "Entrypoint for Nisp->Nix transformation. Will translate FORM +into Nix code, if it is a valid Nisp expression. + +To make code generation slightly easier, each +expression (including literals) is wrapped in an extra pair of +parens." + (concat + "(" + (pcase form + ;; Special keywords + ('() "null") + (`(let . ,rest) (nisp/let form)) + (`(fn . ,rest) (nisp/fn form)) + (`(if ,cond ,then ,else) (nisp/if cond then else)) + + ;; Nix operators & builtins that need special handling + (`(or ,lhs ,rhs) (nisp/infix "||" lhs rhs)) + (`(and ,lhs ,rhs) (nisp/infix "&&" lhs rhs)) + (`(> ,lhs ,rhs) (nisp/infix ">" lhs rhs)) + (`(< ,lhs ,rhs) (nisp/infix "<" lhs rhs)) + (`(>= ,lhs ,rhs) (nisp/infix ">=" lhs rhs)) + (`(<= ,lhs ,rhs) (nisp/infix "<=" lhs rhs)) + (`(+ ,lhs ,rhs) (nisp/infix "+" lhs rhs)) + (`(- ,lhs ,rhs) (nisp/infix "-" lhs rhs)) + (`(* ,lhs ,rhs) (nisp/infix "*" lhs rhs)) + (`(/ ,lhs ,rhs) (nisp/infix "/" lhs rhs)) + (`(-> ,lhs ,rhs) (nisp/infix "->" lhs rhs)) + (`(? ,lhs ,rhs) (nisp/infix "?" lhs rhs)) + (`(// ,lhs ,rhs) (nisp/infix "//" lhs rhs)) + (`(++ ,lhs ,rhs) (nisp/infix "++" lhs rhs)) + (`(== ,lhs ,rhs) (nisp/infix "==" lhs rhs)) + (`(!= ,lhs ,rhs) (nisp/infix "!=" lhs rhs)) + (`(! ,term) (concat "!" (nisp/expr term))) + (`(- ,term) (concat "-" (nisp/expr term))) + + ;; Attribute sets + (`(attrs . ,rest) (nisp/attribute-set form)) + + ;; Function calls + ((and `(,func . ,args) + (guard (symbolp func))) + (nisp/funcall func args)) + + ;; Primitives + ((pred stringp) (json-encode-string form)) + ((pred numberp) (json-encode-number form)) + ((pred keywordp) (substring (symbol-name form) 1)) + ((pred symbolp) (symbol-name form)) + + ;; Lists + ((pred arrayp) (nisp/list form)) + + (other (error "Encountered unhandled form: %s" other))) + ")")) + +(defun nisp/infix (op lhs rhs) + (concat (nisp/expr lhs) " " op " " (nisp/expr rhs))) + +(defun nisp/funcall (func args) + (concat (symbol-name func) " " (s-join " " (-map #'nisp/expr args)))) + +(defun nisp/let (form) + (pcase form + (`(let . (,bindings . (,body . ()))) (concat "let " + (nisp/let bindings) + (nisp/expr body))) + (`((:inherit . ,inherits) . ,rest) (concat (nisp/inherit (car form)) + " " + (nisp/let rest))) + (`((,name . (,value . ())) .,rest) (concat (symbol-name name) " = " + (nisp/expr value) "; " + (nisp/let rest))) + ('() "in ") + (other (error "malformed form '%s' in let expression" other)))) + +(defun nisp/inherit (form) + (pcase form + (`(:inherit . ,rest) (concat "inherit " (nisp/inherit rest))) + (`((,source) . ,rest) (concat "(" (symbol-name source) ") " (nisp/inherit rest))) + (`(,item . ,rest) (concat (symbol-name item) " " (nisp/inherit rest))) + ('() ";"))) + +(defun nisp/if (cond then else) + (concat "if " (nisp/expr cond) + " then " (nisp/expr then) + " else " (nisp/expr else))) + +(defun nisp/list (form) + (cl-check-type form array) + (concat "[ " + (mapconcat #'nisp/expr form " ") + "]")) + + +(defun nisp/attribute-set (form) + "Attribute sets have spooky special handling because they are +not supported by the reader." + (pcase form + (`(attrs . ,rest) (concat "{ " (nisp/attribute-set rest))) + ((and `(,name . (,value . ,rest)) + (guard (keywordp name))) + (concat (substring (symbol-name name) 1) " = " + (nisp/expr value) "; " + (nisp/attribute-set rest))) + ('() "}"))) + +(defun nisp/fn (form) + (pcase form + (`(fn ,args ,body) (concat + (cl-loop for arg in args + concat (format "%s: " arg)) + (nisp/expr body))))) + +;; The following functions are not part of the transform. + +(defun nisp/eval (form) + (interactive "sExpression: ") + (when (stringp form) + (setq form (read form))) + + (message + ;; TODO(tazjin): Construct argv manually to avoid quoting issues. + (s-chomp + (shell-command-to-string + (concat "nix-instantiate --eval -E '" (nisp/expr form) "'"))))) + +(defun nisp/eval-last-sexp () + (interactive) + (nisp/eval (edebug-last-sexp))) diff --git a/users/tazjin/nix.svg b/users/tazjin/nix.svg new file mode 100644 index 000000000000..4da795a4362b --- /dev/null +++ b/users/tazjin/nix.svg @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns="http://www.w3.org/2000/svg" + width="141.5919mm" + height="122.80626mm" + viewBox="0 0 501.70361 435.14028" + id="svg2" + version="1.1"> + <g transform="translate(-156.33871,933.1905)" visibility="hidden"> + <path + id="lambda-path" + d="m 309.54892,-710.38827 122.19683,211.67512 -56.15706,0.5268 -32.6236,-56.8692 -32.85645,56.5653 -27.90237,-0.011 -14.29086,-24.6896 46.81047,-80.4901 -33.22946,-57.8257 z" + /> + <use + id="lambda-1" + href="#lambda-path" + visibility="visible" + transform="rotate(180,407.41868,-715.7565)" + fill="#f8f8ff" /> + <use + id="lambda-2" + visibility="visible" + transform="rotate(-120,407.28823,-715.86995)" + href="#lambda-path" + fill="#0039a6" /> + <use + id="lambda-3" + transform="rotate(-60,407.31177,-715.70016)" + href="#lambda-path" + visibility="visible" + fill="#d52b1e" /> + <use + id="lambda-4" + href="#lambda-path" + visibility="visible" + fill="#f8f8ff" /> + <use + id="lambda-5" + transform="rotate(60,407.11155,-715.78724)" + href="#lambda-path" + visibility="visible" + fill="#0039a6" /> + <use + transform="rotate(120,407.33916,-716.08356)" + id="lambda-6" + href="#lambda-path" + visibility="visible" + fill="#d52b1e" /> + </g> +</svg> diff --git a/users/tazjin/nixos/.gitignore b/users/tazjin/nixos/.gitignore new file mode 100644 index 000000000000..212d3ad270f4 --- /dev/null +++ b/users/tazjin/nixos/.gitignore @@ -0,0 +1 @@ +local-config.nix diff --git a/users/tazjin/nixos/README.md b/users/tazjin/nixos/README.md new file mode 100644 index 000000000000..662f2a36acac --- /dev/null +++ b/users/tazjin/nixos/README.md @@ -0,0 +1,17 @@ +NixOS configuration +=================== + +My NixOS configurations! It configures most of the packages I require +on my systems, sets up Emacs the way I need and does a bunch of other +interesting things. + +System configuration lives in folders, and some of the modules stem +from `//ops/modules`. + +Machines are deployed with the script at `ops.nixos.rebuild-system`. + +## Configured hosts: + +* `tverskoy` - X13 AMD that's travelling around with me +* `frog` - weapon of mass computation (in storage in London) +* `camden` - NUC formerly serving tazj.in (in storage in London) diff --git a/users/tazjin/nixos/camden/default.nix b/users/tazjin/nixos/camden/default.nix new file mode 100644 index 000000000000..6568d6341e1b --- /dev/null +++ b/users/tazjin/nixos/camden/default.nix @@ -0,0 +1,346 @@ +# This file configures camden.tazj.in, my homeserver. +{ depot, pkgs, lib, ... }: + +config: +let + nginxRedirect = { from, to, acmeHost }: { + serverName = from; + useACMEHost = acmeHost; + forceSSL = true; + + extraConfig = "return 301 https://${to}$request_uri;"; + }; + mod = name: depot.path.origSrc + ("/ops/modules/" + name); +in +lib.fix (self: { + imports = [ + (mod "quassel.nix") + (mod "smtprelay.nix") + ]; + + # camden is intended to boot unattended, despite having an encrypted + # root partition. + # + # The below configuration uses an externally connected USB drive + # that contains a LUKS key file to unlock the disk automatically at + # boot. + # + # TODO(tazjin): Configure LUKS unlocking via SSH instead. + boot = { + initrd = { + availableKernelModules = [ + "ahci" + "xhci_pci" + "usbhid" + "usb_storage" + "sd_mod" + "sdhci_pci" + "rtsx_usb_sdmmc" + "r8169" + ]; + + kernelModules = [ "dm-snapshot" ]; + + luks.devices.camden-crypt = { + fallbackToPassword = true; + device = "/dev/disk/by-label/camden-crypt"; + keyFile = "/dev/sdb"; + keyFileSize = 4096; + }; + }; + + loader = { + systemd-boot.enable = true; + efi.canTouchEfiVariables = true; + }; + + cleanTmpDir = true; + }; + + fileSystems = { + "/" = { + device = "/dev/disk/by-label/camden-root"; + fsType = "ext4"; + }; + + "/home" = { + device = "/dev/disk/by-label/camden-home"; + fsType = "ext4"; + }; + + "/boot" = { + device = "/dev/disk/by-label/BOOT"; + fsType = "vfat"; + }; + }; + + nix = { + maxJobs = lib.mkDefault 4; + + trustedUsers = [ "root" "tazjin" ]; + + binaryCaches = [ + "https://tazjin.cachix.org" + ]; + + binaryCachePublicKeys = [ + "tazjin.cachix.org-1:IZkgLeqfOr1kAZjypItHMg1NoBjm4zX9Zzep8oRSh7U=" + ]; + }; + + powerManagement.cpuFreqGovernor = lib.mkDefault "powersave"; + + networking = { + hostName = "camden"; + interfaces.enp1s0.useDHCP = true; + interfaces.enp1s0.ipv6.addresses = [ + { + address = "2a01:4b00:821a:ce02::5"; + prefixLength = 64; + } + ]; + + firewall.enable = false; + }; + + time.timeZone = "UTC"; + + # System-wide application setup + programs.fish.enable = true; + programs.mosh.enable = true; + + fonts = { + fonts = [ pkgs.jetbrains-mono ]; + fontconfig.defaultFonts.monospace = [ "JetBrains Mono" ]; + }; + + environment.systemPackages = + # programs from the depot + (with depot; [ + fun.idual.script + fun.idual.setAlarm + ]) ++ + + # programs from nixpkgs + (with pkgs; [ + bat + curl + direnv + emacs28-nox + fswebcam + git + gnupg + google-cloud-sdk + htop + jq + pass + pciutils + restic + ripgrep + screen + ]); + + users = { + # Set up my own user for logging in and doing things ... + users.tazjin = { + isNormalUser = true; + uid = 1000; + extraGroups = [ "git" "wheel" "quassel" "video" ]; + shell = pkgs.fish; + }; + + # Set up a user & group for general git shenanigans + groups.git = { }; + users.git = { + group = "git"; + isSystemUser = true; + }; + }; + + # Services setup + services.openssh.enable = true; + services.haveged.enable = true; + + # Join Tailscale into home network + services.tailscale.enable = true; + + # Allow sudo-ing via the forwarded SSH agent. + security.pam.enableSSHAgentAuth = true; + + # NixOS 20.03 broke nginx and I can't be bothered to debug it + # anymore, all solution attempts have failed, so here's a + # brute-force fix. + systemd.services.fix-nginx = { + script = "${pkgs.coreutils}/bin/chown -R nginx: /var/spool/nginx /var/cache/nginx"; + + serviceConfig = { + User = "root"; + Type = "oneshot"; + }; + }; + + systemd.timers.fix-nginx = { + wantedBy = [ "multi-user.target" ]; + timerConfig = { + OnCalendar = "minutely"; + }; + }; + + # Provision a TLS certificate outside of nginx to avoid + # nixpkgs#38144 + security.acme = { + acceptTerms = true; + + certs."tazj.in" = { + email = "mail@tazj.in"; + group = "nginx"; + webroot = "/var/lib/acme/acme-challenge"; + extraDomains = { + "cs.tazj.in" = null; + "git.tazj.in" = null; + "www.tazj.in" = null; + + # Local domains (for this machine only) + "camden.tazj.in" = null; + }; + postRun = "systemctl reload nginx"; + }; + + certs."quassel.tazj.in" = { + email = "mail@tazj.in"; + webroot = "/var/lib/acme/challenge-quassel"; + group = "quassel"; + }; + }; + + # Forward logs to Google Cloud Platform + services.journaldriver = { + enable = true; + logStream = "home"; + googleCloudProject = "tazjins-infrastructure"; + applicationCredentials = "/etc/gcp/key.json"; + }; + + services.depot.quassel = { + enable = true; + acmeHost = "quassel.tazj.in"; + bindAddresses = [ + "0.0.0.0" + ]; + }; + + services.bitlbee = { + enable = false; + portNumber = 2337; # bees + }; + + # serve my website(s) + services.nginx = { + enable = true; + enableReload = true; + package = with pkgs; nginx.override { + modules = [ nginxModules.rtmp ]; + }; + + recommendedTlsSettings = true; + recommendedGzipSettings = true; + recommendedProxySettings = true; + + appendConfig = '' + rtmp_auto_push on; + rtmp { + server { + listen 1935; + chunk_size 4000; + + application tvl { + live on; + + allow publish 88.98.195.213; + allow publish 10.0.1.0/24; + deny publish all; + + allow play all; + } + } + } + ''; + + commonHttpConfig = '' + log_format json_combined escape=json + '{' + '"remote_addr":"$remote_addr",' + '"method":"$request_method",' + '"uri":"$request_uri",' + '"status":$status,' + '"request_size":$request_length,' + '"response_size":$body_bytes_sent,' + '"response_time":$request_time,' + '"referrer":"$http_referer",' + '"user_agent":"$http_user_agent"' + '}'; + + access_log syslog:server=unix:/dev/log,nohostname json_combined; + ''; + + virtualHosts.homepage = { + serverName = "tazj.in"; + serverAliases = [ "camden.tazj.in" ]; + default = true; + useACMEHost = "tazj.in"; + root = depot.users.tazjin.homepage; + forceSSL = true; + + extraConfig = '' + ${depot.users.tazjin.blog.oldRedirects} + + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; + + location ~* \.(webp|woff2)$ { + add_header Cache-Control "public, max-age=31536000"; + } + + location /blog/ { + alias ${depot.users.tazjin.blog.rendered}/; + + if ($request_uri ~ ^/(.*)\.html$) { + return 302 /$1; + } + + try_files $uri $uri.html $uri/ =404; + } + + location = /tazjin { + return 200 "tazjin"; + } + + location /blobs/ { + alias /var/www/blobs/; + } + ''; + }; + + virtualHosts.cgit-old = nginxRedirect { + from = "git.tazj.in"; + to = "code.tvl.fyi"; + acmeHost = "tazj.in"; + }; + + virtualHosts.cs-old = nginxRedirect { + from = "cs.tazj.in"; + to = "cs.tvl.fyi"; + acmeHost = "tazj.in"; + }; + }; + + # Timer units that can be started with systemd-run to set my alarm. + systemd.user.services.light-alarm = { + script = "${depot.fun.idual.script}/bin/idualctl wakey"; + postStart = "${pkgs.systemd}/bin/systemctl --user stop light-alarm.timer"; + serviceConfig = { + Type = "oneshot"; + }; + }; + + system.stateVersion = "19.09"; +}) diff --git a/users/tazjin/nixos/default.nix b/users/tazjin/nixos/default.nix new file mode 100644 index 000000000000..b9cae51d7f69 --- /dev/null +++ b/users/tazjin/nixos/default.nix @@ -0,0 +1,10 @@ +{ depot, lib, ... }: + +let systemFor = sys: (depot.ops.nixos.nixosFor sys).system; +in depot.nix.readTree.drvTargets { + camdenSystem = systemFor depot.users.tazjin.nixos.camden; + frogSystem = systemFor depot.users.tazjin.nixos.frog; + tverskoySystem = systemFor depot.users.tazjin.nixos.tverskoy; + polyankaSystem = (depot.ops.nixos.nixosFor depot.users.tazjin.nixos.polyanka).system; + zamalekSystem = systemFor depot.users.tazjin.nixos.zamalek; +} diff --git a/users/tazjin/nixos/frog/default.nix b/users/tazjin/nixos/frog/default.nix new file mode 100644 index 000000000000..35d7f9c775c5 --- /dev/null +++ b/users/tazjin/nixos/frog/default.nix @@ -0,0 +1,287 @@ +{ depot, lib, pkgs, ... }: + +config: +let + inherit (pkgs) lieer; + + quasselClient = pkgs.quassel.override { + client = true; + enableDaemon = false; + monolithic = false; + }; +in +lib.fix (self: { + imports = [ + (depot.path.origSrc + "/ops/modules/v4l2loopback.nix") + ]; + + boot = { + tmpOnTmpfs = true; + kernelModules = [ "kvm-amd" ]; + + loader = { + systemd-boot.enable = true; + efi.canTouchEfiVariables = true; + }; + + initrd = { + luks.devices.frog-crypt.device = "/dev/disk/by-label/frog-crypt"; + availableKernelModules = [ "xhci_pci" "ahci" "nvme" "usb_storage" "usbhid" "sd_mod" ]; + kernelModules = [ "dm-snapshot" ]; + }; + + kernelPackages = pkgs.linuxPackages_latest; + kernel.sysctl = { + "kernel.perf_event_paranoid" = -1; + }; + + # Enable this again if frog is put back into use ... + # + # kernelPatches = [ + # depot.third_party.kernelPatches.trx40_usb_audio + # ]; + }; + + hardware = { + cpu.amd.updateMicrocode = true; + enableRedistributableFirmware = true; + opengl = { + enable = true; + driSupport = true; + driSupport32Bit = true; + }; + + pulseaudio = { + enable = true; + package = pkgs.pulseaudioFull; + }; + + bluetooth = { + enable = true; + }; + }; + + nix = { + maxJobs = 48; + binaryCaches = [ "ssh://nix-ssh@whitby.tvl.fyi" ]; + binaryCachePublicKeys = [ "cache.tvl.fyi:fd+9d1ceCPvDX/xVhcfv8nAa6njEhAGAEe+oGJDEeoc=" ]; + }; + + networking = { + hostName = "frog"; + useDHCP = true; + + # Don't use ISP's DNS servers: + nameservers = [ + "8.8.8.8" + "8.8.4.4" + ]; + + firewall.enable = false; + }; + + # Generate an immutable /etc/resolv.conf from the nameserver settings + # above (otherwise DHCP overwrites it): + environment.etc."resolv.conf" = with lib; { + source = pkgs.writeText "resolv.conf" '' + ${concatStringsSep "\n" (map (ns: "nameserver ${ns}") self.networking.nameservers)} + options edns0 + ''; + }; + + time.timeZone = "Europe/London"; + + fileSystems = { + "/".device = "/dev/disk/by-label/frog-root"; + "/boot".device = "/dev/disk/by-label/BOOT"; + "/home".device = "/dev/disk/by-label/frog-home"; + }; + + # Configure user account + users.extraUsers.tazjin = { + extraGroups = [ "wheel" "audio" "docker" ]; + isNormalUser = true; + uid = 1000; + shell = pkgs.fish; + }; + + security.sudo = { + enable = true; + extraConfig = "wheel ALL=(ALL:ALL) SETENV: ALL"; + }; + + fonts = { + fonts = with pkgs; [ + corefonts + dejavu_fonts + jetbrains-mono + noto-fonts-cjk + noto-fonts-emoji + ]; + + fontconfig = { + hinting.enable = true; + subpixel.lcdfilter = "light"; + + defaultFonts = { + monospace = [ "JetBrains Mono" ]; + }; + }; + }; + + # Configure location (Vauxhall, London) for services that need it. + location = { + latitude = 51.4819109; + longitude = -0.1252998; + }; + + programs.fish.enable = true; + programs.ssh.startAgent = true; + + services.redshift.enable = true; + services.openssh.enable = true; + services.fstrim.enable = true; + services.blueman.enable = true; + + # Required for Yubikey usage as smartcard + services.pcscd.enable = true; + services.udev.packages = [ + pkgs.yubikey-personalization + ]; + + # Enable Docker for Nixery testing + virtualisation.docker = { + enable = true; + autoPrune.enable = true; + }; + + services.xserver = { + enable = true; + layout = "us"; + xkbOptions = "caps:super"; + exportConfiguration = true; + videoDrivers = [ "amdgpu" ]; + displayManager = { + # Give EXWM permission to control the session. + sessionCommands = "${pkgs.xorg.xhost}/bin/xhost +SI:localuser:$USER"; + + lightdm.enable = true; + lightdm.greeters.gtk.clock-format = "%Hยท%M"; # TODO(tazjin): TZ? + }; + + windowManager.session = lib.singleton { + name = "exwm"; + start = "${depot.users.tazjin.emacs}/bin/tazjins-emacs"; + }; + }; + + # Do not restart the display manager automatically + systemd.services.display-manager.restartIfChanged = lib.mkForce false; + + # clangd needs more than ~2GB in the runtime directory to start up + services.logind.extraConfig = '' + RuntimeDirectorySize=16G + ''; + + # Configure email setup + systemd.user.services.lieer-tazjin = { + description = "Synchronise mail@tazj.in via lieer"; + script = "${lieer}/bin/gmi sync"; + + serviceConfig = { + WorkingDirectory = "%h/mail/account.tazjin"; + Type = "oneshot"; + }; + }; + + systemd.user.timers.lieer-tazjin = { + wantedBy = [ "timers.target" ]; + + timerConfig = { + OnActiveSec = "1"; + OnUnitActiveSec = "180"; + }; + }; + + environment.systemPackages = + # programs from the depot + (with depot; [ + fun.idual.script + fun.uggc + lieer + ops.kontemplate + quasselClient + third_party.git + tools.nsfv-setup + users.tazjin.emacs + ]) ++ + + # programs from nixpkgs + (with pkgs; [ + age + bat + chromium + clang-manpages + clang-tools_11 + clang_11 + curl + direnv + dnsutils + emacs28 # mostly for emacsclient + exa + fd + file + gdb + gnupg + go + google-chrome + google-cloud-sdk + htop + hyperfine + i3lock + iftop + imagemagick + jq + kubectl + linuxPackages.perf + man-pages + miller + msmtp + nix-prefetch-github + notmuch + obs-studio + openssh + openssl + pass + pavucontrol + pciutils + pinentry + pinentry-emacs + pmutils + pwgen + ripgrep + rustup + screen + scrot + spotify + tokei + transmission + tree + unzip + usbutils + v4l-utils + vlc + xclip + xsecurelock + yubico-piv-tool + yubikey-personalization + zoxide + + # Commented out because of interim breakage: + # steam + # lutris + ]); + + # ... and other nonsense. + system.stateVersion = "20.03"; +}) diff --git a/users/tazjin/nixos/modules/default.nix b/users/tazjin/nixos/modules/default.nix new file mode 100644 index 000000000000..d747e8e1319a --- /dev/null +++ b/users/tazjin/nixos/modules/default.nix @@ -0,0 +1,2 @@ +# Make readTree happy at this level. +_: { } diff --git a/users/tazjin/nixos/modules/desktop.nix b/users/tazjin/nixos/modules/desktop.nix new file mode 100644 index 000000000000..c78463386c46 --- /dev/null +++ b/users/tazjin/nixos/modules/desktop.nix @@ -0,0 +1,53 @@ +# EXWM and other desktop configuration. +{ depot, lib, pkgs, ... }: + +{ + services = { + pipewire = { + enable = true; + alsa.enable = true; + pulse.enable = true; + }; + + redshift.enable = true; + blueman.enable = true; + + xserver = { + enable = true; + layout = "us"; + xkbOptions = "caps:super"; + + libinput.enable = true; + + displayManager = { + # Give EXWM permission to control the session. + sessionCommands = "${pkgs.xorg.xhost}/bin/xhost +SI:localuser:$USER"; + lightdm.enable = true; + # lightdm.greeters.gtk.clock-format = "%H:%M"; # TODO(tazjin): TZ? + }; + + windowManager.session = lib.singleton { + name = "exwm"; + start = "${depot.users.tazjin.emacs}/bin/tazjins-emacs"; + }; + }; + }; + + # Set variables to enable EXWM-XIM and other Emacs features. + environment.sessionVariables = { + XMODIFIERS = "@im=exwm-xim"; + GTK_IM_MODULE = "xim"; + QT_IM_MODULE = "xim"; + CLUTTER_IM_MODULE = "xim"; + EDITOR = "emacsclient"; + }; + + # Do not restart the display manager automatically + systemd.services.display-manager.restartIfChanged = lib.mkForce false; + + # If something needs more than 10s to stop it should probably be + # killed. + systemd.extraConfig = '' + DefaultTimeoutStopSec=10s + ''; +} diff --git a/users/tazjin/nixos/modules/fonts.nix b/users/tazjin/nixos/modules/fonts.nix new file mode 100644 index 000000000000..3b4461056f24 --- /dev/null +++ b/users/tazjin/nixos/modules/fonts.nix @@ -0,0 +1,24 @@ +# Attempt at configuring reasonable font-rendering. + +{ pkgs, ... }: + +{ + fonts = { + fonts = with pkgs; [ + corefonts + dejavu_fonts + jetbrains-mono + noto-fonts-cjk + noto-fonts-emoji + ]; + + fontconfig = { + hinting.enable = true; + subpixel.lcdfilter = "light"; + + defaultFonts = { + monospace = [ "JetBrains Mono" ]; + }; + }; + }; +} diff --git a/users/tazjin/nixos/modules/hidpi.nix b/users/tazjin/nixos/modules/hidpi.nix new file mode 100644 index 000000000000..7fa3e4193341 --- /dev/null +++ b/users/tazjin/nixos/modules/hidpi.nix @@ -0,0 +1,17 @@ +# Configuration for machines with HiDPI displays, which are a total +# mess, of course. +{ ... }: + +{ + # Expose a variable to all programs that might be interested in the + # screen settings to do conditional initialisation (mostly for Emacs). + environment.variables.HIDPI_SCREEN = "true"; + + # Ensure a larger font size in early boot stage. + hardware.video.hidpi.enable = true; + + # Bump DPI across the board. + # TODO(tazjin): This should actually be set per monitor, but I + # haven't yet figured out the right interface for doing that. + services.xserver.dpi = 161; +} diff --git a/users/tazjin/nixos/modules/home-config.nix b/users/tazjin/nixos/modules/home-config.nix new file mode 100644 index 000000000000..2445afbb52c2 --- /dev/null +++ b/users/tazjin/nixos/modules/home-config.nix @@ -0,0 +1,21 @@ +# Inject the right home-manager config for the machine. + +{ config, depot, pkgs, ... }: + +{ + users.users.tazjin = { + isNormalUser = true; + createHome = true; + extraGroups = [ "wheel" "networkmanager" "video" "adbusers" ]; + uid = 1000; + shell = pkgs.fish; + initialHashedPassword = "$6$d3FywUNCuZnJ4l.$ZW2ul59MLYon1v1xhC3lTJZfZ91lWW6Tpi13MpME0cJcYZNrsx7ABdgQRn.K05awruG2Y9ARAzURnmiJ31WTS1h"; + }; + + nix = { + trustedUsers = [ "tazjin" ]; + }; + + home-manager.useGlobalPkgs = true; + home-manager.users.tazjin = depot.users.tazjin.home."${config.networking.hostName}"; +} diff --git a/users/tazjin/nixos/modules/laptop.nix b/users/tazjin/nixos/modules/laptop.nix new file mode 100644 index 000000000000..da277dd3d636 --- /dev/null +++ b/users/tazjin/nixos/modules/laptop.nix @@ -0,0 +1,14 @@ +# Configuration specifically for laptops that move around. +{ ... }: + +{ + # Automatically detect location for redshift & timezone settings. + services.geoclue2.enable = true; + location.provider = "geoclue2"; + services.localtime.enable = true; + + # Enable power-saving features. + services.tlp.enable = true; + + programs.light.enable = true; +} diff --git a/users/tazjin/nixos/modules/persistence.nix b/users/tazjin/nixos/modules/persistence.nix new file mode 100644 index 000000000000..c81958161fbf --- /dev/null +++ b/users/tazjin/nixos/modules/persistence.nix @@ -0,0 +1,26 @@ +# Configuration for persistent (non-home) data. +{ depot, pkgs, lib, ... }: + +{ + imports = [ + "${depot.third_party.impermanence}/nixos.nix" + ]; + + environment.persistence."/persist" = { + directories = [ + "/etc/NetworkManager/system-connections" + "/etc/mullvad-vpn" + "/var/cache/mullvad-vpn" + "/var/lib/bluetooth" + "/var/lib/systemd/coredump" + "/var/lib/tailscale" + "/var/log" + ]; + + files = [ + "/etc/machine-id" + ]; + }; + + programs.fuse.userAllowOther = true; +} diff --git a/users/tazjin/nixos/modules/physical.nix b/users/tazjin/nixos/modules/physical.nix new file mode 100644 index 000000000000..8b11e1bf0872 --- /dev/null +++ b/users/tazjin/nixos/modules/physical.nix @@ -0,0 +1,90 @@ +# Default configuration settings for physical machines that I use. +{ pkgs, depot, ... }: + +let + pass-otp = pkgs.pass.withExtensions (e: [ e.pass-otp ]); +in +{ + # Install all the default software. + environment.systemPackages = + # programs from the depot + (with depot; [ + users.tazjin.screenLock + users.tazjin.emacs + third_party.agenix.cli + ]) ++ + + # programs from nixpkgs + (with pkgs; [ + amber + audacity + bat + curl + ddcutil + direnv + # dmd # TODO(tazjin): temporarily broken in nixpkgs, reinstall when it works again + dnsutils + electrum + emacsNativeComp # emacsclient + exa + fd + file + firefox + fractal + gdb + gh + git + gnupg + google-chrome + gtk3 # for gtk-launch + htop + hyperfine + iftop + imagemagick + jq + lieer + man-pages + mosh + msmtp + mullvad-vpn + networkmanagerapplet + nix-prefetch-github + nmap + notmuch + openssh + openssl + paperlike-go + pass-otp + pavucontrol + pinentry + pinentry-emacs + pulseaudio # for pactl + pwgen + quasselClient + rink + ripgrep + rustup + screen + scrot + tig + tokei + tree + unzip + vlc + whois + xsecurelock + zoxide + ]); + + # Run services & configure programs for all machines. + services = { + mullvad-vpn.enable = true; + fwupd.enable = true; + }; + + programs = { + fish.enable = true; + mosh.enable = true; + ssh.startAgent = true; + }; +} diff --git a/users/tazjin/nixos/modules/tgsa.nix b/users/tazjin/nixos/modules/tgsa.nix new file mode 100644 index 000000000000..ac6d940c2a1d --- /dev/null +++ b/users/tazjin/nixos/modules/tgsa.nix @@ -0,0 +1,24 @@ +{ config, depot, lib, pkgs, ... }: + +{ + systemd.services.tgsa = { + description = "telegram -> SA bbcode thing"; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + DynamicUser = true; + Restart = "always"; + ExecStart = "${depot.users.tazjin.tgsa}/bin/tgsa"; + }; + }; + + services.nginx.virtualHosts."tgsa" = { + serverName = "tgsa.tazj.in"; + enableACME = true; + forceSSL = true; + + locations."/" = { + proxyPass = "http://127.0.0.1:8472"; + }; + }; +} diff --git a/users/tazjin/nixos/modules/zerotier.nix b/users/tazjin/nixos/modules/zerotier.nix new file mode 100644 index 000000000000..bd503cf8f026 --- /dev/null +++ b/users/tazjin/nixos/modules/zerotier.nix @@ -0,0 +1,14 @@ +# Configuration for my Zerotier network. + +{ + environment.persistence."/persist".directories = [ + "/var/lib/zerotier-one" + ]; + + services.zerotierone.enable = true; + services.zerotierone.joinNetworks = [ + "35c192ce9bd4c8c7" + ]; + + networking.firewall.trustedInterfaces = [ "zt7nnembs4" ]; +} diff --git a/users/tazjin/nixos/polyanka/default.nix b/users/tazjin/nixos/polyanka/default.nix new file mode 100644 index 000000000000..fc30b823735e --- /dev/null +++ b/users/tazjin/nixos/polyanka/default.nix @@ -0,0 +1,122 @@ +# VPS hosted at GleSYS, running my Quassel and some random network +# stuff. + +_: # ignore readTree options + +{ config, depot, lib, pkgs, ... }: + +let + mod = name: depot.path.origSrc + ("/ops/modules/" + name); + usermod = name: depot.path.origSrc + ("/users/tazjin/nixos/modules/" + name); +in +{ + imports = [ + (mod "quassel.nix") + (mod "www/base.nix") + (usermod "tgsa.nix") + ]; + + # Use the GRUB 2 boot loader. + boot.loader.grub.enable = true; + boot.loader.grub.version = 2; + boot.loader.grub.device = "/dev/sda"; # or "nodev" for efi only + boot.initrd.availableKernelModules = [ "ata_piix" "vmw_pvscsi" "sd_mod" "sr_mod" ]; + + # Adjust to disk size increases + boot.growPartition = true; + + virtualisation.vmware.guest.enable = true; + virtualisation.vmware.guest.headless = true; + + nix.settings.trusted-users = [ "tazjin" ]; + + fileSystems."/" = + { + device = "/dev/disk/by-uuid/4c51357a-1e34-4b59-b169-63af1fcdce71"; + fsType = "ext4"; + }; + + networking = { + hostName = "polyanka"; + domain = "tazj.in"; + useDHCP = false; + + # Required for VPN usage + networkmanager.enable = true; + + interfaces.ens192 = { + ipv4.addresses = lib.singleton { + address = "159.253.30.129"; + prefixLength = 24; + }; + + ipv6.addresses = lib.singleton { + address = "2a02:750:7:3305::308"; + prefixLength = 64; + }; + }; + + defaultGateway = "159.253.30.1"; + defaultGateway6.address = "2a02:750:7:3305::1"; + + firewall.enable = true; + firewall.allowedTCPPorts = [ 22 80 443 ]; + + nameservers = [ + "79.99.4.100" + "79.99.4.101" + "2a02:751:aaaa::1" + "2a02:751:aaaa::2" + ]; + }; + + time.timeZone = "UTC"; + + security.acme.acceptTerms = true; + security.acme.certs."polyanka.tazj.in" = { + listenHTTP = ":80"; + email = "mail@tazj.in"; + group = "quassel"; + }; + + users.users.tazjin = { + isNormalUser = true; + extraGroups = [ "wheel" ]; + shell = pkgs.fish; + openssh.authorizedKeys.keys = depot.users.tazjin.keys.all; + }; + + security.sudo.wheelNeedsPassword = false; + + services.depot.quassel = { + enable = true; + acmeHost = "polyanka.tazj.in"; + bindAddresses = [ + "0.0.0.0" + ]; + }; + + # List packages installed in system profile. To search, run: + # $ nix search wget + environment.systemPackages = with pkgs; [ + curl + htop + jq + nmap + bat + emacs-nox + nano + wget + ]; + + programs.mtr.enable = true; + programs.mosh.enable = true; + services.openssh.enable = true; + + services.zerotierone.enable = true; + services.zerotierone.joinNetworks = [ + "35c192ce9bd4c8c7" + ]; + + system.stateVersion = "20.09"; +} diff --git a/users/tazjin/nixos/tverskoy/default.nix b/users/tazjin/nixos/tverskoy/default.nix new file mode 100644 index 000000000000..4a94e58c5f17 --- /dev/null +++ b/users/tazjin/nixos/tverskoy/default.nix @@ -0,0 +1,163 @@ +# tverskoy is my Thinkpad X13 AMD 1st gen +{ depot, lib, pkgs, ... }: + +config: +let + quasselClient = pkgs.quassel.override { + client = true; + enableDaemon = false; + monolithic = false; + }; + + mod = name: depot.path.origSrc + ("/ops/modules/" + name); + usermod = name: depot.path.origSrc + ("/users/tazjin/nixos/modules/" + name); +in +lib.fix (self: { + imports = [ + (mod "open_eid.nix") + (usermod "desktop.nix") + (usermod "fonts.nix") + (usermod "home-config.nix") + (usermod "laptop.nix") + (usermod "persistence.nix") + (usermod "physical.nix") + (usermod "zerotier.nix") + + (pkgs.home-manager.src + "/nixos") + ] ++ lib.optional (builtins.pathExists ./local-config.nix) ./local-config.nix; + + tvl.cache.enable = true; + + boot = rec { + initrd.availableKernelModules = [ "nvme" "ehci_pci" "xhci_pci" "usb_storage" "sd_mod" "rtsx_pci_sdmmc" ]; + initrd.kernelModules = [ ]; + + # Restore /home to the blank snapshot, erasing all ephemeral data. + initrd.postDeviceCommands = lib.mkAfter '' + zfs rollback -r zpool/ephemeral/home@tazjin-clean + ''; + + # Install thinkpad modules for TLP + extraModulePackages = [ kernelPackages.acpi_call ]; + + kernelModules = [ "kvm-amd" "i2c_dev" ]; + kernelPackages = pkgs.linuxPackages_latest; + loader.systemd-boot.enable = true; + loader.efi.canTouchEfiVariables = true; + zfs.enableUnstable = true; + }; + + fileSystems = { + "/" = { + device = "tmpfs"; + fsType = "tmpfs"; + options = [ "defaults" "size=8G" "mode=755" ]; + }; + + "/home" = { + device = "zpool/ephemeral/home"; + fsType = "zfs"; + }; + + "/nix" = { + device = "zpool/local/nix"; + fsType = "zfs"; + }; + + "/depot" = { + device = "zpool/safe/depot"; + fsType = "zfs"; + }; + + "/persist" = { + device = "zpool/safe/persist"; + fsType = "zfs"; + neededForBoot = true; + }; + + # SD card + "/mnt" = { + device = "/dev/disk/by-uuid/c602d703-f1b9-4a44-9e45-94dfe24bdaa8"; + fsType = "ext4"; + }; + + "/boot" = { + device = "/dev/disk/by-uuid/BF4F-388B"; + fsType = "vfat"; + }; + }; + + hardware = { + cpu.amd.updateMicrocode = true; + enableRedistributableFirmware = true; + bluetooth.enable = true; + + opengl = { + enable = true; + extraPackages = with pkgs; [ + vaapiVdpau + libvdpau-va-gl + ]; + }; + }; + + networking = { + hostName = "tverskoy"; + hostId = "3c91827f"; + domain = "tvl.su"; + useDHCP = false; + networkmanager.enable = true; + firewall.enable = false; + + nameservers = [ + "8.8.8.8" + "8.8.4.4" + ]; + }; + + security.rtkit.enable = true; + + services = { + printing.enable = true; + + # expose i2c device as /dev/i2c-amdgpu-dm and make it user-accessible + # this is required for sending control commands to the Dasung screen. + udev.extraRules = '' + SUBSYSTEM=="i2c-dev", ACTION=="add", DEVPATH=="/devices/pci0000:00/0000:00:08.1/0000:06:00.0/i2c-5/i2c-dev/i2c-5", SYMLINK+="i2c-amdgpu-dm", TAG+="uaccess" + ''; + + xserver.videoDrivers = [ "amdgpu" ]; + + # Automatically collect garbage from the Nix store. + depot.automatic-gc = { + enable = true; + interval = "1 hour"; + diskThreshold = 16; # GiB + maxFreed = 10; # GiB + preserveGenerations = "14d"; + }; + }; + + systemd.user.services.lieer-tazjin = { + description = "Synchronise mail@tazj.in via lieer"; + script = "${pkgs.lieer}/bin/gmi sync"; + + serviceConfig = { + WorkingDirectory = "%h/mail/account.tazjin"; + Type = "oneshot"; + }; + }; + + systemd.user.timers.lieer-tazjin = { + wantedBy = [ "timers.target" ]; + + timerConfig = { + OnActiveSec = "1"; + OnUnitActiveSec = "180"; + }; + }; + + services.tailscale.enable = true; + + system.stateVersion = "20.09"; +}) diff --git a/users/tazjin/nixos/zamalek/default.nix b/users/tazjin/nixos/zamalek/default.nix new file mode 100644 index 000000000000..be5b204277ae --- /dev/null +++ b/users/tazjin/nixos/zamalek/default.nix @@ -0,0 +1,82 @@ +# zamalek is my Huawei MateBook X (unknown year) +{ depot, lib, pkgs, ... }: + +config: +let + mod = name: depot.path.origSrc + ("/ops/modules/" + name); + usermod = name: depot.path.origSrc + ("/users/tazjin/nixos/modules/" + name); + + zdevice = device: { + inherit device; + fsType = "zfs"; + }; +in +{ + imports = [ + (usermod "desktop.nix") + (usermod "fonts.nix") + (usermod "hidpi.nix") + (usermod "home-config.nix") + (usermod "laptop.nix") + (usermod "persistence.nix") + (usermod "physical.nix") + (usermod "zerotier.nix") + + (depot.third_party.impermanence + "/nixos.nix") + (pkgs.home-manager.src + "/nixos") + ] ++ lib.optional (builtins.pathExists ./local-config.nix) ./local-config.nix; + + tvl.cache.enable = true; + + boot = { + initrd.availableKernelModules = [ "nvme" "xhci_pci" ]; + loader.systemd-boot.enable = true; + loader.efi.canTouchEfiVariables = true; + supportedFilesystems = [ "zfs" ]; + zfs.devNodes = "/dev/"; + + extraModprobeConfig = '' + options snd_hda_intel power_save=1 + options iwlwifi power_save=1 + options iwldvm force_cam=0 + options i915 enable_guc=3 enable_fbc=1 + ''; + }; + + fileSystems = { + "/" = zdevice "zpool/ephemeral/root"; + "/home" = zdevice "zpool/ephemeral/home"; + "/persist" = zdevice "zpool/persistent/data" // { neededForBoot = true; }; + "/nix" = zdevice "zpool/persistent/nix"; + "/depot" = zdevice "zpool/persistent/depot"; + + "/boot" = { + device = "/dev/disk/by-uuid/2487-3908"; + fsType = "vfat"; + }; + }; + + networking = { + hostName = "zamalek"; + domain = "tvl.su"; + hostId = "ee399356"; + networkmanager.enable = true; + + nameservers = [ + "8.8.8.8" + "8.8.4.4" + ]; + }; + + hardware = { + cpu.intel.updateMicrocode = true; + bluetooth.enable = true; + enableRedistributableFirmware = true; + opengl.enable = true; + }; + + services.xserver.libinput.touchpad.clickMethod = "clickfinger"; + services.tailscale.enable = true; + + system.stateVersion = "21.11"; +} diff --git a/users/tazjin/presentations/bootstrapping-2018/README.md b/users/tazjin/presentations/bootstrapping-2018/README.md new file mode 100644 index 000000000000..e9573ae3f2e1 --- /dev/null +++ b/users/tazjin/presentations/bootstrapping-2018/README.md @@ -0,0 +1,5 @@ +These are the slides for a talk I gave at the Norwegian Unix User Group on +2018-03-13. + +There is more information and a recording on the [event +page](https://www.nuug.no/aktiviteter/20180313-reproduible-compiler/). diff --git a/users/tazjin/presentations/bootstrapping-2018/default.nix b/users/tazjin/presentations/bootstrapping-2018/default.nix new file mode 100644 index 000000000000..2775d0b3fbbb --- /dev/null +++ b/users/tazjin/presentations/bootstrapping-2018/default.nix @@ -0,0 +1,52 @@ +# This derivation builds the LaTeX presentation. + +{ pkgs, ... }: + +with pkgs; + +let + tex = texlive.combine { + inherit (texlive) + beamer + beamertheme-metropolis + etoolbox + euenc + extsizes + fontspec + lualibs + luaotfload + luatex + minted + ms + pgfopts + scheme-basic + translator; + }; +in +stdenv.mkDerivation { + name = "nuug-bootstrapping-slides"; + src = ./.; + + FONTCONFIG_FILE = makeFontsConf { + fontDirectories = [ fira fira-code fira-mono ]; + }; + + buildInputs = [ tex fira fira-code fira-mono ]; + buildPhase = '' + # LaTeX needs a cache folder in /home/ ... + mkdir home + export HOME=$PWD/home + # ${tex}/bin/luaotfload-tool -ufv + + # As usual, TeX needs to be run twice ... + function run() { + ${tex}/bin/lualatex presentation.tex + } + run && run + ''; + + installPhase = '' + mkdir -p $out + cp presentation.pdf $out/ + ''; +} diff --git a/users/tazjin/presentations/bootstrapping-2018/drake-meme.png b/users/tazjin/presentations/bootstrapping-2018/drake-meme.png new file mode 100644 index 000000000000..4b036754384f --- /dev/null +++ b/users/tazjin/presentations/bootstrapping-2018/drake-meme.png Binary files differdiff --git a/users/tazjin/presentations/bootstrapping-2018/nixos-logo.png b/users/tazjin/presentations/bootstrapping-2018/nixos-logo.png new file mode 100644 index 000000000000..ce0c98c2cabb --- /dev/null +++ b/users/tazjin/presentations/bootstrapping-2018/nixos-logo.png Binary files differdiff --git a/users/tazjin/presentations/bootstrapping-2018/notes.org b/users/tazjin/presentations/bootstrapping-2018/notes.org new file mode 100644 index 000000000000..363d75352e62 --- /dev/null +++ b/users/tazjin/presentations/bootstrapping-2018/notes.org @@ -0,0 +1,89 @@ +#+TITLE: Bootstrapping, reproducibility, etc. +#+AUTHOR: Vincent Ambo +#+DATE: <2018-03-10 Sat> + +* Compiler bootstrapping + This section contains notes about compiler bootstrapping, the + history thereof, which compilers need it - and so on: + +** C + +** Haskell + - self-hosted compiler (GHC) + +** Common Lisp + CL is fairly interesting in this space because it is a language + that is defined via an ANSI standard that compiler implementations + normally actually follow! + + CL has several ecosystem components that focus on making + abstracting away implementation-specific calls and if a self-hosted + compiler is written in CL using those components it can be + cross-bootstrapped. + +** Python + +* A note on runtimes + Sometimes the compiler just isn't enough ... + +** LLVM +** JVM + +* References + https://github.com/mame/quine-relay + https://manishearth.github.io/blog/2016/12/02/reflections-on-rusting-trust/ + https://tests.reproducible-builds.org/debian/reproducible.html + +* Slide thoughts: + 1. Hardware trust has been discussed here a bunch, most recently + during the puri.sm talk. Hardware trust is important, as we see + with IME, but it's striking that people often take a leap to "I'm + now on my trusted Debian with free software". + + Unless you built it yourself from scratch (Spoiler: you haven't) + you're placing trust in what is basically foreign binary blobs. + + Agenda: Implications/attack vectors of this, state of the chicken + & egg, the topic of reproducibility, what can you do? (Nix!) + + 2. Chicken-and-egg issue + + It's an important milestone for a language to become self-hosted: + You begin doing a kind of dogfeeding, you begin to enforce + reliability & consistency guarantees to avoid having to redo your + own codebase constantly and so on. + + However, the implication is now that you need your own compiler + to compile itself. + + Common examples: + - C/C++ compilers needed to build C/C++ compilers: + + GCC 4.7 was the last version of GCC that could be built with a + standard C-compiler, nowadays it is mostly written in C++. + + Certain versions of GCC can be built with LLVM/Clang. + + Clang/LLVM can be compiled by itself and also GCC. + + - Rust was originally written in OCAML but moved to being + self-hosted in 2011. Currently rustc-releases are always built + with a copy of the previous release. + + It's relatively new so we can build the chain all the way. + + Notable exceptions: Some popular languages are not self-hosted, + for example Clojure. Languages also have runtimes, which may be + written in something else (e.g. Haskell -> C runtime) +* How to help: + Most of this advice is about reproducible builds, not bootstrapping, + as that is a much harder project. + + - fix reproducibility issues listed in Debian's issue tracker (focus + on non-Debian specific ones though) + - experiment with NixOS / GuixSD to get a better grasp on the + problem space of reproducibility + + If you want to contribute to bootstrapping, look at + bootstrappable.org and their wiki. Several initiatives such as MES + could need help! diff --git a/users/tazjin/presentations/bootstrapping-2018/presentation.pdf b/users/tazjin/presentations/bootstrapping-2018/presentation.pdf new file mode 100644 index 000000000000..7f435fe5b539 --- /dev/null +++ b/users/tazjin/presentations/bootstrapping-2018/presentation.pdf Binary files differdiff --git a/users/tazjin/presentations/bootstrapping-2018/presentation.tex b/users/tazjin/presentations/bootstrapping-2018/presentation.tex new file mode 100644 index 000000000000..d3aa61337554 --- /dev/null +++ b/users/tazjin/presentations/bootstrapping-2018/presentation.tex @@ -0,0 +1,251 @@ +\documentclass[12pt]{beamer} +\usetheme{metropolis} +\newenvironment{code}{\ttfamily}{\par} +\title{Where does \textit{your} compiler come from?} +\date{2018-03-13} +\author{Vincent Ambo} +\institute{Norwegian Unix User Group} +\begin{document} + \maketitle + + %% Slide 1: + \section{Introduction} + + %% Slide 2: + \begin{frame}{Chicken and egg} + Self-hosted compilers are often built using themselves, for example: + + \begin{itemize} + \item C-family compilers bootstrap themselves \& each other + \item (Some!) Common Lisp compilers can bootstrap each other + \item \texttt{rustc} bootstraps itself with a previous version + \item ... same for many other languages! + \end{itemize} + \end{frame} + + \begin{frame}{Chicken, egg and ... lizard?} + It's not just compilers: Languages have runtimes, too. + + \begin{itemize} + \item JVM is implemented in C++ + \item Erlang-VM is C + \item Haskell runtime is C + \end{itemize} + + ... we can't ever get away from C, can we? + \end{frame} + + %% Slide 3: + \begin{frame}{Trusting Trust} + \begin{center} + \huge{Could this be exploited?} + \end{center} + \end{frame} + + %% Slide 4: + \begin{frame}{Short interlude: A quine} + \begin{center} + \begin{code} + ((lambda (x) (list x (list 'quote x))) + \newline\vspace*{6mm} '(lambda (x) (list x (list 'quote x)))) + \end{code} + \end{center} + \end{frame} + + %% Slide 5: + \begin{frame}{Short interlude: Quine Relay} + \begin{center} + \includegraphics[ + keepaspectratio=true, + height=\textheight + ]{quine-relay.png} + \end{center} + \end{frame} + + %% Slide 6: + \begin{frame}{Trusting Trust} + An attack described by Ken Thompson in 1983: + + \begin{enumerate} + \item Modify a compiler to detect when it's compiling itself. + \item Let the modification insert \textit{itself} into the new compiler. + \item Add arbitrary attack code to the modification. + \item \textit{Optional!} Remove the attack from the source after compilation. + \end{enumerate} + \end{frame} + + %% Slide 7: + \begin{frame}{Damage potential?} + \begin{center} + \large{Let your imagination run wild!} + \end{center} + \end{frame} + + %% Slide 8: + \section{Countermeasures} + + %% Slide 9: + \begin{frame}{Diverse Double-Compiling} + Assume we have: + + \begin{itemize} + \item Target language compilers $A$ and $T$ + \item The source code of $A$: $ S_{A} $ + \end{itemize} + \end{frame} + + %% Slide 10: + \begin{frame}{Diverse Double-Compiling} + Apply the first stage (functional equivalence): + + \begin{itemize} + \item $ X = A(S_{A})$ + \item $ Y = T(S_{A})$ + \end{itemize} + + Apply the second stage (bit-for-bit equivalence): + + \begin{itemize} + \item $ V = X(S_{A})$ + \item $ W = Y(S_{A})$ + \end{itemize} + + Now we have a new problem: Reproducibility! + \end{frame} + + %% Slide 11: + \begin{frame}{Reproducibility} + Bit-for-bit equivalent output is hard, for example: + + \begin{itemize} + \item Timestamps in output artifacts + \item Non-deterministic linking order in concurrent builds + \item Non-deterministic VM \& memory states in outputs + \item Randomness in builds (sic!) + \end{itemize} + \end{frame} + + \begin{frame}{Reproducibility} + \begin{center} + Without reproducibility, we can never trust that any shipped + binary matches the source code! + \end{center} + \end{frame} + + %% Slide 12: + \section{(Partial) State of the Union} + + \begin{frame}{The Desired State} + \begin{center} + \begin{enumerate} + \item Full-source bootstrap! + \item All packages reproducible! + \end{enumerate} + \end{center} + \end{frame} + + %% Slide 13: + \begin{frame}{Bootstrapping Debian} + \begin{itemize} + \item Sparse information on the Debian-wiki + \item Bootstrapping discussions mostly resolve around new architectures + \item GCC is compiled by depending on previous versions of GCC + \end{itemize} + \end{frame} + + \begin{frame}{Reproducing Debian} + Debian has a very active effort for reproducible builds: + + \begin{itemize} + \item Organised information about reproducibility status + \item Over 90\% reproducibility in Debian package base! + \end{itemize} + \end{frame} + + \begin{frame}{Short interlude: Nix} + \begin{center} + \includegraphics[ + keepaspectratio=true, + height=0.7\textheight + ]{nixos-logo.png} + \end{center} + \end{frame} + + \begin{frame}{Short interlude: Nix} + \begin{center} + \includegraphics[ + keepaspectratio=true, + height=0.90\textheight + ]{drake-meme.png} + \end{center} + \end{frame} + + \begin{frame}{Short interlude: Nix} + \begin{center} + \includegraphics[ + keepaspectratio=true, + height=0.7\textheight + ]{nixos-logo.png} + \end{center} + \end{frame} + + \begin{frame}{Bootstrapping NixOS} + Nix evaluation can not recurse forever: The bootstrap can not + simply depend on a previous GCC. + + Workaround: \texttt{bootstrap-tools} tarball from a previous + binary cache is fetched and used. + + An unfortunate magic binary blob ... + \end{frame} + + \begin{frame}{Reproducing NixOS} + Not all reproducibility patches have been ported from Debian. + + However: Builds are fully repeatable via the Nix fundamentals! + \end{frame} + + \section{Future Developments} + + \begin{frame}{Bootstrappable: stage0} + Hand-rolled ``Cthulhu's Path to Madness'' hex-programs: + + \begin{itemize} + \item No non-auditable binary blobs + \item Aims for understandability by 70\% of programmers + \item End goal is a full-source bootstrap of GCC + \end{itemize} + \end{frame} + + + \begin{frame}{Bootstrappable: MES} + Bootstrapping the ``Maxwell Equations of Software'': + + \begin{itemize} + \item Minimal C-compiler written in Scheme + \item Minimal Scheme-interpreter (currently in C, but intended to + be rewritten in stage0 macros) + \item End goal is full-source bootstrap of the entire GuixSD + \end{itemize} + \end{frame} + + \begin{frame}{Other platforms} + \begin{itemize} + \item Nix for Darwin is actively maintained + \item F-Droid Android repository works towards fully reproducible + builds of (open) Android software + \item Mobile devices (phones, tablets, etc.) are a lost cause at + the moment + \end{itemize} + \end{frame} + + \begin{frame}{Thanks!} + Resources: + \begin{itemize} + \item bootstrappable.org + \item reproducible-builds.org + \end{itemize} + + @tazjin | mail@tazj.in + \end{frame} +\end{document} diff --git a/users/tazjin/presentations/bootstrapping-2018/quine-relay.png b/users/tazjin/presentations/bootstrapping-2018/quine-relay.png new file mode 100644 index 000000000000..5644dc3900e3 --- /dev/null +++ b/users/tazjin/presentations/bootstrapping-2018/quine-relay.png Binary files differdiff --git a/users/tazjin/presentations/bootstrapping-2018/result.pdfpc b/users/tazjin/presentations/bootstrapping-2018/result.pdfpc new file mode 100644 index 000000000000..b0fa6c9a0ef8 --- /dev/null +++ b/users/tazjin/presentations/bootstrapping-2018/result.pdfpc @@ -0,0 +1,142 @@ +[file] +result +[last_saved_slide] +10 +[font_size] +20000 +[notes] +### 1 +- previous discussions of hardware trust (e.g. purism presentation) +- people leap to "now I'm on my trusted Debian!" +- unless you built it from scratch (spoiler: you haven't) you're *trusting* someone + +Agenda: Implications of trust with focus on bootstrap paths and reproducibility, plus how you can help.### 2 +self-hosting: +- C-family: GCC pre/post 4.7, Clang +- Common Lisp: Sunshine land! (with SBCL) +- rustc: Bootstrap based on previous versions (C++ transpiler underway!) +- many other languages also work this way! + +(Noteable counterexample: Clojure is written in Java!)### 3 + +- compilers are just one bit, the various runtimes exist, too!### 4 + +Could this be exploited? + +People don't think about where their compiler comes from. + +Even if they do, they may only go so far as to say "I'll just recompile it using <other compiler>". + +Unfortunately, spoiler alert, life isn't that easy in the computer world and yes, exploitation is possible.### 5 + +- describe what a quine is +- classic Lisp quine +- explain demo quine +- demo demo quine + +- this is interesting, but not useful - can quines do more than that?### 6 + +- quine-relay: "art project" with 128-language circular quine + +- show source of quine-relay + +- (demo quine relay?) + +- side-note: this program is very, very trustworthy!### 7 + +Ken Thompson (designer of UNIX and a couple other things!) received Turing award in 1983, and described attack in speech. + +- figure out how to detect self-compilation +- make that modification a quine +- insert modification into new compiler +- add attack code to modification +- remove attack from source, distributed binary will still be compromised! it's like evolution :)### 8 + +damage potential is basically infinite: + +- classic "login" attack +=> also applicable to other credentials + +- attack (weaken) crypto algorithms + +- you can probably think of more!### 10 + +idea being: potential vulnerability would have to work across compilers: + +the more compilers we can introduce (e.g. more architectures, different versions, different compilers), the harder it gets for a vulnerability to survive all of those + +The more compilers, the merrier! Lisps are pretty good at this.### 11 + +if we get a bit-mismatch after DDC, not all hope is lost: Maybe the thing just isn't reproducible! + +- many reasons for failures +- timestamps are a classic! artifacts can be build logs, metadata in ZIP-files or whatever +- non-determinism is the devil +- sometimes people actively introduce build-randomness (NaCl)### 12 + +- Does that binary download on the project's website really match the source? + +- Your Linux packages are signed by someone - cool - but what does that mean?### 13 + +Two things should be achieved - gross oversimplification - to get to the ideal "desired state of the union": + +1. full-source bootstrap: without ever introducing any binaries, go from nothing to a full Linux distribution + +2. when packages are distributed, we should be able to know the expected output of a source package beforehand + +=> suddenly binary distributions become a cache! But more on Nix later.### 14 + +- Debian project does not seem as concerned with bootstrapping as with reproducibility +- Debian mostly bootstraps on new architectures (using cross-compilation and similar techniques, from an existing binary base) +- core bootstrap (GCC & friends) is performed with previous Debian version and depending on GCC### 15 + +... however! Debian cares about reproducibility. + +- automated testing of reproducibility +- information about the status of all packages is made available in repos +- Over 90% packages of packages are reproducible! + +< show reproducible builds website > + +Debian is still fundamentally a binary distribution though, but it doesn't have to be that way.### 16 + +Nix - a purely functional package manager + +It's not a new project (10+ years), been discussed here before, has multiple components: package manager, language, NixOS. + +Instead of describing *how* to build a thing, Nix describes *what* to build:### 17 +### 19 + +In Nix, it's impossible to say "GCC is the result of applying GCC to the GCC source", because that happens to be infinite recursion. + +Bootstrapping in Nix works by introducing a binary pinned by its full-hash, which was built on some previous Nix version. + +Unfortunately also just a magic binary blob ... ### 20 + +NixOS is not actively porting all of Debian's reproducibility patches, but builds are fully repeatable: + +- introducing a malicious compiler would produce a different input hash -> different package + +Future slide: hope is not lost! Things are underway.### 21 + +- bootstrappable.org (demo?) is an umbrella page for several projects working on bootstrappability + +- stage0 is an important piece: manually, small, auditable Hex programs to get to a Hex macro expander + +- end goal is a full-source bootrap, but pieces are missing### 22 + +MES is out of the GuixSD circles (explain Guix, GNU Hurd joke) + +- idea being that once you have a Lisp, you have all of computing (as Alan Key said) + +- includes MesCC in Scheme -> can *almost* make a working tinyCC -> can *almost* make a working gcc 4.7 + +- minimal Scheme interpreter, currently built in C to get the higher-level stuff to work, goal is rewrite in hex +- bootstrapping Guix is the end goal### 23 + +- userspace in Darwin has a Nix project +- unsure about other BSDs, but if anyone knows - input welcome! +- F-Droid has reproducible Android packages, but that's also userspace only +- All other mobile platforms are a lost cause + +Generally, all closed-source software is impossible to trust. diff --git a/users/tazjin/presentations/erlang-2016/.skip-subtree b/users/tazjin/presentations/erlang-2016/.skip-subtree new file mode 100644 index 000000000000..e69de29bb2d1 --- /dev/null +++ b/users/tazjin/presentations/erlang-2016/.skip-subtree diff --git a/users/tazjin/presentations/erlang-2016/README.md b/users/tazjin/presentations/erlang-2016/README.md new file mode 100644 index 000000000000..e1b6c83b99cc --- /dev/null +++ b/users/tazjin/presentations/erlang-2016/README.md @@ -0,0 +1,6 @@ +These are the slides for a presentation I gave for the Oslo javaBin meetup in +2016. + +Unfortunately there is no recording of the presentation due to a technical error +(video was recorded, but no audio). This is a bit of a shame because I think +these are some of the best slides I've ever made. diff --git a/users/tazjin/presentations/erlang-2016/presentation.md b/users/tazjin/presentations/erlang-2016/presentation.md new file mode 100644 index 000000000000..526564b88268 --- /dev/null +++ b/users/tazjin/presentations/erlang-2016/presentation.md @@ -0,0 +1,222 @@ +slidenumbers: true +Erlang. +====== + +### Fault-tolerant, concurrent programming. + +--- + +## A brief history of Erlang + +--- + +![](https://www.ericsson.com/thinkingahead/the-networked-society-blog/wp-content/uploads/2014/09/bfW5FSr.jpg) + + +^ Telefontornet in Stockholm, around 1890. Used until 1913. + +--- + +![](https://3.bp.blogspot.com/-UF7W9yTUO2g/VBqw-1HNTzI/AAAAAAAAPeg/KvsMbNSAcII/s1600/6835942484_1531372d8f_b.jpg) + +^ Telephones were operated manually at Switchboards. Anyone old enough to remember? I'm certainly not. + +--- + +![fit](https://russcam.github.io/fsharp-akka-talk/images/ericsson-301-AXD.png) + +^ Eventually we did that in software, and we got better at it over time. Ericsson AXD 301, first commercial Erlang switch. But lets take a step back. + +--- + +## Phone switches must be ... + +Highly concurrent + +Fault-tolerant + +Distributed + +(Fast!) + +![right 150%](http://learnyousomeerlang.com/static/img/erlang-the-movie.png) + +--- + +## ... and so is Erlang! + +--- + +## Erlang as a whole: + +- Unique process model (actors!) +- Built-in fault-tolerance & error handling +- Distributed processes +- Three parts! + +--- + +## Part 1: Erlang, the language + +- Functional +- Prolog-inspired syntax +- Everything is immutable +- *Extreme* pattern-matching + +--- +### Hello Joe + +```erlang +hello_joe. +``` + +--- +### Hello Joe + +```erlang +-module(hello1). +-export([hello_joe/0]). + +hello_joe() -> + hello_joe. +``` + +--- +### Hello Joe + +```erlang +-module(hello1). +-export([hello_joe/0]). + +hello_joe() -> + hello_joe. + +% 1> c(hello1). +% {ok,hello1} +% 2> hello1:hello_joe(). +% hello_joe +``` + +--- +### Hello Joe + +```erlang +-module(hello2). +-export([hello/1]). + +hello(Name) -> + io:format("Hello ~s!~n", [Name]). + +% 3> c(hello2). +% {ok,hello2} +% 4> hello2:hello("Joe"). +% Hello Joe! +% ok +``` + +--- + +## [fit] Hello ~~world~~ Joe is boring! +## [fit] Lets do it with processes. + +--- +### Hello Server + +```erlang +-module(hello_server). +-export([start_server/0]). + +start_server() -> + spawn(fun() -> server() end). + +server() -> + receive + {greet, Name} -> + io:format("Hello ~s!~n", [Name]), + server() + end. +``` + +--- + +## [fit] Some issues with that ... + +- What about unused messages? +- What if the server crashes? + +--- + +## [fit] Part 2: Open Telecom Platform + +### **It's called Erlang/OTP for a reason.** + +--- + +# OTP: An Application Framework + +- Supervision - keep processes alive! + +- OTP Behaviours - common process patterns + +- Extensive standard library + +- Error handling, debuggers, testing, ... + +- Lots more! + +^ Standard library includes lots of things from simple network libraries over testing frameworks to cryptography, complete LDAP clients etc. + +--- + +# Supervision + +![inline](http://erlang.org/doc/design_principles/sup6.gif) + +^ Supervision keeps processes alive, different restart behaviours, everything should be supervised to avoid "process" (and therefore memory) leaks + +--- + +# OTP Behaviours + +* `gen_server` +* `gen_statem` +* `gen_event` +* `supervisor` + +^ gen = generic. explain server, explain statem, event = event handling with registered handlers, supervisor ... + +--- + +`gen_server` + +--- + +## [fit] Part 3: BEAM + +### Bogdan/Bjรธrn Erlang Abstract machine + +--- + +## A VM for Erlang + +* Many were written, BEAM survived +* Concurrent garbage-collection +* Lower-level bytecode than JVM +* Very open to new languages + (Elixir, LFE, Joxa, ...) + +--- + +## What next? + +* Ole's talk, obviously! +* Learn You Some Erlang! + www.learnyousomeerlang.com +* Watch *Erlang the Movie* +* (soon!) Join the Oslo BEAM meetup group + +--- + +# [fit] Questions? + +`@tazjin` diff --git a/users/tazjin/presentations/erlang-2016/presentation.pdf b/users/tazjin/presentations/erlang-2016/presentation.pdf new file mode 100644 index 000000000000..ec8d996704b2 --- /dev/null +++ b/users/tazjin/presentations/erlang-2016/presentation.pdf Binary files differdiff --git a/users/tazjin/presentations/erlang-2016/src/hello.erl b/users/tazjin/presentations/erlang-2016/src/hello.erl new file mode 100644 index 000000000000..56404a0c5a20 --- /dev/null +++ b/users/tazjin/presentations/erlang-2016/src/hello.erl @@ -0,0 +1,5 @@ +-module(hello). +-export([hello_joe/0]). + +hello_joe() -> + hello_joe. diff --git a/users/tazjin/presentations/erlang-2016/src/hello1.erl b/users/tazjin/presentations/erlang-2016/src/hello1.erl new file mode 100644 index 000000000000..ca78261399e1 --- /dev/null +++ b/users/tazjin/presentations/erlang-2016/src/hello1.erl @@ -0,0 +1,5 @@ +-module(hello1). +-export([hello_joe/0]). + +hello_joe() -> + hello_joe. diff --git a/users/tazjin/presentations/erlang-2016/src/hello2.erl b/users/tazjin/presentations/erlang-2016/src/hello2.erl new file mode 100644 index 000000000000..2d1f6c84c401 --- /dev/null +++ b/users/tazjin/presentations/erlang-2016/src/hello2.erl @@ -0,0 +1,11 @@ +-module(hello2). +-export([hello/1]). + +hello(Name) -> + io:format("Hey ~s!~n", [Name]). + +% 3> c(hello2). +% {ok,hello2} +% 4> hello2:hello("Joe"). +% Hello Joe! +% ok diff --git a/users/tazjin/presentations/erlang-2016/src/hello_server.erl b/users/tazjin/presentations/erlang-2016/src/hello_server.erl new file mode 100644 index 000000000000..01df14ac57d5 --- /dev/null +++ b/users/tazjin/presentations/erlang-2016/src/hello_server.erl @@ -0,0 +1,12 @@ +-module(hello_server). +-export([start_server/0, server/0]). + +start_server() -> + spawn(fun() -> server() end). + +server() -> + receive + {greet, Name} -> + io:format("Hello ~s!~n", [Name]), + hello_server:server() + end. diff --git a/users/tazjin/presentations/erlang-2016/src/hello_server2.erl b/users/tazjin/presentations/erlang-2016/src/hello_server2.erl new file mode 100644 index 000000000000..24bb934ee503 --- /dev/null +++ b/users/tazjin/presentations/erlang-2016/src/hello_server2.erl @@ -0,0 +1,36 @@ +-module(hello_server2). +-behaviour(gen_server). +-compile(export_all). + +%%% Start callback for supervisor +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +%%% gen_server callbacks + +init([]) -> + {ok, sets:new()}. + +handle_call({greet, Name}, _From, State) -> + io:format("Hello ~s!~n", [Name]), + NewState = sets:add_element(Name, State), + {reply, ok, NewState}; + +handle_call({bye, Name}, _From, State) -> + io:format("Goodbye ~s!~n", [Name]), + NewState = sets:del_element(Name, State), + {reply, ok, NewState}. + +terminate(normal, State) -> + [io:format("Goodbye ~s!~n", [Name]) || Name <- State], + ok. + +%%% Unused gen_server callbacks +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +handle_cast(_Request, State) -> + {noreply, State}. diff --git a/users/tazjin/presentations/erlang-2016/src/hello_sup.erl b/users/tazjin/presentations/erlang-2016/src/hello_sup.erl new file mode 100644 index 000000000000..7fee0928c575 --- /dev/null +++ b/users/tazjin/presentations/erlang-2016/src/hello_sup.erl @@ -0,0 +1,24 @@ +-module(hello_sup). +-behaviour(supervisor). +-export([start_link/0, init/1]). + +%%% Module API + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +%%% Supervisor callbacks + +init([]) -> + Children = [hello_spec()], + {ok, { {one_for_one, 5, 10}, Children}}. + +%%% Private + +hello_spec() -> + #{id => hello_server2, + start => {hello_server2, start_link, []}, + restart => permanent, + shutdown => 5000, + type => worker, + module => [hello_server2]}. diff --git a/users/tazjin/presentations/servant-2016/Makefile b/users/tazjin/presentations/servant-2016/Makefile new file mode 100644 index 000000000000..96115ec2cbfc --- /dev/null +++ b/users/tazjin/presentations/servant-2016/Makefile @@ -0,0 +1,8 @@ +all: slides + +slides: + lualatex --shell-escape slides.tex + +clean: + rm -f slides.aux slides.log slides.nav \ + slides.out slides.toc slides.snm diff --git a/users/tazjin/presentations/servant-2016/README.md b/users/tazjin/presentations/servant-2016/README.md new file mode 100644 index 000000000000..8cfb04a42417 --- /dev/null +++ b/users/tazjin/presentations/servant-2016/README.md @@ -0,0 +1,7 @@ +These are the slides for my presentation about [servant][] at [Oslo Haskell][]. + +A full video recording of the presentation is available [on Vimeo][]. + +[servant]: https://haskell-servant.github.io/ +[Oslo Haskell]: http://www.meetup.com/Oslo-Haskell/events/227107530/ +[on Vimeo]: https://vimeo.com/153901805 diff --git a/users/tazjin/presentations/servant-2016/slides.pdf b/users/tazjin/presentations/servant-2016/slides.pdf new file mode 100644 index 000000000000..842a667e1bcc --- /dev/null +++ b/users/tazjin/presentations/servant-2016/slides.pdf Binary files differdiff --git a/users/tazjin/presentations/servant-2016/slides.pdfpc b/users/tazjin/presentations/servant-2016/slides.pdfpc new file mode 100644 index 000000000000..ed46003768c0 --- /dev/null +++ b/users/tazjin/presentations/servant-2016/slides.pdfpc @@ -0,0 +1,75 @@ +[file] +slides.pdf +[font_size] +10897 +[notes] +### 1 +13### 2 +Let's talk about servant, which is several things: +API description DSL, we'll speak about how this DSL works +and why it's at the type level + +Interpretations of the types resulting from that DSL, for example in +web servers or API clients + +Servant is commonly used or implementing services with APIs, or for accessing +other APIs with a simple, typed client +### 3 +Why type-level DSLs? +Type-level DSL: express *something*, e.g. endpoints of API, on type level by combining types. Types can be uninhabited + +Phil Wadler's: expression problem: things should be extensible both in the cases of a type, and in the functions operating on the type +Normal data types: can't add new constructors easily +Servant lifts thisup to simply allow the declaration of new types that can be included in the DSL, and new interpretations that can be attached to the types through typeclasses + +APIs become first-class citizens, can pass them around, combine them etc, they are separate from interpretations such as server implementations. In contrast, in most webframeworks, API declaration is implicit + +(Mention previous attemps at type-safe web, Yesod / web-routes + boomerang etc) +### 4 +Three extensions are necessary: +TypeOperators lets us use infix operators on the type level as constructors +DataKinds promotes new type declarations to the kind level, makes type-level literals (strings and natural numbers) available, lets us use type-level lists and pairs in combination with typeoperators +TypeFamilies: Type-level functions, map one set of types to another, come in two forms (type families, non-injective; data families, injective), more powerful than associated types +### 5 +Here you can see servant's general syntax, we define an API type as a simple alias of some other type combinations +strings are type-level strings, not actually values, represent path elements +endpoints are separated by :<|>, all endpoints end in a method with content types and return types +Capture captures path segments, but there are other combinators, for example for headers +Everything that is used from the request is expressed in types, enforcing checkability, no "escape hatch" inside handlers to get request +Every combinator has associated interpretations through typeclasses +### 6 +Explain type alias, point out Capture +Server is a type level function (type family), as mentioned earlier +### 7 +If we expand server (in ghci with kind!) we can see the actual type of the +function +### 8 +Lets speak about some interpretations of these things +### 9 +Servant server is the main interpretation that people are interested in, it's used +for taking a type specification and creating a server from it +Based on WAI, the web application interface, common abstraction for web servers which came out of the Yesod project. Implemented by the web server warp, which Yesod runs on +### 10 +Explain snippet, path gets removed from server type (irrelevant for handler), +route extracts string to value level +### 11 +Explain echo server quickly +### 12 +servant client allows generation of Haskell functions that query the API with the same types +this makes for easy to use RPC for example +### 13 +A lot of other interpretations exist for all kinds of things, mock servers for testing, foreign functions in various languages, documentation ... +### 14 +Demo! +1. Go quickly through code +2. Run server, query with curl +3. Open javascript function +4. Show JS code in the thing +5. Open the map itself +6. Open GHCi, use client +7. Generate docs +### 15 +Conclusion +Servant is pretty good, it's very easy to get started and it's great to raise the level of things that the compiler can tell you about when you do them wrong. +### 16 +Drawbacks. diff --git a/users/tazjin/presentations/servant-2016/slides.tex b/users/tazjin/presentations/servant-2016/slides.tex new file mode 100644 index 000000000000..d5947eb9421a --- /dev/null +++ b/users/tazjin/presentations/servant-2016/slides.tex @@ -0,0 +1,137 @@ +\documentclass[12pt]{beamer} +\usetheme{metropolis} +\usepackage{minted} + +\newenvironment{code}{\ttfamily}{\par} + +\title{servant} +\subtitle{Defining web APIs at the type-level} + +\begin{document} +\metroset{titleformat frame=smallcaps} +\setminted{fontsize=\scriptsize} + + +\maketitle + +\section{Introduction} + +\begin{frame}{Type-level DSLs?} + \begin{itemize} + \item (Uninhabited) types with attached ``meaning'' + \item The Expression Problem (Wadler 1998) + \item API representation and interpretation are separated + \item APIs become first-class citizens + \end{itemize} +\end{frame} + +\begin{frame}{Haskell extensions} + \begin{itemize} + \item TypeOperators + \item DataKinds + \item TypeFamilies + \end{itemize} +\end{frame} + +\begin{frame}[fragile]{A servant example} + \begin{minted}{haskell} + type PubAPI = "pubs" :> Get โ[JSON] [Pub] + :<|> "pubs" :> "tagged" + :> Capture "tag" Text + :> Get โ[JSON] [Pub] + \end{minted} +\end{frame} + +\begin{frame}[fragile]{Computed types} + \begin{minted}{haskell} + type TaggedPubs = "tagged" :> Capture "tag" Text :> ... + + taggedPubsHandler :: Server TaggedPubs + taggedPubsHandler tag = ... + \end{minted} +\end{frame} + +\begin{frame}[fragile]{Computed types} + \begin{minted}{haskell} + type TaggedPubs = "tagged" :> Capture "tag" Text :> ... + + taggedPubsHandler :: Server TaggedPubs + taggedPubsHandler tag = ... + + Server TaggedPubs ~ + Text -> EitherT ServantErr IO [Pub] + \end{minted} +\end{frame} + +\section{Interpretations} + +\begin{frame}{servant-server} + The one everyone is interested in! + + \begin{itemize} + \item Based on WAI, can run on warp + \item Interprets combinators with a simple \texttt{HasServer c} class + \item Easy to use! + \end{itemize} +\end{frame} + +\begin{frame}[fragile]{HasServer ...} + \begin{minted}{haskell} + instance (KnownSymbol path, HasServer sublayout) + => HasServer (path :> sublayout) where + type ServerT (path :> sublayout) m = ServerT sublayout m + + route ... + where + pathString = symbolVal (Proxy :: Proxy path) + \end{minted} +\end{frame} + +\begin{frame}[fragile]{Server example} + \begin{minted}{haskell} + type Echo = Capture "echo" Text :> Get โ[PlainText] Text + + echoAPI :: Proxy Echo + echoAPI = Proxy + + echoServer :: Server Echo + echoServer = return + \end{minted} +\end{frame} + +\begin{frame}{servant-client} + \begin{itemize} + \item Generates Haskell client functions for API + \item Same types as API specification: For RPC the whole ``web layer'' is abstracted away + \item Also easy to use! + \end{itemize} +\end{frame} + +\begin{frame}{servant-docs, servant-js ...} + Many other interpretations exist already, for example: + \begin{itemize} + \item Documentation generation + \item Foreign function export (e.g. Elm, JavaScript) + \item Mock-server generation + \end{itemize} +\end{frame} + +\section{Demo} + +\section{Conclusion} + +\begin{frame}{Drawbacks} + \begin{itemize} + \item Haskell has no custom open kinds (yet) + \item Proxies are ugly + \item Errors can be a bit daunting + \end{itemize} +\end{frame} + +\begin{frame}{Questions?} + รlkartet: github.com/tazjin/pubkartet \\ + Slides: github.com/tazjin/servant-presentation + + @tazjin +\end{frame} +\end{document} diff --git a/users/tazjin/presentations/systemd-2016/.gitignore b/users/tazjin/presentations/systemd-2016/.gitignore new file mode 100644 index 000000000000..1a38620fe9cc --- /dev/null +++ b/users/tazjin/presentations/systemd-2016/.gitignore @@ -0,0 +1,6 @@ +slides.aux +slides.log +slides.nav +slides.out +slides.snm +slides.toc diff --git a/users/tazjin/presentations/systemd-2016/.skip-subtree b/users/tazjin/presentations/systemd-2016/.skip-subtree new file mode 100644 index 000000000000..108b3507ddd1 --- /dev/null +++ b/users/tazjin/presentations/systemd-2016/.skip-subtree @@ -0,0 +1 @@ +No Nix files will ever be under this tree ... diff --git a/users/tazjin/presentations/systemd-2016/Makefile b/users/tazjin/presentations/systemd-2016/Makefile new file mode 100644 index 000000000000..ac5dde3cb32f --- /dev/null +++ b/users/tazjin/presentations/systemd-2016/Makefile @@ -0,0 +1,11 @@ +all: slides.pdf + +slides.toc: + lualatex slides.tex + +slides.pdf: slides.toc + lualatex slides.tex + +clean: + rm -f slides.aux slides.log slides.nav \ + slides.out slides.toc slides.snm diff --git a/users/tazjin/presentations/systemd-2016/README.md b/users/tazjin/presentations/systemd-2016/README.md new file mode 100644 index 000000000000..7f004b7d14ca --- /dev/null +++ b/users/tazjin/presentations/systemd-2016/README.md @@ -0,0 +1,6 @@ +This repository contains the slides for my systemd presentation at Hackeriet. + +Requires LaTeX, [beamer][] and the [metropolis][] theme. + +[beamer]: http://mirror.hmc.edu/ctan/macros/latex/contrib/beamer/ +[metropolis]: https://github.com/matze/mtheme diff --git a/users/tazjin/presentations/systemd-2016/demo/demo-error.service b/users/tazjin/presentations/systemd-2016/demo/demo-error.service new file mode 100644 index 000000000000..b2d4c9d34799 --- /dev/null +++ b/users/tazjin/presentations/systemd-2016/demo/demo-error.service @@ -0,0 +1,7 @@ +[Unit] +Description=Demonstrate failing units +OnFailure=demo-notify@%n.service + +[Service] +Type=oneshot +ExecStart=/usr/bin/false diff --git a/users/tazjin/presentations/systemd-2016/demo/demo-limits.slice b/users/tazjin/presentations/systemd-2016/demo/demo-limits.slice new file mode 100644 index 000000000000..998185d26177 --- /dev/null +++ b/users/tazjin/presentations/systemd-2016/demo/demo-limits.slice @@ -0,0 +1,7 @@ +[Unit] +Description=Limited resources demo +DefaultDependencies=no +Before=slices.target + +[Slice] +CPUQuota=10% diff --git a/users/tazjin/presentations/systemd-2016/demo/demo-notify@.service b/users/tazjin/presentations/systemd-2016/demo/demo-notify@.service new file mode 100644 index 000000000000..e25524b4e230 --- /dev/null +++ b/users/tazjin/presentations/systemd-2016/demo/demo-notify@.service @@ -0,0 +1,6 @@ +[Unit] +Description=Demonstrate systemd templating by sending a notification + +[Service] +Type=oneshot +ExecStart=/usr/bin/notify-send 'Systemd notification' '%i' diff --git a/users/tazjin/presentations/systemd-2016/demo/demo-path.path b/users/tazjin/presentations/systemd-2016/demo/demo-path.path new file mode 100644 index 000000000000..87f1342da995 --- /dev/null +++ b/users/tazjin/presentations/systemd-2016/demo/demo-path.path @@ -0,0 +1,6 @@ +[Unit] +Description=Demonstrate systemd path units + +[Path] +DirectoryNotEmpty=/tmp/hackeriet +Unit=demo.service diff --git a/users/tazjin/presentations/systemd-2016/demo/demo-stress.service b/users/tazjin/presentations/systemd-2016/demo/demo-stress.service new file mode 100644 index 000000000000..7e14f13e29d9 --- /dev/null +++ b/users/tazjin/presentations/systemd-2016/demo/demo-stress.service @@ -0,0 +1,6 @@ +[Unit] +Description=Stress test CPU + +[Service] +Slice=demo.slice +ExecStart=/usr/bin/stress -c 5 diff --git a/users/tazjin/presentations/systemd-2016/demo/demo-timer.timer b/users/tazjin/presentations/systemd-2016/demo/demo-timer.timer new file mode 100644 index 000000000000..34eccb98b02a --- /dev/null +++ b/users/tazjin/presentations/systemd-2016/demo/demo-timer.timer @@ -0,0 +1,12 @@ +[Unit] +Description=Demonstrate systemd timers + +[Timer] +OnActiveSec=2 +OnUnitActiveSec=5 +AccuracySec=5 +Unit=demo.service +# OnCalendar=Thu,Fri 2016-*-1,5 11:12:13 + +[Install] +WantedBy=multi-user.target diff --git a/users/tazjin/presentations/systemd-2016/demo/demo.service b/users/tazjin/presentations/systemd-2016/demo/demo.service new file mode 100644 index 000000000000..fcc710ad933f --- /dev/null +++ b/users/tazjin/presentations/systemd-2016/demo/demo.service @@ -0,0 +1,6 @@ +[Unit] +Description=Demo unit for systemd + +[Service] +Type=oneshot +ExecStart=/usr/bin/echo "Systemd unit activated. Hello Hackeriet." diff --git a/users/tazjin/presentations/systemd-2016/demo/notes.md b/users/tazjin/presentations/systemd-2016/demo/notes.md new file mode 100644 index 000000000000..b4866b1642bb --- /dev/null +++ b/users/tazjin/presentations/systemd-2016/demo/notes.md @@ -0,0 +1,27 @@ +# simple oneshot + +Run `demo-notify@hello.service` + +# simple timer + +Run `demo-timer.timer`, show both + +# enabling + +Enable `demo-timer.timer`, go to symlink folder, disable + +# OnError + +Show & run `demo-error.service` + +# cgroups demo + +Start `demo-stress.service` without, show in htop, stop +Show slice unit, start slice unit +Add Slice=demo-limits.slice +daemon-reload +Start stress again + +# Proper service + +Look at nginx unit diff --git a/users/tazjin/presentations/systemd-2016/slides.pdf b/users/tazjin/presentations/systemd-2016/slides.pdf new file mode 100644 index 000000000000..384db2a6e0af --- /dev/null +++ b/users/tazjin/presentations/systemd-2016/slides.pdf Binary files differdiff --git a/users/tazjin/presentations/systemd-2016/slides.pdfpc b/users/tazjin/presentations/systemd-2016/slides.pdfpc new file mode 100644 index 000000000000..99326bd8bf4e --- /dev/null +++ b/users/tazjin/presentations/systemd-2016/slides.pdfpc @@ -0,0 +1,85 @@ +[file] +slides.pdf +[notes] +### 1 +### 2 +Let's start off by looking at what an init system is, how they used to work and what systemd does different before we go into more systemd-specific details. +### 3 +system processes that are started include for example FS mounts, network settings, powertop... +system services are long-running processes such as daemons, e.g. SSH, database or web servers, session managers, udev ... + +orphans: Process whose parent has finished somehow, gets adopted by init system +-> when a process terminates its parent must call wait() to get its exit() code, if there is no init system adopting orphans the process would become a zombie +### 4 +Before systemd there were simple init systems that just did the tasks listed on the previous slide. +Init scripts -> increased greatly in complexity over time, look at incomprehensible skeleton for Debian service init scripts +Runlevels -> things such as single-user mode, full multiuser mode, reboot, halt + +Init will run all the scripts, but it will not do much more than print information on success/failure of started scripts + +Init scripts run strictly sequential + +Init is unaware of inter-service dependencies, expressed through prefixing scripts with numbers etc. + +Init will not watch processes after system is booted -> crashing daemons will not automatically restart +### 5 +### 6 +How systemd came to be + +Considering the lack of process monitoring, problematic things about init scripts -> legacy init systems have drawbacks + +Apple had already built launchd, a more featured init system that monitored running processes, could automatically restart them and allowed for certain advanced features -> however it is awful to use and wrap your head around + +Lennart Poettering of Pulseaudio fame and Kay Sievers decided to implement a new init system to address these problems, while taking certain clues from Apple's design +### 7 +Systemd's design goals +### 8 +No more init scripts with opaque effects -> services are clearly defined units +Unit dependencies -> systemd can figure out what can be started in parallel +Process supervision: Unit can be configured in many ways, e.g. always restart, only restart on success etc +Service logs: We'll talk more about this later +### 9 +Units are the core component of systemd that users deal with. They define services and everything else that systemd needs to start and manage. +Note that all these are the names of the respective man page on a system with systemd installed +Types: +systemd.service - processes controlled by systemd +systemd.target - equivalent to "runlevels", grouping of units for synchronisation +systemd.timer - more powerful replacement of cron that starts other units +systemd.path - systemd equvialent of inotify, watches files/folders -> launches units +systemd.socket - expose local IPC or network sockets, launch units on connections +systemd.device - trigger units when certain devices are connected +systemd.mount - systemd equivalent of fstab entries +systemd.swap - like mount +systemd.slice - unit groups for resource management purposes +... and a few more specialised ones +### 10 +Linux cgroups are a new resource management feature added quite a long time ago, but not used much. +Cgroups can be created manually and processes can be moved into them in order to control resource utilisation +Few people used them before systemd, limits.conf was often much easier but not as fine-grained +Systemd changed this +### 11 +Systemd collects standard output and stderr from all processes into its journal system +they provide a tool for querying the log, for example grouping service logs together with correct timestamps, querying, +### 12 +Systemd tooling, most important one is systemctl for general service management +journalctl is the query and management tool for journald +systemd-analyze is used for figuring out performance issues, for example by analysing the boot process, can make cool graphs of dependencies +systemd-cgtop is like top, but not on a process level - it's on a cgroup/slice level, shows combined usage of cgroups +systemd-cgls lists contents of systemd's cgroups to see which services are in what group +there also exist a bunch of others that we'll skip for now +### 13 +### 14 +### 15 +Systemd criticism comes from many directions and usually focuses on a few points +feature-creep: systemd is absorbing a lot of different services +### 16 +explain diagram a bit +### 17 +opaque: as a result, systemd has a lot more internal complexity that people can't easily wrap your mind around. However I argue that unless you're using something like suckless' sinit with your own scripts, you probably have no idea what your init does today anyways +unstable: this was definitely true even in the first stable release, with the binary log format getting corrupted for example. I haven't personally experienced any trouble with it recently though. +Another thing is that services start depending on systemd when they shouldn't, a problem for the BSD world (who cares (hey christoph!)) +### 18 +Despite criticism, systemd was adopted rapidly by large portions of the Linux +Initially in RedHat, because Poettering and co work there and it was clear from the beginning that it would be there +ArchLinux (which I'm using) and a few others followed suit quite quickly +Eventually, the big Debian init system discussion - after a lot of flaming - led to Debian adopting it as well, which had a ripple effect for related distros such as Ubuntu which abandoned upstart for it. \ No newline at end of file diff --git a/users/tazjin/presentations/systemd-2016/slides.tex b/users/tazjin/presentations/systemd-2016/slides.tex new file mode 100644 index 000000000000..c613cefd7ec4 --- /dev/null +++ b/users/tazjin/presentations/systemd-2016/slides.tex @@ -0,0 +1,160 @@ +\documentclass[12pt]{beamer} +\usetheme{metropolis} + +\newenvironment{code}{\ttfamily}{\par} + +\title{systemd} +\subtitle{The standard Linux init system} + +\begin{document} +\metroset{titleformat frame=smallcaps} + +\maketitle + +\section{Introduction} + +\begin{frame}{What is an init system?} + An init system is the first userspace process (PID 1) started in a UNIX-like system. It handles: + + \begin{itemize} + \item Starting system processes and services to prepare the environment + \item Adopting and ``reaping'' orphaned processes + \end{itemize} +\end{frame} + +\begin{frame}{Classical init systems} + Init systems before systemd - such as SysVinit - were very simple. + + \begin{itemize} + \item Services and processes to run are organised into ``init scripts'' + \item Scripts are linked to specific runlevels + \item Init system is configured to boot into a runlevel + \end{itemize} + +\end{frame} + +\section{systemd} + +\begin{frame}{Can we do better?} + \begin{itemize} + \item ``legacy'' init systems have a lot of drawbacks + \item Apple is taking a different approach on OS X + \item Systemd project was founded to address these issues + \end{itemize} +\end{frame} + +\begin{frame}{Systemd design goals} + \begin{itemize} + \item Expressing service dependencies + \item Monitoring service status + \item Enable parallel service startups + \item Ease of use + \end{itemize} +\end{frame} + +\begin{frame}{Systemd - the basics} + \begin{itemize} + \item No scripts are executed, only declarative units + \item Units have explicit dependencies + \item Processes are supervised + \item cgroups are utilised to apply resource limits + \item Service logs are managed and centrally queryable + \item Much more! + \end{itemize} +\end{frame} + +\begin{frame}{Systemd units} + Units specify how and what to start. Several types exist: + \begin{code} + \small + \begin{columns}[T,onlytextwidth] + \column{0.5\textwidth} + \begin{itemize} + \item systemd.service + \item systemd.target + \item systemd.timer + \item systemd.path + \item systemd.socket + \end{itemize} + \column{0.5\textwidth} + \begin{itemize} + \item systemd.device + \item systemd.mount + \item systemd.swap + \item systemd.slice + \end{itemize} + \end{columns} + \end{code} +\end{frame} + + +\begin{frame}{Resource management} + Systemd utilises Linux \texttt{cgroups} for resource management, specifically CPU, disk I/O and memory usage. + + \begin{itemize} + \item Hierarchical setup of groups makes it easy to limit resources for a set of services + \item Units can be attached to a \texttt{systemd.slice} for controlling resources for a group of services + \item Resource limits can also be specified directly in the unit + \end{itemize} +\end{frame} + +\begin{frame}{journald} + Systemd comes with an integrated log management solution, replacing software such as \texttt{syslog-ng}. + \begin{itemize} + \item All process output is collected in the journal + \item \texttt{journalctl} tool provides many options for querying and tailing logs + \item Children of processes automatically log to the journal as well + \item \textbf{Caveat:} Hard to learn initially + \end{itemize} +\end{frame} + +\begin{frame}{Systemd tooling} + A variety of CLI-tools exist for managing systemd systems. + \begin{code} + \begin{itemize} + \item systemctl + \item journalctl + \item systemd-analyze + \item systemd-cgtop + \item systemd-cgls + \end{itemize} + \end{code} + + Let's look at some of them. +\end{frame} + +\section{Demo} + +\section{Controversies} + +\begin{frame}{Systemd criticism} + Systemd has been heavily criticised, usually focusing around a few points: + \begin{itemize} + \item Feature-creep: Systemd absorbs more and more other services + \end{itemize} +\end{frame} + +\begin{frame}{Systemd criticism} + \includegraphics[keepaspectratio=true,width=\textwidth]{systemdcomponents.png} +\end{frame} + +\begin{frame}{Systemd criticism} + Systemd has been heavily criticised, usually focusing around a few points: + \begin{itemize} + \item Feature-creep: Systemd absorbs more and more other services + \item Opaque: systemd's inner workings are harder to understand than old \texttt{init} + \item Unstable: development is quick and breakage happens + \end{itemize} +\end{frame} + +\begin{frame}{Systemd adoption} + Systemd was initially adopted by RedHat (and related distributions). + + It spread quickly to others, for example ArchLinux. + + Debian and Ubuntu were the last major players who decided to adopt it, but not without drama. +\end{frame} + +\section{Questions?} + +\end{document} diff --git a/users/tazjin/presentations/systemd-2016/systemdcomponents.png b/users/tazjin/presentations/systemd-2016/systemdcomponents.png new file mode 100644 index 000000000000..a22c762f7e13 --- /dev/null +++ b/users/tazjin/presentations/systemd-2016/systemdcomponents.png Binary files differdiff --git a/users/tazjin/rlox/.gitignore b/users/tazjin/rlox/.gitignore new file mode 100644 index 000000000000..29e65519ba35 --- /dev/null +++ b/users/tazjin/rlox/.gitignore @@ -0,0 +1,3 @@ +result +/target +**/*.rs.bk diff --git a/users/tazjin/rlox/Cargo.lock b/users/tazjin/rlox/Cargo.lock new file mode 100644 index 000000000000..d8107726e067 --- /dev/null +++ b/users/tazjin/rlox/Cargo.lock @@ -0,0 +1,6 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "rlox" +version = "0.1.0" + diff --git a/users/tazjin/rlox/Cargo.toml b/users/tazjin/rlox/Cargo.toml new file mode 100644 index 000000000000..b66af6ba85d3 --- /dev/null +++ b/users/tazjin/rlox/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "rlox" +version = "0.1.0" +authors = ["Vincent Ambo <mail@tazj.in>"] +edition = "2018" + +[features] +# Enables debugging/disassembling in the bytecode interpreter. Off by +# default as it is quite spammy. +disassemble = [] diff --git a/users/tazjin/rlox/README.md b/users/tazjin/rlox/README.md new file mode 100644 index 000000000000..1d2692d09cc1 --- /dev/null +++ b/users/tazjin/rlox/README.md @@ -0,0 +1,7 @@ +This is an interpreter for the Lox language, based on the book "[Crafting +Interpreters](https://craftinginterpreters.com/)". + +The book's original code uses Java, but I don't want to use Java, so I've +decided to take on the extra complexity of porting it to Rust. + +Note: This implements the first of two Lox interpreters. diff --git a/users/tazjin/rlox/default.nix b/users/tazjin/rlox/default.nix new file mode 100644 index 000000000000..e50ac32be452 --- /dev/null +++ b/users/tazjin/rlox/default.nix @@ -0,0 +1,5 @@ +{ depot, ... }: + +depot.third_party.naersk.buildPackage { + src = ./.; +} diff --git a/users/tazjin/rlox/examples/builtins.lox b/users/tazjin/rlox/examples/builtins.lox new file mode 100644 index 000000000000..39af1d73c4c9 --- /dev/null +++ b/users/tazjin/rlox/examples/builtins.lox @@ -0,0 +1 @@ +print clock(); diff --git a/users/tazjin/rlox/examples/fib.lox b/users/tazjin/rlox/examples/fib.lox new file mode 100644 index 000000000000..1b91e9db94ac --- /dev/null +++ b/users/tazjin/rlox/examples/fib.lox @@ -0,0 +1,6 @@ +fun fib(n) { + if (n <= 1) return n; + return fib(n - 2) + fib(n - 1); +} + +print fib(30); diff --git a/users/tazjin/rlox/examples/func.lox b/users/tazjin/rlox/examples/func.lox new file mode 100644 index 000000000000..d197ad11383f --- /dev/null +++ b/users/tazjin/rlox/examples/func.lox @@ -0,0 +1,5 @@ +fun foo(name) { + print("hello " + name); +} + +foo("bar"); diff --git a/users/tazjin/rlox/examples/hello.lox b/users/tazjin/rlox/examples/hello.lox new file mode 100644 index 000000000000..31752d9e2f5e --- /dev/null +++ b/users/tazjin/rlox/examples/hello.lox @@ -0,0 +1,34 @@ +var a = 12; +var b = a * 2; + +{ + var b = a * 3; + a = 42; + print b; +} + +print a; +print b; + +if (5 > 4) + print "it's true"; +else + print "it's false"; + +if (false) + print "it's not true"; + +if (true and false) + print "won't happen"; + +if (true or false) + print "will happen"; + +var n = 5; +while (n > 0) { + print "counting down"; + n = n - 1; +} + +for(var i = 0; i < 10; i = i + 1) + print "bla"; diff --git a/users/tazjin/rlox/examples/if.lox b/users/tazjin/rlox/examples/if.lox new file mode 100644 index 000000000000..5f335c0e8b29 --- /dev/null +++ b/users/tazjin/rlox/examples/if.lox @@ -0,0 +1,7 @@ +if (false) { + print "yes"; +} else { + print "no"; +} + +print "afterwards"; diff --git a/users/tazjin/rlox/examples/scope.lox b/users/tazjin/rlox/examples/scope.lox new file mode 100644 index 000000000000..d563807943ba --- /dev/null +++ b/users/tazjin/rlox/examples/scope.lox @@ -0,0 +1,19 @@ +var a = "global a"; +var b = "global b"; +var c = "global c"; +{ + var a = "outer a"; + var b = "outer b"; + { + var a = "inner a"; + print a; + print b; + print c; + } + print a; + print b; + print c; +} +print a; +print b; +print c; diff --git a/users/tazjin/rlox/examples/scope2.lox b/users/tazjin/rlox/examples/scope2.lox new file mode 100644 index 000000000000..f826c8658803 --- /dev/null +++ b/users/tazjin/rlox/examples/scope2.lox @@ -0,0 +1,10 @@ +var a = "global"; +{ + fun showA() { + print a; + } + + showA(); + var a = "block"; + showA(); +} diff --git a/users/tazjin/rlox/examples/slow.lox b/users/tazjin/rlox/examples/slow.lox new file mode 100644 index 000000000000..dd6fb5e4bf4d --- /dev/null +++ b/users/tazjin/rlox/examples/slow.lox @@ -0,0 +1,9 @@ +fun fib(n) { + if (n < 2) return n; + return fib(n - 1) + fib(n - 2); +} + +var before = clock(); +print fib(40); +var after = clock(); +print after - before; diff --git a/users/tazjin/rlox/examples/var.lox b/users/tazjin/rlox/examples/var.lox new file mode 100644 index 000000000000..7af90b3f0bee --- /dev/null +++ b/users/tazjin/rlox/examples/var.lox @@ -0,0 +1,8 @@ +var a = 10; +var b = 5; + +{ + var b = 10; + var c = 2; + a * b * c; +} diff --git a/users/tazjin/rlox/rustfmt.toml b/users/tazjin/rlox/rustfmt.toml new file mode 100644 index 000000000000..df99c69198f5 --- /dev/null +++ b/users/tazjin/rlox/rustfmt.toml @@ -0,0 +1 @@ +max_width = 80 diff --git a/users/tazjin/rlox/src/bytecode/chunk.rs b/users/tazjin/rlox/src/bytecode/chunk.rs new file mode 100644 index 000000000000..fc5cd34fdf4f --- /dev/null +++ b/users/tazjin/rlox/src/bytecode/chunk.rs @@ -0,0 +1,93 @@ +use std::ops::Index; + +use super::opcode::{CodeIdx, ConstantIdx, OpCode}; +use super::value; + +// In the book, this type is a hand-rolled dynamic array +// implementation in C. The main benefit of following that approach +// would be avoiding issues with OpCode variants not having equal +// sizes, but for the purpose of this I'm going to ignore that +// problem. +#[derive(Debug, Default)] +pub struct Chunk { + pub code: Vec<OpCode>, + lines: Vec<Span>, + constants: Vec<value::Value>, +} + +#[derive(Debug)] +struct Span { + /// Source code line + line: usize, + + /// Number of instructions derived from this line + count: usize, +} + +impl Chunk { + pub fn add_op(&mut self, data: OpCode, line: usize) -> CodeIdx { + let idx = self.code.len(); + self.code.push(data); + self.add_line(line); + CodeIdx(idx) + } + + pub fn add_constant(&mut self, data: value::Value) -> usize { + let idx = self.constants.len(); + self.constants.push(data); + idx + } + + pub fn constant(&self, idx: ConstantIdx) -> &value::Value { + self.constants.index(idx.0) + } + + fn add_line(&mut self, line: usize) { + match self.lines.last_mut() { + Some(span) if span.line == line => span.count += 1, + _ => self.lines.push(Span { line, count: 1 }), + } + } + + pub fn get_line(&self, offset: usize) -> usize { + let mut pos = 0; + for span in &self.lines { + pos += span.count; + if pos > offset { + return span.line; + } + } + + panic!("invalid chunk state: line missing for offset {}", offset); + } +} + +// Disassembler + +/// Print a single disassembled instruction at the specified offset. +/// Some instructions are printed "raw", others have special handling. +#[cfg(feature = "disassemble")] +pub fn disassemble_instruction(chunk: &Chunk, offset: usize) { + print!("{:04} ", offset); + + let line = chunk.get_line(offset); + if offset > 0 && line == chunk.get_line(offset - 1) { + print!(" | "); + } else { + print!("{:4} ", line); + } + + match chunk.code.index(offset) { + OpCode::OpConstant(idx) => { + println!("OpConstant({:?}) '{:?}'", idx, chunk.constant(*idx)) + } + op => println!("{:?}", op), + } +} + +#[cfg(feature = "disassemble")] +pub fn disassemble_chunk(chunk: &Chunk) { + for (idx, _) in chunk.code.iter().enumerate() { + disassemble_instruction(chunk, idx); + } +} diff --git a/users/tazjin/rlox/src/bytecode/compiler.rs b/users/tazjin/rlox/src/bytecode/compiler.rs new file mode 100644 index 000000000000..89584f19d720 --- /dev/null +++ b/users/tazjin/rlox/src/bytecode/compiler.rs @@ -0,0 +1,702 @@ +use super::chunk::Chunk; +use super::errors::{Error, ErrorKind, LoxResult}; +use super::interner::{InternedStr, Interner}; +use super::opcode::{CodeIdx, CodeOffset, ConstantIdx, OpCode, StackIdx}; +use super::value::Value; +use crate::scanner::{self, Token, TokenKind}; + +#[cfg(feature = "disassemble")] +use super::chunk; + +#[derive(Debug)] +enum Depth { + Unitialised, + At(usize), +} + +impl Depth { + fn above(&self, theirs: usize) -> bool { + match self { + Depth::Unitialised => false, + Depth::At(ours) => *ours > theirs, + } + } + + fn below(&self, theirs: usize) -> bool { + match self { + Depth::Unitialised => false, + Depth::At(ours) => *ours < theirs, + } + } +} + +#[derive(Debug)] +struct Local { + name: Token, + depth: Depth, +} + +#[derive(Debug, Default)] +struct Locals { + locals: Vec<Local>, + scope_depth: usize, +} + +struct Compiler<T: Iterator<Item = Token>> { + tokens: T, + chunk: Chunk, + panic: bool, + errors: Vec<Error>, + strings: Interner, + locals: Locals, + + current: Option<Token>, + previous: Option<Token>, +} + +#[derive(Debug, PartialEq, PartialOrd)] +enum Precedence { + None, + Assignment, // = + Or, // or + And, // and + Equality, // == != + Comparison, // < > <= >= + Term, // + - + Factor, // + // + // * / + Unary, // ! - + Call, // . () + Primary, +} + +type ParseFn<T> = fn(&mut Compiler<T>) -> LoxResult<()>; + +struct ParseRule<T: Iterator<Item = Token>> { + prefix: Option<ParseFn<T>>, + infix: Option<ParseFn<T>>, + precedence: Precedence, +} + +impl<T: Iterator<Item = Token>> ParseRule<T> { + fn new(prefix: Option<ParseFn<T>>, infix: Option<ParseFn<T>>, precedence: Precedence) -> Self { + ParseRule { + prefix, + infix, + precedence, + } + } +} + +impl Precedence { + // Return the next highest precedence, if there is one. + fn next(&self) -> Self { + match self { + Precedence::None => Precedence::Assignment, + Precedence::Assignment => Precedence::Or, + Precedence::Or => Precedence::And, + Precedence::And => Precedence::Equality, + Precedence::Equality => Precedence::Comparison, + Precedence::Comparison => Precedence::Term, + Precedence::Term => Precedence::Factor, + Precedence::Factor => Precedence::Unary, + Precedence::Unary => Precedence::Call, + Precedence::Call => Precedence::Primary, + Precedence::Primary => { + panic!("invalid parser state: no higher precedence than Primary") + } + } + } +} + +fn rule_for<T: Iterator<Item = Token>>(token: &TokenKind) -> ParseRule<T> { + match token { + TokenKind::LeftParen => ParseRule::new(Some(Compiler::grouping), None, Precedence::None), + + TokenKind::Minus => ParseRule::new( + Some(Compiler::unary), + Some(Compiler::binary), + Precedence::Term, + ), + + TokenKind::Plus => ParseRule::new(None, Some(Compiler::binary), Precedence::Term), + + TokenKind::Slash => ParseRule::new(None, Some(Compiler::binary), Precedence::Factor), + + TokenKind::Star => ParseRule::new(None, Some(Compiler::binary), Precedence::Factor), + + TokenKind::Number(_) => ParseRule::new(Some(Compiler::number), None, Precedence::None), + + TokenKind::True => ParseRule::new(Some(Compiler::literal), None, Precedence::None), + + TokenKind::False => ParseRule::new(Some(Compiler::literal), None, Precedence::None), + + TokenKind::Nil => ParseRule::new(Some(Compiler::literal), None, Precedence::None), + + TokenKind::Bang => ParseRule::new(Some(Compiler::unary), None, Precedence::None), + + TokenKind::BangEqual => ParseRule::new(None, Some(Compiler::binary), Precedence::Equality), + + TokenKind::EqualEqual => ParseRule::new(None, Some(Compiler::binary), Precedence::Equality), + + TokenKind::Greater => ParseRule::new(None, Some(Compiler::binary), Precedence::Comparison), + + TokenKind::GreaterEqual => { + ParseRule::new(None, Some(Compiler::binary), Precedence::Comparison) + } + + TokenKind::Less => ParseRule::new(None, Some(Compiler::binary), Precedence::Comparison), + + TokenKind::LessEqual => { + ParseRule::new(None, Some(Compiler::binary), Precedence::Comparison) + } + + TokenKind::Identifier(_) => { + ParseRule::new(Some(Compiler::variable), None, Precedence::None) + } + + TokenKind::String(_) => ParseRule::new(Some(Compiler::string), None, Precedence::None), + + _ => ParseRule::new(None, None, Precedence::None), + } +} + +macro_rules! consume { + ( $self:ident, $expected:pat, $err:expr ) => { + match $self.current().kind { + $expected => $self.advance(), + _ => $self.error_at($self.current().line, $err), + } + }; +} + +impl<T: Iterator<Item = Token>> Compiler<T> { + fn compile(&mut self) -> LoxResult<()> { + self.advance(); + + while !self.match_token(&TokenKind::Eof) { + self.declaration()?; + } + + self.end_compiler() + } + + fn advance(&mut self) { + self.previous = self.current.take(); + self.current = self.tokens.next(); + } + + fn expression(&mut self) -> LoxResult<()> { + self.parse_precedence(Precedence::Assignment) + } + + fn var_declaration(&mut self) -> LoxResult<()> { + let idx = self.parse_variable()?; + + if self.match_token(&TokenKind::Equal) { + self.expression()?; + } else { + self.emit_op(OpCode::OpNil); + } + + self.expect_semicolon("expect ';' after variable declaration")?; + self.define_variable(idx) + } + + fn define_variable(&mut self, var: Option<ConstantIdx>) -> LoxResult<()> { + if self.locals.scope_depth == 0 { + self.emit_op(OpCode::OpDefineGlobal(var.expect("should be global"))); + } else { + self.locals + .locals + .last_mut() + .expect("fatal: variable not yet added at definition") + .depth = Depth::At(self.locals.scope_depth); + } + + Ok(()) + } + + fn declaration(&mut self) -> LoxResult<()> { + if self.match_token(&TokenKind::Var) { + self.var_declaration()?; + } else { + self.statement()?; + } + + if self.panic { + self.synchronise(); + } + + Ok(()) + } + + fn statement(&mut self) -> LoxResult<()> { + if self.match_token(&TokenKind::Print) { + self.print_statement() + } else if self.match_token(&TokenKind::If) { + self.if_statement() + } else if self.match_token(&TokenKind::LeftBrace) { + self.begin_scope(); + self.block()?; + self.end_scope(); + Ok(()) + } else { + self.expression_statement() + } + } + + fn print_statement(&mut self) -> LoxResult<()> { + self.expression()?; + self.expect_semicolon("expect ';' after print statement")?; + self.emit_op(OpCode::OpPrint); + Ok(()) + } + + fn begin_scope(&mut self) { + self.locals.scope_depth += 1; + } + + fn end_scope(&mut self) { + debug_assert!(self.locals.scope_depth > 0, "tried to end global scope"); + self.locals.scope_depth -= 1; + + while self.locals.locals.len() > 0 + && self.locals.locals[self.locals.locals.len() - 1] + .depth + .above(self.locals.scope_depth) + { + self.emit_op(OpCode::OpPop); + self.locals.locals.remove(self.locals.locals.len() - 1); + } + } + + fn block(&mut self) -> LoxResult<()> { + while !self.check(&TokenKind::RightBrace) && !self.check(&TokenKind::Eof) { + self.declaration()?; + } + + consume!( + self, + TokenKind::RightBrace, + ErrorKind::ExpectedToken("Expected '}' after block.") + ); + Ok(()) + } + + fn expression_statement(&mut self) -> LoxResult<()> { + self.expression()?; + self.expect_semicolon("expect ';' after expression")?; + // TODO(tazjin): Why did I add this originally? + // self.emit_op(OpCode::OpPop); + Ok(()) + } + + fn if_statement(&mut self) -> LoxResult<()> { + consume!( + self, + TokenKind::LeftParen, + ErrorKind::ExpectedToken("Expected '(' after 'if'") + ); + + self.expression()?; + + consume!( + self, + TokenKind::RightParen, + ErrorKind::ExpectedToken("Expected ')' after condition") + ); + + let then_jump = self.emit_op(OpCode::OpJumpPlaceholder(false)); + self.emit_op(OpCode::OpPop); + self.statement()?; + let else_jump = self.emit_op(OpCode::OpJumpPlaceholder(true)); + self.patch_jump(then_jump); + self.emit_op(OpCode::OpPop); + + if self.match_token(&TokenKind::Else) { + self.statement()?; + } + + self.patch_jump(else_jump); + + Ok(()) + } + + fn number(&mut self) -> LoxResult<()> { + if let TokenKind::Number(num) = self.previous().kind { + self.emit_constant(Value::Number(num), true); + return Ok(()); + } + + unreachable!("internal parser error: entered number() incorrectly") + } + + fn grouping(&mut self) -> LoxResult<()> { + self.expression()?; + consume!( + self, + TokenKind::RightParen, + ErrorKind::ExpectedToken("Expected ')' after expression") + ); + Ok(()) + } + + fn unary(&mut self) -> LoxResult<()> { + // TODO(tazjin): Avoid clone + let kind = self.previous().kind.clone(); + + // Compile the operand + self.parse_precedence(Precedence::Unary)?; + + // Emit operator instruction + match kind { + TokenKind::Bang => self.emit_op(OpCode::OpNot), + TokenKind::Minus => self.emit_op(OpCode::OpNegate), + _ => unreachable!("only called for unary operator tokens"), + }; + + Ok(()) + } + + fn binary(&mut self) -> LoxResult<()> { + // Remember the operator + let operator = self.previous().kind.clone(); + + // Compile the right operand + let rule: ParseRule<T> = rule_for(&operator); + self.parse_precedence(rule.precedence.next())?; + + // Emit operator instruction + match operator { + TokenKind::Minus => self.emit_op(OpCode::OpSubtract), + TokenKind::Plus => self.emit_op(OpCode::OpAdd), + TokenKind::Star => self.emit_op(OpCode::OpMultiply), + TokenKind::Slash => self.emit_op(OpCode::OpDivide), + + TokenKind::BangEqual => { + self.emit_op(OpCode::OpEqual); + self.emit_op(OpCode::OpNot) + } + + TokenKind::EqualEqual => self.emit_op(OpCode::OpEqual), + TokenKind::Greater => self.emit_op(OpCode::OpGreater), + + TokenKind::GreaterEqual => { + self.emit_op(OpCode::OpLess); + self.emit_op(OpCode::OpNot) + } + + TokenKind::Less => self.emit_op(OpCode::OpLess), + TokenKind::LessEqual => { + self.emit_op(OpCode::OpGreater); + self.emit_op(OpCode::OpNot) + } + + _ => unreachable!("only called for binary operator tokens"), + }; + + Ok(()) + } + + fn literal(&mut self) -> LoxResult<()> { + match self.previous().kind { + TokenKind::Nil => self.emit_op(OpCode::OpNil), + TokenKind::True => self.emit_op(OpCode::OpTrue), + TokenKind::False => self.emit_op(OpCode::OpFalse), + _ => unreachable!("only called for literal value tokens"), + }; + + Ok(()) + } + + fn string(&mut self) -> LoxResult<()> { + let val = match &self.previous().kind { + TokenKind::String(s) => s.clone(), + _ => unreachable!("only called for strings"), + }; + + let id = self.strings.intern(val); + self.emit_constant(Value::String(id.into()), true); + + Ok(()) + } + + fn named_variable(&mut self, name: Token) -> LoxResult<()> { + let local_idx = self.resolve_local(&name); + + let ident = if local_idx.is_some() { + None + } else { + Some(self.identifier_constant(&name)?) + }; + + if self.match_token(&TokenKind::Equal) { + self.expression()?; + match local_idx { + Some(idx) => self.emit_op(OpCode::OpSetLocal(idx)), + None => self.emit_op(OpCode::OpSetGlobal(ident.unwrap())), + }; + } else { + match local_idx { + Some(idx) => self.emit_op(OpCode::OpGetLocal(idx)), + None => self.emit_op(OpCode::OpGetGlobal(ident.unwrap())), + }; + } + + Ok(()) + } + + fn variable(&mut self) -> LoxResult<()> { + let name = self.previous().clone(); + self.named_variable(name) + } + + fn parse_precedence(&mut self, precedence: Precedence) -> LoxResult<()> { + self.advance(); + let rule: ParseRule<T> = rule_for(&self.previous().kind); + let prefix_fn = match rule.prefix { + None => unimplemented!("expected expression or something, unclear"), + Some(func) => func, + }; + + prefix_fn(self)?; + + while precedence <= rule_for::<T>(&self.current().kind).precedence { + self.advance(); + match rule_for::<T>(&self.previous().kind).infix { + Some(func) => { + func(self)?; + } + None => { + unreachable!("invalid compiler state: error in parse rules") + } + } + } + + Ok(()) + } + + fn identifier_str(&mut self, token: &Token) -> LoxResult<InternedStr> { + let ident = match &token.kind { + TokenKind::Identifier(ident) => ident.to_string(), + _ => { + return Err(Error { + line: self.current().line, + kind: ErrorKind::ExpectedToken("Expected identifier"), + }) + } + }; + + Ok(self.strings.intern(ident)) + } + + fn identifier_constant(&mut self, name: &Token) -> LoxResult<ConstantIdx> { + let ident = self.identifier_str(name)?; + Ok(self.emit_constant(Value::String(ident.into()), false)) + } + + fn resolve_local(&self, name: &Token) -> Option<StackIdx> { + for (idx, local) in self.locals.locals.iter().enumerate().rev() { + if name.lexeme == local.name.lexeme { + if let Depth::Unitialised = local.depth { + // TODO(tazjin): *return* err + panic!("can't read variable in its own initialiser"); + } + return Some(StackIdx(idx)); + } + } + + None + } + + fn add_local(&mut self, name: Token) { + let local = Local { + name, + depth: Depth::Unitialised, + }; + + self.locals.locals.push(local); + } + + fn declare_variable(&mut self) -> LoxResult<()> { + if self.locals.scope_depth == 0 { + return Ok(()); + } + + let name = self.previous().clone(); + + for local in self.locals.locals.iter().rev() { + if local.depth.below(self.locals.scope_depth) { + break; + } + + if name.lexeme == local.name.lexeme { + return Err(Error { + kind: ErrorKind::VariableShadowed(name.lexeme.into()), + line: name.line, + }); + } + } + + self.add_local(name); + Ok(()) + } + + fn parse_variable(&mut self) -> LoxResult<Option<ConstantIdx>> { + consume!( + self, + TokenKind::Identifier(_), + ErrorKind::ExpectedToken("expected identifier") + ); + + self.declare_variable()?; + if self.locals.scope_depth > 0 { + return Ok(None); + } + + let name = self.previous().clone(); + let id = self.identifier_str(&name)?; + Ok(Some(self.emit_constant(Value::String(id.into()), false))) + } + + fn current_chunk(&mut self) -> &mut Chunk { + &mut self.chunk + } + + fn end_compiler(&mut self) -> LoxResult<()> { + self.emit_op(OpCode::OpReturn); + + #[cfg(feature = "disassemble")] + { + chunk::disassemble_chunk(&self.chunk); + println!("== compilation finished =="); + } + + Ok(()) + } + + fn emit_op(&mut self, op: OpCode) -> CodeIdx { + let line = self.previous().line; + self.current_chunk().add_op(op, line) + } + + fn emit_constant(&mut self, val: Value, with_op: bool) -> ConstantIdx { + let idx = ConstantIdx(self.chunk.add_constant(val)); + + if with_op { + self.emit_op(OpCode::OpConstant(idx)); + } + + idx + } + + fn patch_jump(&mut self, idx: CodeIdx) { + let offset = CodeOffset(self.chunk.code.len() - idx.0 - 1); + + if let OpCode::OpJumpPlaceholder(true) = self.chunk.code[idx.0] { + self.chunk.code[idx.0] = OpCode::OpJump(offset); + return; + } + + if let OpCode::OpJumpPlaceholder(false) = self.chunk.code[idx.0] { + self.chunk.code[idx.0] = OpCode::OpJumpIfFalse(offset); + return; + } + + panic!( + "attempted to patch unsupported op: {:?}", + self.chunk.code[idx.0] + ); + } + + fn previous(&self) -> &Token { + self.previous + .as_ref() + .expect("invalid internal compiler state: missing previous token") + } + + fn current(&self) -> &Token { + self.current + .as_ref() + .expect("invalid internal compiler state: missing current token") + } + + fn error_at(&mut self, line: usize, kind: ErrorKind) { + if self.panic { + return; + } + + self.panic = true; + self.errors.push(Error { kind, line }) + } + + fn match_token(&mut self, token: &TokenKind) -> bool { + if !self.check(token) { + return false; + } + + self.advance(); + true + } + + fn check(&self, token: &TokenKind) -> bool { + return self.current().kind == *token; + } + + fn synchronise(&mut self) { + self.panic = false; + + while self.current().kind != TokenKind::Eof { + if self.previous().kind == TokenKind::Semicolon { + return; + } + + match self.current().kind { + TokenKind::Class + | TokenKind::Fun + | TokenKind::Var + | TokenKind::For + | TokenKind::If + | TokenKind::While + | TokenKind::Print + | TokenKind::Return => return, + + _ => { + self.advance(); + } + } + } + } + + fn expect_semicolon(&mut self, msg: &'static str) -> LoxResult<()> { + consume!(self, TokenKind::Semicolon, ErrorKind::ExpectedToken(msg)); + Ok(()) + } +} + +pub fn compile(code: &str) -> Result<(Interner, Chunk), Vec<Error>> { + let chars = code.chars().collect::<Vec<char>>(); + let tokens = scanner::scan(&chars) + .map_err(|errors| errors.into_iter().map(Into::into).collect::<Vec<Error>>())?; + + let mut compiler = Compiler { + tokens: tokens.into_iter().peekable(), + chunk: Default::default(), + panic: false, + errors: vec![], + strings: Interner::with_capacity(1024), + locals: Default::default(), + current: None, + previous: None, + }; + + compiler.compile()?; + + if compiler.errors.is_empty() { + Ok((compiler.strings, compiler.chunk)) + } else { + Err(compiler.errors) + } +} diff --git a/users/tazjin/rlox/src/bytecode/errors.rs b/users/tazjin/rlox/src/bytecode/errors.rs new file mode 100644 index 000000000000..988031f763cf --- /dev/null +++ b/users/tazjin/rlox/src/bytecode/errors.rs @@ -0,0 +1,51 @@ +use crate::scanner::ScannerError; + +use std::fmt; + +#[derive(Debug)] +pub enum ErrorKind { + UnexpectedChar(char), + UnterminatedString, + ExpectedToken(&'static str), + InternalError(&'static str), + TypeError(String), + VariableShadowed(String), +} + +#[derive(Debug)] +pub struct Error { + pub kind: ErrorKind, + pub line: usize, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "[line NYI] Error: {:?}", self.kind) + } +} + +impl From<ScannerError> for Error { + fn from(err: ScannerError) -> Self { + match err { + ScannerError::UnexpectedChar { line, unexpected } => Error { + line, + kind: ErrorKind::UnexpectedChar(unexpected), + }, + + ScannerError::UnterminatedString { line } => Error { + line, + kind: ErrorKind::UnterminatedString, + }, + } + } +} + +// Convenience implementation as we're often dealing with vectors of +// errors (to report as many issues as possible before terminating) +impl From<Error> for Vec<Error> { + fn from(err: Error) -> Self { + vec![err] + } +} + +pub type LoxResult<T> = Result<T, Error>; diff --git a/users/tazjin/rlox/src/bytecode/interner/mod.rs b/users/tazjin/rlox/src/bytecode/interner/mod.rs new file mode 100644 index 000000000000..1da1a24b2c5f --- /dev/null +++ b/users/tazjin/rlox/src/bytecode/interner/mod.rs @@ -0,0 +1,87 @@ +//! String-interning implementation for values that are likely to +//! benefit from fast comparisons and deduplication (e.g. instances of +//! variable names). +//! +//! This uses a trick from the typed-arena crate for guaranteeing +//! stable addresses by never resizing the existing String buffer, and +//! collecting full buffers in a vector. + +use std::collections::HashMap; + +#[cfg(test)] +mod tests; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct InternedStr { + id: usize, +} + +#[derive(Default)] +pub struct Interner { + map: HashMap<&'static str, InternedStr>, + vec: Vec<&'static str>, + buf: String, + full: Vec<String>, +} + +impl Interner { + pub fn with_capacity(cap: usize) -> Self { + Interner { + buf: String::with_capacity(cap), + ..Default::default() + } + } + + pub fn intern<S: AsRef<str>>(&mut self, name: S) -> InternedStr { + let name = name.as_ref(); + if let Some(&id) = self.map.get(name) { + return id; + } + + let name = self.alloc(name); + let id = InternedStr { + id: self.vec.len() as usize, + }; + + self.map.insert(name, id); + self.vec.push(name); + + debug_assert!(self.lookup(id) == name); + debug_assert!(self.intern(name) == id); + + id + } + + pub fn lookup<'a>(&'a self, id: InternedStr) -> &'a str { + self.vec[id.id] + } + + fn alloc<'a>(&'a mut self, name: &str) -> &'static str { + let cap = self.buf.capacity(); + if cap < self.buf.len() + name.len() { + let new_cap = (cap.max(name.len()) + 1).next_power_of_two(); + let new_buf = String::with_capacity(new_cap); + let old_buf = std::mem::replace(&mut self.buf, new_buf); + self.full.push(old_buf); + } + + let interned: &'a str = { + let start = self.buf.len(); + self.buf.push_str(name); + &self.buf[start..] + }; + + unsafe { + // This is sound for two reasons: + // + // 1. This function (Interner::alloc) is private, which + // prevents users from allocating a supposedly static + // reference. + // + // 2. Interner::lookup explicitly shortens the lifetime of + // references that are handed out to that of the + // reference to self. + return &*(interned as *const str); + } + } +} diff --git a/users/tazjin/rlox/src/bytecode/interner/tests.rs b/users/tazjin/rlox/src/bytecode/interner/tests.rs new file mode 100644 index 000000000000..b34bf6835389 --- /dev/null +++ b/users/tazjin/rlox/src/bytecode/interner/tests.rs @@ -0,0 +1,24 @@ +use super::*; + +#[test] +fn interns_strings() { + let mut interner = Interner::with_capacity(128); + let id = interner.intern("hello world"); + assert_eq!("hello world", interner.lookup(id)); +} + +#[test] +fn deduplicates_strings() { + let mut interner = Interner::with_capacity(128); + let id_1 = interner.intern("hello world"); + let id_2 = interner.intern("hello world"); + assert_eq!(id_1, id_2); +} + +#[test] +fn ids_survive_growing() { + let mut interner = Interner::with_capacity(16); + let id = interner.intern("hello"); + interner.intern("excessively large string that will cause eallocation"); + assert_eq!("hello", interner.lookup(id)); +} diff --git a/users/tazjin/rlox/src/bytecode/mod.rs b/users/tazjin/rlox/src/bytecode/mod.rs new file mode 100644 index 000000000000..117f17824ac6 --- /dev/null +++ b/users/tazjin/rlox/src/bytecode/mod.rs @@ -0,0 +1,30 @@ +//! Bytecode interpreter for Lox. +//! +//! https://craftinginterpreters.com/chunks-of-bytecode.html + +mod chunk; +mod compiler; +mod errors; +mod interner; +mod opcode; +mod value; +mod vm; + +#[cfg(test)] +mod tests; + +pub struct Interpreter {} + +impl crate::Lox for Interpreter { + type Error = errors::Error; + type Value = value::Value; + + fn create() -> Self { + Interpreter {} + } + + fn interpret(&mut self, code: String) -> Result<Self::Value, Vec<Self::Error>> { + let (strings, chunk) = compiler::compile(&code)?; + vm::interpret(strings, chunk).map_err(|e| vec![e]) + } +} diff --git a/users/tazjin/rlox/src/bytecode/opcode.rs b/users/tazjin/rlox/src/bytecode/opcode.rs new file mode 100644 index 000000000000..8a106f96917d --- /dev/null +++ b/users/tazjin/rlox/src/bytecode/opcode.rs @@ -0,0 +1,56 @@ +#[derive(Clone, Copy, Debug)] +pub struct ConstantIdx(pub usize); + +#[derive(Clone, Copy, Debug)] +pub struct StackIdx(pub usize); + +#[derive(Clone, Copy, Debug)] +pub struct CodeIdx(pub usize); + +#[derive(Clone, Copy, Debug)] +pub struct CodeOffset(pub usize); + +#[derive(Debug)] +pub enum OpCode { + /// Push a constant onto the stack. + OpConstant(ConstantIdx), + + // Literal pushes + OpNil, + OpTrue, + OpFalse, + + /// Return from the current function. + OpReturn, + + // Boolean & comparison operators + OpNot, + OpEqual, + OpGreater, + OpLess, + + /// Unary negation + OpNegate, + + // Arithmetic operators + OpAdd, + OpSubtract, + OpMultiply, + OpDivide, + + // Built in operations + OpPrint, + OpPop, + + // Variable management + OpDefineGlobal(ConstantIdx), + OpGetGlobal(ConstantIdx), + OpSetGlobal(ConstantIdx), + OpGetLocal(StackIdx), + OpSetLocal(StackIdx), + + // Control flow + OpJumpPlaceholder(bool), + OpJump(CodeOffset), + OpJumpIfFalse(CodeOffset), +} diff --git a/users/tazjin/rlox/src/bytecode/tests.rs b/users/tazjin/rlox/src/bytecode/tests.rs new file mode 100644 index 000000000000..bc7d6cb878f8 --- /dev/null +++ b/users/tazjin/rlox/src/bytecode/tests.rs @@ -0,0 +1,152 @@ +use super::value::Value; +use super::*; + +use crate::Lox; + +fn expect(code: &str, value: Value) { + let result = Interpreter::create() + .interpret(code.into()) + .expect("evaluation failed"); + assert_eq!(result, value); +} + +fn expect_num(code: &str, value: f64) { + expect(code, Value::Number(value)) +} + +fn expect_bool(code: &str, value: bool) { + expect(code, Value::Bool(value)) +} + +fn expect_str(code: &str, value: &str) { + expect(code, Value::String(value.to_string().into())) +} + +#[test] +fn numbers() { + expect_num("1;", 1.0); + expect_num("13.37;", 13.37); +} + +#[test] +fn negative_numbers() { + // Note: This technically tests unary operators. + expect_num("-1;", -1.0); + expect_num("-13.37;", -13.37); +} + +#[test] +fn terms() { + expect_num("1 + 2;", 3.0); + expect_num("3 - 1;", 2.0); + expect_num("0.7 + 0.3;", 1.0); + expect_num("1 + -3;", -2.0); + expect_num("-1 - -1;", 0.0); + expect_num("10 - -10 + 10;", 30.0); +} + +#[test] +fn factors() { + expect_num("1 * 2;", 2.0); + expect_num("10 / 5;", 2.0); + expect_num("0.7 * 4 / 1.4;", 2.0); + expect_num("10 * -10 / 10;", -10.0); +} + +#[test] +fn arithmetic() { + expect_num("10 - 3 * 2;", 4.0); + expect_num("-4 * -4 + (14 - 5);", 25.0); + expect_num("(702 + 408) - ((239 - 734) / -5) + -4;", 1007.0); +} + +#[test] +fn trivial_literals() { + expect("true;", Value::Bool(true)); + expect("false;", Value::Bool(false)); + expect("nil;", Value::Nil); +} + +#[test] +fn negation() { + expect_bool("!true;", false); + expect_bool("!false;", true); + expect_bool("!nil;", true); + expect_bool("!13.5;", false); + expect_bool("!-42;", false); +} + +#[test] +fn equality() { + expect_bool("42 == 42;", true); + expect_bool("42 != 42;", false); + expect_bool("42 == 42.0;", true); + + expect_bool("true == true;", true); + expect_bool("true == false;", false); + expect_bool("true == !false;", true); + expect_bool("true != true;", false); + expect_bool("true != false;", true); + + expect_bool("42 == false;", false); + expect_bool("42 == true;", false); + expect_bool("!42 == !true;", true); +} + +#[test] +fn comparisons() { + expect_bool("42 > 23;", true); + expect_bool("42 < 23;", false); + expect_bool("42 <= 42;", true); + expect_bool("42 <= 23;", false); + expect_bool("42 >= 42;", true); + expect_bool("42 >= 23;", true); +} + +#[test] +fn strings() { + expect_str("\"hello\";", "hello"); + expect_str("\"hello\" + \" world\";", "hello world"); +} + +#[test] +fn global_variables() { + expect_num("var a = 5; a;", 5.0); + expect_num("var a = 5; var b = 2; a * b;", 10.0); + expect_str( + "var greeting = \"hello\"; var name = \"Zubnog\"; greeting + \" \" + name;", + "hello Zubnog", + ); +} + +#[test] +fn global_assignment() { + expect_str( + r#" + var breakfast = "beignets"; + var beverage = "cafe au lait"; + breakfast = "beignets with " + beverage; + breakfast; + "#, + "beignets with cafe au lait", + ); +} + +#[test] +fn local_variables() { + expect_num( + r#" + var a = 10; + var b = 5; + var result = 0; + { + var b = 10; + var c = 2; + result = a * b * c; + } + + result; + "#, + 200.0, + ); +} diff --git a/users/tazjin/rlox/src/bytecode/value.rs b/users/tazjin/rlox/src/bytecode/value.rs new file mode 100644 index 000000000000..4170efadf8fe --- /dev/null +++ b/users/tazjin/rlox/src/bytecode/value.rs @@ -0,0 +1,37 @@ +use super::interner::InternedStr; + +#[derive(Clone, Debug, PartialEq)] +pub enum Value { + Nil, + Bool(bool), + Number(f64), + String(LoxString), +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum LoxString { + Heap(String), + Interned(InternedStr), +} + +impl From<String> for LoxString { + fn from(s: String) -> Self { + LoxString::Heap(s) + } +} + +impl From<InternedStr> for LoxString { + fn from(s: InternedStr) -> Self { + LoxString::Interned(s) + } +} + +impl Value { + pub fn is_falsey(&self) -> bool { + match self { + Value::Nil => true, + Value::Bool(false) => true, + _ => false, + } + } +} diff --git a/users/tazjin/rlox/src/bytecode/vm.rs b/users/tazjin/rlox/src/bytecode/vm.rs new file mode 100644 index 000000000000..30ffebc79cf7 --- /dev/null +++ b/users/tazjin/rlox/src/bytecode/vm.rs @@ -0,0 +1,272 @@ +use std::collections::HashMap; + +use super::chunk; +use super::errors::*; +use super::interner::Interner; +use super::opcode::OpCode; +use super::value::{LoxString, Value}; + +pub struct VM { + chunk: chunk::Chunk, + + // TODO(tazjin): Accessing array elements constantly is not ideal, + // lets see if something clever can be done with iterators. + ip: usize, + + stack: Vec<Value>, + strings: Interner, + + globals: HashMap<LoxString, Value>, + + // Operations that consume values from the stack without pushing + // anything leave their last value in this slot, which makes it + // possible to return values from interpreters that ran code which + // ended with a statement. + last_drop: Option<Value>, +} + +impl VM { + fn push(&mut self, value: Value) { + self.stack.push(value) + } + + fn pop(&mut self) -> Value { + self.stack.pop().expect("fatal error: stack empty!") + } +} + +macro_rules! with_type { + ( $self:ident, $val:ident, $type:pat, $body:expr ) => { + match $val { + $type => $body, + _ => { + return Err(Error { + line: $self.chunk.get_line($self.ip - 1), + kind: ErrorKind::TypeError(format!( + "Expected type {}, but found value: {:?}", + stringify!($type), + $val, + )), + }) + } + } + }; +} + +macro_rules! binary_op { + ( $vm:ident, $type:tt, $op:tt ) => { + binary_op!($vm, $type, $type, $op) + }; + + ( $vm:ident, $in_type:tt, $out_type:tt, $op:tt ) => {{ + let b = $vm.pop(); + let a = $vm.pop(); + + with_type!($vm, b, Value::$in_type(val_b), { + with_type!($vm, a, Value::$in_type(val_a), { + $vm.push(Value::$out_type(val_a $op val_b)) + }) + }) + }}; +} + +impl VM { + fn run(&mut self) -> LoxResult<Value> { + loop { + let op = &self.chunk.code[self.ip]; + + #[cfg(feature = "disassemble")] + chunk::disassemble_instruction(&self.chunk, self.ip); + + self.ip += 1; + + match op { + OpCode::OpReturn => { + if !self.stack.is_empty() { + let val = self.pop(); + return Ok(self.return_value(val)); + } else if self.last_drop.is_some() { + let val = self.last_drop.take().unwrap(); + return Ok(self.return_value(val)); + } else { + return Ok(Value::Nil); + } + } + + OpCode::OpConstant(idx) => { + let c = self.chunk.constant(*idx).clone(); + self.push(c); + } + + OpCode::OpNil => self.push(Value::Nil), + OpCode::OpTrue => self.push(Value::Bool(true)), + OpCode::OpFalse => self.push(Value::Bool(false)), + + OpCode::OpNot => { + let v = self.pop(); + self.push(Value::Bool(v.is_falsey())); + } + + OpCode::OpEqual => { + let b = self.pop(); + let a = self.pop(); + self.push(Value::Bool(a == b)); + } + + OpCode::OpLess => binary_op!(self, Number, Bool, <), + OpCode::OpGreater => binary_op!(self, Number, Bool, >), + + OpCode::OpNegate => { + let v = self.pop(); + with_type!(self, v, Value::Number(num), self.push(Value::Number(-num))); + } + + OpCode::OpSubtract => binary_op!(self, Number, -), + OpCode::OpMultiply => binary_op!(self, Number, *), + OpCode::OpDivide => binary_op!(self, Number, /), + + OpCode::OpAdd => { + let b = self.pop(); + let a = self.pop(); + + match (a, b) { + (Value::String(s_a), Value::String(s_b)) => { + let mut new_s = self.resolve_str(&s_a).to_string(); + new_s.push_str(self.resolve_str(&s_b)); + self.push(Value::String(new_s.into())); + } + + (Value::Number(n_a), Value::Number(n_b)) => { + self.push(Value::Number(n_a + n_b)) + } + + _ => { + return Err(Error { + line: self.chunk.get_line(self.ip - 1), + kind: ErrorKind::TypeError( + "'+' operator only works on strings and numbers".into(), + ), + }) + } + } + } + + OpCode::OpPrint => { + let val = self.pop(); + println!("{}", self.print_value(val)); + } + + OpCode::OpPop => { + self.last_drop = Some(self.pop()); + } + + OpCode::OpDefineGlobal(name_idx) => { + let name = self.chunk.constant(*name_idx); + with_type!(self, name, Value::String(name), { + let name = name.clone(); + let val = self.pop(); + self.globals.insert(name, val); + }); + } + + OpCode::OpGetGlobal(name_idx) => { + let name = self.chunk.constant(*name_idx); + with_type!(self, name, Value::String(name), { + let val = match self.globals.get(name) { + None => unimplemented!("variable not found error"), + Some(val) => val.clone(), + }; + self.push(val) + }); + } + + OpCode::OpSetGlobal(name_idx) => { + let name = self.chunk.constant(*name_idx).clone(); + let new_val = self.pop(); + with_type!(self, name, Value::String(name), { + match self.globals.get_mut(&name) { + None => unimplemented!("variable not found error"), + Some(val) => { + *val = new_val; + } + } + }); + } + + OpCode::OpGetLocal(local_idx) => { + let value = self.stack[local_idx.0].clone(); + self.push(value); + } + + OpCode::OpSetLocal(local_idx) => { + debug_assert!( + self.stack.len() > local_idx.0, + "stack is not currently large enough for local" + ); + self.stack[local_idx.0] = self.stack.last().unwrap().clone(); + } + + OpCode::OpJumpPlaceholder(_) => { + panic!("unpatched jump detected - this is a fatal compiler error!"); + } + + OpCode::OpJump(offset) => { + self.ip += offset.0; + } + + OpCode::OpJumpIfFalse(offset) => { + if self + .stack + .last() + .expect("condition should leave a value on the stack") + .is_falsey() + { + self.ip += offset.0; + } + } + } + + #[cfg(feature = "disassemble")] + println!("=> {:?}", self.stack); + } + } + + // For some types of values (e.g. interned strings), returns + // should no longer include any references into the interpreter. + fn return_value(&self, val: Value) -> Value { + match val { + Value::String(string @ LoxString::Interned(_)) => { + Value::String(self.resolve_str(&string).to_string().into()) + } + _ => val, + } + } + + fn resolve_str<'a>(&'a self, string: &'a LoxString) -> &'a str { + match string { + LoxString::Heap(s) => s.as_str(), + LoxString::Interned(id) => self.strings.lookup(*id), + } + } + + fn print_value(&self, val: Value) -> String { + match val { + Value::String(LoxString::Heap(s)) => s, + Value::String(LoxString::Interned(id)) => self.strings.lookup(id).into(), + _ => format!("{:?}", val), + } + } +} + +pub fn interpret(strings: Interner, chunk: chunk::Chunk) -> LoxResult<Value> { + let mut vm = VM { + chunk, + strings, + globals: HashMap::new(), + ip: 0, + stack: vec![], + last_drop: None, + }; + + vm.run() +} diff --git a/users/tazjin/rlox/src/main.rs b/users/tazjin/rlox/src/main.rs new file mode 100644 index 000000000000..ee61ae01a106 --- /dev/null +++ b/users/tazjin/rlox/src/main.rs @@ -0,0 +1,71 @@ +use std::io::Write; +use std::{env, fs, io, process}; + +mod bytecode; +mod scanner; +mod treewalk; + +/// Trait for making the different interpreters callable in the same +/// way. +pub trait Lox { + type Value: std::fmt::Debug; + type Error: std::fmt::Display; + + fn create() -> Self; + fn interpret(&mut self, source: String) -> Result<Self::Value, Vec<Self::Error>>; +} + +fn main() { + let mut args = env::args(); + if args.len() > 2 { + println!("Usage: rlox [script]"); + process::exit(1); + } + + match env::var("LOX_INTERPRETER").as_ref().map(String::as_str) { + Ok("treewalk") => pick::<treewalk::interpreter::Interpreter>(args.nth(1)), + _ => pick::<bytecode::Interpreter>(args.nth(1)), + } +} + +fn pick<I: Lox>(file_arg: Option<String>) { + if let Some(file) = file_arg { + run_file::<I>(&file); + } else { + run_prompt::<I>(); + } +} + +// Run Lox code from a file and print results to stdout +fn run_file<I: Lox>(file: &str) { + let contents = fs::read_to_string(file).expect("failed to read the input file"); + let mut lox = I::create(); + run(&mut lox, contents); +} + +// Evaluate Lox code interactively in a shitty REPL. +fn run_prompt<I: Lox>() { + let mut line = String::new(); + let mut lox = I::create(); + + loop { + print!("> "); + io::stdout().flush().unwrap(); + io::stdin() + .read_line(&mut line) + .expect("failed to read user input"); + run(&mut lox, std::mem::take(&mut line)); + line.clear(); + } +} + +fn run<I: Lox>(lox: &mut I, code: String) { + match lox.interpret(code) { + Ok(result) => println!("=> {:?}", result), + Err(errors) => { + for error in errors { + eprintln!("{}", error); + } + } + } +} diff --git a/users/tazjin/rlox/src/scanner.rs b/users/tazjin/rlox/src/scanner.rs new file mode 100644 index 000000000000..314b56d6d380 --- /dev/null +++ b/users/tazjin/rlox/src/scanner.rs @@ -0,0 +1,284 @@ +#[derive(Clone, Debug, PartialEq)] +pub enum TokenKind { + // Single-character tokens. + LeftParen, + RightParen, + LeftBrace, + RightBrace, + Comma, + Dot, + Minus, + Plus, + Semicolon, + Slash, + Star, + + // One or two character tokens. + Bang, + BangEqual, + Equal, + EqualEqual, + Greater, + GreaterEqual, + Less, + LessEqual, + + // Literals. + Identifier(String), + String(String), + Number(f64), + True, + False, + Nil, + + // Keywords. + And, + Class, + Else, + Fun, + For, + If, + Or, + Print, + Return, + Super, + This, + Var, + While, + + // Special things + Eof, +} + +#[derive(Clone, Debug)] +pub struct Token { + pub kind: TokenKind, + pub lexeme: String, + pub line: usize, +} + +pub enum ScannerError { + UnexpectedChar { line: usize, unexpected: char }, + UnterminatedString { line: usize }, +} + +struct Scanner<'a> { + source: &'a [char], + tokens: Vec<Token>, + errors: Vec<ScannerError>, + start: usize, // offset of first character in current lexeme + current: usize, // current offset into source + line: usize, // current line in source +} + +impl<'a> Scanner<'a> { + fn is_at_end(&self) -> bool { + return self.current >= self.source.len(); + } + + fn advance(&mut self) -> char { + self.current += 1; + self.source[self.current - 1] + } + + fn add_token(&mut self, kind: TokenKind) { + let lexeme = &self.source[self.start..self.current]; + self.tokens.push(Token { + kind, + lexeme: lexeme.into_iter().collect(), + line: self.line, + }) + } + + fn scan_token(&mut self) { + match self.advance() { + // simple single-character tokens + '(' => self.add_token(TokenKind::LeftParen), + ')' => self.add_token(TokenKind::RightParen), + '{' => self.add_token(TokenKind::LeftBrace), + '}' => self.add_token(TokenKind::RightBrace), + ',' => self.add_token(TokenKind::Comma), + '.' => self.add_token(TokenKind::Dot), + '-' => self.add_token(TokenKind::Minus), + '+' => self.add_token(TokenKind::Plus), + ';' => self.add_token(TokenKind::Semicolon), + '*' => self.add_token(TokenKind::Star), + + // possible multi-character tokens + '!' => self.add_if_next('=', TokenKind::BangEqual, TokenKind::Bang), + '=' => self.add_if_next('=', TokenKind::EqualEqual, TokenKind::Equal), + '<' => self.add_if_next('=', TokenKind::LessEqual, TokenKind::Less), + '>' => self.add_if_next('=', TokenKind::GreaterEqual, TokenKind::Greater), + + '/' => { + // support comments until EOL by discarding characters + if self.match_next('/') { + while self.peek() != '\n' && !self.is_at_end() { + self.advance(); + } + } else { + self.add_token(TokenKind::Slash); + } + } + + // ignore whitespace + ws if ws.is_whitespace() => { + if ws == '\n' { + self.line += 1 + } + } + + '"' => self.scan_string(), + + digit if digit.is_digit(10) => self.scan_number(), + + chr if chr.is_alphabetic() || chr == '_' => self.scan_identifier(), + + unexpected => self.errors.push(ScannerError::UnexpectedChar { + line: self.line, + unexpected, + }), + }; + } + + fn match_next(&mut self, expected: char) -> bool { + if self.is_at_end() || self.source[self.current] != expected { + false + } else { + self.current += 1; + true + } + } + + fn add_if_next(&mut self, expected: char, then: TokenKind, or: TokenKind) { + if self.match_next(expected) { + self.add_token(then); + } else { + self.add_token(or); + } + } + + fn peek(&self) -> char { + if self.is_at_end() { + return '\0'; + } else { + return self.source[self.current]; + } + } + + fn peek_next(&self) -> char { + if self.current + 1 >= self.source.len() { + return '\0'; + } else { + return self.source[self.current + 1]; + } + } + + fn scan_string(&mut self) { + while self.peek() != '"' && !self.is_at_end() { + if self.peek() == '\n' { + self.line += 1; + } + + self.advance(); + } + + if self.is_at_end() { + self.errors + .push(ScannerError::UnterminatedString { line: self.line }); + return; + } + + // closing '"' + self.advance(); + + // add token without surrounding quotes + let string: String = self.source[(self.start + 1)..(self.current - 1)] + .iter() + .collect(); + self.add_token(TokenKind::String(string)); + } + + fn scan_number(&mut self) { + while self.peek().is_digit(10) { + self.advance(); + } + + // Look for a fractional part + if self.peek() == '.' && self.peek_next().is_digit(10) { + // consume '.' + self.advance(); + + while self.peek().is_digit(10) { + self.advance(); + } + } + + let num: f64 = self.source[self.start..self.current] + .iter() + .collect::<String>() + .parse() + .expect("float parsing should always work"); + + self.add_token(TokenKind::Number(num)); + } + + fn scan_identifier(&mut self) { + while self.peek().is_alphanumeric() || self.peek() == '_' { + self.advance(); + } + + let ident: String = self.source[self.start..self.current].iter().collect(); + + // Determine whether this is an identifier, or a keyword: + let token_kind = match ident.as_str() { + "and" => TokenKind::And, + "class" => TokenKind::Class, + "else" => TokenKind::Else, + "false" => TokenKind::False, + "for" => TokenKind::For, + "fun" => TokenKind::Fun, + "if" => TokenKind::If, + "nil" => TokenKind::Nil, + "or" => TokenKind::Or, + "print" => TokenKind::Print, + "return" => TokenKind::Return, + "super" => TokenKind::Super, + "this" => TokenKind::This, + "true" => TokenKind::True, + "var" => TokenKind::Var, + "while" => TokenKind::While, + _ => TokenKind::Identifier(ident), + }; + + self.add_token(token_kind); + } + + fn scan_tokens(&mut self) { + while !self.is_at_end() { + self.start = self.current; + self.scan_token(); + } + + self.add_token(TokenKind::Eof); + } +} + +pub fn scan<'a>(input: &'a [char]) -> Result<Vec<Token>, Vec<ScannerError>> { + let mut scanner = Scanner { + source: &input, + tokens: vec![], + errors: vec![], + start: 0, + current: 0, + line: 0, + }; + + scanner.scan_tokens(); + + if !scanner.errors.is_empty() { + return Err(scanner.errors); + } + + return Ok(scanner.tokens); +} diff --git a/users/tazjin/rlox/src/treewalk/errors.rs b/users/tazjin/rlox/src/treewalk/errors.rs new file mode 100644 index 000000000000..391663d51b18 --- /dev/null +++ b/users/tazjin/rlox/src/treewalk/errors.rs @@ -0,0 +1,59 @@ +use crate::scanner::ScannerError; +use crate::treewalk::interpreter::Value; + +use std::fmt; + +#[derive(Debug)] +pub enum ErrorKind { + UnexpectedChar(char), + UnterminatedString, + UnmatchedParens, + ExpectedExpression(String), + ExpectedSemicolon, + ExpectedClosingBrace, + ExpectedToken(&'static str), + TypeError(String), + UndefinedVariable(String), + InternalError(String), + InvalidAssignmentTarget(String), + RuntimeError(String), + StaticError(String), + + // This variant is not an error, rather it is used for + // short-circuiting out of a function body that hits a `return` + // statement. + // + // It's implemented this way because in the original book the + // author uses exceptions for control flow, and this is the + // closest equivalent that I had available without diverging too + // much. + FunctionReturn(Value), +} + +#[derive(Debug)] +pub struct Error { + pub line: usize, + pub kind: ErrorKind, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "[line {}] Error: {:?}", self.line, self.kind) + } +} + +impl From<ScannerError> for Error { + fn from(err: ScannerError) -> Self { + match err { + ScannerError::UnexpectedChar { line, unexpected } => Error { + line, + kind: ErrorKind::UnexpectedChar(unexpected), + }, + + ScannerError::UnterminatedString { line } => Error { + line, + kind: ErrorKind::UnterminatedString, + }, + } + } +} diff --git a/users/tazjin/rlox/src/treewalk/interpreter.rs b/users/tazjin/rlox/src/treewalk/interpreter.rs new file mode 100644 index 000000000000..3285775bbec6 --- /dev/null +++ b/users/tazjin/rlox/src/treewalk/interpreter.rs @@ -0,0 +1,498 @@ +use crate::treewalk::errors::{Error, ErrorKind}; +use crate::treewalk::parser::{self, Block, Expr, Literal, Statement}; +use crate::treewalk::resolver; +use crate::treewalk::scanner::{self, TokenKind}; +use crate::Lox; +use std::collections::HashMap; +use std::rc::Rc; +use std::sync::RwLock; + +// Implementation of built-in functions. +mod builtins; + +#[cfg(test)] +mod tests; + +// Tree-walk interpreter + +// Representation of all callables, including builtins & user-defined +// functions. +#[derive(Clone, Debug)] +pub enum Callable { + Builtin(&'static dyn builtins::Builtin), + Function { + func: Rc<parser::Function>, + closure: Rc<RwLock<Environment>>, + }, +} + +impl Callable { + fn arity(&self) -> usize { + match self { + Callable::Builtin(builtin) => builtin.arity(), + Callable::Function { func, .. } => func.params.len(), + } + } + + fn call(&self, lox: &mut Interpreter, args: Vec<Value>) -> Result<Value, Error> { + match self { + Callable::Builtin(builtin) => builtin.call(args), + + Callable::Function { func, closure } => { + let mut fn_env: Environment = Default::default(); + fn_env.enclosing = Some(closure.clone()); + + for (param, value) in func.params.iter().zip(args.into_iter()) { + fn_env.define(param, value)?; + } + + let result = + lox.interpret_block_with_env(Some(Rc::new(RwLock::new(fn_env))), &func.body); + + match result { + // extract returned values if applicable + Err(Error { + kind: ErrorKind::FunctionReturn(value), + .. + }) => Ok(value), + + // otherwise just return the result itself + _ => result, + } + } + } + } +} + +// Representation of an in-language value. +#[derive(Clone, Debug)] +pub enum Value { + Literal(Literal), + Callable(Callable), +} + +impl PartialEq for Value { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Value::Literal(lhs), Value::Literal(rhs)) => lhs == rhs, + // functions do not have equality + _ => false, + } + } +} + +impl From<Literal> for Value { + fn from(lit: Literal) -> Value { + Value::Literal(lit) + } +} + +impl Value { + fn expect_literal(self) -> Result<Literal, Error> { + match self { + Value::Literal(lit) => Ok(lit), + _ => unimplemented!(), // which error? which line? + } + } +} + +#[derive(Debug, Default)] +pub struct Environment { + enclosing: Option<Rc<RwLock<Environment>>>, + values: HashMap<String, Value>, +} + +impl Environment { + fn define(&mut self, name: &scanner::Token, value: Value) -> Result<(), Error> { + let ident = identifier_str(name)?; + self.values.insert(ident.into(), value); + Ok(()) + } + + fn get(&self, ident: &str, line: usize, depth: usize) -> Result<Value, Error> { + if depth > 0 { + match &self.enclosing { + None => { + return Err(Error { + line, + kind: ErrorKind::InternalError(format!( + "invalid depth {} for {}", + depth, ident + )), + }) + } + Some(parent) => { + let env = parent.read().expect("fatal: environment lock poisoned"); + return env.get(ident, line, depth - 1); + } + } + } + + self.values + .get(ident) + .map(Clone::clone) + .ok_or_else(|| Error { + line, + kind: ErrorKind::UndefinedVariable(ident.into()), + }) + } + + fn assign(&mut self, name: &scanner::Token, value: Value) -> Result<(), Error> { + let ident = identifier_str(name)?; + + match self.values.get_mut(ident) { + Some(target) => { + *target = value; + Ok(()) + } + None => { + if let Some(parent) = &self.enclosing { + return parent.write().unwrap().assign(name, value); + } + + Err(Error { + line: name.line, + kind: ErrorKind::UndefinedVariable(ident.into()), + }) + } + } + } +} + +fn identifier_str(name: &scanner::Token) -> Result<&str, Error> { + if let TokenKind::Identifier(ident) = &name.kind { + Ok(ident) + } else { + Err(Error { + line: name.line, + kind: ErrorKind::InternalError("unexpected identifier kind".into()), + }) + } +} + +#[derive(Debug)] +pub struct Interpreter { + env: Rc<RwLock<Environment>>, +} + +impl Lox for Interpreter { + type Value = Value; + type Error = Error; + + /// Create a new interpreter and configure the initial global + /// variable set. + fn create() -> Self { + let mut globals = HashMap::new(); + + globals.insert( + "clock".into(), + Value::Callable(Callable::Builtin(&builtins::Clock {})), + ); + + Interpreter { + env: Rc::new(RwLock::new(Environment { + enclosing: None, + values: globals, + })), + } + } + + fn interpret(&mut self, code: String) -> Result<Value, Vec<Error>> { + let chars: Vec<char> = code.chars().collect(); + + let mut program = scanner::scan(&chars) + .map_err(|errors| errors.into_iter().map(Into::into).collect()) + .and_then(|tokens| parser::parse(tokens))?; + + let globals = self + .env + .read() + .expect("static globals lock poisoned") + .values + .keys() + .map(Clone::clone) + .collect::<Vec<String>>(); + + resolver::resolve(&globals, &mut program).map_err(|e| vec![e])?; + self.interpret_block_with_env(None, &program) + .map_err(|e| vec![e]) + } +} + +impl Interpreter { + // Environment modification helpers + fn define_var(&mut self, name: &scanner::Token, value: Value) -> Result<(), Error> { + self.env + .write() + .expect("environment lock is poisoned") + .define(name, value) + } + + fn assign_var(&mut self, name: &scanner::Token, value: Value) -> Result<(), Error> { + self.env + .write() + .expect("environment lock is poisoned") + .assign(name, value) + } + + fn get_var(&mut self, var: &parser::Variable) -> Result<Value, Error> { + let ident = identifier_str(&var.name)?; + let depth = var.depth.ok_or_else(|| Error { + line: var.name.line, + kind: ErrorKind::UndefinedVariable(ident.into()), + })?; + + self.env + .read() + .expect("environment lock is poisoned") + .get(ident, var.name.line, depth) + } + + /// Interpret the block in the supplied environment. If no + /// environment is supplied, a new one is created using the + /// current one as its parent. + fn interpret_block_with_env( + &mut self, + env: Option<Rc<RwLock<Environment>>>, + block: &parser::Block, + ) -> Result<Value, Error> { + let env = match env { + Some(env) => env, + None => { + let env: Rc<RwLock<Environment>> = Default::default(); + set_enclosing_env(&env, self.env.clone()); + env + } + }; + + let previous = std::mem::replace(&mut self.env, env); + let result = self.interpret_block(block); + + // Swap it back, discarding the child env. + self.env = previous; + + return result; + } + + fn interpret_block(&mut self, program: &Block) -> Result<Value, Error> { + let mut value = Value::Literal(Literal::Nil); + + for stmt in program { + value = self.interpret_stmt(stmt)?; + } + + Ok(value) + } + + fn interpret_stmt(&mut self, stmt: &Statement) -> Result<Value, Error> { + let value = match stmt { + Statement::Expr(expr) => self.eval(expr)?, + Statement::Print(expr) => { + let result = self.eval(expr)?; + let output = format!("{:?}", result); + println!("{}", output); + Value::Literal(Literal::String(output)) + } + Statement::Var(var) => return self.interpret_var(var), + Statement::Block(block) => return self.interpret_block_with_env(None, block), + Statement::If(if_stmt) => return self.interpret_if(if_stmt), + Statement::While(while_stmt) => return self.interpret_while(while_stmt), + Statement::Function(func) => return self.interpret_function(func.clone()), + Statement::Return(ret) => { + return Err(Error { + line: 0, + kind: ErrorKind::FunctionReturn(self.eval(&ret.value)?), + }) + } + }; + + Ok(value) + } + + fn interpret_var(&mut self, var: &parser::Var) -> Result<Value, Error> { + let init = var.initialiser.as_ref().ok_or_else(|| Error { + line: var.name.line, + kind: ErrorKind::InternalError("missing variable initialiser".into()), + })?; + let value = self.eval(init)?; + self.define_var(&var.name, value.clone())?; + Ok(value) + } + + fn interpret_if(&mut self, if_stmt: &parser::If) -> Result<Value, Error> { + let condition = self.eval(&if_stmt.condition)?; + + if eval_truthy(&condition) { + self.interpret_stmt(&if_stmt.then_branch) + } else if let Some(else_branch) = &if_stmt.else_branch { + self.interpret_stmt(else_branch) + } else { + Ok(Value::Literal(Literal::Nil)) + } + } + + fn interpret_while(&mut self, stmt: &parser::While) -> Result<Value, Error> { + let mut value = Value::Literal(Literal::Nil); + while eval_truthy(&self.eval(&stmt.condition)?) { + value = self.interpret_stmt(&stmt.body)?; + } + + Ok(value) + } + + fn interpret_function(&mut self, func: Rc<parser::Function>) -> Result<Value, Error> { + let name = func.name.clone(); + let value = Value::Callable(Callable::Function { + func, + closure: self.env.clone(), + }); + self.define_var(&name, value.clone())?; + Ok(value) + } + + fn eval(&mut self, expr: &Expr) -> Result<Value, Error> { + match expr { + Expr::Assign(assign) => self.eval_assign(assign), + Expr::Literal(lit) => Ok(lit.clone().into()), + Expr::Grouping(grouping) => self.eval(&*grouping.0), + Expr::Unary(unary) => self.eval_unary(unary), + Expr::Binary(binary) => self.eval_binary(binary), + Expr::Variable(var) => self.get_var(var), + Expr::Logical(log) => self.eval_logical(log), + Expr::Call(call) => self.eval_call(call), + } + } + + fn eval_unary(&mut self, expr: &parser::Unary) -> Result<Value, Error> { + let right = self.eval(&*expr.right)?; + + match (&expr.operator.kind, right) { + (TokenKind::Minus, Value::Literal(Literal::Number(num))) => { + Ok(Literal::Number(-num).into()) + } + (TokenKind::Bang, right) => Ok(Literal::Boolean(!eval_truthy(&right)).into()), + + (op, right) => Err(Error { + line: expr.operator.line, + kind: ErrorKind::TypeError(format!( + "Operator '{:?}' can not be called with argument '{:?}'", + op, right + )), + }), + } + } + + fn eval_binary(&mut self, expr: &parser::Binary) -> Result<Value, Error> { + let left = self.eval(&*expr.left)?.expect_literal()?; + let right = self.eval(&*expr.right)?.expect_literal()?; + + let result = match (&expr.operator.kind, left, right) { + // Numeric + (TokenKind::Minus, Literal::Number(l), Literal::Number(r)) => Literal::Number(l - r), + (TokenKind::Slash, Literal::Number(l), Literal::Number(r)) => Literal::Number(l / r), + (TokenKind::Star, Literal::Number(l), Literal::Number(r)) => Literal::Number(l * r), + (TokenKind::Plus, Literal::Number(l), Literal::Number(r)) => Literal::Number(l + r), + + // Strings + (TokenKind::Plus, Literal::String(l), Literal::String(r)) => { + Literal::String(format!("{}{}", l, r)) + } + + // Comparators (on numbers only?) + (TokenKind::Greater, Literal::Number(l), Literal::Number(r)) => Literal::Boolean(l > r), + (TokenKind::GreaterEqual, Literal::Number(l), Literal::Number(r)) => { + Literal::Boolean(l >= r) + } + (TokenKind::Less, Literal::Number(l), Literal::Number(r)) => Literal::Boolean(l < r), + (TokenKind::LessEqual, Literal::Number(l), Literal::Number(r)) => { + Literal::Boolean(l <= r) + } + + // Equality + (TokenKind::Equal, l, r) => Literal::Boolean(l == r), + (TokenKind::BangEqual, l, r) => Literal::Boolean(l != r), + + (op, left, right) => { + return Err(Error { + line: expr.operator.line, + kind: ErrorKind::TypeError(format!( + "Operator '{:?}' can not be called with arguments '({:?}, {:?})'", + op, left, right + )), + }) + } + }; + + Ok(result.into()) + } + + fn eval_assign(&mut self, assign: &parser::Assign) -> Result<Value, Error> { + let value = self.eval(&assign.value)?; + self.assign_var(&assign.name, value.clone())?; + Ok(value) + } + + fn eval_logical(&mut self, logical: &parser::Logical) -> Result<Value, Error> { + let left = eval_truthy(&self.eval(&logical.left)?); + let right = eval_truthy(&self.eval(&logical.right)?); + + match &logical.operator.kind { + TokenKind::And => Ok(Literal::Boolean(left && right).into()), + TokenKind::Or => Ok(Literal::Boolean(left || right).into()), + kind => Err(Error { + line: logical.operator.line, + kind: ErrorKind::InternalError(format!("Invalid logical operator: {:?}", kind)), + }), + } + } + + fn eval_call(&mut self, call: &parser::Call) -> Result<Value, Error> { + let callable = match self.eval(&call.callee)? { + Value::Callable(c) => c, + Value::Literal(v) => { + return Err(Error { + line: call.paren.line, + kind: ErrorKind::RuntimeError(format!("not callable: {:?}", v)), + }) + } + }; + + let mut args = vec![]; + for arg in &call.args { + args.push(self.eval(arg)?); + } + + if callable.arity() != args.len() { + return Err(Error { + line: call.paren.line, + kind: ErrorKind::RuntimeError(format!( + "Expected {} arguments, but got {}", + callable.arity(), + args.len(), + )), + }); + } + + callable.call(self, args) + } +} + +// Interpreter functions not dependent on interpreter-state. + +fn eval_truthy(lit: &Value) -> bool { + if let Value::Literal(lit) = lit { + match lit { + Literal::Nil => false, + Literal::Boolean(b) => *b, + _ => true, + } + } else { + false + } +} + +fn set_enclosing_env(this: &RwLock<Environment>, parent: Rc<RwLock<Environment>>) { + this.write() + .expect("environment lock is poisoned") + .enclosing = Some(parent); +} diff --git a/users/tazjin/rlox/src/treewalk/interpreter/builtins.rs b/users/tazjin/rlox/src/treewalk/interpreter/builtins.rs new file mode 100644 index 000000000000..c502d2a1718a --- /dev/null +++ b/users/tazjin/rlox/src/treewalk/interpreter/builtins.rs @@ -0,0 +1,25 @@ +use std::fmt; +use std::time::{SystemTime, UNIX_EPOCH}; + +use crate::treewalk::errors::Error; +use crate::treewalk::interpreter::Value; +use crate::treewalk::parser::Literal; + +pub trait Builtin: fmt::Debug { + fn arity(&self) -> usize; + fn call(&self, args: Vec<Value>) -> Result<Value, Error>; +} + +// Builtin to return the current timestamp. +#[derive(Debug)] +pub struct Clock {} +impl Builtin for Clock { + fn arity(&self) -> usize { + 0 + } + + fn call(&self, _args: Vec<Value>) -> Result<Value, Error> { + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); + Ok(Value::Literal(Literal::Number(now.as_secs() as f64))) + } +} diff --git a/users/tazjin/rlox/src/treewalk/interpreter/tests.rs b/users/tazjin/rlox/src/treewalk/interpreter/tests.rs new file mode 100644 index 000000000000..2fc6f4fee978 --- /dev/null +++ b/users/tazjin/rlox/src/treewalk/interpreter/tests.rs @@ -0,0 +1,97 @@ +use super::*; + +/// Evaluate a code snippet, returning a value. +fn parse_eval(code: &str) -> Value { + Interpreter::create() + .interpret(code.into()) + .expect("could not interpret code") +} + +#[test] +fn test_if() { + let result = parse_eval( + r#" +if (42 > 23) + "pass"; +else + "fail"; +"#, + ); + + assert_eq!(Value::Literal(Literal::String("pass".into())), result,); +} + +#[test] +fn test_scope() { + let result = parse_eval( + r#" +var result = ""; + +var a = "global a, "; +var b = "global b, "; +var c = "global c"; + +{ + var a = "outer a, "; + var b = "outer b, "; + + { + var a = "inner a, "; + result = a + b + c; + } +} +"#, + ); + + assert_eq!( + Value::Literal(Literal::String("inner a, outer b, global c".into())), + result, + ); +} + +#[test] +fn test_binary_operators() { + assert_eq!(Value::Literal(Literal::Number(42.0)), parse_eval("40 + 2;")); + + assert_eq!( + Value::Literal(Literal::String("foobar".into())), + parse_eval("\"foo\" + \"bar\";") + ); +} + +#[test] +fn test_functions() { + let result = parse_eval( + r#" +fun add(a, b, c) { + a + b + c; +} + +add(1, 2, 3); +"#, + ); + + assert_eq!(Value::Literal(Literal::Number(6.0)), result); +} + +#[test] +fn test_closure() { + let result = parse_eval( + r#" +fun makeCounter() { + var i = 0; + fun count() { + i = i + 1; + } + + return count; +} + +var counter = makeCounter(); +counter(); // "1". +counter(); // "2". +"#, + ); + + assert_eq!(Value::Literal(Literal::Number(2.0)), result); +} diff --git a/users/tazjin/rlox/src/treewalk/mod.rs b/users/tazjin/rlox/src/treewalk/mod.rs new file mode 100644 index 000000000000..2d82b3320a90 --- /dev/null +++ b/users/tazjin/rlox/src/treewalk/mod.rs @@ -0,0 +1,6 @@ +use crate::scanner; + +mod errors; +pub mod interpreter; +mod parser; +mod resolver; diff --git a/users/tazjin/rlox/src/treewalk/parser.rs b/users/tazjin/rlox/src/treewalk/parser.rs new file mode 100644 index 000000000000..5794b42d1577 --- /dev/null +++ b/users/tazjin/rlox/src/treewalk/parser.rs @@ -0,0 +1,700 @@ +// This implements the grammar of Lox as described starting in the +// Crafting Interpreters chapter "Representing Code". Note that the +// upstream Java implementation works around Java being bad at value +// classes by writing a code generator for Java. +// +// My Rust implementation skips this step because it's unnecessary, we +// have real types. +use crate::treewalk::errors::{Error, ErrorKind}; +use crate::treewalk::scanner::{Token, TokenKind}; +use std::rc::Rc; + +// AST + +#[derive(Debug)] +pub struct Assign { + pub name: Token, + pub value: Box<Expr>, + pub depth: Option<usize>, +} + +#[derive(Debug)] +pub struct Binary { + pub left: Box<Expr>, + pub operator: Token, + pub right: Box<Expr>, +} + +#[derive(Debug)] +pub struct Logical { + pub left: Box<Expr>, + pub operator: Token, + pub right: Box<Expr>, +} + +#[derive(Debug)] +pub struct Grouping(pub Box<Expr>); + +#[derive(Debug, Clone, PartialEq)] +pub enum Literal { + Boolean(bool), + Number(f64), + String(String), + Nil, +} + +#[derive(Debug)] +pub struct Unary { + pub operator: Token, + pub right: Box<Expr>, +} + +#[derive(Debug)] +pub struct Call { + pub callee: Box<Expr>, + pub paren: Token, + pub args: Vec<Expr>, +} + +// Not to be confused with `Var`, which is for assignment. +#[derive(Debug)] +pub struct Variable { + pub name: Token, + pub depth: Option<usize>, +} + +#[derive(Debug)] +pub enum Expr { + Assign(Assign), + Binary(Binary), + Grouping(Grouping), + Literal(Literal), + Unary(Unary), + Call(Call), + Variable(Variable), + Logical(Logical), +} + +// Variable assignment. Not to be confused with `Variable`, which is +// for access. +#[derive(Debug)] +pub struct Var { + pub name: Token, + pub initialiser: Option<Expr>, +} + +#[derive(Debug)] +pub struct Return { + pub value: Expr, +} + +#[derive(Debug)] +pub struct If { + pub condition: Expr, + pub then_branch: Box<Statement>, + pub else_branch: Option<Box<Statement>>, +} + +#[derive(Debug)] +pub struct While { + pub condition: Expr, + pub body: Box<Statement>, +} + +pub type Block = Vec<Statement>; + +#[derive(Debug)] +pub struct Function { + pub name: Token, + pub params: Vec<Token>, + pub body: Block, +} + +#[derive(Debug)] +pub enum Statement { + Expr(Expr), + Print(Expr), + Var(Var), + Block(Block), + If(If), + While(While), + Function(Rc<Function>), + Return(Return), +} + +// Parser + +// program โ declaration* EOF ; +// +// declaration โ funDecl +// | varDecl +// | statement ; +// +// funDecl โ "fun" function ; +// function โ IDENTIFIER "(" parameters? ")" block ; +// parameters โ IDENTIFIER ( "," IDENTIFIER )* ; +// +// +// statement โ exprStmt +// | forStmt +// | ifStmt +// | printStmt +// | returnStmt +// | whileStmt +// | block ; +// +// forStmt โ "for" "(" ( varDecl | exprStmt | ";" ) +// expression? ";" +// expression? ")" statement ; +// +// returnStmt โ "return" expression? ";" ; +// +// whileStmt โ "while" "(" expression ")" statement ; +// +// exprStmt โ expression ";" ; +// +// ifStmt โ "if" "(" expression ")" statement +// ( "else" statement )? ; +// +// printStmt โ "print" expression ";" ; +// +// expression โ assignment ; +// assignment โ IDENTIFIER "=" assignment +// | logic_or ; +// logic_or โ logic_and ( "or" logic_and )* ; +// logic_and โ equality ( "and" equality )* ; +// equality โ comparison ( ( "!=" | "==" ) comparison )* ; +// comparison โ term ( ( ">" | ">=" | "<" | "<=" ) term )* ; +// term โ factor ( ( "-" | "+" ) factor )* ; +// factor โ unary ( ( "/" | "*" ) unary )* ; +// unary โ ( "!" | "-" ) unary | call ; +// call โ primary ( "(" arguments? ")" )* ; +// arguments โ expression ( "," expression )* ; +// primary โ NUMBER | STRING | "true" | "false" | "nil" +// | "(" expression ")" ; + +struct Parser { + tokens: Vec<Token>, + current: usize, +} + +type ExprResult = Result<Expr, Error>; +type StmtResult = Result<Statement, Error>; + +impl Parser { + // recursive-descent parser functions + + fn declaration(&mut self) -> StmtResult { + if self.match_token(&TokenKind::Fun) { + return self.function(); + } + + if self.match_token(&TokenKind::Var) { + return self.var_declaration(); + } + + self.statement() + } + + fn function(&mut self) -> StmtResult { + let name = self.identifier("Expected function name.")?; + + self.consume( + &TokenKind::LeftParen, + ErrorKind::ExpectedToken("Expect '(' after function name."), + )?; + + let mut params = vec![]; + + if !self.check_token(&TokenKind::RightParen) { + loop { + if params.len() >= 255 { + return Err(Error { + line: self.peek().line, + kind: ErrorKind::InternalError("255 parameter limit exceeded.".into()), + }); + } + + params.push(self.identifier("Expected parameter name.")?); + + if !self.match_token(&TokenKind::Comma) { + break; + } + } + } + + self.consume( + &TokenKind::RightParen, + ErrorKind::ExpectedToken("Expect ')' after parameters."), + )?; + + self.consume( + &TokenKind::LeftBrace, + ErrorKind::ExpectedToken("Expect '{' before function body."), + )?; + + Ok(Statement::Function(Rc::new(Function { + name, + params, + body: self.block_statement()?, + }))) + } + + fn var_declaration(&mut self) -> StmtResult { + // Since `TokenKind::Identifier` carries data, we can't use + // `consume`. + let mut var = Var { + name: self.identifier("Expected variable name.")?, + initialiser: None, + }; + + if self.match_token(&TokenKind::Equal) { + var.initialiser = Some(self.expression()?); + } + + self.consume(&TokenKind::Semicolon, ErrorKind::ExpectedSemicolon)?; + Ok(Statement::Var(var)) + } + + fn statement(&mut self) -> StmtResult { + if self.match_token(&TokenKind::Print) { + self.print_statement() + } else if self.match_token(&TokenKind::LeftBrace) { + Ok(Statement::Block(self.block_statement()?)) + } else if self.match_token(&TokenKind::If) { + self.if_statement() + } else if self.match_token(&TokenKind::While) { + self.while_statement() + } else if self.match_token(&TokenKind::For) { + self.for_statement() + } else if self.match_token(&TokenKind::Return) { + self.return_statement() + } else { + self.expr_statement() + } + } + + fn print_statement(&mut self) -> StmtResult { + let expr = self.expression()?; + self.consume(&TokenKind::Semicolon, ErrorKind::ExpectedSemicolon)?; + Ok(Statement::Print(expr)) + } + + fn block_statement(&mut self) -> Result<Block, Error> { + let mut block: Block = vec![]; + + while !self.check_token(&TokenKind::RightBrace) && !self.is_at_end() { + block.push(self.declaration()?); + } + + self.consume(&TokenKind::RightBrace, ErrorKind::ExpectedClosingBrace)?; + + Ok(block) + } + + fn if_statement(&mut self) -> StmtResult { + self.consume( + &TokenKind::LeftParen, + ErrorKind::ExpectedToken("Expected '(' after 'if'"), + )?; + let condition = self.expression()?; + self.consume( + &TokenKind::RightParen, + ErrorKind::ExpectedToken("Expected ')' after condition"), + )?; + + let then_branch = Box::new(self.statement()?); + + let mut stmt = If { + condition, + then_branch, + else_branch: Option::None, + }; + + if self.match_token(&TokenKind::Else) { + stmt.else_branch = Some(Box::new(self.statement()?)); + } + + Ok(Statement::If(stmt)) + } + + fn while_statement(&mut self) -> StmtResult { + self.consume( + &TokenKind::LeftParen, + ErrorKind::ExpectedToken("Expected '(' after 'while'"), + )?; + + let condition = self.expression()?; + + self.consume( + &TokenKind::RightParen, + ErrorKind::ExpectedToken("Expected ')' after 'while'"), + )?; + + Ok(Statement::While(While { + condition, + body: Box::new(self.statement()?), + })) + } + + fn for_statement(&mut self) -> StmtResult { + // Parsing of clauses ... + self.consume( + &TokenKind::LeftParen, + ErrorKind::ExpectedToken("Expected '(' after 'for'"), + )?; + + let initialiser = if self.match_token(&TokenKind::Semicolon) { + None + } else if self.match_token(&TokenKind::Var) { + Some(self.var_declaration()?) + } else { + Some(self.expr_statement()?) + }; + + let condition = if self.check_token(&TokenKind::Semicolon) { + // unspecified condition => infinite loop + Expr::Literal(Literal::Boolean(true)) + } else { + self.expression()? + }; + + self.consume(&TokenKind::Semicolon, ErrorKind::ExpectedSemicolon)?; + + let increment = if self.check_token(&TokenKind::RightParen) { + None + } else { + Some(self.expression()?) + }; + + self.consume( + &TokenKind::RightParen, + ErrorKind::ExpectedToken("Expected ')' after for clauses"), + )?; + + let mut body = self.statement()?; + + // ... desugaring to while + + if let Some(inc) = increment { + body = Statement::Block(vec![body, Statement::Expr(inc)]); + } + + body = Statement::While(While { + condition, + body: Box::new(body), + }); + + if let Some(init) = initialiser { + body = Statement::Block(vec![init, body]); + } + + Ok(body) + } + + fn return_statement(&mut self) -> StmtResult { + let value = self.expression()?; + self.consume(&TokenKind::Semicolon, ErrorKind::ExpectedSemicolon)?; + Ok(Statement::Return(Return { value })) + } + + fn expr_statement(&mut self) -> StmtResult { + let expr = self.expression()?; + self.consume(&TokenKind::Semicolon, ErrorKind::ExpectedSemicolon)?; + Ok(Statement::Expr(expr)) + } + + fn expression(&mut self) -> ExprResult { + self.assignment() + } + + fn assignment(&mut self) -> ExprResult { + let expr = self.logic_or()?; + + if self.match_token(&TokenKind::Equal) { + let equals = self.previous().clone(); + let value = self.assignment()?; + + if let Expr::Variable(Variable { name, .. }) = expr { + return Ok(Expr::Assign(Assign { + name, + value: Box::new(value), + depth: None, + })); + } + + return Err(Error { + line: equals.line, + kind: ErrorKind::InvalidAssignmentTarget(format!("{:?}", equals)), + }); + } + + Ok(expr) + } + + fn logic_or(&mut self) -> ExprResult { + let mut expr = self.logic_and()?; + + while self.match_token(&TokenKind::Or) { + expr = Expr::Logical(Logical { + left: Box::new(expr), + operator: self.previous().clone(), + right: Box::new(self.logic_and()?), + }) + } + + Ok(expr) + } + + fn logic_and(&mut self) -> ExprResult { + let mut expr = self.equality()?; + + while self.match_token(&TokenKind::And) { + expr = Expr::Logical(Logical { + left: Box::new(expr), + operator: self.previous().clone(), + right: Box::new(self.equality()?), + }) + } + + Ok(expr) + } + + fn equality(&mut self) -> ExprResult { + self.binary_operator( + &[TokenKind::BangEqual, TokenKind::EqualEqual], + Self::comparison, + ) + } + + fn comparison(&mut self) -> ExprResult { + self.binary_operator( + &[ + TokenKind::Greater, + TokenKind::GreaterEqual, + TokenKind::Less, + TokenKind::LessEqual, + ], + Self::term, + ) + } + + fn term(&mut self) -> ExprResult { + self.binary_operator(&[TokenKind::Minus, TokenKind::Plus], Self::factor) + } + + fn factor(&mut self) -> ExprResult { + self.binary_operator(&[TokenKind::Slash, TokenKind::Star], Self::unary) + } + + fn unary(&mut self) -> ExprResult { + if self.match_token(&TokenKind::Bang) || self.match_token(&TokenKind::Minus) { + return Ok(Expr::Unary(Unary { + operator: self.previous().clone(), + right: Box::new(self.unary()?), + })); + } + + return self.call(); + } + + fn call(&mut self) -> ExprResult { + let mut expr = self.primary()?; + + loop { + if self.match_token(&TokenKind::LeftParen) { + expr = self.finish_call(expr)?; + } else { + break; + } + } + + Ok(expr) + } + + fn finish_call(&mut self, callee: Expr) -> ExprResult { + let mut args = vec![]; + + if !self.check_token(&TokenKind::RightParen) { + loop { + // TODO(tazjin): Check for max args count + args.push(self.expression()?); + if !self.match_token(&TokenKind::Comma) { + break; + } + } + } + + let paren = self.consume( + &TokenKind::RightParen, + ErrorKind::ExpectedToken("Expect ')' after arguments."), + )?; + + Ok(Expr::Call(Call { + args, + callee: Box::new(callee), + paren, + })) + } + + fn primary(&mut self) -> ExprResult { + let next = self.advance(); + let literal = match next.kind { + TokenKind::True => Literal::Boolean(true), + TokenKind::False => Literal::Boolean(false), + TokenKind::Nil => Literal::Nil, + TokenKind::Number(num) => Literal::Number(num), + TokenKind::String(string) => Literal::String(string), + + TokenKind::LeftParen => { + let expr = self.expression()?; + self.consume(&TokenKind::RightParen, ErrorKind::UnmatchedParens)?; + return Ok(Expr::Grouping(Grouping(Box::new(expr)))); + } + + TokenKind::Identifier(_) => { + return Ok(Expr::Variable(Variable { + name: next, + depth: None, + })) + } + + unexpected => { + eprintln!("encountered {:?}", unexpected); + return Err(Error { + line: next.line, + kind: ErrorKind::ExpectedExpression(next.lexeme), + }); + } + }; + + Ok(Expr::Literal(literal)) + } + + // internal helpers + + fn identifier(&mut self, err: &'static str) -> Result<Token, Error> { + if let TokenKind::Identifier(_) = self.peek().kind { + Ok(self.advance()) + } else { + Err(Error { + line: self.peek().line, + kind: ErrorKind::ExpectedToken(err), + }) + } + } + + /// Check if the next token is in `oneof`, and advance if it is. + fn match_token(&mut self, token: &TokenKind) -> bool { + if self.check_token(token) { + self.advance(); + return true; + } + + false + } + + /// Return the next token and advance parser state. + fn advance(&mut self) -> Token { + if !self.is_at_end() { + self.current += 1; + } + + return self.previous().clone(); + } + + fn is_at_end(&self) -> bool { + self.check_token(&TokenKind::Eof) + } + + /// Is the next token `token`? + fn check_token(&self, token: &TokenKind) -> bool { + self.peek().kind == *token + } + + fn peek(&self) -> &Token { + &self.tokens[self.current] + } + + fn previous(&self) -> &Token { + &self.tokens[self.current - 1] + } + + fn consume(&mut self, kind: &TokenKind, err: ErrorKind) -> Result<Token, Error> { + if self.check_token(kind) { + return Ok(self.advance()); + } + + Err(Error { + line: self.peek().line, + kind: err, + }) + } + + fn synchronise(&mut self) { + self.advance(); + + while !self.is_at_end() { + if self.previous().kind == TokenKind::Semicolon { + return; + } + + match self.peek().kind { + TokenKind::Class + | TokenKind::Fun + | TokenKind::Var + | TokenKind::For + | TokenKind::If + | TokenKind::While + | TokenKind::Print + | TokenKind::Return => return, + + _ => { + self.advance(); + } + } + } + } + + fn binary_operator( + &mut self, + oneof: &[TokenKind], + each: fn(&mut Parser) -> ExprResult, + ) -> ExprResult { + let mut expr = each(self)?; + + while oneof.iter().any(|t| self.match_token(t)) { + expr = Expr::Binary(Binary { + left: Box::new(expr), + operator: self.previous().clone(), + right: Box::new(each(self)?), + }) + } + + return Ok(expr); + } +} + +pub fn parse(tokens: Vec<Token>) -> Result<Block, Vec<Error>> { + let mut parser = Parser { tokens, current: 0 }; + let mut program: Block = vec![]; + let mut errors: Vec<Error> = vec![]; + + while !parser.is_at_end() { + match parser.declaration() { + Err(err) => { + errors.push(err); + parser.synchronise(); + } + Ok(decl) => { + program.push(decl); + } + } + } + + if errors.is_empty() { + Ok(program) + } else { + Err(errors) + } +} diff --git a/users/tazjin/rlox/src/treewalk/resolver.rs b/users/tazjin/rlox/src/treewalk/resolver.rs new file mode 100644 index 000000000000..3d12973aa089 --- /dev/null +++ b/users/tazjin/rlox/src/treewalk/resolver.rs @@ -0,0 +1,199 @@ +// Resolves variable access to their specific instances in the +// environment chain. +// +// https://craftinginterpreters.com/resolving-and-binding.html + +use std::collections::HashMap; +use std::rc::Rc; + +use crate::treewalk::errors::{Error, ErrorKind}; +use crate::treewalk::parser::{self, Expr, Statement}; +use crate::treewalk::scanner::Token; + +#[derive(Default)] +struct Resolver<'a> { + scopes: Vec<HashMap<&'a str, bool>>, +} + +impl<'a> Resolver<'a> { + // AST traversal + fn resolve(&mut self, program: &'a mut parser::Block) -> Result<(), Error> { + self.begin_scope(); + for stmt in program { + self.resolve_stmt(stmt)?; + } + self.end_scope(); + + Ok(()) + } + + fn resolve_stmt(&mut self, stmt: &'a mut Statement) -> Result<(), Error> { + match stmt { + Statement::Expr(expr) => self.resolve_expr(expr), + Statement::Print(expr) => self.resolve_expr(expr), + Statement::Var(var) => self.resolve_var(var), + Statement::Return(ret) => self.resolve_expr(&mut ret.value), + Statement::Block(block) => self.resolve(block), + + Statement::If(if_stmt) => { + self.resolve_expr(&mut if_stmt.condition)?; + self.resolve_stmt(&mut if_stmt.then_branch)?; + + if let Some(branch) = if_stmt.else_branch.as_mut() { + self.resolve_stmt(branch)?; + } + + Ok(()) + } + + Statement::While(while_stmt) => { + self.resolve_expr(&mut while_stmt.condition)?; + self.resolve_stmt(&mut while_stmt.body) + } + + Statement::Function(func) => match Rc::get_mut(func) { + Some(func) => self.resolve_function(func), + // The resolver does not clone references, so unless + // the interpreter is called before the resolver this + // case should never happen. + None => { + return Err(Error { + line: 0, + kind: ErrorKind::InternalError( + "multiple function references before interpretation".into(), + ), + }) + } + }, + } + } + + fn resolve_var(&mut self, var: &'a mut parser::Var) -> Result<(), Error> { + self.declare(&var.name.lexeme); + + if let Some(init) = &mut var.initialiser { + self.resolve_expr(init)?; + } + + self.define(&var.name.lexeme); + + Ok(()) + } + + fn resolve_function(&mut self, func: &'a mut parser::Function) -> Result<(), Error> { + self.declare(&func.name.lexeme); + self.define(&func.name.lexeme); + + self.begin_scope(); + + for param in &func.params { + self.declare(¶m.lexeme); + self.define(¶m.lexeme); + } + + for stmt in &mut func.body { + self.resolve_stmt(stmt)?; + } + + self.end_scope(); + + Ok(()) + } + + fn resolve_expr(&mut self, expr: &'a mut Expr) -> Result<(), Error> { + match expr { + Expr::Variable(var) => self.resolve_variable(var), + Expr::Assign(assign) => self.resolve_assign(assign), + Expr::Grouping(grouping) => self.resolve_expr(&mut grouping.0), + Expr::Call(call) => self.resolve_call(call), + Expr::Literal(_) => Ok(()), + Expr::Unary(unary) => self.resolve_expr(&mut unary.right), + + Expr::Logical(log) => { + self.resolve_expr(&mut log.left)?; + self.resolve_expr(&mut log.right) + } + + Expr::Binary(binary) => { + self.resolve_expr(&mut binary.left)?; + self.resolve_expr(&mut binary.right) + } + } + } + + fn resolve_variable(&mut self, var: &'a mut parser::Variable) -> Result<(), Error> { + if let Some(scope) = self.scopes.last_mut() { + if let Some(false) = scope.get(var.name.lexeme.as_str()) { + return Err(Error { + line: var.name.line, + kind: ErrorKind::StaticError( + "can't read local variable in its own initialiser".into(), + ), + }); + } + } + + var.depth = self.resolve_local(&var.name); + Ok(()) + } + + fn resolve_assign(&mut self, assign: &'a mut parser::Assign) -> Result<(), Error> { + self.resolve_expr(&mut assign.value)?; + assign.depth = self.resolve_local(&assign.name); + Ok(()) + } + + fn resolve_local(&mut self, name: &'a Token) -> Option<usize> { + for (c, scope) in self.scopes.iter().rev().enumerate() { + if scope.contains_key(name.lexeme.as_str()) { + return Some(c); + } + } + + None + } + + fn resolve_call(&mut self, call: &'a mut parser::Call) -> Result<(), Error> { + self.resolve_expr(&mut call.callee)?; + + for arg in call.args.iter_mut() { + self.resolve_expr(arg)?; + } + + Ok(()) + } + + // Internal helpers + + fn declare(&mut self, name: &'a str) { + if let Some(scope) = self.scopes.last_mut() { + scope.insert(&name, false); + } + } + + fn define(&mut self, name: &'a str) { + if let Some(scope) = self.scopes.last_mut() { + scope.insert(&name, true); + } + } + + fn begin_scope(&mut self) { + self.scopes.push(Default::default()); + } + + fn end_scope(&mut self) { + self.scopes.pop(); + } +} + +pub fn resolve(globals: &[String], block: &mut parser::Block) -> Result<(), Error> { + let mut resolver: Resolver = Default::default(); + + // Scope for static globals only starts, never ends. + resolver.begin_scope(); + for global in globals { + resolver.define(global); + } + + resolver.resolve(block) +} diff --git a/users/tazjin/russian/helpers.el b/users/tazjin/russian/helpers.el new file mode 100644 index 000000000000..41d4aa34f47e --- /dev/null +++ b/users/tazjin/russian/helpers.el @@ -0,0 +1,7 @@ +;; Helper functions for creating the other files. + +(defun wiktionary-lookup-at-point (ask-lang) + (interactive "P") + (let ((language (if ask-lang (read-string "Language code? ") "ru"))) + (eww (concat "https://ru.wiktionary.org/wiki/" + (thing-at-point 'word))))) diff --git a/users/tazjin/russian/roots.el b/users/tazjin/russian/roots.el new file mode 100644 index 000000000000..77d09b47261c --- /dev/null +++ b/users/tazjin/russian/roots.el @@ -0,0 +1,28 @@ +;; '(root explanation) +;; +;; All roots without explanations are TODOs. +;; +;; In some cases, roots are not direct morphological roots of their +;; descendent words (e.g. -ะณะพะปะพะฒ- => ะณะปะฐะฒะฝัะน) + +'(("-ะฒะตัั-" "everything, all, every, etc.") + ("-ะฒะธะด-" "seeing, viewing etc.") + ("-ะฒัะตะผ-" "time") + ("-ะณะพะฒะพั-" "related to talking") + ("-ะณะพะปะพะฒ-" "head, main, etc.") + ("-ะดััะณ-" nil) + ("-ะดัะผ-" "thinking, thoughts") + ("-ะถะธ-" "life") + ("-ะทะฝะฐ-" "knowing, knowledge") + ("-ะธะผั-" "name") + ("-ะน-" "walking, moving to") + ("-ะผะพัั-" "ability, permission") + ("-ะฝะพะฒ-" "new") + ("-ะพะฑั-" "common?") + ("-ะฟัะฐะฒะด-" "truth") + ("-ะฟัะพั-" "question") + ("-ัะบะฐะท-" nil) + ("-ัะผะพัั-" "watching, viewing") + ("-ัััะฐะฝ-" "country?") + ("-ั ะพะด-" "movement") + ("-ั ะพัะพั-" "goodness, niceness")) diff --git a/users/tazjin/russian/russian.el b/users/tazjin/russian/russian.el new file mode 100644 index 000000000000..28f1addeaa04 --- /dev/null +++ b/users/tazjin/russian/russian.el @@ -0,0 +1,97 @@ +(require 'cl-macs) +(require 'ht) +(require 'seq) +(require 's) + +;; Type definitions for Russian structures + +(cl-defstruct russian-word + "Definition and metadata of a single Russian word." + (word nil :type string) + (translations :type list + :documentation "List of lists of strings, each a set of translations.") + + (notes nil :type list ;; of string + :documentation "free-form notes about this word") + + (roots nil :type list ;; of string + :documentation "list of strings that correspond with roots (exact string match)")) + +(defun russian--merge-words (previous new) + "Merge two Russian word definitions together. If no previous + definition exists, only the new one will be returned." + (if (not previous) new + (cl-assert (equal (russian-word-word previous) + (russian-word-word new)) + "different words passed into merge function") + (make-russian-word :word (russian-word-word previous) + :translations (-concat (russian-word-translations previous) + (russian-word-translations new)) + :notes (-concat (russian-word-notes previous) + (russian-word-notes new)) + :roots (-concat (russian-word-roots previous) + (russian-word-roots new))))) + +;; Definitions for creating a data structure of all Russian words. + +(defvar russian-words (make-hash-table) + "Table of all Russian words in the corpus.") + +(defun russian--define-word (word) + "Define a single word in the corpus, optionally merging it with + another entry." + (let ((key (russian-word-word word))) + (ht-set russian-words key (russian--merge-words + (ht-get russian-words key) + word)))) + +(defmacro define-russian-words (&rest words) + "Define the list of all available words. There may be more than + one entry for a word in some cases." + (declare (indent defun)) + + ;; Clear the table before proceeding with insertion + (setq russian-words (make-hash-table)) + + (seq-map + (lambda (word) + (russian--define-word (make-russian-word :word (car word) + :translations (cadr word) + :notes (caddr word) + :roots (cadddr word)))) + words) + + '(message "Defined %s unique words." (ht-size russian-words))) + +;; Helpers to train Russian words through passively. + +(defun russian--format-word (word) + "Format a Russian word suitable for echo display." + (apply #'s-concat + (-flatten + (list (russian-word-word word) + " - " + (s-join ", " (russian-word-translations word)) + (when-let ((roots (russian-word-roots word))) + (list " [" (s-join ", " roots) "]")) + (when-let ((notes (russian-word-notes word))) + (list " (" (s-join "; " notes) ")")))))) + +(defun display-russian-words () + "Convert Russian words to passively terms and start passively." + (interactive) + (setq passively-learn-terms (make-hash-table)) + (ht-map + (lambda (k v) + (ht-set passively-learn-terms k (russian--format-word v))) + russian-words) + (passively-enable)) + +(defun lookup-last-russian-word (in-eww) + "Look up the last Russian word in Wiktionary" + (interactive "P") + (let ((url (concat "https://ru.wiktionary.org/wiki/" passively-last-displayed))) + (if in-eww (eww url) + (browse-url url)))) + +(provide 'russian) diff --git a/users/tazjin/russian/words.el b/users/tazjin/russian/words.el new file mode 100644 index 000000000000..784e5bddde5e --- /dev/null +++ b/users/tazjin/russian/words.el @@ -0,0 +1,723 @@ +;; entries :: '(entry ...)' +;; entry :: '(word translations note roots) +;; note :: (or nil string) +;; translations :: '(translation ...) +;; roots :: '(root ...) + +(require 'russian) + +(define-russian-words + ;; 1-50 + ("ะธ" ("and" "though")) + ("ะฒ" ("in" "at")) + ("ะฝะต" ("not")) + ("ะพะฝ" ("he")) + ("ะฝะฐ" ("on" "it" "at" "to")) + ("ั" ("I")) + ("ััะพ" ("what" "that" "why")) + ("ัะพั" ("that")) + ("ะฑััั" ("to be")) + ("ั" ("with" "and" "from" "of")) + ("ะฐ" ("while" "and" "but")) + ("ะฒะตัั" ("all" "everything") nil ("-ะฒะตัั-")) + ("ััะพ" ("that" "this" "it")) + ("ะบะฐะบ" ("how" "what" "as" "like")) + ("ะพะฝะฐ" ("she")) + ("ะฟะพ" ("on" "along" "by")) + ("ะฝะพ" ("but")) + ("ะพะฝะธ" ("they")) + ("ะบ" ("to" "for" "by")) + ("ั" ("by" "with" "of")) + ("ัั" ("you")) + ("ะธะท" ("from" "of" "in")) + ("ะผั" ("we")) + ("ะทะฐ" ("behind" "over" "at" "after")) + ("ะฒั" ("you")) + ("ัะฐะบ" ("so" "thus" "then")) + ("ะถะต" ("and" "as for" "but" "same")) + ("ะพั" ("from" "of" "for")) + ("ัะบะฐะทะฐัั" ("to say" "to speak") nil ("-ัะบะฐะท-")) + ("ััะพั" ("this")) + ("ะบะพัะพััะน" ("which" "who" "that")) + ("ะผะพัั" ("be able" "can") nil ("-ะผะพัั-")) + ("ัะตะปะพะฒะตะบ" ("man" "person")) + ("ะพ" ("of" "about" "against")) + ("ะพะดะธะฝ" ("one" "some" "alone")) + ("ะตัั" ("still" "yet")) + ("ะฑั" ("would")) + ("ัะฐะบะพะน" ("such" "so" "some")) + ("ัะพะปัะบะพ" ("only" "merely" "but")) + ("ัะตะฑั" ("myself" "himself" "herself")) + ("ัะฒะพั" ("one's own" "my" "our")) + ("ะบะฐะบะพะน" ("what" "which" "how")) + ("ะบะพะณะดะฐ" ("when" "while" "as")) + ("ัะถะต" ("already" "by now")) + ("ะดะปั" ("for" "to")) + ("ะฒะพั" ("here" "there" "this is" "that's") + ("calling attention to something")) + ("ะบัะพ" ("who" "that" "some")) + ("ะดะฐ" ("yes" "but") ("affirmation (..., right?)")) + ("ะณะพะฒะพัะธัั" ("to say" "to tell" "to speak") nil ("-ะณะพะฒะพั-")) + ("ะณะพะด" ("year")) + + ;; 51 - 100 + ("ะทะฝะฐัั" ("to know" "be aware") nil ("-ะทะฝะฐ-")) + ("ะผะพะน" ("my" "mine")) + ("ะดะพ" ("to" "up to" "about" "before")) + ("ะธะปะธ" ("or")) + ("ะตัะปะธ" ("if")) + ("ะฒัะตะผั" ("time" "season") nil ("-ะฒัะตะผ-")) + ("ััะบะฐ" ("hand" "arm")) + ("ะฝะตั" ("no" "not" "but")) + ("ัะฐะผัะน" ("most" "the very" "the same")) + ("ะฝะธ" ("not a" "not" "neither ... nor")) + ("ััะฐัั" ("to become" "begin" "come")) + ("ะฑะพะปััะพะน" ("big" "large" "important")) + ("ะดะฐะถะต" ("even")) + ("ะดััะณะพะน" ("other" "another" "different") nil ("-ะดััะณ-")) + ("ะฝะฐั" ("our" "ours")) + ("ัะฒะพะน" ("one's own")) + ("ะฝั" ("now" "right" "well" "come on")) + ("ะฟะพะด" ("under" "for" "towards" "to")) + ("ะณะดะต" ("where")) + ("ะดะตะปะพ" ("business" "affair" "matter")) + ("ะตััั" ("to eat" "to be")) + ("ัะฐะผ" ("oneself")) + ("ัะฐะท" ("time" "once" "since")) + ("ััะพะฑั" ("that" "in order that")) + ("ะดะฒะฐ" ("two")) + ("ัะฐะผ" ("there" "then")) + ("ัะตะผ" ("than" "instead of") + ("ัะตะผ ..., ัะตะผ ...")) + ("ะณะปะฐะท" ("eye" "sight")) + ("ะถะธะทะฝั" ("life") nil ("-ะถะธ-")) + ("ะฟะตัะฒัะน" ("first" "front" "former")) + ("ะดะตะฝั" ("day")) + ("ััั" ("here" "now" "then")) + ("ะฒะพ" ("in" "at") + ("as particle also: wow, exactly, ...")) + ("ะฝะธััะพ" ("nothing")) + ("ะฟะพัะพะผ" ("afterwards" "then")) + ("ะพัะตะฝั" ("very")) + ("ัะพ" ("with")) + ("ั ะพัะตัั" ("to want")) + ("ะปะธ" ("whether" "if")) + ("ะฟัะธ" ("attached to" "in the presence of" "by" "about")) + ("ะณะพะปะพะฒะฐ" ("head" "mind" "brains") nil ("-ะณะพะปะพะฒ-")) + ("ะฝะฐะดะพ" ("over" "above" "ought to")) + ("ะฑะตะท" ("without")) + ("ะฒะธะดะตัั" ("to see") nil ("-ะฒะธะด-")) + ("ะธะดัะธ" ("to go" "to come")) + ("ัะตะฟะตัั" ("now" "nowadays")) + ("ัะพะถะต" ("also" "as well" "too")) + ("ััะพััั" ("to stand" "be" "stand up")) + ("ะดััะณ" ("friend")) + ("ะดะพะผ" ("house" "home")) + + ;; 101-150 + ("ัะตะนัะฐั" ("now" "presently" "soon")) + ("ะผะพะถะฝะพ" ("possible" "permitted") nil ("-ะผะพัั-")) + ("ะฟะพัะปะต" ("after" "afterwards")) + ("ัะปะพะฒะพ" ("word")) + ("ะทะดะตัั" ("here")) + ("ะดัะผะฐัั" ("to think" "to believe") nil ("-ะดัะผ-")) + ("ะผะตััะพ" ("place" "seat")) + ("ัะฟัะพัะธัั" ("to ask") nil ("-ะฟัะพั-")) + ("ัะตัะตะท" ("through" "across")) + ("ะปะธัะพ" ("face" "person")) + ("ััะพ" ("what" "which" "that")) + ("ัะพะณะดะฐ" ("then")) + ("ั ะพัะพัะธะน" ("good" "nice") nil ("-ั ะพัะพั-")) + ("ะบะฐะถะดัะน" ("every" "each")) + ("ะฝะพะฒัะน" ("new" "modern") nil ("-ะฝะพะฒ-")) + ("ะถะธัั" ("to live") nil ("-ะถะธ-")) + ("ะดะพะปะถะฝัะน" ("due" "proper" "should")) + ("ัะผะพััะตัั" ("to look" "watch")) + ("ะฟะพัะตะผั" ("why")) + ("ะฟะพัะพะผั" ("that's why")) + ("ััะพัะพะฝะฐ" ("side" "party")) + ("ะฟัะพััะพ" ("simply")) + ("ะฝะพะณะฐ" ("foot" "leg")) + ("ัะธะดะตัั" ("to sit")) + ("ะฟะพะฝััั" ("to understand" "to realise")) + ("ะธะผะตัั" ("to own" "to have")) + ("ะบะพะฝะตัะฝัะน" ("final" "last")) + ("ะดะตะปะฐัั" ("to do" "make")) + ("ะฒะดััะณ" ("suddenly")) + ("ะฝะฐะด" ("above" "over")) + ("ะฒะทััั" ("to take")) + ("ะฝะธะบัะพ" ("nobody")) + ("ะฟะพะฝะธะผะฐัั" ("to understand")) + ("ะบะฐะทะฐัััั" ("to seem" "to appear")) + ("ัะฐะฑะพัะฐ" ("work" "job")) + ("ััะธ" ("three")) + ("ะฒะฐั" ("yours")) + ("ัะถ" ("really" "already")) + ("ะทะตะผะปั" ("earth" "land" "soil")) + ("ะบะพะฝะตั" ("end" "distance")) + ("ะฝะตัะบะพะปัะบะพ" ("several" "some")) + ("ัะฐั" ("hour" "time")) + ("ะณะพะปะพั" ("voice")) + ("ะณะพัะพะด" ("town" "city")) + ("ะฟะพัะปะตะดะฝะธะน" ("last" "the latest" "new")) + + ;; 151-200 + ("ะฟะพะบะฐ" ("for the present")) ;; TODO(tazjin): review + ("ั ะพัะพัะพ" ("well") nil ("-ั ะพัะพั-")) + ("ะดะฐะฒะฐัั" ("to give" "to grant")) + ("ะฒะพะดะฐ" ("water")) + ("ะฑะพะปะตะต" ("more")) + ("ั ะพัั" ("although")) + ("ะฒัะตะณะดะฐ" ("always")) + ("ะฒัะพัะพะน" ("second")) + ("ะบัะดะฐ" ("where" "what for" "much")) + ("ะฟะพะนัะธ" ("to go") nil ("-ะน-")) + ("ััะพะป" ("table" "desk" "board")) + ("ัะตะฑัะฝะพะบ" ("child" "kid" "infant")) + ("ัะฒะธะดะตัั" ("to see")) + ("ัะธะปะฐ" ("strength" "force")) + ("ะพัะตั" ("father")) + ("ะถะตะฝัะธะฝะฐ" ("woman")) + ("ะผะฐัะธะฝะฐ" ("car" "machine" "engine")) + ("ัะปััะฐะน" ("case" "occasion" "incident")) + ("ะฝะพัั" ("night")) + ("ััะฐะทั" ("at once" "right away" "just")) + ("ะผะธั" ("world" "peace")) + ("ัะพะฒัะตะผ" ("quite" "entirely" "totally")) + ("ะพััะฐัััั" ("to remain" "to stay")) + ("ะพะฑ" ("about" "of")) + ("ะฒะธะด" ("appearance" "look" "view")) + ("ะฒัะนัะธ" ("to go out" "to exit" "to come out" "to appear") nil ("-ะน-")) + ("ะดะฐัั" ("to give")) + ("ัะฐะฑะพัะฐัั" ("to work")) + ("ะปัะฑะธัั" ("to work")) + ("ััะฐััะน" ("old")) + ("ะฟะพััะธ" ("almost")) + ("ััะด" ("row" "line")) + ("ะพะบะฐะทะฐัััั" ("find oneself" "turn out")) + ("ะฝะฐัะฐะปะพ" ("beginning" "origin" "source")) + ("ัะฒะพะน" ("your" "yours")) + ("ะฒะพะฟัะพั" ("question" "matter" "problem") nil ("-ะฟัะพั-")) + ("ะผะฝะพะณะพ" ("many" "much")) + ("ะฒะพะนะฝะฐ" ("war")) + ("ัะฝะพะฒะฐ" ("again")) + ("ะพัะฒะตัะธัั" ("to answer" "to reply")) + ("ะผะตะถะดั" ("between" "among")) + ("ะฟะพะดัะผะฐัั" ("to think")) + ("ะพะฟััั" ("again")) + ("ะฑะตะปัะน" ("white")) + ("ะดะตะฝัะณะธ" ("money")) + ("ะทะฝะฐัะธัั" ("to mean" "to signify") nil ("-ะทะฝะฐ-")) + ("ะฟัะพ" ("about" "for")) + ("ะปะธัั" ("only" "as soon as")) + ("ะผะธะฝััะฐ" ("minute" "moment")) + ("ะถะตะฝะฐ" ("wife")) + + ;; 201-300 + ("ะฟะพัะผะพััะตัั" ("to watch" "to look" "to inspect") nil ("-ัะผะพัั-")) + ("ะฟัะฐะฒะดะฐ" ("truth") nil ("-ะฟัะฐะฒะด-")) + ("ะณะปะฐะฒะฝัะน" ("main" "chief") nil ("-ะณะพะปะพะฒ-")) + ("ัััะฐะฝะฐ" ("country") nil ("-ัััะฐะฝ-")) + ("ัะฒะตั" ("light" "world")) + ("ะถะดะฐัั" ("to wait")) + ("ะผะฐัั" ("mother")) + ("ะฑัะดัะพ" ("as if" "as though")) + ("ะฝะธะบะพะณะดะฐ" ("never")) + ("ัะพะฒะฐัะธั" ("comrade" "friend")) + ("ะดะพัะพะณะฐ" ("road" "way" "journey")) + ("ะพะดะฝะฐะบะพ" ("however" "although")) + ("ะปะตะถะฐัั" ("to lie" "to be situated")) + ("ะธะผะตะฝะฝะพ" ("namely" "just" "exactly") nil ("-ะธะผั-")) + ("ะพะบะฝะพ" ("window")) + ("ะฝะธะบะฐะบะพะน" ("no" "none")) + ("ะฝะฐะนัะธ" ("to find" "to discover") nil ("-ะน-")) + ("ะฟะธัะฐัั" ("to write")) + ("ะบะพะผะฝะฐัะฐ" ("room")) + ("ะะพัะบะฒะฐ" ("Moscow")) + ("ัะฐััั" ("part" "share" "department")) + ("ะฒะพะพะฑัะต" ("in general" "altogether" "on the whole") nil ("-ะพะฑั-")) + ("ะบะฝะธะณะฐ" ("book")) + ("ะผะฐะปะตะฝัะบะธะน" ("small" "little")) + ("ัะปะธัะฐ" ("street")) + ("ัะตะถะธัั" ("to decide" "to solve")) + ("ะดะฐะปะตะบะธะน" ("distant" "remote")) + ("ะดััะฐ" ("soul" "spirit")) + ("ัััั" ("hardly" "slightly")) + ("ะฒะตัะฝััััั" ("to return")) + ("ัััะพ" ("morning")) + ("ะฝะตะบะพัะพััะน" ("some")) + ("ััะธัะฐัั" ("to count" "to consider")) + ("ัะบะพะปัะบะพ" ("how much" "how many")) + ("ะฟะพะผะฝะธัั" ("to remember")) + ("ะฒะตัะตั" ("evening")) + ("ะฟะพะป" ("floor" "gender")) + ("ัะฐะบะธ" ("after all")) + ("ะฟะพะปััะธัั" ("to receive" "to get" "to obtain")) + ("ะฝะฐัะพะด" ("people" "nation")) + ("ะฟะปะตัะพ" ("shoulder" "upper arm")) + ("ั ะพัั" ("even" "if you want" "though")) + ("ัะตะณะพะดะฝั" ("today")) + ("ะฑะพะณ" ("god")) + ("ะฒะผะตััะต" ("together")) + ("ะฒะทะณะปัะด" ("look" "glance" "view")) + ("ั ะพะดะธัั" ("to go" "to walk") nil ("-ั ะพะด-")) + ("ะทะฐัะตะผ" ("what for" "why")) + ("ัะพะฒะตััะบะธะน" ("Soviet")) + ("ััััะบะธะน" ("Russian")) + ("ะฑัะฒะฐัั" ("to be" "to visit" "to happen")) + ("ะฟะพะปะฝัะน" ("full" "complete" "whole")) + ("ะฟัะธะนัะธ" ("to arrive" "to come") nil ("-ะน-")) + ("ะฟะฐะปะตั" ("finger" "toe")) + ("ะ ะพััะธั" ("Russia")) + ("ะปัะฑะพะน" ("any" "every")) + ("ะธััะพัะธั" ("history" "story" "event")) + ("ะฝะฐะบะพะฝะตั" ("finally" "at least")) + ("ะผััะปั" ("thought" "idea")) + ("ัะทะฝะฐัั" ("to know" "to learn" "to recognise") nil ("-ะทะฝะฐ-")) + ("ะฝะฐะทะฐะด" ("back" "backwards" "ago")) + ("ะพะฑัะธะน" ("general" "common") nil ("-ะพะฑั-")) + ("ะทะฐะผะตัะธัั" ("to notice" "to observe")) + ("ัะปะพะฒะฝะพ" ("as if" "like")) + ("ะฟัะพัะปัะน" ("past" "vergangen") nil ("-ะน-")) + ("ัะนัะธ" ("to leave" "to go away") nil ("-ะน-")) + ("ะธะทะฒะตััะฝัะน" ("well-known" "famous")) + ("ะดะฐะฒะฝะพ" ("long ago")) + ("ัะปััะฐัั" ("to hear")) + ("ัะปััะฐัั" ("to listen" "to hear")) + ("ะฑะพััััั" ("to be afraid" "fear")) + ("ััะฝ" ("son")) + ("ะฝะตะปัะทั" ("it is impossible" "can't")) + ("ะฟััะผะพ" ("straight" "frankly")) + ("ะดะพะปะณะพ" ("for a long time")) + ("ะฑััััะพ" ("fast" "quickly")) + ("ะปะตั" ("forest")) + ("ะฟะพั ะพะถะธะน" ("similar" "alike") nil ("-ั ะพะด-")) + ("ะฟะพัะฐ" ("time" "pore")) + ("ะฟััั" ("five")) + ("ะณะปัะดะตัั" ("to look" "to gaze")) + ("ะพะฝะพ" ("it")) + ("ัะตััั" ("to sit")) + ("ะธะผั" ("name") nil ("-ะธะผั-")) + ("ะถ" ("and" "as for" "but")) + ("ัะฐะทะณะพะฒะพั" ("conversation" "talk") nil ("-ะณะพะฒะพั-")) + ("ัะตะปะพ" ("body")) + ("ะผะพะปะพะดะพะน" ("young")) + ("ััะตะฝะฐ" ("wall")) + ("ะบัะฐัะฝัะน" ("red")) + ("ัะธัะฐัั" ("to read")) + ("ะฟัะฐะฒะพ" ("right")) + ("ััะฐัะธะบ" ("old man")) + ("ัะฐะฝะฝะธะน" ("early")) + ("ั ะพัะตัััั" ("to want" "to like")) + ("ะผะฐะผะฐ" ("mummy" "mum")) + ("ะพััะฐะฒะฐัััั" ("to remain" "to stay")) + ("ะฒััะพะบะธะน" ("tall" "high")) + ("ะฟััั" ("way" "track" "path")) + ("ะฟะพััะพะผั" ("therefore")) + + ;; 301-400 + ("ัะพะฒะตััะตะฝะฝะพ" ("absolutely" "quite")) + ("ะบัะพะผะต" ("except" "besides")) + ("ัััััะฐ" ("a thousand")) + ("ะผะตััั" ("month")) + ("ะฑัะฐัั" ("to take" "to hire")) + ("ะฝะฐะฟะธัะฐัั" ("to write")) + ("ัะตะปัะน" ("intact" "whole" "entire")) + ("ะพะณัะพะผะฝัะน" ("huge" "enormous")) + ("ะฝะฐัะธะฝะฐัั" ("to begin")) + ("ัะฟะธะฝะฐ" ("back")) + ("ะฝะฐััะพััะธะน" ("present" "real" "true")) + ("ะฟัััั" ("let's" "though")) + ("ัะทัะบ" ("tongue" "language")) + ("ัะพัะฝะพ" ("exactly")) + ("ััะตะดะธ" ("among")) + ("ััััะฒะพะฒะฐัั" ("to feel")) + ("ัะตัะดัะต" ("heart")) + ("ะฒะตััะธ" ("to lead")) + ("ะธะฝะพะณะดะฐ" ("sometimes")) + ("ะผะฐะปััะธะบ" ("boy")) + ("ััะฟะตัั" ("to be in time" "to be successful")) + ("ะฝะตะฑะพ" ("sky")) + ("ะถะธะฒะพะน" ("living" "lively" "alive")) + ("ัะผะตััั" ("death")) + ("ะฟัะพะดะพะปะถะฐัั" ("to continue")) + ("ะดะตะฒััะบะฐ" ("girl")) + ("ะพะฑัะฐะท" ("shape" "form" "image")) + ("ะบะพ" ("to" "towards" "by")) + ("ะทะฐะฑััั" ("to forget")) + ("ะฒะพะบััะณ" ("around")) + ("ะฟะธััะผะพ" ("letter")) + ("ะฒะปะฐััั" ("power")) + ("ัััะฝัะน" ("black")) + ("ะฟัะพะนัะธ" ("to pass" "go by" "be over") nil ("-ะน-")) + ("ะฟะพัะฒะธัััั" ("to appear" "to show up")) + ("ะฒะพะทะดัั " ("air")) + ("ัะฐะทะฝัะน" ("different")) + ("ะฒัั ะพะดะธัั" ("to go out" "to exit") ("MR says 'to nurse'??") ("-ั ะพะด-")) + ("ะฟัะพัะธัั" ("to ask")) + ("ะฑัะฐั" ("brat")) + ("ัะพะฑััะฒะตะฝะฝัะน" ("one's own")) + ("ะพัะฝะพัะตะฝะธะต" ("relationship" "attitude")) + ("ะทะฐัะตะผ" ("then" "after that")) + ("ะฟััะฐัััั" ("to try")) + ("ะฟะพะบะฐะทะฐัั" ("to show" "to display")) + ("ะฒัะฟะพะผะฝะธัั" ("to remember" "to recall")) + ("ัะธััะตะผะฐ" ("system")) + ("ัะตัััะต" ("four")) + ("ะบะฒะฐััะธัะฐ" ("flat" "apartment")) + ("ะดะตัะถะฐัั" ("to hold" "to keep")) + ("ัะฐะบะถะต" ("also" "as well" "too")) + ("ะปัะฑะพะฒั" ("love")) + ("ัะพะปะดะฐั" ("soldier")) + ("ะพัะบัะดะฐ" ("from where")) + ("ััะพะฑ" ("that" "in order that")) + ("ะฝะฐะทัะฒะฐัั" ("to call" "to name")) + ("ััะตัะธะน" ("third")) + ("ั ะพะทัะธะฝ" ("master" "boss" "host")) + ("ะฒัะพะดะต" ("like" "not unlike")) + ("ัั ะพะดะธัั" ("to leave" "to go away") nil ("-ั ะพะด-")) + ("ะฟะพะดะพะนัะธ" ("to approach" "to come up") nil ("-ะน-")) + ("ะฟะพะดะฝััั" ("to lift" "to raise")) + ("ัะฟัะฐัะธะฒะฐัั" ("to ask" "to inquire")) + ("ะฝะฐัะฐะปัะฝะธะบ" ("chief" "head" "superior")) + ("ะพะฑะฐ" ("both")) + ("ะฑัะพัะธัั" ("to throw")) + ("ัะบะพะปะฐ" ("school")) + ("ะฟะฐัะตะฝั" ("boy" "fellow" "guy")) + ("ะบัะพะฒั" ("blood")) + ("ะดะฒะฐะดัะฐัั" ("twenty")) + ("ัะพะปะฝัะต" ("sun")) + ("ะฝะตะดะตะปั" ("week")) + ("ะฟะพัะปะฐัั" ("to send" "to dispatch")) + ("ะฝะฐั ะพะดะธัััั" ("to be found" "to turn up") nil ("-ั ะพะด-")) + ("ัะตะฑััะฐ" ("guys" "children")) + ("ะฟะพััะฐะฒะธัั" ("to put" "to place" "to set")) + ("ะฒััะฐัั" ("to get up" "to rise" "to stand up")) + ("ะฝะฐะฟัะธะผะตั" ("for example" "for instance")) + ("ัะฐะณ" ("step")) + ("ะผัะถัะธะฝะฐ" ("man" "male")) + ("ัะฐะฒะฝะพ" ("alike" "in like manner")) + ("ะฝะพั" ("nose")) + ("ะผะฐะปะพ" ("little" "few")) + ("ะฒะฝะธะผะฐะฝะธะต" ("attention")) + ("ะบะฐะฟะธัะฐะฝ" ("captain" "master")) + ("ัั ะพ" ("ear")) + ("ััะดะฐ" ("to there")) + ("ััะดะฐ" ("to here")) + ("ะธะณัะฐัั" ("to play")) + ("ัะปะตะดะพะฒะฐัั" ("to follow" "to come next")) + ("ัะฐััะบะฐะทะฐัั" ("to tell" "to narrate")) + ("ะฒะตะปะธะบะธะน" ("great")) + ("ะดะตะนััะฒะธัะตะปัะฝะพ" ("indeed" "really")) + ("ัะปะธัะบะพะผ" ("too much")) + ("ััะถัะปัะน" ("heavy")) + ("ัะฟะฐัั" ("to sleep")) + ("ะพััะฐะฒะธัั" ("to leave" "to abandon")) + ("ะฒะพะนัะธ" ("to enter" "to come in") nil ("-ะน-")) + ("ะดะปะธะฝะฝัะน" ("long")) + + ;; 401 - 500 + ("ััะฒััะฒะพ" ("feeling")) + ("ะธะพะปัะฐัั" ("to keep silence" "make no complaint" "say nothing")) + ("ัะฐััะบะฐะทัะฒะฐัั" ("to tell" "narrate")) + ("ะพัะฒะตัะฐัั" ("to answer" "to reply")) + ("ััะฐะฝะพะฒะธัััั" ("to stand" "to become")) + ("ะพััะฐะฝะพะฒะธัััั" ("to stop")) + ("ะฑะตัะตะณ" ("bank" "shore" "coast")) + ("ัะตะผัั" ("family")) + ("ะธัะบะฐัั" ("to search")) + ("ะณะตะฝะตัะฐะป" ("general")) + ("ะผะพะผะตะฝั" ("moment" "instant")) + ("ะดะตัััั" ("ten")) + ("ะฝะฐัะฐัั" ("to begin")) + ("ัะปะตะดัััะธะน" ("next" "following")) + ("ะปะธัะฝัะน" ("personal")) + ("ัััะด" ("labour" "work")) + ("ะฒะตัะธัั" ("to believe")) + ("ะณััะฟะฟะฐ" ("group")) + ("ะฝะตะผะฝะพะณะพ" ("a little")) + ("ะฒะฟัะพัะตะผ" ("however" "though")) + ("ะฒะธะดะฝะพ" ("evidently" "obviously")) + ("ัะฒะปััััั" ("to appear")) + ("ะผัะถ" ("husband")) + ("ัะฐะทะฒะต" ("really?" "perhaps") ("when pondering something")) + ("ะดะฒะธะถะตะฝะธะต" ("movement" "motion")) + ("ะฟะพััะดะพะบ" ("order")) + ("ะพัะฒะตั" ("answer" "reply")) + ("ัะธั ะพ" ("quietly" "silently") ("also as exclamation")) + ("ะทะฝะฐะบะพะผัะน" ("familiar" "acquainted")) + ("ะณะฐะทะตัะฐ" ("newspaper")) + ("ะฟะพะผะพัั" ("help")) + ("ัะธะปัะฝัะน" ("strong" "powerful")) + ("ัะบะพััะน" ("quick" "fast")) + ("ัะพะฑะฐะบะฐ" ("dog")) + ("ะดะตัะตะฒะพ" ("tree")) + ("ัะฝะตะณ" ("snow")) + ("ัะพะฝ" ("dream")) + ("ัะผััะป" ("sense" "meaning" "purpose") ("making sense" "in the sense")) + ("ัะผะพัั" ("to be able") ("ัะฒ")) + ("ะฟัะพัะธะฒ" ("against" "opposite" "contrary to")) + ("ะฑะตะถะฐัั" ("to run" "to hurry")) + ("ะดะฒะพั" ("yard" "court")) + ("ัะพัะผะฐ" ("form" "shape" "uniform")) + ("ะฟัะพััะพะน" ("simple" "easy" "plain")) + ("ะฟัะธะตั ะฐัั" ("to arrive" "to come")) + ("ะธะฝะพะน" ("different" "other")) + ("ะบัะธัะฐัั" ("to cry" "to shout")) + ("ะฒะพะทะผะพะถะฝะพััั" ("possibility" "opportunity" "chance")) + ("ะพะฑัะตััะฒะพ" ("society")) + ("ะทะตะปัะฝัะน" ("green")) + ("ะณััะดั" ("breast" "chest")) + ("ัะณะพะป" ("corner" "angle")) + ("ะพัะบัััั" ("to open")) + ("ะฟัะพะธัั ะพะดะธัั" ("to happen" "to occur" "to take place")) + ("ะปะฐะดะฝะพ" ("well" "all right" "okay")) + ("ัััะฝัะน" ("black") ("noun (m.): as in 'she wears black'")) + ("ะฒะตะบ" ("century" "age")) + ("ะบะฐัะผะฐะฝ" ("pocket")) + ("ะตั ะฐัั" ("to go" "ride" "drive" "travel")) + ("ะฝะตะผะตั" ("German")) + ("ะฝะฐะฒะตัะฝะพะต" ("probably" "most likely")) + ("ะณัะฑะฐ" ("lip")) + ("ะดัะดั" ("uncle")) + ("ะฟัะธั ะพะดะธัั" ("to come" "to arrive")) + ("ัะฐััะพ" ("often")) + ("ะดะพะผะพะน" ("home") ("as in direction")) + ("ะพะณะพะฝั" ("fire")) + ("ะฟะธัะฐัะตะปั" ("writer")) + ("ะฐัะผะธั" ("army")) + ("ัะพััะพัะฝะธะต" ("state" "condition" "fortune")) + ("ะทัะฑ" ("tooth")) + ("ะพัะตัะตะดั" ("queue" "line" "turn")) + ("ะบะพะน" ("which") ("old-fashioned, literary (in set expressions)")) + ("ะฟะพะดะฝััััั" ("to rise" "to climb")) + ("ะบะฐะผะตะฝั" ("stone")) + ("ะณะพััั" ("guest")) + ("ะฟะพะบะฐะทะฐัััั" ("to appear" "to come in sight")) + ("ะฒะตัะตั" ("window")) + ("ัะพะฑะธัะฐัััั" ("to gather" "to assemble" "to intend") ("TODO: intend??")) + ("ะฟะพะฟะฐััั" ("to hit" "to find oneself") ("to get (in phrases)")) + ("ะฟัะธะฝััั" ("to take" "to admit" "to accept")) + ("ัะฝะฐัะฐะปะฐ" ("at first" "from the beginning")) + ("ะปะธะฑะพ" ("or")) + ("ะฟะพะตั ะฐัั" ("to depart" "to set off")) + ("ััะปััะฐัั" ("to hear")) + ("ัะผะตัั" ("to be able" "know" "can")) + ("ัะปััะธัััั" ("to happen")) + ("ัััะฐะฝะฝัะน" ("strange")) + ("ะตะดะธะฝััะฒะตะฝะฝัะน" ("only" "sole")) + ("ัะพัะฐ" ("company") ("(military)")) + ("ะทะฐะบะพะฝ" ("law" "act" "statute")) + ("ะบะพัะพัะบะธะน" ("short")) + ("ะผะพัะต" ("sea")) + ("ะดะพะฑััะน" ("kind")) + ("ััะผะฝัะน" ("dark")) + ("ะณะพัะฐ" ("mountain" "hill")) + ("ะฒัะฐั" ("doctor")) + ("ะบัะฐะน" ("border, edge" "land, country")) + ("ััะฐัะฐัััั" ("to try" "to endeavour")) + ("ะปัััะธะน" ("better" "best")) + + ;; 501 - 600 + ("ัะตะบะฐ" ("river")) + ("ะฒะพะตะฝะฝัะน" ("military")) + ("ะผะตัะฐ" ("measure" "step")) + ("ัััะฐัะฝัะน" ("terrible" "frightful")) + ("ะฒะฟะพะปะฝะต" ("quite" "fully")) + ("ะทะฒะฐัั" ("to call")) + ("ะฟัะพะธะทะพะนัะธ" ("to happen" "to occur" "take place")) + ("ะฒะฟะตัะตะด" ("forward")) + ("ะผะตะดะปะตะฝะฝะพ" ("slowly")) + ("ะฒะพะทะปะต" ("by" "near" "close by")) + ("ะฝะธะบะฐะบ" ("in no way" "by no means")) + ("ะทะฐะฝะธะผะฐัััั" ("to be occupied" "to engage")) + ("ะดะตะนััะฒะธะต" ("action" "effort")) + ("ะดะพะฒะพะปัะฝะพ" ("enough" "rather")) + ("ะฒะตัั" ("thing")) + ("ะฝะตะพะฑั ะพะดะธะผัะน" ("necessary") ("not possible to go around")) + ("ั ะพะด" ("move")) + ("ะฑะพะปั" ("pain")) + ("ััะดัะฑะฐ" ("fate" "fortune" "destiny")) + ("ะฟัะธัะธะฝะฐ" ("cause" "reason" "motive")) + ("ะฟะพะปะพะถะธัั" ("to lay down" "put down" "place")) + ("ะตะดะฒะฐ" ("hardly" "just" "barely")) + ("ัะตััะฐ" ("line" "boundary" "trait")) + ("ะดะตะฒะพัะบะฐ" ("girl" "little girl")) + ("ะปัะณะบะธะน" ("light" "easy")) + ("ะฒะพะปะพั" ("hair")) + ("ะบัะฟะธัั" ("to buy" "purchase")) + ("ะฝะพะผะตั" ("number" "size" "room" "issue")) + ("ะพัะฝะพะฒะฝะพะน" ("main")) + ("ัะธัะพะบะธะน" ("wide")) + ("ัะผะตัะตัั" ("to die")) + ("ะดะฐะปะตะบะพ" ("far" "far off")) + ("ะฟะปะพั ะพ" ("badly")) + ("ะณะปะฐะฒะฐ" ("head" "chief")) + ("ะบัะฐัะธะฒัะน" ("beautiful")) + ("ัะตััะน" ("grey" "dull")) + ("ะฟะธัั" ("to drink")) + ("ะบะพะผะฐะฝะดะธั" ("commander" "officer")) + ("ะพะฑััะฝะพ" ("usually")) + ("ะฟะฐััะธั" ("party")) + ("ะฟัะพะฑะปะตะผะฐ" ("problem" "issue")) + ("ัััะฐั " ("fear")) + ("ะฟัะพั ะพะดะธัั" ("to pass" "go" "study")) + ("ััะฝะพ" ("clear" "clearly")) + ("ัะฝััั" ("to take away" "take off")) + ("ะฑัะผะฐะณะฐ" ("paper")) + ("ะณะตัะพะน" ("hero")) + ("ะฟะฐัะฐ" ("pair" "couple")) + ("ะณะพััะดะฐัััะฒะพ" ("State")) + ("ะดะตัะตะฒะฝั" ("village")) + ("ัะตัั" ("speech")) + ("ะฝะฐัะฐัััั" ("to begin")) + ("ััะตะดััะฒะพ" ("means" "remedy")) + ("ะฟะพะปะพะถะตะฝะธะต" ("position" "posture" "condition" "state")) + ("ัะฒัะทั" ("tie, bond" "connection, relation")) + ("ะฝะตะฑะพะปััะพะน" ("small" "not great")) + ("ะฟัะตะดััะฐะฒะปััั" ("to present" "introduce" "imagine")) + ("ะทะฐะฒััะฐ" ("tomorrow")) + ("ะพะฑัััะฝะธัั" ("to explain")) + ("ะฟัััะพะน" ("empty" "hollow" "idle")) + ("ะฟัะพะธะทะฝะตััะธ" ("to pronounce" "say" "utter")) + ("ัะตะปะพะฒะตัะตัะบะธะน" ("human")) + ("ะฝัะฐะฒะธัััั" ("to please" "be likeable to")) + ("ะพะดะฝะฐะถะดั" ("once" "one day")) + ("ะผะธะผะพ" ("past" "by")) + ("ะธะฝะฐัะต" ("otherwise" "differently|")) + ("ัััะตััะฒัะพะฒะฐัั" ("to exist" "to be")) + ("ะบะปะฐัั" ("class")) + ("ัะดะฐัััั" ("turn out well" "succeed" "manage")) + ("ัะพะปัััะน" ("thick" "heavy" "fat")) + ("ัะตะปั" ("goal" "object" "target")) + ("ัะบะฒะพะทั" ("through")) + ("ะฟัะธะนัะธัั" ("to fit" "fall" "have to") ("ัะตะฑะต ะฟัะธะดัััั - you have to")) + ("ัะธัััะน" ("clean" "pure")) + ("ะทะฝะฐัั" ("to know")) + ("ะฟัะตะถะฝะธะน" ("former")) + ("ะฟัะพัะตััะพั" ("professor")) + ("ะณะพัะฟะพะดะธะฝ" ("gentleman" "Mr.")) + ("ััะฐัััะต" ("happiness" "luck")) + ("ั ัะดะพะน" ("thin" "skinny")) + ("ะดัั " ("spirit")) + ("ะฟะปะฐะฝ" ("plan")) + ("ััะถะพะน" ("somebody else's" "strange" "foreign")) + ("ะทะฐะป" ("hall")) + ("ะฟัะตะดััะฐะฒะธัั" ("to present" "produce" "introduce")) + ("ะพัะพะฑัะน" ("special")) + ("ะดะธัะตะบัะพั" ("director" "manager")) + ("ะฑัะฒัะธะน" ("former" "ex-")) + ("ะฟะฐะผััั" ("memory")) + ("ะฑะปะธะทะบะธะน" ("near" "similar" "intimate")) + ("ัะตะน" ("this")) + ("ัะตะทัะปััะฐั" ("result" "outcome")) + ("ะฑะพะปัะฝะพะน" ("sick")) + ("ะดะฐะฝะฝัะน" ("given" "present")) + ("ะบััะฐัะธ" ("to the point" "at the same time")) + ("ะฝะฐะทะฒะฐัั" ("to call" "name")) + ("ัะปะตะด" ("track" "footprint")) + ("ัะปัะฑะฐัััั" ("to smile") ("ะฝัะฒ")) + ("ะฑัััะปะบะฐ" ("bottle")) + + ;; 601 - 700 + ("ัััะดะฝะพ" ("with difficulty")) + ("ััะปะพะฒะธะต" ("condition" "term")) + ("ะฟัะตะถะดะต" ("before")) + ("ัะผ" ("mind" "brains" "intellect")) + ("ัะปัะฑะฝััััั" ("to smile")) + ("ะฟัะพัะตัั" ("process")) + ("ะบะฐััะธะฝะฐ" ("picture" "painting")) + ("ะฒะผะตััะพ" ("instead")) + ("ััะฐััะธะน" ("elder" "senior")) + ("ะปะตะณะบะพ" ("easily" "lightly")) + ("ัะตะฝัั" ("center")) + ("ะฟะพะดะพะฑะฝัะน" ("similar" "like")) + ("ะฒะพะทะผะพะถะฝะพ" ("possible") ("as ... as possible")) + ("ะพะบะพะปะพ" ("by" "near")) + ("ัะผะตััััั" ("to laugh")) + ("ััะพ" ("hundred")) + ("ะฑัะดััะตะต" ("future")) + ("ั ะฒะฐัะฐัั" ("to snatch" "to seize" "to suffice") ("ะฝัะฒ")) + ("ัะธัะปะพ" ("number")) + ("ะฒััะบะพะต" ("any" "every")) + ("ััะฑะปั" ("ruble")) + ("ะฟะพััะฒััะฒะพะฒะฐัั" ("to feel") ("ัะฒ")) + ("ะฟัะธะฝะตััะธ" ("to bring")) + ("ะฒะตัะฐ" ("faith" "belief")) + ("ะฒะพะฒัะต" ("quiet" "not ... at all")) + ("ัะดะฐั" ("blow" "stroke")) + ("ัะตะปะตัะพะฝ" ("telephone")) + ("ะบะพะปะตะฝะพ" ("knee")) + ("ัะพะณะปะฐัะธัััั" ("to agree" "to consent")) + ("ะผะฐะปะพ" ("little" "few" "not enough")) + ("ะบะพัะธะดะพั" ("corridor" "passage")) + ("ะผัะถะธะบ" ("man")) + ("ะฟัะฐะฒัะน" ("right")) + ("ะฐะฒัะพั" ("author")) + ("ั ะพะปะพะดะฝัะน" ("cold" "cool")) + ("ั ะฒะฐัะธั" ("to snatch" "to seize" "to suffice") ("ัะฒ")) + ("ะผะฝะพะณะธะต" ("many")) + ("ะฒัััะตัะฐ" ("meeting" "reception")) + ("ะบะฐะฑะธะฝะตั" ("study" "room" "office suite")) + ("ะดะพะบัะผะตะฝั" ("document")) + ("ัะฐะผะพะปัั" ("airplane")) + ("ะฒะฝะธะท" ("down" "downwards")) + ("ะฟัะธะฝะธะผะฐัั" ("to take" "to admit" "to accept")) + ("ะธะณัะฐ" ("game" "play")) + ("ัะฐััะบะฐะท" ("story")) + ("ั ะปะตะฑ" ("bread")) + ("ัะฐะทะฒะธัะธะต" ("development")) + ("ัะฑะธัั" ("to kill")) + ("ัะพะดะฝะพะน" ("own" "native" "dear")) + ("ะพัะบััััะน" ("open")) + ("ะผะตะฝะตะต" ("less")) + ("ะฟัะตะดะปะพะถะธัั" ("to offer" "to propose" "to suggest")) + ("ะถัะปััะน" ("yellow")) + ("ะฟัะธั ะพะดะธัััั" ("to fit" "to fall" "to have to")) + ("ะฒัะฟะธัั" ("to drink")) + ("ะบัะธะบะฝััั" ("to cry" "to shout")) + ("ัััะฑะบะฐ" ("tube" "roll" "pipe")) + ("ะฒัะฐะณ" ("enemy")) + ("ะฟะพะบะฐะทัะฒะฐัั" ("to show" "to display")) + ("ะดะฒะพะต" ("two") ("cardinal number")) + ("ะดะพะบัะพั" ("doctor")) + ("ะปะฐะดะพะฝั" ("palm")) + ("ะฒัะทะฒะฐัั" ("to call" "to send")) + ("ัะฟะพะบะพะนะฝะพ" ("quietly")) + ("ะฟะพะฟัะพัะธัั" ("to ask")) + ("ะฝะฐัะบะฐ" ("science")) + ("ะปะตะนัะตะฝะฐะฝั" ("lieutenant")) + ("ัะปัะถะฑะฐ" ("service" "work")) + ("ะพะบะฐะทัะฒะฐัััั" ("to turn out" "to find oneself")) + ("ะฟัะธะฒะตััะธ" ("to bring")) + ("ัะพัะพะบ" ("forty")) + ("ัััั" ("bill" "account")) + ("ะฒะพะทะฒัะฐัะฐัััั" ("to return")) + ("ะทะพะปะพัะพะน" ("golden")) + ("ะผะตััะฝัะน" ("local")) + ("ะบัั ะฝั" ("kitchen")) + ("ะบััะฟะฝัะน" ("large" "big" "prominent")) + ("ัะตัะตะฝะธะต" ("decision" "conclusion")) + ("ะผะพะปะพะดะฐั" ("bride" "young")) + ("ััะธะดัะฐัั" ("thirty")) + ("ัะพะผะฐะฝ" ("novel" "romance")) + ("ะบะพะผะฟะฐะฝะธั" ("company")) + ("ัะฐัััะน" ("frequent")) + ("ัะพััะธะนัะบะธะน" ("Russian")) + ("ัะฐะฑะพัะธะน" ("worky")) + ("ะฟะพัะตัััั" ("to lose")) + ("ัะตัะตะฝะธะต" ("current")) + ("ัะธะฝะธะน" ("dark blue")) + ("ััะพะปัะบะพ" ("so much" "so many")) + ("ััะฟะปัะน" ("warm")) + ("ะผะตัั" ("metre")) + ("ะดะพััะฐัั" ("to reach" "get" "obtain")) + ("ะถะตะปะตะทะฝัะน" ("ferreous" "iron")) + ("ะธะฝััะธััั" ("institute")) + ("ัะพะพะฑัะธัั" ("to report" "to let know")) + ("ะธะฝัะตัะตั" ("interest")) + ("ะพะฑััะฝัะน" ("usual" "ordinary")) + ("ะฟะพัะฒะปััััั" ("to appear" "to show up")) + ("ัะฟะฐััั" ("to fall"))) + +(provide 'russian-words) diff --git a/users/tazjin/rustfmt.toml b/users/tazjin/rustfmt.toml new file mode 100644 index 000000000000..0c719dcfec6f --- /dev/null +++ b/users/tazjin/rustfmt.toml @@ -0,0 +1,22 @@ +edition = "2021" +newline_style = "Unix" + +# Default code with is 100 characters, comments should follow +# suit. +wrap_comments = true + +# The default of this option creates hard-to-read nesting of +# conditionals, turn it off. +combine_control_expr = false + +# Group imports by module, but no higher. This avoids hard-to-read +# nested use statements. +imports_granularity = "Module" + +# Avoid vertical visual clutter by unnecessarily exploding +# block-like arguments. +overflow_delimited_expr = true + +# Miscellaneous +format_code_in_doc_comments = true +normalize_comments = true diff --git a/users/tazjin/tgsa/.gitignore b/users/tazjin/tgsa/.gitignore new file mode 100644 index 000000000000..29e65519ba35 --- /dev/null +++ b/users/tazjin/tgsa/.gitignore @@ -0,0 +1,3 @@ +result +/target +**/*.rs.bk diff --git a/users/tazjin/tgsa/Cargo.lock b/users/tazjin/tgsa/Cargo.lock new file mode 100644 index 000000000000..d5d034dde45e --- /dev/null +++ b/users/tazjin/tgsa/Cargo.lock @@ -0,0 +1,1296 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ef4730490ad1c4eae5c4325b2a95f521d023e5c885853ff7aca0a6a1631db3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "697ed7edc0f1711de49ce108c541623a0af97c6c60b2f6e2b65229847ac843c2" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "anyhow" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" + +[[package]] +name = "ascii" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbf56136a5198c7b01a49e3afcbef6cf84597273d298f54432926024107b0109" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "buf_redux" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" +dependencies = [ + "memchr", + "safemem", +] + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "winapi", +] + +[[package]] +name = "chunked_transfer" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crimp" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe8f9a320ad9c1a2e3bacedaa281587bd297fb10a10179fd39f777049d04794" +dependencies = [ + "curl", + "serde", + "serde_json", +] + +[[package]] +name = "cssparser" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa 0.4.8", + "matches", + "phf", + "proc-macro2", + "quote", + "smallvec", + "syn", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfae75de57f2b2e85e8768c3ea840fd159c8f33e2b6522c7835b7abac81be16e" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "curl" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d855aeef205b43f65a5001e0997d81f8efca7badad4fad7d897aa7f0d0651f" +dependencies = [ + "curl-sys", + "libc", + "openssl-probe", + "openssl-sys", + "schannel", + "socket2", + "winapi", +] + +[[package]] +name = "curl-sys" +version = "0.4.53+curl-7.82.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8092905a5a9502c312f223b2775f57ec5c5b715f9a15ee9d2a8591d1364a0352" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", + "winapi", +] + +[[package]] +name = "deflate" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f95bf05dffba6e6cce8dfbb30def788154949ccd9aed761b472119c21e01c70" +dependencies = [ + "adler32", + "gzip-header", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + +[[package]] +name = "dtoa-short" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03329ae10e79ede66c9ce4dc930aa8599043b0743008548680f25b91502d6" +dependencies = [ + "dtoa", +] + +[[package]] +name = "ego-tree" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a68a4904193147e0a8dec3314640e6db742afd5f6e634f428a6af230d9b3591" + +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + +[[package]] +name = "filetime" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0408e2626025178a6a7f7ffc05a25bc47103229f19c113755de7bf63816290c" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "winapi", +] + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.10.2+wasi-snapshot-preview1", +] + +[[package]] +name = "gzip-header" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0131feb3d3bb2a5a238d8a4d09f6353b7ebfdc52e77bccbf4ea6eaa751dde639" +dependencies = [ + "crc32fast", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "html5ever" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "httparse" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6330e8a36bd8c859f3fa6d9382911fbb7147ec39807f63b923933a247240b9ba" + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb691a747a7ab48abc15c5b42066eaafde10dc427e3b6ee2a1cf43db04c763bd" + +[[package]] +name = "libz-sys" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f35facd4a5673cb5a48822be2be1d4236c1c99cb4113cab7061ac720d5bf859" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "multipart" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182" +dependencies = [ + "buf_redux", + "httparse", + "log", + "mime", + "mime_guess", + "quick-error", + "rand 0.8.5", + "safemem", + "tempfile", + "twoway", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aba1801fb138d8e85e11d0fc70baf4fe1cdfffda7c6cd34a854905df588e5ed0" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "995f667a6c822200b0433ac218e05582f0e2efa1b922a3fd2fbaadc5f87bab37" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros", + "phf_shared 0.8.0", + "proc-macro-hack", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pkg-config" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro2" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom 0.2.6", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "redox_syscall" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +dependencies = [ + "bitflags", +] + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "rouille" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18b2380c42510ef4a28b5f228a174c801e0dec590103e215e60812e2e2f34d05" +dependencies = [ + "base64", + "brotli", + "chrono", + "deflate", + "filetime", + "multipart", + "num_cpus", + "percent-encoding", + "rand 0.8.5", + "serde", + "serde_derive", + "serde_json", + "sha1", + "threadpool", + "time", + "tiny_http", + "url", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "scraper" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e02aa790c80c2e494130dec6a522033b6a23603ffc06360e9fe6c611ea2c12" +dependencies = [ + "cssparser", + "ego-tree", + "getopts", + "html5ever", + "matches", + "selectors", + "smallvec", + "tendril", +] + +[[package]] +name = "selectors" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +dependencies = [ + "bitflags", + "cssparser", + "derive_more", + "fxhash", + "log", + "matches", + "phf", + "phf_codegen", + "precomputed-hash", + "servo_arc", + "smallvec", + "thin-slice", +] + +[[package]] +name = "semver" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65bd28f48be7196d222d95b9243287f48d27aca604e08497513019ff0502cc4" + +[[package]] +name = "serde" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +dependencies = [ + "itoa 1.0.1", + "ryu", + "serde", +] + +[[package]] +name = "servo_arc" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +dependencies = [ + "sha1_smol", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] +name = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + +[[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + +[[package]] +name = "socket2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "string_cache" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213494b7a2b503146286049378ce02b482200519accc31872ee8be91fa820a08" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared 0.10.0", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", +] + +[[package]] +name = "syn" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "tgsa" +version = "0.1.0" +dependencies = [ + "anyhow", + "crimp", + "ego-tree", + "rouille", + "scraper", + "url", +] + +[[package]] +name = "thin-slice" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "time" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" +dependencies = [ + "libc", + "num_threads", +] + +[[package]] +name = "tiny_http" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce51b50006056f590c9b7c3808c3bd70f0d1101666629713866c227d6e58d39" +dependencies = [ + "ascii", + "chrono", + "chunked_transfer", + "log", + "url", +] + +[[package]] +name = "tinyvec" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "twoway" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" +dependencies = [ + "memchr", +] + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5acdd78cb4ba54c0045ac14f62d8f94a03d10047904ae2a40afa1e99d8f70825" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" + +[[package]] +name = "windows_i686_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" + +[[package]] +name = "windows_i686_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" diff --git a/users/tazjin/tgsa/Cargo.toml b/users/tazjin/tgsa/Cargo.toml new file mode 100644 index 000000000000..105333c942dd --- /dev/null +++ b/users/tazjin/tgsa/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "tgsa" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0" +crimp = "0.2" +ego-tree = "0.6" # in tandem with 'scraper' +rouille = "3.5" +scraper = "0.12" +url = "2.2" diff --git a/users/tazjin/tgsa/default.nix b/users/tazjin/tgsa/default.nix new file mode 100644 index 000000000000..ef8842ea26be --- /dev/null +++ b/users/tazjin/tgsa/default.nix @@ -0,0 +1,10 @@ +{ depot, pkgs, ... }: + +depot.third_party.naersk.buildPackage { + src = ./.; + + buildInputs = with pkgs; [ + pkgconfig + openssl + ]; +} diff --git a/users/tazjin/tgsa/src/main.rs b/users/tazjin/tgsa/src/main.rs new file mode 100644 index 000000000000..ed72569f9204 --- /dev/null +++ b/users/tazjin/tgsa/src/main.rs @@ -0,0 +1,343 @@ +use anyhow::{anyhow, Context, Result}; +use std::collections::HashMap; +use std::sync::RwLock; +use std::time::{Duration, Instant}; + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +struct TgLink { + username: String, + message_id: usize, +} + +impl TgLink { + fn human_friendly_url(&self) -> String { + format!("t.me/{}/{}", self.username, self.message_id) + } + + fn to_url(&self) -> String { + format!("https://t.me/{}/{}?embed=1", self.username, self.message_id) + } + + fn parse(url: &str) -> Option<Self> { + let url = url.strip_prefix("/")?; + let parsed = url::Url::parse(url).ok()?; + + if parsed.host()? != url::Host::Domain("t.me") { + // only t.me links are supported + return None; + } + + let parts = parsed.path_segments()?.collect::<Vec<&str>>(); + if parts.len() != 2 { + // only message links are supported + return None; + } + + Some(TgLink { + username: parts[0].into(), + message_id: parts[1].parse().ok()?, + }) + } +} + +fn fetch_embed(link: &TgLink) -> Result<String> { + println!("fetching {}#{}", link.username, link.message_id); + let response = crimp::Request::get(&link.to_url()) + .send() + .context("failed to fetch embed data")? + .as_string() + .context("failed to decode embed data")? + .error_for_status(|resp| { + anyhow!("telegram request failed: {} ({})", resp.body, resp.status) + })?; + + Ok(response.body) +} + +#[derive(Debug)] +struct TgMessage { + author: String, + message: Option<String>, + photos: Vec<String>, + videos: Vec<String>, + has_audio: bool, +} + +fn extract_photo_url(style: &str) -> Option<&str> { + let url_start = style.find("url('")? + 5; + let url_end = style.find("')")?; + + Some(&style[url_start..url_end]) +} + +fn parse_tgmessage(embed: &str) -> Result<TgMessage> { + use scraper::{Html, Selector}; + + let doc = Html::parse_document(embed); + + let author_sel = Selector::parse("a.tgme_widget_message_owner_name").unwrap(); + let author = doc + .select(&author_sel) + .next() + .ok_or_else(|| anyhow!("failed to find message author"))? + .text() + .collect::<Vec<&str>>() + .concat(); + + let msg_sel = Selector::parse("div.tgme_widget_message_text.js-message_text").unwrap(); + + // The ElementRef::text() iterator does not yield newlines present + // in the message, so it is partially reimplemented here. + let message = if let Some(msg_elem) = doc.select(&msg_sel).next() { + use ego_tree::iter::Edge; + use scraper::node::Node; + + let mut out = String::new(); + + for edge in &mut msg_elem.traverse() { + if let Edge::Open(node) = edge { + match node.value() { + Node::Text(ref text) => out.push_str(&*text), + Node::Element(elem) if elem.name() == "br" => out.push_str("\n"), + _ => {} + } + } + } + + Some(out) + } else { + // Not all Telegram messages have a textual message. + None + }; + + let photo_sel = Selector::parse("a.tgme_widget_message_photo_wrap").unwrap(); + let mut photos = vec![]; + + for photo in doc.select(&photo_sel) { + if let Some(style) = photo.value().attr("style") { + if let Some(url) = extract_photo_url(style) { + photos.push(url.to_string()) + } + } + } + + let video_sel = Selector::parse("i.tgme_widget_message_video_thumb").unwrap(); + let mut videos = vec![]; + + for video in doc.select(&video_sel) { + if let Some(style) = video.value().attr("style") { + if let Some(url) = extract_photo_url(style) { + videos.push(url.to_string()) + } + } + } + + let audio_sel = Selector::parse("audio.tgme_widget_message_voice.js-message_voice").unwrap(); + let mut has_audio = false; + if doc.select(&audio_sel).next().is_some() { + has_audio = true; + } + + Ok(TgMessage { + author, + message, + photos, + videos, + has_audio, + }) +} + +// create a permanent media url that tgsa can redirect if telegram +// changes its upstream links. +// +// assumes that tgsa lives at tgsa.tazj.in (which it does) +fn media_url(link: &TgLink, idx: usize) -> String { + format!( + "https://tgsa.tazj.in/img/{}/{}/{}", + link.username, link.message_id, idx + ) +} + +fn to_bbcode(link: &TgLink, msg: &TgMessage) -> String { + let mut out = String::new(); + + out.push_str(&format!("[quote=\"{}\"]\n", msg.author)); + + for video in 0..msg.videos.len() { + out.push_str(&format!("[url=\"{}\"]", link.to_url())); + + // video thumbnail links are appended to the photos, hence the + // addition here + out.push_str(&format!( + "[img]{}[/img]", + media_url(link, video + msg.photos.len()) + )); + + out.push_str("[/url]\n"); + out.push_str("[sub](Click thumbnail to open video)[/sub]\n") + } + + for photo in 0..msg.photos.len() { + out.push_str(&format!("[timg]{}[/timg]\n", media_url(link, photo))); + } + + if msg.has_audio { + out.push_str(&format!( + "[i]This message has audio attached. Go [url=\"{}\"]to Telegram[/url] to listen.[/i]", + link.to_url(), + )); + } + + if let Some(message) = &msg.message { + out.push_str(message); + } + + out.push_str("\n[/quote]\n"); + + out.push_str(&format!( + "[sub](from [url=\"{}\"]{}[/url], via [url=\"https://tgsa.tazj.in\"]tgsa[/url])[/sub]\n", + link.to_url(), + link.human_friendly_url(), + )); + + out +} + +// cache everything for one hour +const CACHE_EXPIRY: Duration = Duration::from_secs(60 * 60); + +#[derive(Clone)] +struct TgPost { + bbcode: String, + at: Instant, + media: Vec<String>, +} + +type Cache = RwLock<HashMap<TgLink, TgPost>>; + +fn fetch_with_cache(cache: &Cache, link: &TgLink) -> Result<TgPost> { + if let Some(entry) = cache.read().unwrap().get(&link) { + if Instant::now() - entry.at < CACHE_EXPIRY { + println!("serving {}#{} from cache", link.username, link.message_id); + return Ok(entry.clone()); + } + } + + // limit concurrent fetching + // TODO(tazjin): per link? + let mut writer = cache.write().unwrap(); + + let embed = fetch_embed(&link)?; + let mut msg = parse_tgmessage(&embed)?; + let bbcode = to_bbcode(&link, &msg); + + let mut media = vec![]; + media.append(&mut msg.photos); + media.append(&mut msg.videos); + + let post = TgPost { + bbcode, + media, + at: Instant::now(), + }; + + writer.insert(link.clone(), post.clone()); + + Ok(post) +} + +fn handle_img_redirect(cache: &Cache, img_path: &str) -> Result<rouille::Response> { + // img_path: + // + // RWApodcast/113/1 + // ^ ^ ^ + // | | | + // | | image (0-indexed) + // | post ID + // username + + let img_parts: Vec<&str> = img_path.split("/").collect(); + + if img_parts.len() != 3 { + println!("invalid image link: {}", img_path); + return Err(anyhow!("not a valid image link: {}", img_path)); + } + + let link = TgLink { + username: img_parts[0].into(), + message_id: img_parts[1].parse().context("failed to parse message_id")?, + }; + + let img_idx: usize = img_parts[2].parse().context("failed to parse img_idx")?; + let post = fetch_with_cache(cache, &link)?; + + if img_idx >= post.media.len() { + return Err(anyhow!( + "there is no {}. image in {}/{}", + img_idx, + link.username, + link.message_id + )); + } + + Ok(rouille::Response::redirect_303(post.media[img_idx].clone())) +} + +fn handle_tg_link(cache: &Cache, link: &TgLink) -> Result<rouille::Response> { + let post = fetch_with_cache(cache, link)?; + Ok(rouille::Response::text(post.bbcode)) +} + +fn main() { + crimp::init(); + + let cache: Cache = RwLock::new(HashMap::new()); + + rouille::start_server("0.0.0.0:8472", move |request| { + let response = loop { + if request.raw_url().starts_with("/img/") { + break handle_img_redirect(&cache, &request.raw_url()[5..]); + } + + break match TgLink::parse(request.raw_url()) { + None => Ok(rouille::Response::text( + r#"tgsa +---- + +this is a stupid program that lets you turn telegram message links +into BBcode suitable for pasting on somethingawful dot com + +you can use it by putting a valid telegram message link in the url and +waiting for some bbcode to show up. + +for example: + + https://tgsa.tazj.in/https://t.me/RWApodcast/113 + +yes, that looks stupid, but it works + +if you see this message and think you did the above correctly, you +didn't. try again. idiot. + +pm me on the forums if this makes you mad or something. +"#, + )), + Some(link) => handle_tg_link(&cache, &link), + }; + }; + + match response { + Ok(resp) => resp, + Err(err) => { + println!("something failed: {}", err); + rouille::Response::text(format!( + r#"ugh, something broke: {} + +nobody has been alerted about this and it has probably not been +logged. pm me on the forums if you think it's important."#, + err + )) + } + } + }); +} diff --git a/users/tazjin/wallpapers/bio_thehost_1920.webp b/users/tazjin/wallpapers/bio_thehost_1920.webp new file mode 100644 index 000000000000..1b904c06fadf --- /dev/null +++ b/users/tazjin/wallpapers/bio_thehost_1920.webp Binary files differdiff --git a/users/tazjin/wallpapers/busride2_1920.webp b/users/tazjin/wallpapers/busride2_1920.webp new file mode 100644 index 000000000000..ad6ec446f661 --- /dev/null +++ b/users/tazjin/wallpapers/busride2_1920.webp Binary files differdiff --git a/users/tazjin/wallpapers/by_belltowers_2880.webp b/users/tazjin/wallpapers/by_belltowers_2880.webp new file mode 100644 index 000000000000..f7477f168914 --- /dev/null +++ b/users/tazjin/wallpapers/by_belltowers_2880.webp Binary files differdiff --git a/users/tazjin/wallpapers/by_crossing_2560.webp b/users/tazjin/wallpapers/by_crossing_2560.webp new file mode 100644 index 000000000000..efa263790b45 --- /dev/null +++ b/users/tazjin/wallpapers/by_crossing_2560.webp Binary files differdiff --git a/users/tazjin/wallpapers/by_gathering3_2880.webp b/users/tazjin/wallpapers/by_gathering3_2880.webp new file mode 100644 index 000000000000..e6b83bdcd430 --- /dev/null +++ b/users/tazjin/wallpapers/by_gathering3_2880.webp Binary files differdiff --git a/users/tazjin/wallpapers/by_mainservers1_1920.webp b/users/tazjin/wallpapers/by_mainservers1_1920.webp new file mode 100644 index 000000000000..f88d237e2b91 --- /dev/null +++ b/users/tazjin/wallpapers/by_mainservers1_1920.webp Binary files differdiff --git a/users/tazjin/wallpapers/by_warmachines1_2560.webp b/users/tazjin/wallpapers/by_warmachines1_2560.webp new file mode 100644 index 000000000000..848bf62bd7ed --- /dev/null +++ b/users/tazjin/wallpapers/by_warmachines1_2560.webp Binary files differdiff --git a/users/tazjin/wallpapers/by_warmachines3_1920.webp b/users/tazjin/wallpapers/by_warmachines3_1920.webp new file mode 100644 index 000000000000..6002ad695a1e --- /dev/null +++ b/users/tazjin/wallpapers/by_warmachines3_1920.webp Binary files differdiff --git a/users/tazjin/wallpapers/clever-man_2880.webp b/users/tazjin/wallpapers/clever-man_2880.webp new file mode 100644 index 000000000000..eb4d3f1bfa33 --- /dev/null +++ b/users/tazjin/wallpapers/clever-man_2880.webp Binary files differdiff --git a/users/tazjin/wallpapers/december1994_1920.webp b/users/tazjin/wallpapers/december1994_1920.webp new file mode 100644 index 000000000000..d2c4da80187c --- /dev/null +++ b/users/tazjin/wallpapers/december1994_1920.webp Binary files differdiff --git a/users/tazjin/wallpapers/flyby_1920.webp b/users/tazjin/wallpapers/flyby_1920.webp new file mode 100644 index 000000000000..8df5b1132e74 --- /dev/null +++ b/users/tazjin/wallpapers/flyby_1920.webp Binary files differdiff --git a/users/tazjin/wallpapers/gaussfraktarna_1920_badge.webp b/users/tazjin/wallpapers/gaussfraktarna_1920_badge.webp new file mode 100644 index 000000000000..3274a3a2d21d --- /dev/null +++ b/users/tazjin/wallpapers/gaussfraktarna_1920_badge.webp Binary files differdiff --git a/users/tazjin/wallpapers/kraftahq_1920.webp b/users/tazjin/wallpapers/kraftahq_1920.webp new file mode 100644 index 000000000000..62a6debf476f --- /dev/null +++ b/users/tazjin/wallpapers/kraftahq_1920.webp Binary files differdiff --git a/users/tazjin/wallpapers/peripheral2_1920.webp b/users/tazjin/wallpapers/peripheral2_1920.webp new file mode 100644 index 000000000000..e454072ac42a --- /dev/null +++ b/users/tazjin/wallpapers/peripheral2_1920.webp Binary files differdiff --git a/users/tazjin/wallpapers/ship14_1920.webp b/users/tazjin/wallpapers/ship14_1920.webp new file mode 100644 index 000000000000..502f5dac903e --- /dev/null +++ b/users/tazjin/wallpapers/ship14_1920.webp Binary files differdiff --git a/users/tazjin/wallpapers/shipyard_1920.webp b/users/tazjin/wallpapers/shipyard_1920.webp new file mode 100644 index 000000000000..3d4115305d10 --- /dev/null +++ b/users/tazjin/wallpapers/shipyard_1920.webp Binary files differdiff --git a/users/tazjin/wallpapers/specky_1920.webp b/users/tazjin/wallpapers/specky_1920.webp new file mode 100644 index 000000000000..b8246618bebc --- /dev/null +++ b/users/tazjin/wallpapers/specky_1920.webp Binary files differdiff --git a/users/tazjin/wallpapers/summerlove2_1920.webp b/users/tazjin/wallpapers/summerlove2_1920.webp new file mode 100644 index 000000000000..d64a1cb867ec --- /dev/null +++ b/users/tazjin/wallpapers/summerlove2_1920.webp Binary files differdiff --git a/users/tazjin/wallpapers/t50_1920_badge.webp b/users/tazjin/wallpapers/t50_1920_badge.webp new file mode 100644 index 000000000000..f8cb6107f30c --- /dev/null +++ b/users/tazjin/wallpapers/t50_1920_badge.webp Binary files differdiff --git a/users/tazjin/wallpapers/theflood1_1920.webp b/users/tazjin/wallpapers/theflood1_1920.webp new file mode 100644 index 000000000000..335efb057172 --- /dev/null +++ b/users/tazjin/wallpapers/theflood1_1920.webp Binary files differdiff --git a/users/tazjin/wallpapers/thelan_1920.webp b/users/tazjin/wallpapers/thelan_1920.webp new file mode 100644 index 000000000000..55e6c22ad212 --- /dev/null +++ b/users/tazjin/wallpapers/thelan_1920.webp Binary files differdiff --git a/users/tazjin/wallpapers/vadrare_1920_badge.webp b/users/tazjin/wallpapers/vadrare_1920_badge.webp new file mode 100644 index 000000000000..887c891da36c --- /dev/null +++ b/users/tazjin/wallpapers/vadrare_1920_badge.webp Binary files differ |