Compare commits

..

3 Commits

Author SHA1 Message Date
b4a1097845 buncha stuff 2023-08-16 21:45:48 -07:00
adc582116b wip sidenotes post 2023-08-15 11:48:07 -07:00
7d5c696fa7 start working on sidenotes post 2023-08-15 11:10:22 -07:00
71 changed files with 6685 additions and 6775 deletions

7
.gitignore vendored
View File

@ -3,10 +3,3 @@ node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
**/_test.*
/scratch

2
.npmrc
View File

@ -1,2 +0,0 @@
engine-strict=true
resolution-mode=highest

View File

@ -1,6 +1,6 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte);
## Creating a project
@ -8,12 +8,14 @@ If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
npm init svelte@next
# create a new project in my-app
npm create svelte@latest my-app
npm init svelte@next my-app
```
> Note: the `@next` is temporary
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
@ -27,12 +29,10 @@ npm run dev -- --open
## Building
To create a production version of your app:
Before creating a production version of your app, install an [adapter](https://kit.svelte.dev/docs#adapters) for your target environment. Then:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
> You can preview the built app with `npm run preview`, regardless of whether you installed an adapter. This should _not_ be used to serve your app in production.

11
jsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"$lib": ["src/lib"],
"$lib/*": ["src/lib/*"]
}
},
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
}

8942
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,25 +1,18 @@
{
"name": "blog.jfmonty2.com",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/adapter-static": "^2.0.3",
"@sveltejs/kit": "^1.20.4",
"hast-util-to-html": "^9.0.0",
"hast-util-to-text": "^4.0.0",
"mdast-util-to-string": "^4.0.0",
"mdsvex": "^0.11.0",
"sass": "^1.69.5",
"svelte": "^4.0.5",
"unist-util-find": "^3.0.0",
"unist-util-visit": "^5.0.0",
"vite": "^4.4.2"
},
"type": "module"
"name": "blog",
"version": "0.0.1",
"scripts": {
"dev": "svelte-kit dev",
"build": "svelte-kit build",
"preview": "svelte-kit preview"
},
"devDependencies": {
"@sveltejs/adapter-static": "^1.0.0-next.21",
"@sveltejs/kit": "next",
"mdsvex": "^0.9.8",
"node-sass": "^6.0.1",
"svelte": "^3.42.6",
"svelte-preprocess": "^4.9.8"
},
"type": "module"
}

View File

@ -3,11 +3,13 @@
<head>
<meta charset="utf-8" />
<link rel="preload" href="/Tajawal-Regular.woff2" as="font" type="font/woff2" />
<link rel="alternate" type="application/atom+xml" href="/feed">
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
<link rel="preload" href="/Baskerville-Regular.woff2" as="font" type="font/woff2" />
<link rel="icon" href="/favicon.png" />
<link rel="stylesheet" href="/style.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%svelte.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
<body>
<div id="svelte">%svelte.body%</div>
</body>
</html>

View File

@ -1,9 +0,0 @@
<script>
let classes = '';
export {classes as class};
</script>
<p>Hello world!</p>
<pre class={classes}>
<slot></slot>
</pre>

View File

@ -2,10 +2,10 @@
// Usage: <Dropcap word="Lorem">ipsum dolor sit amet...</Dropcap>
export let word;
const initial = word.slice(0, 1).toUpperCase();
const initial = word.slice(0, 1);
const remainder = word.slice(1);
// a few letters are narrower at the top, so we need to shift the remainder to compensate
// a few letters are narrower at the top, so we need more of a shift
const shiftValues = {
A: '-0.45em',
L: '-0.3em',
@ -16,23 +16,16 @@
</script>
<style>
@font-face {
font-family: 'Baskerville';
font-style: normal;
font-weight: 400;
src: url(/Baskerville-Regular.woff2) format('woff2');
font-display: block;
}
.drop-cap {
display: block;
float: left;
margin-right: 0.1em;
color: var(--accent-color);
font-size: calc(var(--content-size) * 1.5 * 2);
line-height: 0.8;
font-family: 'Baskerville';
text-transform: uppercase;
color: #8c0606;
/* box-sizing: border-box;*/
font-size: calc(var(--content-size) * var(--content-line-height) * 1.75);
float: left;
font-family: 'Baskerville';
line-height: 0.8;
margin-right: 0.1em;
display: block;
}
.first-word {
@ -41,12 +34,9 @@
}
</style>
<svelte:head>
<link rel="preload" href="/Baskerville-Regular.woff2" as="font" type="font/woff2">
</svelte:head>
<span class="drop-cap">{initial}</span>
{#if remainder.length}
<p>
<span class="drop-cap">{initial}</span>
<span class="first-word" style:--shift={shift}>{remainder}</span>
{/if}
<slot></slot>
</p>

View File

@ -1,68 +0,0 @@
<script>
export let level;
export let id = '';
const tag = `h${level}`;
</script>
<style lang="scss">
.h {
position: relative;
}
// shift the anchor link to hang off the left side of the content when there's room
.anchor-wrapper {
// slightly overlap the span with the heading so that it doesn't
// lose its hover state as the cursor moves between them
position: absolute;
padding-right: 0.5em;
left: -1.25em;
@media(max-width: 58rem) {
position: revert;
}
}
a {
// works better to set the size here for line-height reasons
font-size: 0.9em;
// give the anchor link a faded appearance by default
color: hsl(0deg, 0%, 29%);
opacity: 40%;
transition: opacity 150ms, color 150ms;
&:hover {
border-bottom: 0.05em solid currentcolor;
}
}
// emphasize anchor link when heading is hovered or when clicked (the latter for mobile)
.h:hover a, .anchor-wrapper:hover a, .h a:active {
color: var(--accent-color);
opacity: 100%;
}
svg {
// undo the reset that makes images block
display: inline;
width: 1em;
// tiny tweak for optical alignment
transform: translateY(2px);
}
</style>
<svelte:element this={tag} {id} class="h">
<span>
<slot></slot>
</span>
<!-- Icon from https://heroicons.com/ -->
<span class="anchor-wrapper">
<a href="#{id}" >
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
</svg>
</a>
</span>
</svelte:element>

View File

@ -10,35 +10,19 @@
return null;
}
}
function ext(url) {
}
</script>
<script>
export let href;
export let rel = null;
let url = null;
try {
url = new URL(href);
}
catch {}
let isLocal = false;
if (href.startsWith('/') || url?.host === $page.url.host) {
isLocal = true;
}
// if href is not a valid url, assume that it's a relative link
const path = url?.pathname || href;
// set rel="external" on links to static files (i.e. local links with a dot in them)
if (isLocal && path.search(/\.\w+$/) > -1) {
rel = 'external';
}
export let href; // we don't care about other attributes
</script>
<a data-sveltekit-preload-data={isLocal ? 'hover' : null} {href} {rel}>
<slot></slot>
</a>
{#if href.startsWith('/') || host(href) === $page.host}
<a sveltekit:prefetch {href}>
<slot></slot>
</a>
{:else}
<a {href}>
<slot></slot>
</a>
{/if}

View File

@ -1,150 +1,33 @@
<script context="module">
import '$styles/prose.scss';
import '$styles/code.scss';
import { onMount } from 'svelte';
import { formatDate } from './datefmt.js';
import { makeSlug } from '$lib/utils.js';
import { makeSlug } from '$lib/slug.js';
import Toc from './Toc.svelte';
import Link from './Link.svelte';
export { Link as a };
</script>
<script>
export let title, date;
export let description = '';
export const description = '';
export const draft = false;
export let toc = null;
export let slug;
export let prev = null;
export let next = null;
</script>
<style lang="scss">
.page {
/* 3-column grid: left gutter, center content, and right gutter */
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, var(--content-width)) minmax(0, 1fr);
/* a bit of breathing room for narrow screens */
padding: 0 var(--content-padding);
}
/* container for the table of contents */
.left-gutter {
grid-column: 1 / 2;
justify-self: end;
}
.title {
grid-column: 2 / 3;
}
<style>
.subtitle {
font-size: 0.9em;
font-style: italic;
margin-top: -0.75rem;
}
.post {
grid-column: 2 / 3;
}
hr {
grid-column: 2 / 3;
width: 100%;
border-top: 1px solid hsl(0 0% 75%);
border-bottom: none;
margin: 2.5rem 0;
}
.footer {
grid-column: 2 / 3;
margin-bottom: 2.5rem;
display: flex;
& a {
display: flex;
align-items: center;
gap: 0.45em;
font-size: 1.25rem;
color: var(--content-color-faded);
text-decoration: underline;
text-underline-offset: 0.25em;
text-decoration-color: transparent;
transition: 150ms;
&:hover {
text-decoration-color: currentColor;
text-decoration: underline;
}
}
& svg {
width: 1em;
transition: 150ms;
}
& .prev:hover svg {
transform: translateX(-50%);
}
& .next:hover svg {
transform: translateX(50%);
}
margin-top: -0.5rem;
}
</style>
<svelte:head>
<title>{title} | Joe's Blog</title>
<meta property="og:title" content="{title} | Joe's Blog">
<meta property="og:type" content="article">
<meta property="og:url" content="https://blog.jfmonty2.com/{slug}">
<meta property="og:description" content={description}>
<meta property="og:site_name" content="Joe's Blog">
<!-- Put this here for now, until I can get custom components working for codeblocks -->
<link rel="preload" href="/Hack-Regular.woff2" as="font" type="font/woff2">
<title>{title}</title>
<link rel="stylesheet" href="/prism-dracula.css" />
</svelte:head>
<div class="page prose">
<div class="title">
<h1 id="{makeSlug(title)}">{title}</h1>
<p class="subtitle">{formatDate(date)}</p>
</div>
<div class="left-gutter">
{#if toc && toc.length !== 0}
<Toc items={toc} />
{/if}
</div>
<div class="post">
<slot></slot>
</div>
<hr>
<div class="footer">
{#if prev}
<a href="/{prev}" class="prev" data-sveltekit-preload-data="hover">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
</svg>
Previous
</a>
{/if}
{#if next}
<!-- we use margin-left rather than justify-content so it works regardless of whether the "previous" link exists -->
<a href="/{next}" class="next" style="margin-left: auto;" data-sveltekit-preload-data="hover">
<span style:vertical-align={'center'}>Next</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
</svg>
</a>
{/if}
</div>
<div id="post">
<h1 id="{makeSlug(title)}">{title}</h1>
<p class="subtitle">{formatDate(date)}</p>
<slot></slot>
</div>

View File

@ -1,219 +1,174 @@
<style lang="scss">
// minimum desirable sidenote width is 15rem, so breakpoint is
// content-width + 2(gap) + 2(15rem) + 2(scrollbar buffer)
$sidenote-breakpoint: 89.5rem;
// this has to be global because otherwise we can't target the body
/* always applicable */
:global(body) {
counter-reset: sidenote;
}
.counter.anchor {
.counter {
counter-increment: sidenote;
color: #444;
margin-left: 0.065rem;
font-size: 0.75em;
position: relative;
bottom: 0.375rem;
color: var(--accent-color);
margin-left: 0.05rem;
@media(max-width: $sidenote-breakpoint) {
&:hover {
color: var(--content-color);
cursor: pointer;
}
// only top-level anchors get brackets
&:not(.nested)::before {
content: '[';
}
&:not(.nested)::after {
content: ']';
}
&:after {
font-size: 0.75em;
position: relative;
bottom: 0.3rem;
color: #8c0606;
}
}
.counter.floating {
position: absolute;
transform: translateX(calc(-100% - 0.4em));
color: var(--accent-color);
.sidenote {
color: #555;
font-size: 0.8rem;
&:before {
content: counter(sidenote) " ";
/* absolute positioning puts it at the top-left corner of the sidenote, overlapping with the content
(because the sidenote is floated it counts as a positioned parent, I think) */
position: absolute;
/* translate moves it out to the left (and just a touch up to mimic the superscript efect)
-100% refers to the width of the element, so it pushes it out further if necessary (i.e. two digits instead of one) */
transform: translate(calc(-100% - 0.2rem), -0.15rem);
font-size: 0.75rem;
color: #8c0606;
}
}
// hidden checkbox that tracks the state of the mobile sidenote
.sidenote-toggle {
display: none;
}
.sidenote {
// anchor the counter, which is absolutely positioned
position: relative;
color: #555;
font-size: var(--content-size-sm);
line-height: 1.25;
hyphens: auto;
/* desktop display */
@media(min-width: 70em) {
.counter:after {
content: counter(sidenote);
}
// desktop display, this can't coexist with mobile styling
@media(min-width: $sidenote-breakpoint) {
// max sidenote width is 20rem, if the window is too small then it's
// the width of the gutter, minus the gap between sidenote and gutter,
// minus an extra 1.5rem to account for the scrollbar on the right
--gap: 2.5rem;
--gutter-width: calc(50vw - var(--content-width) / 2);
--sidenote-width: min(
24rem,
calc(var(--gutter-width) - var(--gap) - 1.5rem)
);
.sidenote {
--gap: 2rem;
--sidenote-width: min(14rem, calc(50vw - var(--gap) - var(--content-width) / 2));
width: var(--sidenote-width);
hyphens: auto;
position: relative;
float: right;
clear: right;
margin-right: calc(-1 * var(--sidenote-width) - var(--gap));
margin-bottom: 0.75rem;
margin-right: calc(0rem - var(--sidenote-width) - var(--gap)); // gives us 2rem of space between content and sidenote
margin-bottom: 0.7rem;
}
@media(max-width: $sidenote-breakpoint) {
position: fixed;
left: 0;
right: 0;
bottom: 0;
// since headings have relative position, any that come after
// the current sidenote in the DOM get stacked on top by default
z-index: 10;
// give us a horizontal buffer for the counter and dismiss button
--padding-x: calc(var(--content-padding) + 1.5rem);
padding: 1rem var(--padding-x);
background-color: white;
box-shadow: 0 -2px 4px -1px rgba(0, 0, 0, 0.06), 0 -2px 12px -2px rgba(0, 0, 0, 0.1);
// show the sidenote only when the corresponding checkbox is checked
transform: translateY(calc(100% + 2rem));
transition: transform 125ms;
// when moving from shown -> hidden, ease-in
transition-timing-function: ease-in;
.sidenote-toggle:checked + & {
transform: translateY(0);
// when moving hidden -> shown, ease-out
transition-timing-function: ease-out;
// the active sidenote should be on top of any other sidenotes as well
// (this isn't critical unless you have JS disabled, but it's still annoying)
z-index: 20;
}
}
}
.sidenote-content {
max-width: var(--content-width);
margin: 0 auto;
&.nested {
margin-right: 0;
margin-top: 0.75rem;
margin-bottom: 0;
}
}
.dismiss {
display: block;
width: max-content;
margin: 0.5rem auto 0;
border-radius: 100%;
background: white;
border: 1px solid hsl(0deg, 0%, 75%);
box-shadow: 1px 1px 4px -1px rgba(0, 0, 0, 0.1);
padding: 0.25rem;
color: hsl(0deg, 0%, 50%);
&:hover, &:active {
color: var(--accent-color);
border: 1px solid var(--accent-color);
}
display: none;
@media(max-width: $sidenote-breakpoint) {
display: block;
}
cursor: pointer;
& label {
cursor: pointer;
}
& svg {
height: 1.5rem;
}
}
// nesting still needs work
/* @media(min-width: $sidenote-breakpoint) {
.nested.sidenote {
margin-right: 0;
margin-top: 0.7rem;
margin-bottom: 0;
}
} */
.dismiss {
display: none;
}
}
/* mobile display */
@media (max-width: 70em) {
.counter:after {
content: "[" counter(sidenote) "]";
}
.counter:hover:after {
color: #000;
cursor: pointer;
}
.sidenote {
box-sizing: border-box;
position: fixed;
z-index: 1;
left: 0;
bottom: 0;
width: 100vw;
padding-top: 1rem;
padding-bottom: 1rem;
--pad: max(1rem, calc(50vw - var(--content-width) / 2));
padding-left: var(--pad);
padding-right: var(--pad);
background-color: #fff;
box-shadow: 0 -2px 4px -1px rgba(0, 0, 0, 0.06), 0 -2px 12px -2px rgba(0, 0, 0, 0.1);
display: none;
}
.sidenote-toggle:checked + .sidenote {
display: block;
}
.dismiss {
position: absolute;
right: 1.5rem;
top: -0.2rem;
font-size: 1.25rem;
color: #8c0606;
cursor: pointer;
&:hover {
transform: scale(1.1);
font-weight: 800;
}
}
}
// /* slight tweaks for in between state */
// @media (min-width: 52.5em) and (max-width: 70em) {
// .sidenote {
// padding-left: calc(50vw - 19rem);
// }
// }
// @media (max-width: 52.5em) {
// .sidenote {
// padding-left: 2rem;
// }
// }
</style>
<script context="module">
import { writable } from 'svelte/store';
let activeSidenote = writable(null);
var activeToggle = null;
</script>
<script>
import { onMount } from 'svelte';
export let count;
let noteBody;
let nested = false;
onMount(() => {
// check to see if the parent node is also a sidenote, if so move this one to the end
let parentContent = noteBody.parentElement.closest('div.sidenote-content');
if (parentContent) {
// extract just the content of the nested note, ditch the rest (i.e. the button)
const noteContent = noteBody.firstChild;
let parentNote = noteBody.parentElement.closest('span.sidenote');
if (parentNote) {
noteBody.remove();
parentContent.appendChild(noteContent);
parentNote.appendChild(noteBody);
nested = true;
}
});
const id = Math.random().toString().slice(2);
let toggle;
activeSidenote.subscribe(activeCount => {
// if we were the active toggle, but are no longer, hide
if (toggle?.checked && activeCount !== count) {
toggle.checked = false;
}
})
function toggleState() {
// if we are the active sidenote, deactivate us (upating the store will trigger subscription)
if ($activeSidenote === count) {
$activeSidenote = null;
if (activeToggle === toggle) {
activeToggle = null;
}
else if (activeToggle !== null) {
activeToggle.checked = false;
activeToggle = toggle;
}
// otherwise, we are becoming active
else {
$activeSidenote = count;
activeToggle = toggle;
}
}
</script>
<label for={count} class="counter anchor" class:nested>{count}</label>
<input id={count} bind:this={toggle} on:click={toggleState} type="checkbox" class="sidenote-toggle" />
<!-- outer element so that on mobile it can extend the whole width of the viewport -->
<div class="sidenote" bind:this={noteBody}>
<!-- inner element so that content can be centered -->
<div class="sidenote-content" class:nested>
<span class="counter floating">{count}</span>
<slot></slot>
</div>
<button class="dismiss">
<label for={count}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>
</label>
</button>
</div>
<label for={id} on:click={toggleState} class="counter"></label>
<input {id} bind:this={toggle} type="checkbox" class="sidenote-toggle" />
<span class="sidenote" class:nested bind:this={noteBody}>
<label class="dismiss" for={id} on:click={toggleState}>&times;</label>
<slot></slot>
</span>

View File

@ -1,166 +0,0 @@
<script>
import { onMount } from 'svelte';
import { makeSlug } from '$lib/utils.js';
export let items;
items.forEach(i => i.slug = makeSlug(i.text));
let headings = [];
let currentHeadingSlug = null;
let currentSubheadingSlug = null;
function setCurrentHeading() {
for (const h of headings) {
const yPos = h.getBoundingClientRect().y;
if (yPos > (window.innerHeight / 3)) {
break;
}
if (h.tagName === 'H2') {
currentHeadingSlug = h.id;
currentSubheadingSlug = null;
}
if (h.tagName === 'H3') {
currentSubheadingSlug = h.id
}
}
}
function ellipsize(text) {
return text;
// not sure about this, decide on it later
// if (text.length > 40) {
// text = text.slice(0, 40);
// // looks weird when we have an ellipsis following a space
// if (text.slice(-1) === ' ') {
// text = text.slice(0, -1);
// }
// return text + '…';
// }
// return text;
}
onMount (() => {
// These shouldn't change over the life of the page, so we can cache them
headings = Array.from(document.querySelectorAll('h2[id], h3[id]'));
setCurrentHeading();
});
</script>
<svelte:window on:scroll={setCurrentHeading} />
<style lang="scss">
#toc {
position: sticky;
top: 1.5rem;
margin-left: 1rem;
margin-right: 4rem;
max-width: 18rem;
color: var(--content-color-faded);
// minimum desirable TOC width is 8rem
// add 4rem for margins, giving total gutter width of 12.5rem
// multiply by 2 since there are two equally-sized gutters, then add content-width (52.5rem)
@media(max-width: 77.5rem) {
display: none;
}
}
// margin-left is to match the padding on the top-level list items,
// but here it needs to be margin so that the border is also shifted
h5 {
font-variant: petite-caps;
font-weight: 500;
max-width: fit-content;
margin-top: 0;
margin-bottom: 0.25em;
padding-bottom: 0.25em;
border-bottom: 1px solid currentcolor;
// make the border stretch beyond the text just a bit, because I like the effect
padding-right: 1.5rem;
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
li {
position: relative;
margin-top: 0.45em;
font-size: var(--content-size-sm);
// make sure that one item wrapped across multiple lines doesn't just looke like multiple items
line-height: 1.1;
&.depth-2 {
align-items: stretch;
margin-bottom: 0.2rem;
}
&.depth-3 {
align-items: center;
margin-bottom: 0.05rem;
}
&.current, &:hover {
color: var(--content-color);
}
}
.marker {
position: absolute;
left: -0.6rem;
.current &, li:hover & {
background-color: var(--accent-color);
}
&.bar {
width: 0.125rem;
height: 100%;
}
&.dot {
width: 0.2rem;
height: 0.2rem;
border-radius: 50%;
// vertically center within its containing block
top: 0;
bottom: 0;
margin: auto 0;
}
}
// default link styling messes everything up again
a {
color: inherit;
text-decoration: none;
}
</style>
<div id="toc">
<h5>
<span class="heading">Contents</span>
</h5>
<ul>
{#each items as item}
{#if item.depth === 2}
<li class="depth-2" class:current={item.slug === currentHeadingSlug} style:align-items="stretch">
<span class="marker bar"></span>
<a href="#{item.slug}">{ellipsize(item.text)}</a>
</li>
{:else if item.depth === 3}
<li class="depth-3" class:current={item.slug === currentSubheadingSlug} style:align-items="center" style:margin-left="0.75em">
<span class="marker dot"></span>
<a href="#{item.slug}">{ellipsize(item.text)}</a>
</li>
{/if}
{/each}
</ul>
</div>

View File

@ -0,0 +1,20 @@
<script>
import Step from './Step.svelte';
import {onMount} from 'svelte';
let frame;
onMount(() => {
frame.setAttribute('srcdoc', frame.innerHTML);
})
</script>
<iframe bind:this={frame}>
<html>
<head></head>
<body>
<Step />
<p>Goodbye world!</p>
</body>
</html>
</iframe>

View File

@ -0,0 +1,7 @@
<script>
let count = 0;
</script>
<p>hello world!</p>
<button on:click={() => count++}>Increment</button>
<p>The count is: {count}</p>

50
src/lib/slug.js Normal file
View File

@ -0,0 +1,50 @@
const nonAlphaNum = /[^A-Za-z0-9\-]/g;
const space = /\s/g
export function makeSlug(text) {
return text
.toLowerCase()
.replace(space, '-')
.replace(nonAlphaNum, '')
}
function apply(node, types, fn) {
if (typeof types === 'string') {
types = new Set([types]);
}
else if (!(types instanceof Set)) {
types = new Set(types)
console.log(types)
}
if (types.has(node.type)) {
fn(node);
}
if ('children' in node) {
for (let child of node.children) {
apply(child, types, fn);
}
}
}
function getTextContent(node) {
let segments = [];
apply(node, 'text', textNode => {
// skip all-whitespace strings
if (textNode.value.match(/^\s+$/)) return;
segments.push(textNode.value.trim());
});
return segments.join(' ');
}
export default function slug() {
return (tree) => {
apply(tree, 'element', e => {
if (e.tagName.match(/h[1-6]/)) {
let text = getTextContent(e);
e.properties.id = makeSlug(text);
}
})
}
}

View File

@ -1,8 +0,0 @@
const nonAlphaNum = /[^A-Za-z0-9\-]/g;
const space = /\s+/g;
export function makeSlug(text) {
return text
.toLowerCase()
.replace(space, '-')
.replace(nonAlphaNum, '');
}

View File

@ -1,94 +0,0 @@
// const Node = {
// addChild(child) {
// this.children.push(child);
// return child;
// }
// }
export function tag(name, attrs, children) {
return {
type: 'tag',
tag: name,
attrs: attrs || {},
children: children || [],
addTag(name, attrs, children) {
const child = tag(name, attrs, children);
this.children.push(child);
return child;
},
};
}
export function text(content) {
return {
type: 'text',
text: content,
};
}
export function serialize(node, depth) {
if (!depth) {
depth = 0;
}
const indent = ' '.repeat(depth * 4);
let fragments = [];
// version tag, if this is the top level
if (depth === 0) {
fragments.push('<?xml version="1.0" encoding="UTF-8"?>\n')
}
fragments.push(`${indent}<${node.tag}`);
// this happens if there are multiple text nodes within the same parent
if (node.type === 'text') {
return `${indent}${escape(node.text)}`;
}
if (node.children === undefined) {
console.log(node);
}
// opening tag <element attr="value">
for (const attr in node.attrs) {
fragments.push(` ${attr}="${node.attrs[attr]}"`);
}
if (node.children.length === 0) {
fragments.push(' />');
return fragments.join('');
}
fragments.push('>');
// if the only child is a single text node, skip recursion and just dump contents directly
if (node.children.length === 1 && node.children[0].type === 'text') {
const text = escape(node.children[0].text);
fragments.push(text);
}
// otherwise, start a new line for each child node, then recurse
else {
for (const child of node.children) {
fragments.push('\n');
fragments.push(serialize(child, depth + 1));
}
// no need to verify that there were children, we already did that
fragments.push(`\n${indent}`);
}
fragments.push(`</${node.tag}>`);
return fragments.join('');
}
function escape(text) {
// we aren't going to bother with escaping attributes, so we won't worry about quotes
return text
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;');
}

View File

@ -1,116 +0,0 @@
import { visit, CONTINUE, EXIT, SKIP, } from 'unist-util-visit';
import { find } from 'unist-util-find';
import { toText } from 'hast-util-to-text';
import { makeSlug } from '../lib/utils.js';
import {writeFileSync} from 'node:fs';
import {toHtml} from 'hast-util-to-html';
export function localRehype() {
return (tree, vfile) => {
const needsDropcap = vfile.data.fm.dropcap !== false
let dropcapAdded = false;
let sidenotesCount = 0;
let moduleScript;
let imports = new Set();
if (needsDropcap) {
imports.add("import Dropcap from '$lib/Dropcap.svelte';");
}
visit(tree, node => {
// add slugs to headings
if (isHeading(node)) {
processHeading(node);
imports.add("import Heading from '$lib/Heading.svelte';");
return SKIP;
}
// mdsvex adds a <script context="module"> so we just hijack that for our own purposes
if (isModuleScript(node)) {
moduleScript = node;
}
// convert first letter/word of first paragraph to <Dropcap word="{whatever}">
if (needsDropcap && !dropcapAdded && isParagraph(node)) {
addDropcap(node);
dropcapAdded = true;
}
// add `count` prop to each <Sidenote> component
if (isSidenote(node)) {
// increment the counter first so that the count starts at 1
sidenotesCount += 1;
addSidenoteCount(node, sidenotesCount);
}
});
// insert our imports at the top of the `<script context="module">` tag
if (imports.size > 0) {
const script = moduleScript.value;
// split the script where the opening tag ends
const i = script.indexOf('>');
const openingTag = script.slice(0, i + 1);
const remainder = script.slice(i + 1);
// mdvsex uses tabs so we will as well
const importScript = Array.from(imports).join('\n\t');
moduleScript.value = `${openingTag}\n\t${importScript}${remainder}`;
}
// const name = vfile.filename.split('/').findLast(() => true);
// writeFileSync(`scratch/${name}.json`, JSON.stringify(tree, undefined, 4));
}
}
function processHeading(node) {
const level = node.tagName.slice(1);
node.tagName = 'Heading';
node.properties.level = level;
node.properties.id = makeSlug(toText(node));
}
function addDropcap(par) {
let txtNode = find(par, {type: 'text'});
const i = txtNode.value.search(/\s/);
const firstWord = txtNode.value.slice(0, i);
const remainder = txtNode.value.slice(i);
par.children.unshift({
type: 'raw',
value: `<Dropcap word="${firstWord}" />`,
});
txtNode.value = remainder;
}
function addSidenoteCount(node, count) {
// get the index of the closing >
const i = node.value.search(/>\s*$/);
if (i < 0) {
throw new Error('Failed to add counter to element, closing angle bracket not found.');
}
// splice in the count prop
node.value = `${node.value.slice(0, i)} count={${count}}>`;
}
function isHeading(node) {
return node.type === 'element' && node.tagName.match(/h[1-6]/);
}
function isModuleScript(node) {
return node.type === 'raw' && node.value.match(/^<script context="module">/);
}
function isParagraph(node) {
return node.type === 'element' && node.tagName === 'p';
}
function isSidenote(node) {
return node.type === 'raw' && node.value.match(/<\s*Sidenote/);
}

View File

@ -1,50 +0,0 @@
import { visit } from 'unist-util-visit';
import { toString } from 'mdast-util-to-string';
import fs from 'node:fs';
// build table of contents and inject into frontmatter
export function localRemark() {
return (tree, vfile) => {
let toc = [];
let description = null;
visit(tree, ['heading', 'paragraph'], node => {
// build table of contents and inject into frontmatter
if (node.type === 'heading' && vfile.data.fm.toc !== false) {
toc.push({
text: toString(node),
depth: node.depth,
});
}
// inject description (first 25 words of the first paragraph)
if (node.type === 'paragraph' && description === null) {
description = summarize(node);
}
});
if (vfile.data.fm.toc !== false) {
vfile.data.fm.toc = toc;
}
vfile.data.fm.description = description;
}
}
// convert paragraph to single string after stripping everything between html tags
function summarize(par) {
let newChildren = [];
let push = true;
for (const child of par.children) {
if (child.type === 'html') {
push = !push;
continue;
}
if (push) {
newChildren.push(child);
}
}
return toString({type: 'paragraph', children: newChildren});
}

View File

@ -1,169 +0,0 @@
<script context="module">
import { writable } from 'svelte/store';
let activePreview = writable(null);
</script>
<script>
import { tick } from 'svelte';
import data from './books.json';
const images = import.meta.glob('./images/*.jpg', {eager: true});
export let ref;
const {type, title, author, description, url} = data[ref];
const imageUrl = images[`./images/${ref}.jpg`].default;
$: visible = $activePreview === ref;
let mousePresent = false;
let offset, popover;
async function show() {
$activePreview = ref;
mousePresent = true;
await tick();
const rect = popover.getBoundingClientRect();
// 12px is approximately var(--content-padding)
if (rect.x < 12) {
offset = `${12 - rect.x}px`;
}
}
function hide() {
mousePresent = false;
// mouseenter fires when the mouse moves into the floating div as well,
// so this gives us a "grace period" that applies to either anchor or popover
setTimeout(
() => {
if (!mousePresent && $activePreview === ref) {
$activePreview = null;
}
},
300
);
}
function clickLink(evt) {
// if click happened without hover, then we must be on mobile
if (!visible) {
$activePreview = ref;
evt.preventDefault();
}
// if visible, but mouse is not present, also mobile
else if (visible && !mousePresent) {
$activePreview = null;
evt.preventDefault();
}
}
let detailsLink;
function blurLink(evt) {
// do this in the next task, in case the click was inside the popover
setTimeout(
() => {
// check this here in case it got changed by a different event handler
if ($activePreview == ref) {
$activePreview = null;
}
},
0
)
// if ($activePreview == ref) {
// setTimeout(() => $activePreview = null, 0);
// }
}
</script>
<style lang="scss">
.base {
position: relative;
// on mobile, we want the popover's position to be calculated
// relative to the whole document, not the link text
@media(max-width: 27rem) {
position: static;
}
}
.popover {
position: absolute;
// popover should float above the link text by a bit
bottom: calc(100% + 0.5rem);
// and be centered relative to the link, unless that would put it off screen
left: 50%;
transform: translateX(
calc(-50% + var(--offset, 0px))
);
@media(max-width: 27rem) {
// bounding box is now the whole document
// we want to start from its initial vertical position
bottom: unset;
// center it horizontally, with some space on the sides
width: unset;
left: 1rem;
right: 1rem;
margin-left: auto;
margin-right: auto;
// and move it back up so it's above the text again
transform: translateY(
calc(-100% - 1.5em - 0.5rem)
);
}
// visibility is controlled by the .visible class
display: none;
&.visible {
display: flex;
}
// two-column layout, one for image and one for text
gap: 1rem;
width: 25rem;
height: 192px;
overflow-y: auto;
padding: 0.35rem;
background: white;
box-shadow: 1px 2px 6px rgba(0, 0, 0, 0.1);
border: 1px solid var(--content-color);
z-index: 1;
font-size: var(--content-size-sm);
}
img {
height: 100%;
// sticky position ensures that the image stays visible when we scroll the text
position: sticky;
top: 0;
}
a.details {
color: var(--primary-color);
&:visited {
color: var(--accent-color);
}
}
a:active {
color: var(--accent-color);
}
</style>
<span class="base" on:mouseenter={show} on:mouseleave={hide}><!-- get rid of whitespace
--><a href={url} target="_blank" on:click={clickLink} on:blur={blurLink}>
<slot></slot><!--
--></a><!--
--><div class="popover" bind:this={popover} class:visible style:--offset={offset}>
<img src={imageUrl}>
<div>
<h4>{title}</h4>
<p>
{description}
<a class="details" href={url} target="_blank" bind:this={detailsLink}>More</a>
</p>
</div>
</div><!--
--></span>

View File

@ -1,23 +0,0 @@
{
"lotr": {
"type": "trilogy",
"title": "The Lord of the Rings",
"author": "J. R. R. Tolkien",
"description": "Epic fantasy trilogy written by Oxford professor and linguist J. R. R. Tolkien. Considered by many to be the major trend-setter for the modern fantasy genre.",
"url": "https://www.goodreads.com/series/66175-the-lord-of-the-rings"
},
"neverwhere": {
"type": "book",
"title": "Neverwhere",
"author": "Neil Gaiman",
"description": "Under the streets of London there's a world most people could never dream of. A city of monsters and saints, murderers and angels, knights in armour and pale girls in black velvet. \"Neverwhere\" is the London of the people who have fallen between the cracks.",
"url": "https://www.goodreads.com/book/show/14497.Neverwhere"
},
"earthsea": {
"type": "series",
"title": "Earthsea Cycle",
"author": "Ursula K. Le Guin",
"description": "Series of high fantasy stories set in an archipelago world where names are power and dragons roam the skies.",
"url": "https://www.goodreads.com/series/40909-earthsea-cycle"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

View File

@ -1,2 +0,0 @@
import '../styles/main.scss';
export const prerender = true;

View File

@ -1,40 +0,0 @@
<style lang="scss">
.header {
background: var(--primary-color-faded);
}
nav {
max-width: 30rem;
margin: 0 auto;
display: flex;
justify-content: space-between;
& a {
flex: 1;
max-width: 8rem;
padding: 0.25rem 1rem;
font-size: 1.75rem;
color: white;
text-decoration: none;
text-align: center;
&:hover {
background: hsl(0deg 0% 0% / 10%);
}
}
}
</style>
<div class="header">
<nav>
<a data-sveltekit-preload-data="hover" href="/">Home</a>
<a data-sveltekit-preload-data="hover" href="/posts">Posts</a>
<a data-sveltekit-preload-data="hover" href="/about">About</a>
</nav>
</div>
<main>
<slot></slot>
</main>

View File

@ -1,8 +0,0 @@
export async function load({ data }) {
let post = await import(`./_posts/${data.slug}.svx`);
post.metadata.slug = data.slug;
post.metadata.next = data.next;
return {
post: post.default,
}
}

View File

@ -1,11 +0,0 @@
import { postData, siblingPosts } from './_posts/all.js';
// this is in a "servserside" loader so that we don't end up embedding the metadata
// for every post into the final page
export function load() {
return {
slug: postData[0].slug,
next: postData[1].slug,
};
}

View File

@ -1,5 +0,0 @@
<script>
export let data;
</script>
<svelte:component this={data.post} />

View File

@ -1,14 +0,0 @@
<style>
h1 {
margin-top: 6rem;
}
h1, p {
text-align: center;
}
</style>
<h1>404</h1>
<p>That page doesn't exist. Sorry!</p>

24
src/routes/[slug].svelte Normal file
View File

@ -0,0 +1,24 @@
<script context="module">
export async function load({ url, params }) {
try {
let post = await import(`./_posts/${params.slug}.svx`);
return {
props: {
post: post.default
}
}
}
catch (err) {
return {
status: 404,
error: `Not found: ${url.pathname}`,
}
}
}
</script>
<script>
export let post;
</script>
<svelte:component this={post} />

View File

@ -1,24 +0,0 @@
import { error } from '@sveltejs/kit';
export async function load({ url, params, data }) {
let post;
try {
post = await import(`../_posts/${params.slug}.svx`);
}
catch (err) {
if (err.message.match(/Unknown variable dynamic import/)) {
throw error(404, `Not found: ${url.pathname}`);
}
else {
throw err;
}
}
post.metadata.slug = params.slug;
post.metadata.prev = data.prev;
post.metadata.next = data.next;
return {
post: post.default,
}
}

View File

@ -1,10 +0,0 @@
import { postData } from '../_posts/all.js';
export function load({ params }) {
const i = postData.findIndex(p => p.slug === params.slug);
return {
prev: i < postData.length - 1 ? postData[i + 1].slug : null,
next: i > 0 ? postData[i - 1].slug : null,
};
}

View File

@ -1,5 +0,0 @@
<script>
export let data;
</script>
<svelte:component this={data.post} />

View File

@ -0,0 +1,45 @@
<style>
:global(main) {
--content-width: 42rem;
box-sizing: border-box;
max-width: var(--content-width);
margin: 0 auto;
padding: 0 15px;
}
#header {
background-color: #4f5f68;
}
#nav-main {
max-width: 30rem;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(3, minmax(6rem, 8rem));
justify-content: space-between;
}
#nav-main a {
font-size: 1.5rem;
color: white;
text-decoration: none;
text-align: center;
padding: 0.25rem 0;
}
#nav-main a:hover {
background-color: #00000025;
}
</style>
<div id="header">
<nav id="nav-main">
<a sveltekit:prefetch href="/">Home</a>
<a sveltekit:prefetch href="/posts">Posts</a>
<a sveltekit:prefetch href="/">About</a>
</nav>
</div>
<main>
<slot></slot>
</main>

View File

@ -1,503 +0,0 @@
---
title: 'Advent of Languages 2024, Day 1: C'
date: 2024-12-02
---
<script>import Sidenote from '$lib/Sidenote.svelte';</script>
As time goes on, it's becoming increasingly clear to me that I'm a bit of a programming-language dilletante. I'm always finding weird niche languages like [Pony](https://www.ponylang.io/) or [Roc](https://www.roc-lang.org), going "wow that looks cool," spending a bunch of time reading the documentation, and then never actually using it and forgetting all about it for the next three years.
This year, I've decided I'm going either buck that trend or double down on it, depending on your point of view. Instead of not engaging _at all_ with whatever random language strikes my fancy, I'm going to engage with it to the absolute minimum degree possible, then move on. Win-win, right? I get to _feel_ like I'm being more than a dilletante, but I don't have to do anything hard like _really_ learn a new language.
I should probably mention here, as a disclaimer, that I've never gotten all the way through an AoC in my life, and there's no way I'm going to do _better_ with _more_ problems to worry about. I'm guessing I'll peter out by day 12 or so, that's about as far as I usually get. Oh, and there's no way I'm going to stick to the one-day cadence either. It'll probably be May or so before I decide that enough is enough and I'm going to call it.<Sidenote>It's already December 2nd and I just finished the first half of Day 1, so clearly I'm shooting for more of a "slow and steady wins the race" cadence here.</Sidenote> Also, figuring out a new programming language every day is going to take enough time as it is, so I'm going to do this very stream-of-consciousness style. I apologize in advance for the haphazard organization of this and subsequent posts.
Anyway, I've decided to start with C, mostly because I'm scared of C and Day 1 of AoC is always the easiest, so I won't have to really get into it at all.<Sidenote>Ok, it's _also_ because I know that C doesn't have much in the way of built-in datastructures like hash maps and whatnot, so if you need one you end up having to either figure out how to use third-party C libraries (eugh) or write your own (even worse).</Sidenote>
## [The C Programming Language](https://en.wikipedia.org/wiki/The_C_Programming_Language)
C, of course, needs no introduction. It's known for being small, fast, and the language in which Unix was implemented, not to mention most (all?) other major OS kernels. It's also known for being the Abode of Monsters, i.e. there are few-to-no safeguards, and if you screw up the consequences might range from bad (segfaults in userland), to worse (kernel panics), to catastrophic (your program barfs out millions of users' highly sensitive data to anyone who asks).<Sidenote>To be fair, this last category isn't limited to C. Any language can be insecure if you try hard enough. Yes, even Rust.</Sidenote> I've seen it described as "Like strapping a jet engine to a skateboard - you'll go really fast, but if you screw up you'll end up splattered all over the sidewalk."<Sidenote>Sadly I can no longer locate the original source for this, but I have a vague recollection of it being somewhere on the [varnish](https://varnish-cache.org/) website, or possibly on the blog of someone connected with Varnish.</Sidenote>
All of which explains why I'm just a tad bit apprehensive to dip my toes into C. Thing is, for all its downsides, C is _everywhere_. Not only does it form the base layer of most computing infrastructure like OS kernels and network appliances, it's also far and away the most common language used for all the little computers that form parts of larger systems these days. You know, like cars, industrial controllers, robotics, and so on. So I feel like it would behoove me to at least acquire a passing familiarity with C one of these days, if only to be able to say that I have.
Oh, but to make it _extra_ fun, I've decided to try to get through at least the first part of Day 1 without using _any_ references at all, beyond what's already available on my computer (like manpages and help messages of commands). This is a terrible idea. Don't do things this way. Also, if you're in any way, shape, or form competent in C, please don't read the rest of this post, for your own safety and mine. Thank you.
## Experiments in C
Ok, let's get the basics out of the way first. Given a program, can I actually compile it and make it run? Let's try:
```c
#include "stdio.h" // pretty sure I've seen this a lot, I think it's for stuff like reading from stdin and writing to stdout
int main() { // the `int` means that this function returns an int, I think?
printf("hello, world!");
}
```
Now, I'm not terribly familiar with C toolchains, having mostly used them from several layers of abstraction up, but I'm _pretty_ sure I can't just compile this and run it, right? I think compiling will turn this into "object code", which has all the right bits in it that the computer needs to run it, but in order to put it all in a format that can actually be executed I need to "link" it, right?
Anyway, let's just try it and see.
```
$ cc 01.c
$ ls
>>> 01.c a.out
$ ./a.out
>>> "hello, world!"
```
Well, what do you know. It actually worked.<Sidenote>Amusingly, I realized later that it was totally by accident that I forgot to put a semicolone after the `#include`, but apparently this is the correct syntax so it just worked.</Sidenote> I guess the linking part is only necessary if you have multiple source files, or something?
## The Puzzle, Part 1
This is pretty encouraging, so let's tackle the actual puzzle for Day 1. [There's a bunch of framing story like there always is](https://adventofcode.com/2024/day/1), but the upshot is that we're given two lists arranged side by side, and asked to match up the smallest number in the first with the smallest number in the second, the second-smallest in the first with the second-smallest in the second, etc. Then we have to find out how far apart each of those pairs is, then add up all of those distances, and the total is our puzzle answer.
This is conceptually very easy, of course (it's only Day 1, after all). Just sort the two lists, iterate over them to grab the pairs, take `abs(a - b)` for each pair, and sum those all up. Piece of cake.
Except of course, that this is C, and I haven't the first idea how to do most of those things in C.<Sidenote>Ok, I'm pretty sure I could handle summing up an array of numbers in C. But how to create the array, or how to populate it with the numbers in question? Not a clue.</Sidenote>
### Loading data
Ok, so first off we'll need to read in the data from a file. That shouldn't be too hard, right? I know `fopen` is a thing, and I am (thankfully) on Linux, so I can just `man fopen` and see what I get, right? _type type_ Aha, yes! half a moment, I'll be back.
Mmmk, so `man fopen` gives me these very helpful snippets:
```
SYNOPSIS
#include <stdio.h>
FILE *fopen(const char *pathname, const char *mode);
(...)
The argument mode points to a string beginning with one of the following sequences (possibly followed by additional characters, as described below):
r Open text file for reading. The stream is positioned at the beginning of the file.
(...)
```
Ok, so let's just try opening the file and then dumping the pointer to console to see what we have.
```c
#include "stdio.h"
int main() {
int f_ptr = fopen("data/01.txt", "r");
printf(f_ptr);
}
```
```
$ cc 01.c
>>> 01.c: In function main:
01.c:4:17: warning: initialization of int from FILE * makes integer from pointer without a cast [-Wint-conversion]
4 | int f_ptr = fopen("data/01.txt", "r");
| ^~~~~
01.c:5:12: warning: passing argument 1 of printf makes pointer from integer without a cast [-Wint-conversion]
5 | printf(f_ptr);
| ^~~~~
| |
| int
In file included from 01.c:1:
/usr/include/stdio.h:356:43: note: expected const char * restrict but argument is of type int
356 | extern int printf (const char *__restrict __format, ...);
| ~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~
01.c:5:5: warning: format not a string literal and no format arguments [-Wformat-security]
5 | printf(f_ptr);
```
...oh that's right, this is C. we can't just print an integer, it would interpret that integer as a pointer to a string and probably segfault. In fact...
```
$ ./a.out
>>> Segmentation fault (core dumped)
```
Right. Ok, well, `man` was our friend last time, maybe it can help here too?
`man printf`
Why, yes! Yes it--oh wait, no. No, this isn't right at all.
Oh yeah, `printf` is _also_ a standard Unix shell command, so `man printf` gives you the documentation for _that_. I guess `man fopen` only worked because `fopen` is a syscall, as well as a library function. Oh well, let's just see if we can guess the right syntax.
```c
#include "stdio.h"
int main() {
int f_ptr = fopen("data/01.txt", "r");
printf("%i", f_ptr);
}
```
```
$ cc 01.c
$ ./a.out
>>> 832311968
```
Hey, would you look at that! Weirdly enough, so far it's been my Python experience that's helped most, first with the `fopen` flags and now this. I guess Python wears its C heritage with pride.
I'm cheating a little, by the way. Well, kind of a lot. I switched editors recently and am now using [Zed](https://zed.dev) primarily (for languages it supports, at least), and Zed automatically runs a C language server by default when you're working in C.<Sidenote>Actually, it might be a C++ language server? At least, it keeps suggesting things from `std::` which I think is a C++ thing.</Sidenote> Which is pretty helpful, because now I know the _proper_ (at least, more proper) way to do this is:
```c
FILE *file = fopen("data/01.txt", "r");
```
so now we have a pointer to a `FILE` struct, which we can give to `fread()` I think? `man fread` gives us this:
```
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
```
Which means, I think, that `fread()` accepts a pointer to a region of memory _into_ which it's reading data, an item size and a number of items,<Sidenote>Which surprised me, I was expecting just a number of bytes to read from the stream. Probably, again, because of my Python experience (e.g. this is how sockets work in Python).</Sidenote> and of course the pointer to the `FILE` struct.
Ok, great. Before we can do that, though, we need to get that first pointer, the one for the destination. `man malloc` is helpful, telling me that I just need to give it a number and it gives me back a `void *` pointer. I think it's `void` because it doesn't really have a type--it's a pointer to uninitialized memory, so you can write to it, but if you try to read from it or otherwise interpret it as being of any particular type it might blow up in your face.
Anyway:
```c
#include "stdio.h"
#include "stdlib.h"
int main() {
FILE *file = fopen("data/01.txt", "r");
void *data = malloc(16384);
size_t data_len = fread(data, 1, 16384, file);
printf("%zu", n_read);
}
}
```
I happen to know that my personal puzzle input is 14KB, so this will be enough. If the file were bigger, I'd have to either allocate more memory or read it in multiple passes. Oh, the joys of working in a non-memory-managed language.
Running this outputs `14000`, so I think it worked. I'm not sure if there's a performance penalty for using an item size of 1 with `fread`, but I'm guessing not. I highly doubt, for instance, that under the hood this is translating to 14,000 individual syscalls, because that would be a) completely bonkers and b) unnecessary since it already knows ahead of time what the max size of the read operation is going to be.<Sidenote>Reading up on this further suggests that the signature of `fread` is mostly a historical accident, and most people either do `fread(ptr, 1, bufsize, file)` (if reading less than the maximum size is acceptable) or `fread(ptr, bufsize, 1, file)` (if incomplete reads are to be avoided.)</Sidenote>
### Splitting ~~hairs~~ strings
Ok, next up we're going to have to a) somehow split the file into lines, b) split each line on the whitespace that separates the two columns, and c) parse those strings as integers. Some poking at the language server yields references to `strsep`, which appears to do exactly what I'm looking for:
```
char *strsep(char **stringp, const char *delim);
If *stringp is NULL, the strsep() function returns NULL and does nothing else.
Otherwise, this function finds the first token in the string *stringp, that is
delimited by one of the bytes in the string delim. This token is terminated by
overwriting the delimiter with a null byte ('\0'), and *stringp is updated to
point past the token.
```
I'm not quite sure what that `**stringp` business is, though. It wants a pointer to a pointer, I guess?<Sidenote>Coming back to this later, I realized: I think this is just how you do mutable arguments in C? If you just pass in a regular argument it seems to get copied, so changes to it aren't visible to the caller. So instead you pass in a pointer. But in this case, what needs to be mutated is _already a pointer_, so you have to pass a pointer to a pointer.</Sidenote> The language server suggests that `&` is how you create a pointer to something that you already have, so let's try that (by the way, I'm going to stop including all the headers and the `int main()` and all, and just include the relevant bits from now on):
```c
#include "string.h"
char *test = "hello.world";
char* res = strsep(&test, ".");
```
```
$ cc 01.c && ./a.out
>>> Segmentation fault (core dumped)
```
Hmm. That doesn't look too good.
However, further reflection suggests that my issue may just be that I'm using a string literal as `stringp` here, which means (I think) that it's going to be encoded into the data section of my executable, which makes it _not writable_ when the program is running. So you know what, let's just YOLO it. Going back to what we had:
```c
FILE *file = fopen("data/01.txt", "r");
void *data = malloc(16384);
size_t data_len = fread(data, 1, 16384, file);
char* first_line = strsep(&data, "\n");
printf("%s", first_line);
```
Compiling this generates dire warnings about passing an argument of type `void **` to a function expecting `char **`, but this is C, so I can just ignore those and it will operate on the assumption that I know what I'm doing<Sidenote>Oh, you sweet, simple summer child.</Sidenote> and treat that pointer as if it were `char **` anyway. And lo and behold:
```
$ ./a.out
>>> 88450 63363
```
It works!
Next question: Can I `strsep` on a multi-character delimiter?
```c
char* first_word = strsep(&first_line, " ");
printf("%s", first_word);
```
```
$ cc 01.cc && ./a.out
>>> 88450 6336388450
```
Aw, it didn't w--wait, no it did. It's just still printing `first_line` from above, and not printing a newline after that, so `first_word` gets jammed right up against it. Hooray!
### Integer-ation hell
Ok, last piece of the data-loading puzzle is to convert that string to an integer. I'm pretty sure I remember seeing a `strtoi` function in C examples that I've seen before, so let's try that.
Wait, no. There is no `strtoi`, but there _is_ a `strtol` ("string to long integer"), so let's try that instead.
```c
int i = strtol(first_word, NULL, 10);
printf("%i", i);
```
and...
```
$ cc 01.c && ./a.out
>>> 88450
```
Aww yeah. We got integers, baby! (Apparently `int` and `long` are synonymous? At least, they are for me right now on this machine, which is enough to be going on with.)
That second argument to `strtol`, by the way, is apparently `endptr`, about which the manpage has this to say:
```
If endptr is not NULL, strtol() stores the address of the first invalid character
in *endptr. If there were no digits at all, strtol() stores the original value of
nptr in *endptr (and returns 0). In particular, if *nptr is not '\\0' but **endptr
is '\\0' on return, the entire string is valid.
```
Sounds kind of like we could use that to avoid a second call to `strsep`, but it seems like a six-of-one, half-a-dozen-of-the-other situation, and I'm too lazy to figure it out, so whatever.
### Who needs arrays, anyway?
Ok, so we have the basic shape of how to parse our input data. Now we just need somewhere to put it, and for that we're obviously going to need an array. Now, as I understand it, C arrays are basically just pointers. The compiler keeps track of the size of the type being pointed to, so when you access an array you're literally just multiplying the index by the item size, adding that to the pointer that marks the start of the array, and praying the whole thing doesn't come crashing down around your ears.
I'm not sure of the appropriate way to create a new array, but I'm pretty sure `malloc` is going to have to be involved somehow, so let's just force the issue. `sizeof` tells me that an `int` (or `long`) has a size of 4 (so it's a 32-bit integer). I don't know exactly how many integers are in my puzzle input, but I know that it's 14,000 bytes long, and each line must consume at least 6 bytes (first number, three spaces, second number, newline), so the absolute upper bound on how many lines I'm dealing with is 2333.333... etc. Since each integer is 4 bytes that means each array will need to be just under 10 KB, but I think it's standard practice to allocate in powersof 2, so whatever, let's just do 16 KiB again.
Not gonna lie here, this one kind of kicked my butt. I would have expected the syntax for declaring an array to be `int[] arr = ...`, but apparently no, it's actually `int arr[] = ...`. Ok, that's fine, but `int arr[] = malloc(16384)` gets me `error: Invalid initializer`, without telling me what the initializer is.
Okay, fine. I'll look up the proper syntax for Part 2. For now let's just use pointers for everything. Whee! Who now? Safety? Never heard of her. BEHOLD!
```c
void* nums_l = malloc(16384);
void* nums_r = malloc(16384);
int nlines = 0;
while (1) {
char* line = strsep(&data, "\n");
int left = strtol(line, &line, 10);
int right = strtol(line, NULL, 10);
// if `strtol` fails, it apparently just returns 0, how helpful
if (left == 0 && right == 0) {
break;
}
int *addr_l = (int *)(nums_l + nlines * 4);
*addr_l = left;
int *addr_r = (int *)(nums_r + nlines * 4);
*addr_r = right;
nlines++;
}
```
Doesn't that just fill you with warm cozy feelings? No? Huh, must be just me then.
Oh yeah, I did end up figuring out how to do the `endptr` thing with `strtol`, it wasn't too hard.
### Sorting and finishing touches
Ok, next up, we have to sort these arrays. Is there even a sorting algorithm of any kind in the C standard library? I can't find anything promising from the autosuggestions that show up when I type `#include "`, and I don't feel like trying to implement quicksort without even knowing the proper syntax for declaring an array,<Sidenote>Or having a very deep understanding of quicksort, for that matter.</Sidenote> so I guess it's To The Googles We Must Go.
..._Gosh_ darn it, it's literally just called `qsort`. Ok, fine, at least I won't use Google for the _usage_.
You have to pass it a comparison function, which, sure, but that function accepts two arguments of type `const void *`, which makes the compiler scream at me when I attempt to a) pass it a function that takes integer pointers instead, or b) cast the void pointers to integers. Not sure of the proper way to do this so I'm just going to ignore the warnings for now because it _seems_ to work, and...
```c
int cmp(const int *a, const int *b) {
int _a = (int)(*a);
if (*a > *b) {
return 1;
}
else if (*a == *b) {
return 0;
}
else {
return -1;
}
}
// later, in main()
qsort(nums_l, nlines, 4, cmp);
qsort(nums_r, nlines, 4, cmp);
int sum = 0;
for (int i = 0; i < nlines - 1; i++) {
int *left = (int *)(nums_l + i * 4);
int *right = (int *)(nums_r + i * 4);
int diff = *left - *right;
if (diff < 0) {
diff = diff * -1;
}
sum += diff;
}
printf("%i", sum);
```
```
$ cc 01.c && ./a.out
>>> (compiler warnings)
2580759
```
Could it be? Is this it?
...nope. Knew it was too good to be true.
Wait, why am I using `nlines - 1` as my upper bound? I was trying to avoid an off-by-one error, because of course the "array" is "zero-indexed" (or would be if I were using arrays properly) and I didn't want to go past the end. But, of course, I forgot that `i < nlines` will _already_ stop the loop after the iteration where `i = 999`. Duh. That's not even a C thing, I could have made that mistake in Javascript. Golly. I guess my excuse is that I'm so busy focusing on How To C that I'm forgetting things I already knew?
Anyway, after correcting that error, my answer does in fact validate, so hooray! Part 1 complete!
Ok, before I go on to Part 2, I am _definitely_ looking up how to do arrays.
## Interlude: Arrays in C
Ok, so turns out there are fixed-size arrays (where the size is known at compile time), and there are dynamically-sized arrays, and they work a little differently. Fixed-sized arrays can be declared like this:
```c
int arr[] = {1, 2, 3, 4}; // array-literal syntax
int arr[100]; // declaring an array of a certain size, but uninitialized
```
Then you have dynamically-sized arrays, where the size of the array might not be known until runtime, so you have to use `malloc` (or `alloca` I guess, but you have to be real careful not to overflow your stack when you do that):
```c
int *arr = malloc(1000 * sizeof(int));
```
That's it, you're done. Apparently the fact that `arr` is declared as what looks like (to me, anyhow) _a pointer to an int_ is enough to tell the compiler, when accessed with square brackets, that this is actually a pointer to an _array_ of ints, and to multiply the index by the appropriate size (so I guess 4 in this case) to get the element at that array index.
Interestingly, with the above snippet, when I started accessing various indexes over 1000 to see what would happen, I got all the way to 32768 before it started to segfault.<Sidenote>The actual boundary is somewhere between 33000 and 34000, not sure where exactly because I got bored trying different indices.</Sidenote> I guess `malloc` doesn't even get out of bed for allocations less than 128 KiB?<Sidenote>Actually, what's probably happening is that `malloc` is requesting a bigger chunk _from the system_ in order to speed up any future allocations I might want to do. So if I were to call `malloc` again, it would just give me another chunk from that same region of memory. But of course C doesn't care what memory I access, it's only the system that enforces memory boundaries, and those are process-wide, so as long as I'm within that same chunk I'm fine. Just a guess, though.</Sidenote>
Armed with this new superpower, my full solution to Part 1 becomes:<Sidenote>I could (and probably should) also be using fixed-size arrays here, since it doesn't seem like there's any advantage to using `malloc`.</Sidenote>
```c
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
int cmp(const int *a, const int *b) {
if (*a > *b) {
return 1;
}
else if (*a == *b) {
return 0;
}
else {
return -1;
}
}
int main() {
FILE *file = fopen("data/01.txt", "r");
char *data = (char *)malloc(16384);
size_t data_len = fread(data, 1, 16384, file);
int *nums_l = malloc(16384);
int *nums_r = malloc(16384);
int nlines = 0;
while (1) {
char* line = strsep(&data, "\n");
int left = strtol(line, &line, 10);
int right = strtol(line, NULL, 10);
// if `strtol` fails, it apparently just returns 0, how helpful
if (left == 0 && right == 0) {
break;
}
nums_l[nlines] = left;
nums_r[nlines] = right;
nlines++;
}
qsort(nums_l, nlines, 4, cmp);
qsort(nums_r, nlines, 4, cmp);
int sum = 0;
for (int i = 0; i < nlines; i++) {
int diff = nums_l[i] - nums_r[i];
if (diff < 0) {
diff = diff * -1;
}
sum += diff;
}
printf("%i", sum);
}
```
Still getting compiler warnings about `cmp` not matching the required signature, though. Maybe I'll figure that out for Part 2.<Sidenote>Later addendum: I did end up figuring this out. Short version, I was just forgetting that the arguments are pointers. Instead of casting to `int` I needed to cast to `int *`.</Sidenote>
## Part 2
Part 2 has us counting frequencies instead of doing one-for-one comparisons. For each integer in the left column, we need to multiply it by the number of times it occurs in the right column, then add all those products together.
Obviously the _right_ way to do this would be to count occurrences for every integer in the right column, store those counts in a hash table, and then use those counts as we work through the left column. But C doesn't have a native hash table, and I don't particularly feel like trying to implement one (although I'm sure I would learn a lot more about C that way). But you know what? C is fast, and our arrays of numbers here are only 4 KB. My CPU has **64** KiB of L1 cache, so I'm _pretty sure_ that I can just be super-duper naive about this and iterate over the _entirety_ of the right column for every value in the left column. Sure, it's O(N^2), but N in this case is 1000, and a million operations on data in L1 cache isn't going to take hardly any time at all. So let's give it a shot.
```c
int count_occurrences(int num, int arr[], int len) {
int total = 0;
for (int i = 0; i < len; i++) {
if (arr[i] == num) {
total++;
}
}
return total;
}
int part2(int nums_l[], int nums_r[], int len) {
int score = 0;
for (int i = 0; i < len; i++) {
score += nums_l[i] * count_occurrences(nums_l[i], nums_r, len);
}
return score;
}
int main() {
// ...
int solution_2 = part2(nums_l, nums_r, nlines);
printf("Part 2: %i\n", solution_2);
}
```
And what do you know? It works!
And it takes about 30ms to run. [Shlemiel the Painter](https://www.joelonsoftware.com/2001/12/11/back-to-basics/)? Never heard of him.
I was going to make impressed comments here about how fast C is, but then I decided to try it in Python, and it takes less then a second there too, so... you know. Day 1 is just easy, even for brute-force solutions.
## And That's It
Hey, that wasn't so bad! I'm sure it would have been a lot harder had I waited until one of the later days, but even so, I can kind of see where C lovers are coming from now. It's a little bit freeing to be able to just throw pointers around and cast types to other types because hey, they're all just bytes in the end. I'm sure if I tried to do anything really complex in C, or read someone else's code, it would start to fall apart pretty quickly, but for quick-and-dirty one-off stuff--it's actually pretty good! Plus all the *nix system interfaces are C-native, so next time I'm fiddling with something at the system level I might just whip out the ol' cc and start throwing stuff at the wall to see what sticks.
By the way, if you'd like to hear more of my thoughts on C, I expect to be invited to speak at at least three major C conferences next year<Sidenote>_Are_ there even three major C conferences? I know there are lots for C++, but C++ is a lot more complex than C.</Sidenote> since I am now a Certified Expert Practitioner, and my schedule is filling up fast. Talk to my secretary before it's too late!

View File

@ -1,313 +0,0 @@
---
title: 'Advent of Languages 2024, Day 2: C++'
date: 2024-12-03
---
<script>import Sidenote from '$lib/Sidenote.svelte';</script>
Well, [Day 1](/advent-of-languages-2024-01) went swimmingly, more or less, so let's push on to Day 2: C++! C++, of course, is famous for being what happens when you take C and answer "Yes" to every question that starts "Can I have" and ends with a language feature. Yes, you can have classes and inheritance. Yes, even multiple inheritance. Yes, you can have constructors and destructors. Yes, you can have iterators (sorta).<Sidenote>More on that later.</Sidenote> Yes, you can have metaprogramming. Yes, you can have move semantics. Yes, you can also have raw pointers, why not?
It's ubiquitous in any context requiring a) high performance and b) a large codebase, such as browsers and game engines. It has a reputation for nightmarish complexity matched only by certain legal codes and the Third Edition of Dungeons & Dragons.<Sidenote>I'd be willing to bet you dollars to donuts that a non-trivial fraction of advanced C++ practitioners are also advanced D&D practitioners.</Sidenote> If using C is like firing a gun with a strong tendency to droop toward your feet any time your focus slips, then using C++ is like firing a Rube Goldberg machine composed of a multitude of guns which may or may not be pointed at your feet at any given time, and the only way to know is to pull the trigger.
How better, then, to spend Day 2 of Advent of Code?
## Will It ~~Blend~~ Compile?
I seem to recall hearing somewhere that C++ is a superset of C, so let's just start with the same hello-world as last time:
```c
#include "stdio.h"
int main() {
printf("hello, world!");
}
```
```
$ cpp 02.cpp
>>> # 0 "02.cpp"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "02.cpp"
# 1 "/usr/include/stdio.h" 1 3 4
# 27 "/usr/include/stdio.h" 3 4
(...much more in this vein)
```
Oh. Oh dear. That's not what I was hoping for at all.
So it seems that `cpp` doesn't produce executable code as its immediate artifact the way `cc` does. Actually, it looks kind of like it just barfs out C (non-++) code, and then you have to compile that with a separate C compiler? Let's try that.
```
$ cpp 02.cpp | cc
>>> cc: error: -E or -x required when input is from standard input
```
Hmm, well, that's progress, I guess? According to `cc --help`, `-E` tells it to "Preprocess only; do not compile, assemble or link", so that's not what I'm looking for. But wait, what's this?
```
-x <language> Specify the language of the following input files.
Permissible languages include: c c++ assembler none
'none' means revert to the default behavior of
guessing the language based on the file's extension.
```
Oho! Wait, does that mean I can just--
```
$ cc 02.cpp && ./a.out
>>> hello, world!
```
Well. That was a lot less complicated than I expected.<Sidenote>You may be thinking, of course it worked, you just fed plain C to a C compiler and it compiled, what's the big deal. I'm _pretty_ sure, though, that the `.cpp` extension does in fact tell the compiler to compile this _as C++_, if the help message is to be believed. The subsequent error when I try to use some actual C++ constructs has to do with whether and how much of the standard library is included by default--apparently there is a way to make plain `cc` work with `std::cout` and so on as well, it's just a little more involved.</Sidenote> I've got to say, I was expecting hours of frustration just getting the basic compiler toolchains to work with these OG languages like C and C++, but so far it's been surprisingly simple. I'm sure all of that goes right out the window the moment you need to make use of third-party code (beyond glibc that is), but for straightforward write-everything-yourself-the-old-fashioned-way work it's refreshingly simple.
Of course, after a little looking around I see that this isn't the idomatic way of outputting text in C++. That would be something more like this:
```cpp
#include <iostream>
int main() {
std::cout << "hello, world!";
}
```
```
$ cc 02.cpp && ./a.out
>>> /usr/bin/ld: /tmp/ccZD7l7S.o: warning: relocation against `_ZSt4cout' in read-only section `.text'
/usr/bin/ld: /tmp/ccZD7l7S.o: in function `main':
02.cpp:(.text+0x15): undefined reference to `std::cout'
/usr/bin/ld: 02.cpp:(.text+0x1d): undefined reference to `std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)'
/usr/bin/ld: /tmp/ccZD7l7S.o: in function `__static_initialization_and_destruction_0(int, int)':
02.cpp:(.text+0x54): undefined reference to `std::ios_base::Init::Init()'
/usr/bin/ld: 02.cpp:(.text+0x6f): undefined reference to `std::ios_base::Init::~Init()'
/usr/bin/ld: warning: creating DT_TEXTREL in a PIE
collect2: error: ld returned 1 exit status
```
Oh. Well, that's... informative. Or would be, if I knew what to look at.
I think the money line is this: `undefined reference to std::cout`, but I'm not sure what it means. The language server seemed to think that including `iostream` would make `std::cout` available.
Thankfully the ever-helpful Stack Overflow [came to the rescue](https://stackoverflow.com/a/28236905) and I was able to get it working by using `g++` rather than `cc`. Ok, I take back some of what I said about the simplicity of C-language toolchains.
## Day 2, Part 1
Ok, so let's look at [the actual puzzle](https://adventofcode.com/2024/day/2).
So we've got a file full of lines of space-separated numbers (again), but this time the lines are of variable length. Our job is, for every line, to determine whether or not the numbers as read left to right meet certain criteria. They have to be either all increasing or all decreasing, and they have to change by at least 1 but no more than 3 from one to the next.
Now, I know C++ has a much richer standard library than plain C, starting with `std::string`, so let's see what we can make it do. I'll start by just counting lines, to make sure I've got the whole reading-from-file thing working:
```cpp
#include <fstream>
#include <iostream>
#include <string>
using namespace std;
int main() {
ifstream file("data/02.txt");
string line;
int count = 0;
while (getline(file, line)) {
count++;
}
cout << count;
}
```
```
$ g++ 02.cpp && ./a.out
>>> 0
```
Oh, uh. Hmm.
Wait, I never actually downloaded my input for Day 2. `data/02.txt` doesn't actually exist. Apparently this isn't a problem? I guess I can see it being ok to construct an `ifstream` that points to a file that doedsn't exist (after all, you might be about to _create_ said file) but I'm a little confused that it will happily "read" from a non-existent file like this. If the file were present, but empty, it would presumably do the same thing, so I guess... non-extant and empty are considered equivalent? That's convenient for Redis, but I don't know that I approve of it in a language context.
Anyway, downloading the data and running the program again prints 1000, which seems right, so I think we're cooking with gas now.
### Interlude: Fantastic Files and How to Read Them
(I really need to find another joke, this one's wearing a bit thin.)
If you were wondering, by the way,<Sidenote>I was.</Sidenote> [a reference I found](https://cplusplus.com/reference/fstream/ifstream/) says that "Objects of this class maintain a `filebuf` object as their internal stream buffer, which performs input/output operations on the file they are associated with (if any)." So my guess is that we aren't actually doing 1000 separate reads from disk here, we're probably doing a few more reasonably-sized reads and buffering those in memory.
It does bug me a little bit that I'm copying each line for every iteration, but after some [tentative looking](https://brevzin.github.io/c++/2020/07/06/split-view/) for some equivalent of Rust's "iterate over string as a series of `&str`s" functionality I'm sufficiently cowed<Sidenote>Apparently C++ has a pipe operator? Who knew?</Sidenote> to just stick with the simple, obvious approach.
One thing's for sure in C++ world: Given a cat, there are guaranteed to be quite a few different ways to skin it.
### The rest of the owl
Anyway, let's do this.
```cpp
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
using namespace std;
vector<int> parse_line(string line) {
int start = 0;
vector<int> result;
while (start < line.length()) {
int end = line.find(" ", start);
if (end == -1) {
break;
}
string word = line.substr(start, end - start);
int n = stoi(word);
result.push_back(n);
start = end + 1;
}
return result;
}
bool is_valid(vector<int> report) {
int *prev_diff = nullptr;
for (int i = 1; i < report.size(); i++) {
int diff = report[i] - report[i - 1];
if (diff < -3 || diff == 0 || diff > 3) {
return false;
}
if (prev_diff == nullptr) {
*prev_diff = diff;
continue;
}
if ((diff > 0 && *prev_diff < 0) || (diff < 0 && *prev_diff > 0)) {
return false;
}
*prev_diff = diff;
}
return true;
}
int main() {
ifstream file("data/02.txt");
string line;
int count = 0;
while (getline(file, line)) {
auto report = parse_line(line);
if (is_valid(report)) {
count++;
}
}
cout << count;
}
```
And...<Sidenote>You may notice that my earlier concerns about unnecessary copying have been replaced with a cavalier disregard for memory allocations in every context. I subscribe to the ancient wisdom of "if you can't solve a problem, create another worse problem somewhere else and no one will care any more."</Sidenote>
```
$ g++ 02.cpp && ./a.out
>>> Segmentation fault (core dumped)
```
Oh.
Right, ok. I was trying to be fancy and use a pointer-to-an-int as sort of a poor man's `optional<T>`, mostly because I couldn't figure out how to instantiate an `optional<T>`. But of course, I can't just declare a pointer to an int as a null pointer, then do `*prev_diff = diff`, because that pointer still has to point _somewhere_, after all.
I could declare an int, then a _separate_ pointer which is _initially_ null, but then becomes a pointer to it later, but at this point I realized there's a much simpler solution:
```cpp
bool is_valid(vector<int> report) {
int prev_diff = 0;
for (int i = 1; i < report.size(); i++) {
int diff = report[i] - report[i - 1];
if (diff < -3 || diff == 0 || diff > 3) {
return false;
}
// on the first iteration, we can't compare to the previous difference
if (i == 1) {
prev_diff = diff;
continue;
}
if ((diff > 0 && prev_diff < 0) || (diff < 0 && prev_diff > 0)) {
return false;
}
prev_diff = diff;
}
return true;
}
```
This at least doesn't segfault, but it also doesn't give me the right answer.
Some debugging, a little frustration, and a few minutes later, though, it all works,<Sidenote>It was the parse function. I was breaking the loop too soon, so I was failing to parse the last integer from each line.</Sidenote> so it's time to move on to part 2!
## Part 2
In a pretty typical Advent of Code escalation, we now have to determine whether any of the currently-invalid lines would become valid with the removal of any one number. Now, I'm sure there are more elegant ways to do this, but...
```cpp
while (getline(file, line)) {
auto report = parse_line(line);
if (is_valid(report)) {
count_part1++;
count_part2++;
}
else {
for (int i = 0; i < report.size(); i++) {
int n = report[i];
report.erase(report.begin() + i);
if (is_valid(report)) {
count_part2++;
break;
}
report.insert(report.begin() + i, n);
}
}
}
cout << "Part 1: " << count_part1 << "\n";
cout << "Part 2: " << count_part2 << "\n";
}
```
The only weird thing here, once again [solved with the help of Stack Overflow](https://stackoverflow.com/questions/875103/how-do-i-erase-an-element-from-stdvector-by-index), was how the `erase` and `insert` methods for a vector expect not plain ol' integers but a `const_iterator`, which apparently is some sort of opaque type representing an index into a container? It's certainly not an "iterator" in the sense I'm familiar with, which is a state machine which successively yields values from some collection (or from some other iterator).
I'm just not sure why it needs to exist. The informational materials I can find [talk about](https://home.csulb.edu/~pnguyen/cecs282/lecnotes/iterators.pdf) how this is much more convenient than using integers, because look at this:
```cpp
for (j = 0; j < 3; ++j) {
...
}
```
Gack! Ew! Horrible! Who could possibly countenance such an unmaintainable pile of crap!
On the other hand, with _iterators_:
```cpp
for (i = v.begin(); i != v.end(); ++i) {
...
}
```
Joy! Bliss! Worlds of pure contentment and sensible, consistent programming practices!
Based on [further research](https://stackoverflow.com/questions/131241/why-use-iterators-instead-of-array-indices) it seems like iterators are essentially the C++ answer to the standardized iteration interfaces found in languages like Python, and that have since been adopted by virtually every language under the sun because they're hella convenient. In most languages, though, that takes the form of essentially a `foreach` loop, which is far and away (in my opinion) the most sensible way of approaching iteration. C++ just had to be different, I guess.<Sidenote>But never fear, C++ _also_ has a `foreach` loop!</Sidenote>
I should probably hold my criticism, though. After all, I've been using this language for less than 24 hours, whereas the C++ standards committee _presumably_ has a little more experience than that. And I'm sure the C++ standards committee has never made a bad decision, so I must just be failing to appreciate the depth and perspicacity of their design choices.
Anyway this all works now, so I guess that's Day 2 completed. Join us next time when we take on the great-graddad of all systems languages, **assembly**!
Just kidding, I'm not doing assembly. Not yet, anyway. Maybe next year.

View File

@ -1,327 +0,0 @@
---
title: 'Advent of Languages 2024, Day 3: Forth'
date: 2024-12-07
---
<script>import Sidenote from '$lib/Sidenote.svelte';</script>
My original plan was to stick with the "systems language" theme for Day 3 and go with Zig, but the more I thought about it the more I started to think, you know, Zig is nice and clean and modern. It hasn't had time to get all warty and ugly with bolted-on afterthoughts and contentious features that divide the community into warring tribes, and it has things like common datastructures in its standard library. I should probably save it for one of the later days, when I anticipate spending more time fighting _the problem_ and less time fighting _the language_. Also I looked at Day 3 and it (the first part at least) looked very simple, which makes me even less inclined to use a big honkin' heavy-duty language like Zig. Instead, today I'm going to take a look at Forth!<Sidenote>I know, I know, I would have been able to make all kinds of terrible jokes had I just waited for the _forth_ day of the AoC, but hey, we can't all get what we want.</Sidenote>
## May the Forth be with you
Forth is an old language, older even than C (by a few years at least), so you know right away it's going to be lacking a lot of modern conveniences like local variables or even, you know, structs. With named fields and all? Yep, not here.<Sidenote>I later discovered that this is implementation-specific--some Forths _do_ have structs, but others don't.</Sidenote> Forth is a [stack-oriented](https://en.wikipedia.org/wiki/Stack-oriented_programming) language, which I _think_ is a different kind of stack from the "stack/heap" you deal with in systems languages, although I might be wrong about that.
It's also _aggresively_ simple, both syntactically and conceptually. Syntactially, you could fit the grammar on the back of your hand. It's so slim that even _comparison operators_ like `<` and `=` (that's a comparison operator in Forth, like in SQL) are implemented as part of the _standard library_. Conceptually it's (if anything) even simpler.<Sidenote>Note that "simple" is not the same thing as "easy". C's memory model is simple: allocate memory, free memory. Don't free more than once, and don't free if it's still in use. Done! That doesn't stop it from being the root cause of some of the worst [security bugs](https://heartbleed.com/) of all time, or [massive worldwide outages](https://en.wikipedia.org/wiki/2024_CrowdStrike-related_IT_outages) affecting everything from banks to airlines.</Sidenote> There's a stack, and you put things onto it, and you take them off. Done.<Sidenote>Ok, ok, it's not _quite_ that simple. There are branching constructs, for instance (loops and conditionals), so not _everything_ can be modeled as pure stack operations. But it sure is a _lot_ simpler than most languages.</Sidenote>
Forth is--well, I think it's a bit of a stretch to describe it as "common" in any circumstance, but perhaps we can say "best-represented" in embedded systems contexts, where resources are often very heavily constrained and you're typically operating very close to the hardware. In fact it thrives in an environment where there's just a single contiguous block of not-very-much memory, because of the whole stack paradigm.
So of course, I'm going to use it for Advent of Code, where I have 32GB of RAM and a full-fat multi-process OS with extremely sophisticated thread scheduling, memory management, and so on. I wonder about myself sometimes.
## Where get?
Like a lot of older languages, there isn't a single official implementation of Forth which is your one-stop shop for compilers, linters, formatters, editor plugins, language servers, and all the various and sundry paraphernelia that have become de rigeur for a new language these days.<Sidenote>I don't know for sure what drives this difference, but my guess is that the Internet has made it much easier to coordinate something like programming-language development across a widely-separated group of people and organizations, so people naturally end up pooling their resources these days and all contributing to the One True Implementation.</Sidenote> It has a [standard](https://forth-standard.org/), and there are various implementations of that standard (scroll down to the "Systems" section on that page). [Gforth](https://gforth.org/) looks like the easiest to get up and running in, so let's give that a try.
The front page of the Gforth website has instructions for adding the Debian repository, but unfortunately that repository doesn't seem to be online anymore, so let's try building it from source, for which there are also instructions on the front page. The Git repository at least is hosted at a subdomain of `gnu.org`, so it might still be online?
_a brief interlude_
Okay, I'm back, and what do you know? It seems to have worked. There was a warning about `Swig without -forth feature, library interfaces will not be generated.`, but hopefully that's not important.
Okay, so we're good to go, right? At least, I can run `gforth` now and get a REPL, so I think we're good.
Wait a second, I forgot to run `make install`. But it's on my PATH already! What?
```
$ ls -l /usr/bin/gforth
>>> lrwxrwxrwx 1 root root 12 Sep 10 2021 /usr/bin/gforth -> gforth-0.7.3
```
...It was _already installed_?!
Well, paint me purple and call me a grape if that isn't the most embarrassing thing I've done all day.
## The real treasure was the friends we made along the way
I hope you enjoyed this brief exercise in futility. Let's just move on and pretend it never happened, hmmm?
Let's start Forth<Sidenote>Ok I'll stop now, I promise.</Sidenote> the same place we started C and C++, with hello-world. Unfortunately `highlight.js` has no syntax for Forth, so you'll just have to imagine the colors this time.
```fs
." hello world "
```
```
$ gforth 03.fs
>>> hello world Gforth 0.7.3, Copyright (C) 1995-2008 Free Software Foundation, Inc.
Gforth comes with ABSOLUTELY NO WARRANTY; for details type `license'
Type `bye' to exit
```
Uh. Okay, it looks like Forth doesn't exit unless you explicitly end your program with the `bye` command, it just does whatever you specified and then dumps you into a REPL so you can keep going, if you want. Hey, I wonder if giving it a non-interactive stdin would make a difference?
```
$ gforth </dev/null 03.fs
>>> hello world Gforth 0.7.3, Copyright (C) 1995-2008 Free Software Foundation, Inc.
Gforth comes with ABSOLUTELY NO WARRANTY; for details type `license'
Type `bye' to exit
```
Nope. I mean, it doesn't sit there waiting for input, of course. But it does print the start message. No, the only way to keep it from doing that is to put `bye` at the end of the script. Fascinating.
You may be struck by the spaces _inside_ the quote marks here: this is necessary. Without them, for example, `."hello` would be interpreted as a single token, and since there is no such "word" (the Forth term for functions, basically) defined it would error.<Sidenote>Actually, I discovered later that only the space after the _first_ quote mark is necessary. The second one is superfluous, and in fact gets interpreted as part of the string.</Sidenote>
## Day 3
Okay, so today we have a very minimal parsing challenge. We're given a long string of characters (just a single line this time) that contains a bunch of garbage and also some valid instructions, we have to parse out the instructions and execute them. There's only one instruction, `mul(x,y)` where x and y are numbers. Everything else is to be discarded, for Part 1 at least. I have a feeling that for Part 2 we may find out some of that garbage wasn't quite so garbacious after all.
This would be absolutely trivial with even the most basic of regular-expression libraries, but we're in Forth land here, so--wait. Maybe I should check some things before I make confident assertions.
...Yep, Gforth [does in fact](https://gforth.org/manual/Regular-Expressions.html) include regular expressions. Using them looks [pretty wild](https://git.savannah.gnu.org/cgit/gforth.git/tree/test/regexp-test.fs), though. I think I need to go read some more tutorials.
...
I'm back! It's been two days. Forth is confusing!
### Data loadin'
I've figured out how to at least open the file and read the data into memory, though, which is Great Success in my book. Here, let me show you:
```fs
create data 20000 allot
variable fileid
s" data/03.txt" r/o open-file
drop
fileid !
variable data-len
data 20000 fileid @ read-file
drop
data-len !
```
After executing this with `gforth`, I am dumped into an interactive Forth REPL, as before, but now I have a 20K buffer at the address specified by `data` containing the contents of my puzzle input, which is great!
Forth is _weird_ to anybody coming from the more standard type of language that has, you know, syntactic function calls and operators and all of that fancy stuff. None of that here! Remember how earlier I said that comparison operators were part of the standard library? Well yeah, it turns out that Forth doesn't even _have_ operators the same way other languages I've used do.<Sidenote>I'm aware that a lot of languages use operators as syntax sugar for functions defined somewhere else, but syntax sugar is just that: _syntax_. Forth doesn't make even a _syntactic_ distinction between operators and regular function calls. It's the equivalent of calling `add(a, b)` every time you want to add some numbers.</Sidenote> There are symbols like `>` and `=`, but really those are just conveniently-named "words" (again, the Forth name for a function) which operate on values from the stack just like other words with longer names. Crazy, huh?
Anyway, I want to go through this line-by-line because it's so far afield. Like I said, it took me 2 days to get this far. Note that because of the whole stack-oriented thing, Forth is syntactically backwards for most operations - you put the arguments _first_, thus putting them on the stack, then call the operation, which takes them off the stack and leaves its result (if any) on the stack. So to add 6 and 7 you would do `6 7 +`, which results in a stack containing 13. Wonky, I know.
* `create data 20000 allot`--Ok, I'm not actually 100% sure what this part is doing, I stole it from the gforth [tutorial](https://gforth.org/manual/Files-Tutorial.html#Open-file-for-input) on files, which is... minimal, shall we say? I think what's happening is that we're allocating memory, but we're "stack-allocating" (In the C sense, not the Forth sense). `create data` is actually the rare Forth construct that _isn't_ backwards, I think because it's a _compile-time_ operation rather than a runtime operation: we are `create`-ing a word, `data`, whose operation is simply to retun its own address. Then we are `allot`-ing 20K bytes, starting _immediately after that address_, I think? Anyway, the net result is that we end up with 20K of writable memory at the address returned by `data`, which is good enough for me.
* `variable fileid`--Another non-backwards construct here, `variable`. Again, I think this is because it creates a new _word_, which makes it a compile-time operation. I'm pretty sure `variable` is actually just a layer on top of `create ... allot`, because it does basically the same thing, it just only reserves enough space for a single integer. (Forth's native integer type is a signed 32-bit integer, by the way.)
* `s" data/03.txt" r/o open-file`--Now we're cooking with gas! `s" ..."` creates a string somewhere in memory (I think it actually allocates, under the hood) and puts the address and length of that string on the stack. `r/o` is just a convenience word that puts `0` on the stack; it represents the mode in which we're opening the file. `open-file` opens the file, using three values from the stack: 1) the address of the string containing the name of the file, 2) the length of that string, and 3) the mode. It returns two numbers to the stack, 1) the "file id" and 2) a status code.
* `drop`--I'm going to ignore the status code, so I `drop` it from the stack.
* `fileid !`--At this point we have the file ID on the stack, but we want to save it for future reference, so we store it at the address returned by `fileid`. The `!` word is just a dumb "store" command, it takes a value and an address on the stack and stores the value at that address. The reciprocal operation is `@`, which we'll see in a moment.
* `variable data-len`--Create a variable named `data-len` (again, just a word named `data-len` that returns an address where you can write data)
* `data 20000 fileid @ read-file`--This is the goods! We are reading (up to) 20K bytes from the file whose ID is stored in the variable `fileid` into the region of memory starting at address `data`. Note the `fileid @` bit, I got hung up here for the _longest_ time because I kept trying to just use `fileid` by itself. But that meant that I was giving `read-file` the _address of the variable itself_, not the value contained _at that varible_. In C terms, `@` is like dereferencing a pointer. (In fact, under the hood that's probably exactly what's going on, I think gforth is implemented largely in C.)
* `drop`--That last operation returned the number of bytes read, which we want, and a status code, which we don't. So we drop the status code.
* `data-len !`--Store the number of bytes read in the variable `data-len`.
At this point we have the contents of our file in memory, hooray! Boy, Forth is low-level. In some ways it's even lower-level than C, which is mind-boggling to me.
### Wait, regular expressions solved a problem?
Ok, so now we have our input, we need to process it.
My first attempt at this was an extremely laborious manual scanning of the string, attempting to parse out the digits from valid `mul(X,Y)` sequences, multiply them, keep a running total, etc. I got lost in all the complexity pretty quickly, so eventually I decided to just knuckle down and figure out the gforth regular expression library. And you know what? It wasn't too bad after all! Here's the regex I ended up coming up with:
```fs
require regexp.fs
: mul-instr ( addr u -- flag )
(( =" mul(" \\( {++ \\d ++} \\) ` , \\( {++ \\d ++} \\) ` ) )) ;
```
`: name ... ;` is the syntax for defining a new word, by the way. In this case, the body of the word _is_ the regex. I'm pretty sure this is required, because it's doing its magic at compile time, again--at least, if I try using a "bare" regex in the REPL, outside of a word, I get nothing. This seems to be the deal for most Forth constructs that have both a start and an end delimiter, like if/then, do/loop, and so on. The exceptions are of course the define-a-word syntax itself, and also string literals for some reason. Not sure why that is, but I'm guessing they're a special case because inline string literals are just _so_ useful.
Anyway, to elaborate a bit on the regex:
* `((` and `))` mark the start and end of the regex.
* `=" mul("` means "match the literal string `mul(`.
* `\(` and `\)` delimit a capturing group.
* `{++` and `++}` delimit a sequence repeated one-or-more times (like `+` in most regex syntaxes).
* A backtick followed by a single character means to match a single occurence of that character.
So this looks for the literal string `mul(`, followed by 1 or more digits, followed by the character `,` followed by 1 or more digits, followed by the character `)`. Not too bad, once you get your head around it.
Oh, and referencing captured groups is just `\1`, `\2`, `\3` etc. It seems like you can do this at any time after the regex has matched? I guess there's global state going on inside the regex library somewhere.
Anyway, here's my full solution to Day 3 Part 1!
```fs
create data 20000 allot \\ create a 20K buffer
variable fileid \\ create a variable `fileid`
s" data/03.txt" r/o open-file \\ open data file
drop \\ drop the top value from the stack, it's just a status code and we aren't going to bother handling errors
fileid ! \\ save the file id to the variable
variable data-len
data 20000 fileid @ read-file \\ read up to 20k bytes from the file
drop \\ drop the status code, again
data-len ! \\ store the number of bytes read in `data-len`
: data-str ( -- addr u )
\\ convenience function for putting the address and length of data on the stack
data data-len @ ;
: chop-prefix ( addr u u2 -- addr2 u2 )
\\ chop the first `u2` bytes off the beginning of the string at `addr u`
tuck \\ duplicate `u2` and store it "under" the length of the string
- \\ subtract `u2` from the length of the string
-rot \\ stick the new string length underneath the start pointer
+ \\ increment the start pointer by `u2`
swap \\ put them back in the right order
;
require regexp.fs
: mul-instr ( addr u -- flag )
\\\\ match a string of the form `mul(x,y)` where x and y are integers and capture those integers
(( =" mul(" \\( {++ \\d ++} \\) ` , \\( {++ \\d ++} \\) ` ) )) ;
: get-product ( addr u -- u2 )
mul-instr \\ match the string from `addr u` against the above regex
if \\ if the regex matches, then:
\\1 s>number drop \\ convert the first capture from string to number, drop the status code (we already know it will succeed)
\\2 s>number drop \\ convert the second capture from string to number, drop the status code
* \\ multiply, and leave the answer on the stack
else
0 \\ otherwise, leave 0 on the stack
then
;
variable result \\ initialize `result` with 0
0 result !
: sum-mul-instrs ( addr u -- u2 )
begin \\ start looping
s" mul(" search \\ search for the string "mul("
if \\ if successful, top 2 values on stack will be start address of "mul(" and remainder of original string
2dup \\ duplicate address and remaining length of string
get-product \\ pass those to get-product above
result @ + \\ load `result` and add to product
result ! \\ store this new value back in `result`
4 chop-prefix \\ bump the start of the string by 4 characters
else \\ if not successful, we have finished scanning through the string
2drop \\ dump the string address and length
result @ exit \\ put the result on top of the stack and return to caller
then
again
;
data-str sum-mul-instrs .
bye
```
Not exactly terse, but honestly it could have been a lot worse. And my extremely heavy use of comments makes it look bigger than it really is.
The `( addr u -- u2 )` bits are comments, by the way. By convention when you define a word that either expects things on the stack or leaves things on the stack, you put in a comment with the stack of the stack in `( before -- after )` format.
## Part 2
Onward and upward! In Part 2 we discover that yes, in fact, not quite all of the "garbage" instructions were truly garbage. Specifically, there are two instructions, `do()` and `don't()` which enable and disable the `mul(x,y)` instruction. So once you hit a `don't()`, you ignore all `mul(x,y)` instructions, no matter how well-formed, until you hit a `do()` again.
Easy enough, but I'm going to have to change some things. Right now I'm using the `search` word to find the start index of every possible `mul(` candidate, then using the regex library to both parse and validate at the same time. Obviously I can't do that any more, since now I have to search for any of three possible constructs rather than just one.
I spent quite a while trying to figure out how to get the regex library to spit out the address of the match it finds, but to no avail. There are some interesting hints about a "loop through all matches and execute arbitrary code on each match" functionality that I could _probably_ have shoehorned into what I needed here, but in the end I decided to just scan through the string the old-fashioned way and test for plain string equality at each position. In the end it came out looking like this:
```fs
...
variable enabled
-1 enabled ! \\ idiomatically -1 is "true" (really anything other than 0 is true)
: handle-mul ( addr u -- )
get-product \\ pass those to get-product above
result @ + \\ load `result` and add to product
result ! \\ store this new value back in `result`
;
: sum-mul-instrs ( addr u -- u2 )
\\ we want to loop from addr to (addr + u - 8), because 8 is the min length of a valid mul(x,y) instruction
\\ we also want to have addr + u on the top of the stack when we enter the loop,
\\ so that we can use that to compute the remaining length of the string from our current address
over + \\ copy addr to top of stack and add to length
dup 8 - \\ duplicate, then subtract 8 from the top value
rot \\ move original addr to top of stack
( stack at this point: [ addr + u, addr + u - 8, addr ] )
( i.e. [ end-of-string, loop-limit, loop-start ] )
do \\ start looping
I 4 s" do()" str= \\ compare the length-4 substring starting at I to the string "do()"
if \\ if valid do() instruction,
-1 enabled ! \\ set enabled=true
then
I 7 s" don't()" str= \\ compare length-7 substring to "don't()"
if \\ if valid don't() instruction,
0 enabled ! \\ set enabled=false
then
I 4 s" mul(" str= \\ compare length-4 substring to "mul("
enabled @ and \\ combine with current value of `enabled`
if \\ if a candidate for `mul(x,y)` instruction, and enabled=true, then
dup I - \\ subtract current string pointer from end-of-string pointer to get length of remaining string
I swap handle-mul \\ put current pointer onto stack again, swap so stack is ( addr len), and handle
then
loop
drop \\ get rid of end-of-string pointer
result @ \\ return value of result
;
s" data/03.txt" load-data
sum-mul-instrs .
bye
```
Oh yeah, I also decided to extract `load-data` into a word and pass in the filename, to make it easier to switch between test and live data. The whole thing is [here](https://git.jfmonty2.com/jfmonty2/advent/src/branch/master/2024/03.fs) if you're interested.
I'm actually surprised by how satisfied I am with this in the end. It's not exactly what I would call pretty, but it's reasonably comprehensible, and I feel like I'm starting to get the hang of this stack-manipulation business. It could definitely be more efficient - I'm still looping over every index in the string, even when I know I could skip some because, say, they already validated as being the start of a known instruction. Also, I should really avoid testing for subsequent instructions every time one of the prior ones validates. I just couldn't bring myself to do nested if statements because they [look like this](https://www.forth.com/starting-forth/4-conditional-if-then-statements/#h-nested-if-then-statements), which is horrible.
## Nobody asked for my opinion, but here it is anyway
So what do I think of Forth? It's certainly interesting! It's a very different way of approaching programming than I've encountered before. But I don't know that I'd want to use it for a serious project, because it's pretty lacking in the code-organization department.
A lost of older languages have shortcomings with regard to things like namespaces, good facilities for spreading code across multiple files (and complex file hierarchies), and tools for building on _other_ peoples' code. Forth has those problems too, but they aren't really fundamental. It's easy to imagine a version of Forth with some namespacing, a package manager, etc.<Sidenote>In fact, this is basically what [Factor](https://factorcode.org/) looks to be.</Sidenote> No, what worries me about Forth from a code-organization standpoint is _the stack itself_.
More specifically, it's the fact that there's only _one_ stack, and it's shared between every code-unit in the entire program. This might be ok if it were used exclusively for passing data around between different code units, but it isn't. From my limited experience, I get the impression that the stack is expected to be used for _most_ values. Sure, there are variables, but the amount of ceremony involved in using them makes it feel like Forth doesn't really want you to use them heavily. Plus, they're all global anyway, so they're hardly a help when it comes to code organization.
The problem with the stack is, as I said, that it's shared, and that everybody has it. That means that if you're storing something on the stack, and you invoke a word, _it might just mess with that something on the stack_. Sure, maybe it isn't _supposed_ to, but you know, bugs happen. The history of computer science is arguably a long and mostly-fruitless quest in search of _programming paradigms that result in fewer bugs_. "Just trust me bro" is not a useful approach to code encapsulation in complex projects.
Sure, you may say, but a function in any language can return incorrect values, so what's the big deal? Yes, that's true, but in most languages a function can't return _too many_ values, or too few. If it does, that's either a compile-time error or an _immediate_ runtime error, meaning the error occurs _at the point the misbehavior occurred._ This is critical, because errors get harder and harder to debug as their "skid distance" increases--i.e. the distance between the root cause of an error and the point at which that error actually manifests.
Even more worrisome, as I alluded to previously, is the fact that the stack makes it possible for words to mess with things that are (from the programmer's point of view) _totally unrelated to them_. You could end up with a situation like this:
```fs
put-some-memory-address-on-the-stack
17 do-something-unrelated
\\ an error in do-something-unrelated causes it to delete the memory address from the stck
do-more-things
...
attempt-to-access-missing-address \\ ERROR! What happened? Why isn't that address where I expected it to be?
```
This is the sort of thing that causes people to get woken up at 3AM to deal with production outages. The entire _class_ of purely-functional languages exists on the thesis data being twiddled with by bits of code that aren't supposed to is _so bad_ that it's better to disallow _all_ data-twiddling _forever_, full stop. I shudder to think what would happen if you dropped a Haskellite into a Forth world. He'd probably just keel over and die on the spot.
## So what is it actually good for?
The short answer is, embedded systems. That has traditionally been the wheelhouse of Forth, and as far as I can tell it continues to be so insofar as Forth is still used at all for real live projects.
This makes a lot of sense, when you think about it. Embedded code is often:
* Very tightly focused, concerned with just doing a few specific things under all circumstances (as contrasted with, say, your typical webapp which might have to add file uploads or start interfacing with some remote API any day of the week)
* Resource-constrained, particularly where memory is concerned (CPU time is usually less of a concern)
* Developed by a small team, often just a single person
* Fire-and-forget, there's no "ongoing maintenance" when you would have to perform a surgery or recall a satellite from orbit to do a firmware upgrade
All of this works to minimize the downsides of Forth's lack of organizational capabilities. Organizing is easier the less there is to organize, of course, and when there's only one or a few people doing the organizing. And when it's all done in one shot--I can't count the number of times I've come back to code _I wrote_ after a year or two or three and spent the day going "What was I _thinking_?" while trying to unravel the tangled web I wove. That sort of thing is much less likely when your code gets deployed as part of Industrial Sewage Pump Controller #BS94A, because who wants to go swimming in sewage if they don't have to?
The other reason I suspect that Forth has been so successful in embedded contexts is that it's _a lot_ easier to implement than most languages. This is true of stack-oriented languages in general, I think--there's a reason that a lot of VMs are stack-based as well--and in an embedded context, if you want a runtime of any kind you often have to build it yourself. I don't know this for sure, but I wouldn't be surprised if Forth got used for a lot of embedded stuff back in the day because it was the most feasible language to implement in raw assembly for the target architecture.<Sidenote>Of course, these days C is the lingua franca of everything, so I suspect this effect is weaker than it once was. Maybe there are some wild esoteric architectures out there that don't even have a C compiler, but I highly doubt there are many.</Sidenote>
Of course, I've constructed this tottering tower of linguistic philosophy on the basis of a few days' playing with Forth when I've had time, so take what I say with a few spoonfuls of salt. There are people out there who will [claim](http://collapseos.org/forth.html) that they used to prefer C to Forth, but after enough time with Forth their eyes were opened and their mind ascended to the heights, and they spoke with the tongues of the cherubim and painted with the colors of the wind--
Sorry, that got a little out of hand. My point is, some people like Forth even for relatively complex tasks like an OS kernel.<Sidenote>Although that particular guy is also prophesying the [end of civilization as we know it](http://www.collapseos.org/), so, I dunno. Maybe he just sees things in a different light.</Sidenote> Ultimately, though, I think the proof is in the pudding. Forth and C hit the scene at around the same time, but one of them went on to underpin the critical infrastructure of basically all computing, while the other continues to languish in relative obscurity, and we all know which is which.

View File

@ -1,363 +0,0 @@
---
title: 'Advent of Languages 2024, Day 4: Fortran'
date: 2024-12-10
---
<script>import Sidenote from '$lib/Sidenote.svelte';</script>
Oh, you thought we were done going back in time? Well I've got news for you, Doc Brown, you'd better not mothball the ol' time machine just yet, because we're going back even further. That's right, for Day 4 I've decided to use Fortran!<Sidenote>Apparently it's officially called `Fortran` now and not `FORTRAN` like it was in days of yore, and has been ever since the 1990s. That's right, when most languages I've used were just getting their start, Fortran was going through its mid-life identity crisis.</Sidenote><Sidenote>When I told my wife that I was going to be using a language that came out in the 1950s, she wanted to know if the next one would be expressed in Egyptian hieroglyphs.</Sidenote>
Really, though, it's because this is day _four_, and I had to replace all those missed Forth jokes with _something_.
## The old that is strong does not wither
Fortran dates back to 1958, making it the oldest programming language still in widespread use.<Sidenote>Says Wikipedia, at least. Not in the article about Fotran, for some reason, but in [the one about Lisp](https://en.wikipedia.org/wiki/Lisp_(programming_language)).</Sidenote> Exactly how widespread is debatable--the [TIOBE index](https://www.tiobe.com/tiobe-index/) puts it at #8, but the TIOBE index also puts Delphi Pascal at #11 and Assembly at #19, so it might have a different idea of what makes a language "popular" than you or I.<Sidenote>For contrast, Stack Overflow puts it at #38, right below Julia and Zig, which sounds a little more realistic to me.</Sidenote> Regardless, it's undeniable that it gets pretty heavy use even today--much more than Forth, I suspect--because of its ubiquity in the scientific and HPC sectors. The website mentions "numerical weather and ocean prediction, computational fluid dynamics, applied math, statistics, and finance" as particularly strong areas. My guess is that this largely comes down to intertia, plus Fortran being "good enough" for the things people wanted to use it for that it was easier to keep updating Fortran than to switch to something else wholesale.<Sidenote>Unlike, say, BASIC, which is so gimped by modern standards that it _doesn't even have a call stack_. That's right, you can't do recursion in BASIC, at least not without managing the stack yourself.</Sidenote>
And update they have! Wikipedia lists 12 major versions of Fortran, with the most recent being Fortran 2023. That's a pretty impressive history for a programming language. It's old enough to retire!
The later versions of Fortran have added all sorts of modern conveniences, like else-if conditionals (77), properly namespaced modules (90), growable arrays (also 90), local variables (2008), and finally, just last year, ternary expressions and the ability infer the length of a string variable from a string literal! Wow!
I have to say, just reading up on Fortran is already feeling modern than it did for Forth, or even C/C++. It's got a [snazzy website](https://fortran-lang.org/)<Sidenote>With a dark/light mode switcher, so you know it's hip.</Sidenote> with obvious links to documentation, sitewide search, and even an online playground. This really isn't doing any favors for my former impression of Fortran as a doddering almost-septegenarian with one foot in the grave and the other on a banana peel.
## On the four(tran)th day of Advent, my mainframe gave to me
The Fortran getting-started guide [literally gives you](https://fortran-lang.org/learn/quickstart/hello_world/) hello-world, so I won't bore you with that here. Instead I'll just note some interesting aspects of the language that jumped out at me:
* There's no `main()` function like C and a lot of other compiled languages, but there are mandatory `program <name> ... end program` delimiters at the start and end of your outermost layer of execution. Modules are defined outside of the `program ... end program` block. Not sure yet whether you can have multiple `program` blocks, but I'm leaning towards no?
* Variables are declared up-front, and are prefixed with their type name followed by `::`. You can leave out the type qualifier, in which case the type of the variable will be inferred not from the value to which it is first assigned, but from its _first letter_: variables whose names start with `i`, `j`, `k`, `l`, `m`, `n` are integers, everything else is a `real` (floating-point). Really not sure what drove that decision, but it's described as deprecated, legacy behavior anyway, so I plan to ignore it.
* Arrays are 1-indexed. Also, multi-dimensional arrays are a native feature! I'm starting to see that built-for-numerical-workloads heritage.
* It has `break` and `continue`, but they're named `exit` and `cycle`.
* There's a _built-in_ parallel-loop construct,<Sidenote>It uses different syntax to define its index and limit. That's what happens when your language development is spread over the last 65 years, I guess.</Sidenote> which "informs the compiler that it may use parallelization/SIMD to speed up execution". I've only ever seen this done at the library level before. If you're lucky your language has enough of a macro system to make it look semi-natural, otherwise, well, I hope you like map/reduce.
* It has functions, but it _also_ has "subroutines". The difference is that functions return values and are expected not to modify their arguments, and subroutines don't return values but may modify their arguments. I guess you're out of luck if you want to modify an argument _and_ return a value (say, a status code or something).
* Function and subroutine arguments are mentioned in the function signature (which looks like it does in most languages), but you really get down to brass tacks in the function body itself, which is where you specify the type and in-or-out-ness of the parameters. Reminds me of PowerShell, of all things.
* The operator for accessing struct fields is `%`. Where other languages do `sometype.field`, in Fortran you'd do `sometype%field`.
* Hey look, it's OOP! We can have methods! Also inheritance, sure, whatever.
Ok, I'm starting to get stuck in the infinite docs-reading rut for which I criticized myself at the start of this series, so buckle up, we're going in.
## The Puzzle
We're given a two-dimensional array of characters and asked to find the word `XMAS` everywhere it occurs, like those [word search](https://en.wikipedia.org/wiki/Word_search) puzzles you see on the sheets of paper they hand to kids at restaurants in a vain attempt to keep them occupied so their parents can have a chance to enjoy their meal.
Hey, Fortran might actually be pretty good at this! At least, multi-dimensional arrays are built in, so I'm definitely going to use those.
First things first, though, we have to load the data before we can start working on it.<Sidenote>Getting a Fortran compiler turned out to be as simple as `apt install gfortran`.</Sidenote>
My word-search grid appears to be 140 characters by 140, so I'm just going to hard-code that as the dimensions of my array. I'm sure there's a way to size arrays dynamically, but life's too short.
### Loading data is hard this time
Not gonna lie here, this part took me _way_ longer than I expected it to. See, the standard way to read a file in Fortran is with the `read()` statement. (It looks like a function call, but it's not.) You use it something like this:
```fortran
read(file_handle, *) somevar, anothervar, anothervar2
```
Or at least, that's one way of using it. But here's the problem: by default, Fortran expects to read data stored in a "record-based" format. In short, this means that it's expected to consist of lines, and each line will be parsed as a "record". Records consist of some number of elements, separated by whitespace. The "format" of the record, i.e. how the line should be parsed, can either be explicitly specified in a slightly arcane mini-language reminiscent of string-interpolation placeholders (just in reverse), or it can be inferred from the number and types of the variables specified after `read()`.
Initially, I thought I might be able to do this:
```fortran
character, dimension(140, 140) :: grid
! ...later
read(file_handle, *) grid
```
The top line is just declaring `grid` as a 2-dimensional array characters, 140 rows by 140 columns. Neat, huh?
But sadly, this kept spitting out errors about how it had encountered the end of the file unexpectedly. I think what was happening was that when you give `read()` an array, it expects to populate each element of the array with one record from the file, and remember records are separated by lines, so this was trying to assign one line per array element. My file had 140 lines, but my array had 140 * 140 elements, so this was never going to work.
My next try looked something like this:
```fortran
do row = 1, 100
read(file_handle, *) grid(row, :)
end do
```
But this also resulted in end-of-file errors. Eventually I got smart and tried running this read statement just _once_, and discovered that it was populating the first row of the array with the first letter of _each_ line in the input file. I think what's going on here is that `grid(1, :)` creates a slice of the array that's 1 row by the full width (so 140), and the `read()` statement sees that and assumes that it needs to pull 140 records from the file _each time this statement is executed_. But records are (still) separated by newlines, so the first call to `read()` pulls all 140 rows, dumps everything but the first character from each (because, I think, the type of the array elements is `character`), puts that in and continues on. So after just a single call to `read()` it's read every line but dumped most of the data.
I'm pretty sure the proper way to do this would be to figure out how to set the record separator, but it's tricky because the "records" (if we want each character to be treated as a record) within each line are smashed right up against each other, but have newline characters in between lines. So I'd have to specify that the separator is sometimes nothing, and sometimes `\n`, and I didn't feel like figuring that out because all of the references I could find about Fortran format specifiers were from ancient plain-HTML pages titled things like "FORTRAN 77 INTRINSIC SUBROUTINES REFERENCE" and hosted on sites like `web.math.utk.edu` where they probably _do_ date back to something approaching 1977.
So instead, I decided to just make it dumber.
```fortran
program advent04
implicit none
character, dimension(140, 140) :: grid
integer :: i
grid = load()
do i = 1, 140
print *, grid(i, :)
end do
contains
function load() result(grid)
implicit none
integer :: handle
character, dimension(140, 140) :: grid
character(140) :: line
integer :: row
integer :: col
open(newunit=handle, file="data/04.txt", status="old", action="read")
do row = 1, 140
! `line` is a `character(140)` variable, so Fortran knows to look for 140 characters I guess
read(handle, *) line
do col = 1, 140
! just assign each character of the line to array elements individually
grid(row, col) = line(col:col)
end do
end do
close(handle)
end function load
end program advent04
```
I am more than sure that there are several dozen vastly better ways of accomplishing this, but look, it works and I'm tired of fighting Fortran. I want to go on to the fun part!
### The fun part
The puzzle specifies that occurrences of `XMAS` can be horizontal, verical, or even diagonal, and can be written either forwards or backwards. The obvious way to do this would be to scan through the array, stop on every `X` character and cheak for the complete word `XMAS` in each of the eight directions individually, with a bunch of loops. Simple, easy, and probably more than performant enough because this grid is only 140x140, after all.<Sidenote>Although AoC has a way of making the second part of the puzzle punish you if you were lazy and went with the brute-force approach for the first part, so we'll see how this holds up when we get there.</Sidenote>
But! This is Fortran, and Fortran's whole shtick is operations on arrays, especially multidimensional arrays. So I think we can make this a lot more interesting. Let's create a "test grid" that looks like this:
```
S . . S . . S
. A . A . A .
. . M M M . .
S A M X M A S
. . M M M . .
. A . A . A .
S . . S . . S
```
Which has all 8 possible orientationS of the word `XMAS` starting from the central X. Then, we can just take a sliding "window" of the same size into our puzzle grid and compare it to the test grid. This is a native operation in Fortran--comparing two arrays of the same size results in a third array whose elements are the result of each individual comparison from the original arrays. Then we can just call `count()` on the resulting array to get the number of true values, and we know how many characters matched up. Subtract 1 for the central X we already knew about, then divide by 3 since there are 3 letters remaining in each occurrence of `XMAS`, and Bob's your uncle, right?
...Wait, no. That won't work because it doesn't account for partial matches. Say we had a "window" that looked like this (I'm only showing the bottom-right quadrant of the window for simplicity):
```
X M X S
S . . .
A . . .
X . . .
```
If we were to apply the process I just described to this piece of the grid, we would come away thinking there was 1 full match of `XMAS`, because there are one each of `X`, `M`, `A`, and `S` in the right positions. Problem is, they aren't all in the right places to be part of the _same_ XMAS, meaning that there isn't actually a match here at all.
To do this properly, we need some way of distinguishing the individual "rays" of the "star", which is how I've started thinking about the test grid up above, so that we know whether _all_ of any given "ray" is present. So what if we do it this way?
1. Apply the mask to the grid as before, but this time, instead of just counting the matches, we're going to convert them all to 1s. Non-matches will be converted to 0.
2. Pick a prime number for each "ray" of the "star". We can just use the first 8 prime numbers (excluding 1, of course). Create a second mask with these values subbed in for each ray, and 1 in the middle. So the ray extending from the central X directly to the right, for instance, would look like this, assuming we start assigning our primes from the top-left ray and move clockwise: `1 7 7 7`
3. Multiply this array by the array that we got from our initial masking operation. Now any matched characters will be represented by a prime number _specific to that ray of the star_.
4. Convert all the remaining 0s in the resulting array to 1s, then take the product of all values in the array.
5. Test whether that product is divisible by the cube of each of the primes used. E.g. if it's divisible by 8, we _know_ that there must have been three 2's in the array, so we _know_ that the top-left ray is entirely present. So we can add 1 to our count of valid `XMAS`es originating at this point.
Will this work? Is it even marginally more efficient than the stupidly obvious way of just using umpty-gazillion nested for loops--excuse me, "do loops"--to test each ray individually? No idea! It sure does sound like a lot more fun, though.
Ok, first things first. Let's adjust the data-loading code to pad the grid with 3 bogus values on each edge, so that we can still generate our window correctly when we're looking at a point near the edge of the grid.
```fortran
grid = '.' ! probably wouldn't matter if we skipped this, uninitialized memory just makes me nervous
open(newunit=handle, file="data/04.txt", status="old", action="read")
do row = 4, 143
read(handle, *) line
do col = 1, 140
grid(row, col + 3) = line(col:col)
end do
end do
```
Turns out assigning a value element to an array of that type of value (like `grid = '.'` above) just sets every array element to that value, which is very convenient.
Now let's work on the whole masking thing.
Uhhhh. Wait. We might have a problem here. When we take the product of all values in the array after the various masking and prime-ization stuff, we could _conceivably end up multiplying the cubes of the first 8 prime numbers. What's the product of the cubes of the first 8 prime numbers?
```
912585499096480209000
```
Hm, ok, and what's the max value of a 64-bit integer?
```
9223372036854775807
```
Oh. Oh, _noooo_.
It's okay, I mean, uh, it's not _that_ much higher. Only two orders of magnitude, and what are the odds of all eight versions of `XMAS` appearing in the same window, anyway? Something like 1/4<sup>25</sup>? Maybe we can still make this work.
```fortran
integer function count_xmas(row, col) result(count)
implicit none
integer, intent(in) :: row, col
integer :: i
integer(8) :: prod
integer(8), dimension(8) :: primes
character, dimension(7, 7) :: test_grid, window
integer(8), dimension(7, 7) :: prime_mask, matches, matches_prime
test_grid = reshape( &
[&
'S', '.', '.', 'S', '.', '.', 'S', &
'.', 'A', '.', 'A', '.', 'A', '.', &
'.', '.', 'M', 'M', 'M', '.', '.', &
'S', 'A', 'M', 'X', 'M', 'A', 'S', &
'.', '.', 'M', 'M', 'M', '.', '.', &
'.', 'A', '.', 'A', '.', 'A', '.', &
'S', '.', '.', 'S', '.', '.', 'S' &
], &
shape(test_grid) &
)
primes = [2, 3, 5, 7, 11, 13, 17, 19]
prime_mask = reshape( &
[ &
2, 1, 1, 3, 1, 1, 5, &
1, 2, 1, 3, 1, 5, 1, &
1, 1, 2, 3, 5, 1, 1, &
19, 19, 19, 1, 7, 7, 7, &
1, 1, 17, 13, 11, 1, 1, &
1, 17, 1, 13, 1, 11, 1, &
17, 1, 1, 13, 1, 1, 11 &
], &
shape(prime_mask) &
)
window = grid(row - 3:row + 3, col - 3:col + 3)
matches = logical_to_int64(window == test_grid)
matches_prime = matches * prime_mask
prod = product(zero_to_one(matches_prime))
count = 0
do i = 1, 8
if (mod(prod, primes(i) ** 3) == 0) then
count = count + 1
end if
end do
end function count_xmas
elemental integer(8) function logical_to_int64(b) result(i)
implicit none
logical, intent(in) :: b
if (b) then
i = 1
else
i = 0
end if
end function logical_to_int64
elemental integer(8) function zero_to_one(x) result(y)
implicit none
integer(8), intent(in) :: x
if (x == 0) then
y = 1
else
y = x
end if
end function zero_to_one
```
Those `&`s are line-continuation characters, by the way. Apparently you can't have newlines inside a function call or array literal without them. And the whole `reshape` business is a workaround for the fact that there _isn't_ actually a literal syntax for multi-dimensional arrays, so instead you have to create a 1-dimensional array and "reshape" it into the desired shape.
Now we just have to put it all together:
```fortran
total = 0
do col = 4, 143
do row = 4, 143
if (grid(row, col) == 'X') then
total = total + count_xmas(row, col)
end if
end do
end do
print *, total
```
These `elemental` functions, by the way, are functions you can ~~explain to Watson~~ apply to an array element-wise. So `logical_to_int64(array)` returns an array of the same shape with all the "logical" (boolean) values replaced by 1s and 0s.
This actually works! Guess I dodged a bullet with that 64-bit integer thing.<Sidenote>Of course I discovered later, right before posting this article, that Fortran totally has support for 128-bit integers, so I could have just used those and not worried about any of this.</Sidenote>
I _did_ have to go back through and switch out all the `integer` variables in `count_xmas()` with `integer(8)`s (except for the loop counter, of course). This changed my answer significantly. I can only assume that calling `product()` on an array of 32-bit integers, then sticking the result in a 64-bit integer, does the multiplication as 32-bit first and only _then_ converts to 64-bit, after however much rolling-over has happened. Makes sense, I guess.
Ok, great! On to part 2!
## Part 2
It's not actually too bad! I was really worried that it was going to tell me to discount all the occurrences of `XMAS` that overlapped with another one, and that was going to be a royal pain the butt with this methodology. But thankfully, all we have to do is change our search to look for _two_ occurrences of the sequence `M-A-S` arranged in an X shape, like this:
```
M . S
. A .
M . S
```
This isn't too difficult with our current approach. Unfortunately it will require four test grids applied in sequence, rather than just one, because again the sequence can be written either forwards or backwards, and we have to try all the permutations. On the plus side, we can skip the whole prime-masking thing, because each test grid is going to be all-or-nothing now. In fact, we can even skip checking any remaining test grids whenver we find a match, because there's no way the same window could match more than one.
Hmm, I wonder if there's a way to take a single starting test grid and manipulate it to reorganize the characters into the other shapes we need?
Turns out, yes! Yes there is. We can use a combination of slicing with a negative step, and transposing, which switches rows with columns, effectively rotating and flipping the array. So setting up our test grids looks like this:
```fortran
character, dimension(3, 3) :: window, t1, t2, t3, t4
t1 = reshape( &
[ &
'M', '.', 'S', &
'.', 'A', '.', &
'M', '.', 'S' &
], &
shape(t1) &
)
t2 = t1(3:1:-1, :) ! flip t1 top-to-bottom
t3 = transpose(t1) ! swap t1 rows for columns
t4 = t3(:, 3:1:-1) ! flip t3 left-to-right
```
Then we can just compare the window to each test grid:
```fortran
window = grid(row - 1:row + 1, col - 1:col + 1)
if ( &
count_matches(window, t1) == 5 &
.or. count_matches(window, t2) == 5 &
.or. count_matches(window, t3) == 5 &
.or. count_matches(window, t4) == 5 &
) then
count = 1
else
count = 0
end if
```
To my complete and utter astonishment, this actualy worked the first time I tried it, once I had figured out all of the array-flipping-and-rotating I needed to create the test grids. It always makes me suspicious when that happens, but Advent of Code confirmed it, so I guess we're good!<Sidenote>Or I just managed to make multiple errors that all cancelled each other out.</Sidenote>
It did expose a surprisingly weird limitation in the Fortran parser, though. Initially I kept trying to write the conditions like this: `if(count(window == t1) == 5)`, and couldn't understand the syntax errors it was throwing. Finally I factored out `count(array1 == array2)` into a separate function, and everything worked beautifully. My best guess is that the presence of two `==` operators inside a single `if` condition, not separated by `.and.` or `.or.`, is just a no-no. The the things we learn.
## Lessons ~~and carols~~
(Whoa now, we're not _that_ far into Advent yet.)
Despite being one of the oldest programming languages still in serious use, Fortran manages to feel surprisingly familiar. There are definite archaisms, like having to define the types of all your variables at the start of your program/module/function,<Sidenote>Even throwaway stuff like loop counters and temporary values.</Sidenote>, having to declare function/subroutine names at the beginning _and end_, and the use of the word "subroutine". But overall it's kept up surprisingly well, given--and I can't stress this enough--that it's _sixty-six years old_. It isn't even using `CAPITAL LETTERS` for everything any more,<Sidenote>Although the language is pretty much case-insensitive so you can still use CAPITALS if you want.</Sidenote> which puts it ahead of SQL,<Sidenote>Actually, I suspect the reason the CAPITALS have stuck around in SQL is that more than most languages, you frequently find yourself writing SQL _in a string_ from another language. Occasionally editors will be smart enough to syntax-highlight it as SQL for you, but for the times they aren't, using `CAPITALS` for all the `KEYWORDS` serves as a sort of minimal DIY syntax highlighting. That's what I think, at least.</Sidenote> and SQL is 10+ years younger.
It still has _support_ for a lot of really old stuff. For instance, you can label statements with numbers and then `go to` a numbered statement, but there's really no use for that in new code. We have functions, subroutines, loops, if-else-if-else conditionals--basically everything you would (as I understand it) use `goto` for back in the day.
Runs pretty fast, too. I realized after I already had a working solution that I had been compiling without optimizations the whole time, so I decided to try enabling them, only to discover that the actual execution time wasn't appreciably different. I figured the overhead of spawning a process was probably eating the difference, so I tried timing just the execution of the main loop and sure enough, without optimizations it took about 2 milliseconds whereas with optimizations it was 690 microseconds. Whee! Native-compiled languages are so fun. I'm too lazy to try rewriting this in Python just to see how much slower it would be, but I'm _pretty_ sure that this time it would be quite noticeable.
Anyway, that about wraps it up for Fortran. My only remaining question is: What is the appropriate demonym for users of Fortran? Python has Pythonistas, Rust has Rustaceans, and so on. I was going to suggest "trannies" for Fortran users, but everyone kept giving me weird looks for some reason.

View File

@ -1,113 +0,0 @@
---
title: Axes of Fantasy
date: 2023-12-26
draft: true
---
<script>
import Sidenote from '$lib/Sidenote.svelte';
import BookPreview from '$projects/fantasy/BookPreview.svelte';
</script>
For a while now, I've had a private taxonomy of fantasy books, based on the distinction (or lack thereof) between the fantasy world and our own. It goes something like this:
* *High Fantasy* is set in a world completely separate from our own, with no passage from one to the other. At the most, there might be faint hints that the fantasy world represents ours in the distant past.
* *Low Fantasy* is set in a fantasy world that is separate from ours, but that can be reached (at least some of the time) by some means, such as a portal, magic object, ritual, etc.
* *Urban Fantasy* is fantasy in which the fantasy world is contained _within_ our world, but typically hidden from the prying eyes of mere mortals in some fashion.
I refer to this as a "personal" taxonomy because as far as I can tell, nobody else shares it. The terms are well-known, of course, and there's some overlap--what most people call "High Fantasy" certainly does tend to be set in fantasy worlds with no connection to our own - but "Urban Fantasy" in particular means a whole lot more to most people than just "fantasy set in our world." Most people seem to agree that urban fantasy should be, well, urban - not that it has to take place in _cities_ stricly speaking, but it should at least portray the fantastical elements in and around the real world, and pay some attention to the question of how it stays out of sight of Muggles.
Obviously, my personal classification system is much simpler and stricter than this. To be honest, it's not terribly useful on its own - while the relationship between a fantasy world and our own is certainly _an_ attribute worth considering for classification purposes, it's far from the only one.
So then I got to thinking: If world-overlap is one "axis" along which fantasy can be ranked, what others are there? If we come up with a sufficiently comprehensive set of axes, can we start identifying existing labels (High Fantasy, Urban Fantasy, Epic Fantasy, etc.) as "clusters" of stories which share the same position on _multiple_ axes?
This means that the ideal basis for an axis should be:
* One-dimensional: Ideally, we'd like to be able to give each fantasy work a number, say from 1-100, so that any one is easily relatable to any other. This can't be a true ratiometric scale, obviously, but having something numeric makes it much easier to do fun stuff like searching for "neighbor" stories that sit near a given story on multiple axes.
* Orthogonal: Axes should be _conceptually_ unrelated to one another. Obviously a lot of axes will _tend_ to cluster, just like certain ingredients are commonly paired across a variety of dishes, but it should be possible _in principle_ for a story to occupy any positions on any given pair of axes.
* Objective: As much as possible, at least. Our existing axis of world overlap does well on this metric: it's usually pretty clear where a given story should fall. Sure, there are a few cases where different people might disagree about which of two stories has more or less overlap, but only when they have a very similar amount of overlap to start with. It doesn't seem likely that different people could end up placing the same story on opposite ends of the axis.
* Impactful: A story's position on the axis should go at least some way toward determining what kind of story it is. For example, the climate of the fantasy world would _not_ do well on this metric, since it doesn't matter a lot whether the world is hot or cold when you're asking how it should be classified.<Sidenote>The most impact I can imagine a fantasy world's climate having is something like the situation in A Song of Ice and Fire, where the extremely-long cycle of seasons (each cycle takes decades, if I recall correctly) lead to political differences because e.g. people younger than a certain age have never experienced a winter. But even then, it isn't the climate itself that most people would base their classification on, it's the political situation. It still seems largely incidental, _for classification purposes_, that the political complexity comes partly from environmental factors.</Sidenote>
Okay, so what different axes can we come up with? Obviously we can start with the original one that I wanted to base my taxonomy on:
## World overlap
This is an easy aspect to use for classification, because it's usually quite clear where a given setting should fall. At the left-most extreme you have what I'll call "Otherworld" fantasy, where the fantasy world has absolutely no connection at all to our own world. At the opposite end you'll find most "Urban" fantasy, where the world depicted _is_ the real world, just with added fantastical bits.
Notable subregions include:
### Otherworld Fantasy
No overlap at all. I think most fantasy that's written tends to fall here. At least, it's what most people think of when you say "fantasy book," and the Wikipedia definition of "Fantasy" specifies that it's "typically set in a fictional universe," so I think it's fair to say that this is the "standard" position for a fantasy story to occupy on this axis.
Examples: <BookPreview ref="lotr">_The Lord of the Rings_</BookPreview>, <BookPreview ref="earthsea">_Earthsea_</BookPreview>, _The Prydain Chronicles_, _Wheel of Time_, _Belgariad_, _A Song of Ice and Fire_, etc. Pick up a book from the fantasy section of a bookstore and there's at least a 50% chance it will fall into this category.
### Mythopoeic Fantasy
Much rarer than the previous category, stories of this type are set in the real world, but in a long-forgotten vanished age of which only the faintest echoes are now known. Think of the "A long time ago, in a galaxy far far away" intro to _Star Wars_<Sidenote>_Star Wars_, of course, isn't typically categorized as fantasy, but rather sci-fi - which is funny because _Star Wars_ has a lot more in common with most fantasy than most sci-fi. Starting with straight-up magic, i.e. the Force.</Sidenote> - the fantasy world has _some_ relation to our own, but for all practical purposes it might as well not exist.
The _Conan_ stories are the only ones I know of that fall clearly into this category, although I'm sure there must be others.
Interestingly enough Tolkien's original goal in writing developing his legendarium was to construct this type of setting. In his own words:<Sidenote>This comes from a letter that Tolkien wrote to Milton Waldman, who I believe was his publisher, in 1951.</Sidenote>
> I was from early days grieved by the poverty of my own beloved country: it had no stories of its own (bound up with its tongue and soil), not of the quality that I sought, and found (as an ingredient) in legends of other lands ... Do not laugh! But once upon a time (my crest has long since fallen) I had a mind to make a body of more or less connected legend, ranging from the large and cosmogonic, to the level of romantic fairy-story - the larger founded on the lesser in contact with the earth, the lesser drawing splendour from the vast backcloths - which I could dedicate simply to: to England, to my country.
Unfortunately, perhaps, for Tolkien, but quite fortunately for fans of modern fantasy as we see it today, he wound up creating what is pretty undeniably an Otherworld fantasy. There are occasional references to "nowadays" or "in later times" but even then, the conceit seems to be that the narrator is writing from the later days _of Middle Earth_.
### Portal Fantasy
We now make a rather significant jump into what I'm pretty sure is the second-largest category on this scale, which I'm calling "portal fantasy."<Sidenote>This is actually a term that I've seen used elsewhere, in contrast to the previous two which I just made up.</Sidenote> The "codifying" work for this category<Sidenote>In the same way the LOTR was codifying for much of otherworld fantasy, i.e. it's extremely common now for fantasy worlds to feature Elves, Dwarves, and Men, who all share roughly the same set of characteristics as Tolkien's versions. Even the spelling is Tolkien's - previously, "dwarves" would have been considered incorrect; the standard spelling was "dwarfs."</Sidenote> is, of course, the _Chronicles of Narnia_, but there are plenty of other examples. Apparently there's even a Japanese word for it, _isekai_.<Sidenote>I'm not at all familiar with this subgenre, so I don't know if it's exactly the same thing as what I'm calling "portal fantasy" or just shares some key traits with it.</Sidenote>
Note that the mere existence of portals between worlds, or some sort of established "multiverse," doesn't by itself qualify a story for this category. It's required that one of the worlds in question be _the real world_. Otherwise it's just a different flavor of otherworld fantasy. So no _Riftwar_, _Skyhold_, _Traitor Son Cycle_, etc.
One of the fun things about this classification is that it's a mini-axis in its own right, differentiated primarily by how easy or difficult it is to cross from the real world into the fantasy world or back again:
* On the left or "less overlapping" side you have stories like _Narnia_ or _The Last Rune_, where passage between worlds is spotty and _mostly_ doesn't happen at the behest of the characters, but by happenstance or by the action of some Greater Power that overstrides both worlds.
* Moving rightward, you find stories where points of passage are rare but knowable, usually requiring both a certain time and a certain place. _The Paradise War_ is a good example of this.
* Next you have stories where passage can seemingly be accomplished at any time, but requires a great deal of effort and/or arcane knowledge - think large assemblies of wizards gathered together, chanting in unison around a rune-inscribed circle that glows with eerie light, that sort of thing. _The Wizardry Compiled_ is pretty close to this, from what I remember.
* Finally there are some portal fantasies where the portal exists in a fixed location and can be crossed at any time, e.g. _Stardust_. This seems to be the rarest version, at least based on my own reading.
Other examples of portal fantasy include _The Chronicles of Amber_,<Sidenote>Originally I actually had this split into a separate category that I was going to call "nested-world fantasy", but on further reflection I realized that didn't make sense because a) if the fantasy world is nested inside the real world then it's just some verion of [urban fantasy](#urban-fantasy), and b) if it's the other way around, well, every portal fantasy already postulates the existence of some sort of "magical multiverse" that also contains the real world, and it's fundamentally no different whether the main story is set in the multiverse as a whole or just in some particular part of it.</Sidenote> _The Fionavar Tapestry_, the _Oz_ books, _Droon_,<Sidenote>I'm only putting this here for completeness, not because I've read a bunch of them or anything. _furtive glances from side to side_</Sidenote> the _Fairyland_ books, _The Phantom Tollbooth_,<Sidenote>I think, at least? I never actually finished this one.</Sidenote>, _Shades of Magic_, and _The Keys to the Kingdom_. There are buckets more, but that's all I can think of right now. Plus, this isn't meant to be an exhaustive catalog or anything.
### Alternate History
Fantasy that's set in our world, but with magic.<Sidenote>Or other fantastical elements, of course. Doesn't have to be literally magic-with-an-M.</Sidenote> I hemmed and hawed a lot about whether to even give this category a spot on this scale. You could easily make the argument that a fantasy version of the real world is just a different world, and all of these stories belong in the Otherworld category.
In the end, though, I decided that the point of this axis is to classify fantasy according to how much the fantasy world overlaps with our own, and alternate history involves _quite a lot of overlap_, even though the end result is a world that's not _quite_ identical with the real world.
Broadly speaking there are two variants of alternate history: 1) Either the fantastic has always been a part of life, or 2) it was suddenly introduced into the world by some (usually fairly cataclysmic) event.
Examples of the first variant include _Cecelia and Kate_, the _Temeraire_ books, _Jonathan Strange and Mr Norrell_, etc. An interesting quirk of this variant is that it's almost always set significantly in the past, but for some reason not _quite_ as far back as the quasi-Medieval era that is the bread and butter of most "standard" fantasy. The Napoleonic/Regency era is popular, as is the Victorian era. Modern-day alternate-history stories of this type seem fairly uncommon--_Bartimaeus_ is the only example I can think of off the top of my head.
Examples of the secont variant include _Unsong_, _Reckoners_, and _The Tapestry_. Stories of this type are much more commonly set in the modern day--understandably so, since "what would happen to society if magic were suddenly introduced" is a pretty interesting question to explore.
### Urban Fantasy
This is another term that you'll run across a lot if you do any research at all into fantasy subgenres. Here I'm using it in a very restricted sense, that is, _only_ to refer to the integration of the fantastical elements with the real world, without any of the other themes that are often indicated by the term.
To me, the defining characteristic of urban fantasy is that it's set in the real world, where the fantastical is _present_, but _hidden_. It _has_ to be hidden, because if it weren't then it would unavoidably have a major impact on the world, at which point we'd be back to alternate history.<Sidenote>The ever-relevant TVTropes has [some things to say](https://tvtropes.org/pmwiki/pmwiki.php/Main/Masquerade) on this subject as well.</Sidenote>
So urban fantasy depicts a world where there's magic<Sidenote>Or dragons, or fairies, or whatever fantastical elements the author wants. I'll just use "magic" generally to refer to "the fantastical" for the rest of this section.</Sidenote> but for whatever reason this is completely unknown to most people. Occasional exceptions may be made for top-secret government programs - it isn't that much of a stretch to imagine that if there were magic in the world, then at least some of the powers that be would be aware of it and using it to their advantage. The _Milkweed Triptych_ and the _Checquey Files_ are both examples of this variant.
For the most part, though, the people who know about magic are the people who have magic, plus the occasional Ascended Muggle Sidekick who's there for flavor (and to act as an audience surrogate, probably.) In fact, quite frequently the main conflict of the story is about _preventing_ the magical part of the world from being exposed, either because the magicians are afraid that a world full of angry normies would actually pose a threat to them<Sidenote>In this case the Salem witch trials and similar events are frequently invoked, in-universe as cautionary tales of what might happen "if _they_ find out about us."</Sidenote>, or because the wise and benevolent Wizards' Council has declared that even though they _could_ rule the world, it wouldn't be fair to the poor normies.
Other notable examples of the genre include _The Dresden Files_, _Percy Jackson and the Olympians_, _The Laundry Files_, _Neverwhere_, _American Gods_, the _Artemis Fowl_ books, the _Mither Mages_ series, the _Iron Druid_ series, _Monster Hunter International_, and of course _Harry Potter_.
## Quasi-Historical Era
Another good dimension for differentiating fantasy stories is their "era," so to speak: What real-world historical period provides the basis for the level of technology, social structures, etc?<Sidenote>This is necessarily bound up with the question of _where_ the fantasical cultures get their inspiration, but unfortunately that side of it isn't nearly as easy to map onto a single numerical scale, so I'm going to mostly ignore it for now.</Sidenote>
I don't think it's worth trying to be too precise with the exact historical placement of most fantasy works because most are filled with anachronisms and/or cobbled together from patchworks of different specific times and places,<Sidenote>E.g. the _Belgariad_, which has one country filled with more-or-less French knights of the late Medieval era, and another country populated by not-quite Vikings, in the same world.</Sidenote> so they don't really belong to _one_ precise era.<Sidenote>Exceptions, of course, being things like the _Traitor Son Cycle_, which is _very_ clearly set in an analogue of the late 14th century.</Sidenote> But you can usually get a rough sense of the aesthetic that the author is going for, in broad strokes.
### Antiquity
This is pretty rare, but there are a few examples. The _Codex Alera_ is set in something approximating Imperial Rome, and though I haven't read them I've heard that David Gemmell has written some in an Ancient-Greece-ish setting. I've heard rumors of some ancient Egyptian ones as well.
### Dark Ages
If anything, even rarer than the above. The only ones of which I am aware are the _Belisarius_ series,<Sidenote>Wikipedia terms this "Alternate history science fiction" but it isn't very science-y at all, from what I remember, and it _is_ more than a little magic-y, so I'll call it fantasy.</Sidenote> and (although I haven't read this one) the _Sarantine Mosaic_, both set around 500-600 AD.<Sidenote>So post-fall-of-Rome, which I think counts as Dark Ages.</Sidenote>
### Middle Ages
The _vast_ majority of fantasy is set in something with approximately the aesthetics of Medieval Europe. Tolkien, obviously, was the trend-setter here,<Sidenote>Although interestingly, Tolkien's work seems to clock in rather earlier than your Standard Formulaic Fantasy Setting 1A. Tolkien's world is closest to the _early_ middle ages (circa 1050 or so), from what I understand - e.g. his characters consistently use chain mail rather than plate armor; presumably plate armor doesn't exist in Middle Earth. The obvious reason for this, of course, is that this was the era Tolkien himself had studied most intensively - mostly its literature, from what I understand, but you can't become an expert in the literature of a period without developing at least _some_ sense of what life was like then. So naturally, this was what he drew on when crafting his fantasy settings. Later fantasy, on the other hand, seems to draw most heavily on the _late_ middle ages, circa 1300-1500, with plate armor, chivalry, the occasional joust, etc.</Sidenote>

View File

@ -31,7 +31,7 @@ docker network create \\
-o parent=eth0 \\
lan
docker run --network lan --ip 192.168.50.24 some/image:tag
docker run --network lan --ip 192.168.50.24 some/image:version
```
That's it! You're done, congratulations. (Obviously `--subnet`, `--gateway`, and `--parent` should be fed values appropriate to your network.)

View File

@ -3,7 +3,6 @@ title: Imagining A Passwordless Future
description: Can we replace passwords with something more user-friendly?
date: 2021-04-30
draft: true
dropcap: false
---
<script>
import Sidenote from '$lib/Sidenote.svelte';

View File

@ -1,128 +0,0 @@
---
title: The Kubernetes Alternative I Wish Existed
date: 2023-10-01
draft: true
---
<script>
import Sidenote from '$lib/Sidenote.svelte';
</script>
I use Kubernetes on my personal server, largely because I wanted to get some experience working with it. It's certainly been helpful in that regard, but after a couple of years I think I can pretty confidently say that it's not the ideal tool for my use-case. Duh, I guess? But I think it's worth talking about _why_ that's the case, and what exactly _would_ be the ieal tool.
## The Kubernetes Way™
Kubernetes is a very intrusive orchestration system. It would very much like the apps you're running to be doing things _its_ way, and although that's not a _hard_ requirement it tends to make everything subtly more difficult when that isn't the case. In particular, Kubernetes is targeting the situation where you:
* Have a broad variety of applications that you want to support,
* Have written all or most of those applications yourself,<Sidenote>"You" in the organizational sense, not the personal one.</Sidenote>
* Need those applications to operate at massive scale, e.g. concurrent users in the millions.
That's great if you're Google, and surprise! Kubernetes is a largely Google-originated project,<Sidenote>I'm told that it's a derivative of Borg, Google's in-house orchestration platform.</Sidenote> but it's absolute _garbage_ if you (like me) are just self-hosting apps for your own personal use and enjoyment. It's garbage because, while you still want to support a broad variety of applications, you typically _didn't_ write them yourself and you _most definitely don't_ need to scale to millions of concurrent users. More particularly, this means that the Kubernetes approach of expecting everything to be aware that it's running in Kubernetes and make use of the platform (via cluster roles, CRD's etc) is very much _not_ going to fly. Instead, you want your orchestration platform to be as absolutely transparent as possible: ideally, a running application should need to behave no differently in this hypothetical self-hosting-focused orchestration system than it would if it were running by itself on a Raspberry Pi in your garage. _Most especially_, all the distributed-systems crap that Kubernetes forces on you is pretty much unnecessary, because you don't need to support millions<Sidenote>In fact, typically your number of concurrent users is going to be either 1 or 0.</Sidenote> of concurrent users, and you don't care if you incur a little downtime when the application needs to be upgraded or whatever.
## But Wait
So then why do you need an orchestration platform at all? Why not just use something like [Harbormaster](https://gitlab.com/stavros/harbormaster) and call it a day? That's a valid question, and maybe you don't! In fact, it's quite likely that you don't - orchestration platforms really only make sense when you want to distribute your workload across multiple physical servers, so if you only have the one then why bother? However, I can still think of a couple of reasons why you'd want a cluster even for your personal stuff:
* You don't want everything you host to become completely unavailable if you bork up your server somehow. Yes, I did say above that you can tolerate some downtime, and that's still true - but especially if you like tinkering around with low-level stuff like filesystems and networking, it's quite possible that you'll break things badly enough<Sidenote>And be sufficiently busy with other things, given that we're assuming this is just a hobby for you.</Sidenote> that it will be days or weeks before you can find the time to fix them. If you have multiple servers to which the workloads can migrate while one is down, that problem goes away.
* You don't want to shell out up front for something hefty enough to run All Your Apps, especially as you add more down the road. Maybe you're starting out with a Raspberry pi, and when that becomes insufficient you'd like to just add more Pis rather than putting together a beefy machine with enough RAM to feed your [Paperless](https://github.com/paperless-ngx/paperless-ngx) installation, your [UniFi controller](https://help.ui.com/hc/en-us/articles/360012282453-Self-Hosting-a-UniFi-Network-Server), your Minecraft server(s), and your [Matrix](https://matrix.org) server.
* You have things running in multiple geographical locations and you'd like to be able to manage them all together. Maybe you built your parents a NAS with Jellyfin on it for their files and media, or you run a tiny little proxy (another Raspberry Pi, presumably) in your grandparents' network so that you can inspect things directly when they call you for help because they can't print their tax return.
Okay, sure, maybe this is still a bit niche. But you know what? This is my blog, so I get to be unrealistic if I want to.
## So what's different?
Our hypothetical orchestrator system starts out in the same place as Kubernetes--you have a bunch of containerized applications that need to be run, and a pile of physical servers on which you'd like to run them. You want to be able to specify at a high level in what ways things should run, and how many of them, and so on. You don't want to worry about the fiddly details like deciding which container goes on which host, or manually moving all of `odin`'s containers to `thor` when the Roomba runs over `odin`'s power cable while you're on vacation on the other side of the country. You _might_ even want to be able to specify that a certain service should run _n_ replicas, and be able to scale that up and down as needed, though that's a decidedly less-central feature for our orchestrator than it is for Kubernetes. Like I said above, you don't typically need to replicate your services for traffic capacity, so _if_ you're replicating anything it's probably for availability reasons instead. But true HA is usually quite a pain to achieve, especially for anything that wasn't explicitly designed with that in mind, so I doubt a lot of people bother.
So that much is the same. But we're going to do everything else differently.
Where Kubernetes is intrusive, we want to be transparent. Where Kubernetes is flexible and pluggable, we will be opinionated. Where Kubernetes wants to proliferate statelessness and distributed-systems-ism, we will be perfectly content with stateful monotliths.<Sidenote>And smaller things, too. Microliths?</Sidenote> Where Kubernetes expects cattle, we will accept pets. And so on.
The basic resources of servering are ~~wheat~~ ~~stone~~ ~~lumber~~ compute, storage, and networking, so let's look at each in detail.
### Compute
"Compute" is an amalgamate of CPU and memory, with a side helping of GPU when necessary. Obviously these are all different things, but they tend to work together more directly than either of them does with the other two major resources.
### Scheduling
Every orchestrator of which I am aware is modeled as a first-class distributed system: It's assumed that it will consist of more than one instance, often _many_ more than one, and this is baked in at the ground level.<Sidenote>Shoutout to [K3s](https://k3s.io) here for bucking this trend a bit: while it's perfectly capable of functioning in multi-node mode, it's capable of running as a single node and just using SQLite as its storage backend, which is actually quite nice for the single-node use case.</Sidenote>
I'm not entirely sure this needs to be the case! Sure, for systems like Kubernetes that are, again intended to map _massive_ amounts of work across _huge_ pools of resources it definitely makes sense; the average `$BIGCORP`-sized Kubernetes deployment probably couldn't even _fit_ the control plane on anything short of practically-a-supercomputer. But for those of us who _don't_ have to support massive scale, I question how necessary this is.
The obvious counterpoint is that distributing the system isn't just for scale, it's also for resiliency. Which is true, and if you don't care about resiliency at all then you should (again) probably just be using Harbormaster or something. But here's the thing: We care about stuff running _on_ the cluster being resilient, but how much do we care about the _control plane_ being resilient? If there's only a single control node, and it's down for a few hours, can't the workers just continue happily running their little things until told otherwise?
We actually have a large-scale example of something sort of like this in the Cloudflare outage from a while back: Their control plane was completely unavailable for quite a while (over a day if I recall corectly), but their core CDN and anti-DDoS services seemingly continued to function pretty well.
### Virtualization
The de facto standard unit of virtualization in the orchesetration world is the container. Containers have been around in one form or another for quite a while, but they really started to take off with the advent of Docker, because Docker made them easy. I want to break with the crowd here, though, and use a different virtualization primitive, namely:
#### AWS Firecracker
You didn't write all these apps yourself, and you don't trust them any further than you can throw them. Containers are great and all, but you'd like a little more isolation. Enter Firecracker. This does add some complexity where resource management is concerned, especially memory, since by default Firecracker wants you to allocate everything up front. But maybe that's ok, or maybe we can build in some [ballooning](https://github.com/firecracker-microvm/firecracker/blob/main/docs/ballooning.md) to keep things under control.
VM's are (somewhat rightfully) regarded as being a lot harder to manage than containers, partly because (as mentioned previously) they tend to be less flexible with regard to memory requirements, but also because it's typically a lot more difficult to do things like keep them all up to date. Managing a fleet of VM's is usually just as operationally difficult as managing a fleet of physical machines.
But [it doesn't have to be this way!](https://fly.io/blog/docker-without-docker/) It's 2023 and the world has more or less decided on Docker<Sidenote>I know we're supposed to call them "OCI Images" now, but they'll always be Docker images to me. Docker started them, Docker popularized them, and then Docker died because it couldn't figure out how to monetize an infrastructure/tooling product. The least we can do is honor its memory by keeping the name alive.</Sidenote> images as the preferred format for packaging server applications. Are they efficient? Hell no. Are they annoying and fiddly, with plenty of [hidden footguns](https://danaepp.com/finding-api-secrets-in-hidden-layers-within-docker-containers)? You bet. But they _work_, and they've massively simplified the process of getting a server application up and running. As someone who has had to administer a Magento 2 installation, it's hard not to find that appealing.
They're especially attractive to the self-hosting-ly inclined, because a well-maintained Docker image tends to keep _itself_ up to date with a bare minimum of automation. I know "automatic updates" are anathema to some, but remember, we're talking self-hosted stuff here--sure, the occasional upgrade may break your Gitea<Sidenote>Or maybe not. I've been running Gitea for years now and never had a blip.</Sidenote> server, but I can almost guarantee that you'll spend less time fixing that than you would have manually applying every update to every app you ever wanted to host, forever.
So we're going to use Docker _images_ but we aren't going to use Docker to run them. This is definitely possible, as alluded to above. Aside from the linked Fly post though, other [attempts](https://github.com/weaveworks-liquidmetal/flintlock) in the same [direction](https://github.com/firecracker-microvm/firecracker-containerd) don't seem to have taken off, so there's probably a fair bit of complexity here that needs to be sorted out.
## Networking
Locked-down by default. You don't trust these apps, so they don't get access to the soft underbelly of your LAN. So it's principle-of-least-privilege all the way. Ideally it should be possible when specifying a new app that it gets network access to an existing app, rather than having to go back and modify the existing one.
## Storage is yes
Kubernetes is famous for kinda just punting on storage, at least if you're running it on bare metal. Oh sure, there are lots of [storage-related resources](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/persistent-volume-v1/), but if you look closely at those you'll notice they mostly just _describe_ storage stuff, and leave it up to the cluster operator to bring their own _actual implementation_ that provisions, attaches, maintains, and cleans up the ~~castles in the sky~~ PersistentVolumeClaims and StorageClasses and whatnot.
This makes sense for Kubernetes because, although it took me an embarassingly long time to realize this, Kubernetes has never been about enabling self-hosting. Its primary purpose has always been _cloud mobility_, i.e. enabling you to pick up your cloud-hosted systems and plonk them down over in a completely different cloud. Unfortunately this leaves the self-hosting among us out in the cold: since we don't typically have the luxury of EBS or its equivalent in our dinky little homelabs, we are left to bring our own storage systems, which is something of a [nightmare hellscape of doom](https://kubernetes-csi.github.io/docs/introduction.html).
I want my hypothetical storage system to completely flip this on its head. There _should_ be a built-in storage implementation, and it _should_ be usable when you're self-hosting, with minimal configuration. None of this faffing about with the layers and layers of abstraction hell that Kubernetes forces on you as soon as you give in to the siren song of having a single stateful application in your cluster. If I want to give my application a persistent disk I should jolly well be able to do that with no questions asked.
## Sounds great, but how?
For starters, we're going to give up on synchronous replication. Synchronous replication is one of those things that _sounds_ great, because it makes your distributed storage system theoretically indistinguishable from a purely-local filesystem, but having used a [storage system that prioritizes synchronous replication](https://longhorn.io/) I can pretty confidently say that I would be much happier without it. It absolutely _murders_ performance, causing anything from a 3x-20x slowdown in my [testing](https://serverfault.com/a/1145529/409057), and the worst part of that is that I'm pretty sure it's _completely unnecessary_.
Here's the thing: You only _really_ need synchronous replication if you have multiple instances of some application using the same files at the same time. But nobody actually does this! In _any_ clustering setup I've ever encountered, you handle multi-consumer access to persistent state in one of three ways:
1. You delegate your state management to something else that _doesn't_ need to run multiple copies, i.e. the "replicate your web app but run one DB" approach,
2. You shard your application and make each shard the exclusive owner of its slice of state, or
3. You do something really fancy with distributed systems theory and consensus algorithms.
Here's the thing, though: _none of these approaches require synchronous replication._ Really, the _only_ use case I've found so far for _actually_ sharing state between multiple instances of the same application are things like Docker registry layer storage, which is a special case because it's basically a content-addressed filesystem and therefore _can't_ suffer from write contention. Maybe this is my lack of experience showing, but I have a lot of difficulty imagining a use-case for simultaneous multi-writer access to the same files that isn't better served by something else.
Conceptually, then, our storage system will consist of a set of directories somewhere on disk which we mount into containers, and which are _asynchronously_ replicated to other nodes with a last-writer-wins policy. Actually, we'll probably want to have multiple locations (we can call them "pool" like ZFS does) on disk so that we can expose multiple different types of media to the cluster (e.g. small/fast, large/slow).
This is super simple as long as we're willing to store a full copy of all the data on every node. That might be fine! But I lean toward thinking it's not, because it's not all that uncommon in my experience to have a heterogenous "cluster" where one machine is your Big Storage Monster and other machines are much more bare-bones. There are two basic ways of dealing with this:
1. We can restrict scheduling so that workloads can only be scheduled on nodes that have a copy of their data, or
2. We can make the data accessible over the network, SAN-style.
My inclination is to go with 1) here, because 2) introduces some pretty hefty performance penalties. We could maybe mitigate that with aggressive caching, but now you've got wildly unpredictable performance for your storage based on whether the data is in cache or not. Practically--remember, we're targeting _small_ setups here--I don't think it would be much of a problem to specify a set of nodes when defining a storage pool, or even just make pools a node-local configuration so that each node declares what pools it participates in, and then replicate each pool to every participating node. Again, we're not dealing with Big Data here, we don't need to spread our storage across N machines because it's literally too big to fit on one.
Kubernetes tends to work best with stateless applications. It's not entirely devoid of [tools](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/) for dealing with state, but state requires persistent storage and persistent storage is hard in clusters.<Sidenote>In fact, I get the sense that for a long time you were almost completely on your own with storage, unless you were using a managed Kubernetes project like GKE where you're just supposed to use whatever the provider offers for storage. More recently things like Longhorn have begun improving the situation, but "storage on bare-metal Kubernetes" still feels decidedly like a second-class citizen to me.</Sidenote>
Regardless, we're selfhosting here, which means virtually _everything_ has state. But fear not! Distributed state is hard, yes, but most of our apps aren't going to be truly distributed. That is, typically there's only going to be one instance running at a time, and it's acceptable to shut down the existing instance before spinning up a new one. So let's look at what kind of complexity we can avoid by keeping that in mind.
**We don't need strong consistency:** You're probably just going to be running a single instance of anything that involves state. Sure, you can have multiple SQLite processes writing to the same database and it _can_ be ok with that, unless it isn't.<Sidenote> From the SQLite FAQ: "SQLite uses reader/writer locks to control access to the database. But use caution: this locking mechanism might not work correctly if the database file is kept on an NFS filesystem. This is because `fcntl()` file locking is broken on many NFS implementations."</Sidenote> But you _probably_ don't want to run the risk of data corruption just to save yourself a few seconds of downtime.
This means that whatever solution we come up with for storage is going to be distributed and replicated almost exclusively for durability reasons, rather than for keeping things in sync. Which in turn means that it's _probably fine_ to default to an asynchronous-replication mode, where (from the application's point of view) writes complete before they're confirmed to have safely made it to all the other replicas in the cluster. This is good because the storage target will now appear to function largly like a local storage target, rather than a networked one, so applications that were written with the expectation of using local storage for their state will work just fine. _Most especially_, this makes it _actually realistic_ to distribute our storage across multiple geographic locations, whereas with a synchronous-replication model the latency impact of doing that would make it a non-starter.
**Single-writer, multi-reader is default:** With all that said, inevitably people are going to find a reason to try mounting the same storage target into multiple workloads at once, which will eventually cause conflicts. There's only so much we can do to prevent people from shooting themselves in the foot, but one easy win would be to default to a single-writer, multi-reader mode of operation. That way at least we can prevent write conflicts unless someone intentionally flips the enable-write-conflicts switch, in which case, well, they asked for it.
## Configuration
YAML, probably? It's fashionable to hate on YAML right now, but I've always found it rather pleasant.<Sidenote>Maybe people hate it because their primary experience of using it has been in Kubernetes manifests, which, fair enough.</Sidenote> JSON is out because no comments. TOML is out because nesting sucks. Weird niche supersets of JSON like HuJSON and JSON5 are out because they've been around long enough that if they were going to catch on, they would have by now. Docker Swarm config files<Sidenote>which are basically just Compose files with a few extra bits.</Sidenote> are my exemplar par excellence here. (comparison of Kubernetes and Swarm YAML?) (Of course they are, DX has always been Docker's Thing.)
We are also _definitely_ going to eschew the Kubernetes model of exposing implementation details in the name of extensibility.<Sidenote>See: ReplicaSets, EndpointSlices. There's no reason for these to be first-class API resources like Deployments or Secrets, other than to enable extensibility. You never want users creating EndpointSlices manually, but you might (if you're Kubernetes) want to allow an "operator" service to fiddle with them, so you make them first-class resources because you have no concept of the distinction between external and internal APIs.</Sidenote>
## Workload Grouping
It's always struck me as odd that Kubernetes doesn't have a native concept for a heterogenous grouping of pods. Maybe it's because Kubernetes assumes it's being used to deploy mostly microservices, which are typically managed by independent teams--so workloads that are independent but in a provider/consumer relationship are being managed by different people, probably in different cluster namespaces anyway, so why bother trying to group them?
Regardless, I think Nomad gets this exactly right with the job/group/task hierarchy. I'd like to just copy that wholesale, but with more network isolation.

View File

@ -1,30 +0,0 @@
---
title: 'Mixing GUIs and CLIs on Windows: A Cautionary Tale'
date: 2024-06-17
---
<script>import Sidenote from '$lib/Sidenote.svelte';</script>
If you've used desktop Linux, then I'm sorry for you.<Sidenote>I also use desktop Linux. I'm sorry for me, too.</Sidenote> You will, however, most likely be familiar with the practice of using the same app from either the CLI or the GUI, depending on how you invoke it and what you want to do with it. In some cases, the CLI merely replicates the functionality of the GUI, but (due to being, you know, a CLI) is much easier to incorporate in scripts and such. In other cases ([Wezterm](https://wezfurlong.org/wezterm/) is a good example) the GUI app acts as a "server" with which the CLI communicates to cause it to do various things while it runs.
On Linux, this is as natural as breathing. There's nothing in particular that distinguishes a "GUI app" from a "CLI app", other than that the GUI app _happens_ to ultimately call whatever system APIs are involved in creating windows, drawing text, and so on. Moreover, even when running its GUI, a Linux app always has stdout, stderr, etc. In most day-to-day usage, these get stashed in some inscrutable location at the behest of Gnome or XFCE or whatever ends up being responsible for spawning and babysitting GUI apps most of the time, but you can always see them if you launch the app from a terminal instead. In fact, this is a common debugging step when an app is misbehaving: launch it from a terminal so you can see whether it's spitting out errors to console that might help diagnose the problem.
Since Windows also has both GUIs and CLIs, you might naively expect the same sort of thing to work there, but woe betide you if you try to do this. Windows thinks every app must be _either_ a GUI app or a CLI app, and never the twain shall meet, or at lest never the twain shall meet without quite a significant degree of jank.
Every Windows executable is flagged somehow<Sidenote>I don't know how precisely, probably a magic bit somewhere in the executable header or something like that.</Sidenote> as "GUI app" or "CLI app", and this results in different behavior on launch. CLI apps are allocated a [console](https://learn.microsoft.com/en-us/windows/console/definitions), on which concept I'm not _entirely_ clear but which seems somewhat similar to a [pty](https://man7.org/linux/man-pages/man7/pty.7.html) on Linux. GUI apps, on the other hand, are not expected to produce console output and so are not allocated a console at all, which means that if they try to e.g. write to stdout they just... don't. I'm not sure what exactly happens when they try: in my experience e.g. `println!()` in Rust just becomes a no-op, but it's possible that this is implemented on the Rust side because writing to stdout from a GUI app would crash your program otherwise, or something.
Aha, says the clever Windows developer, but I am a practitioner of the Deep Magicks, and I know of APIs such as [`AllocConsole`](https://learn.microsoft.com/en-us/windows/console/allocconsole) and [`FreeConsole`](https://learn.microsoft.com/en-us/windows/console/freeconsole) which allow an app to control the existence and attached-ness of its Windows consoles. But not so fast, my wizardly acquaintance. Yes, you can do this, but it's still _janky as hell_. There are two basic approaches: you can either a) flag the executable as a GUI app, then call `AllocConsole` and `AttachConsole` to get a console which can then be used for stdout/err/etc, or you can b) flag the executable as a CLI app, so it gets allocated a console by default, then call `FreeConsole` to get rid of it if you decide you don't want it.
If you do a), the problem is that the app doesn't have a console at its inception, so `AllocConsole` creates an entirely _new_ console, with no connection to the console from which you invoked the app. So it pops up in a new window, which is typically the default terminal emulator<Sidenote>On Windows 10 and earlier, this defaults to `conhost.exe`, which is the terminal emulator equivalent of a stone knife chipped into chape by bashing it against other stones.</Sidenote> rather than whatever you have set up, and - even worse - _it disappears as soon as your app exits_, because of course its lifecycle is tied to that of the app. So the _extremely standard_ CLI behavior of "execute, print some output, then exit" doesn't work, because there's no time to _read_ that output before the app exits and the window disappears.
Alternatively, you can call `AttachConsole` with the PID of the parent process, or you can just pass `-1` instead of a real PID to say "use the console of the parent process". But this almost as terrible, because - again - the app _doesn't have a console when it launches_, so whatever shell you used to launch it will just assume that it doesn't need to wait for any output and blithely continue on its merry way. If you then attempt to write to stdout, you _will_ see the output, but it will be interleaved with your shell prompt, keyboard input, and so on, so again, not really usable.
Ok, so you do b) - flag your app as a CLI app, then call `FreeConsole` as soon as it launches to detach from the console that gets automatically assigned to it. Unfortunately this doesn't work either. When you launch a CLI app in a context that expects a GUI, such as the Start menu, it gets assigned a brand-new console window, again using whatever is the default terminal emulator. In my experience, it isn't consistently possible (from within the process at least) to call `FreeConsole` quickly enough to prevent this window from at least flashing briefly on the desktop. Livable? Sure, I guess, but it would be a sad world indeed if we never aimed higher than just _livable_.
Up until now, my solution has been to simply create two copies of my executable, one GUI and one CLI, put them in different directories, and add the directory of the CLI executable to my `PATH` so that it's the one that gets invoked when I run `mycommand` in a terminal. This works ok, despite being fairly inelegant, but just today I discovered a better way via [this rant](https://www.devever.net/~hl/win32con).<Sidenote>With whose sentiments I must agree in every particular.</Sidenote> Apparently you can specify the `CREATE_NO_WINDOW` flag when creating the process, which prevents it from creating a new window. Unfortunately, as that page notes, this requires you to control the invocation of the process, so the only way to make proper use of it is to create a "shim" executable that calls your main executable (which will be, in this case, the CLI-first version) with the `CREATE_NO_WINDOW` flag, for when you want to run in GUI mode. That post also points out that if you have a `.exe` file and a `.com` file alongside each other, Windows will prefer the `.com` file when the app is invoked via the CLI, so your shim can be `app.exe` and your main executable `app.com`. I haven't tried this yet myself, but it sounds like it would work, and it's a general enough solution<Sidenote>One could even imagine a generalized "shim" executable which, when executed, simply looks at its current executable path, then searches in the same directory for another executable with the same name but the `.com` extension, and executes that with the `CREATE_NO_WINDOW` flag.</Sidenote> that app frameworks such as [Tauri](https://tauri.app/)<Sidenote>Building a Tauri app is how I encountered this problem in the first place, so I would be _quite_ happy if Tauri were to provide a built-in solution.</Sidenote> might eventually handle it for you.
Another solution that was suggested to me recently (I think this is the more "old school" way of handling this problem) is to create a new virtual desktop, then ensure that the spurious console window gets created there so that it's out of sight. I haven't tried this myself, so I'm not familiar with the details, but my guess is that like the above it would require you to control the invocation of the process, so there isn't really any advantage over the other method, and it's still hacky as hell.
Things like this really drive home to me how thoroughly Windows relegates the CLI to being a second-class citizen. In some ways it almost feels like some of the products of overly-optimistic 1960s-era futurism, like [designing a fighter jet without a machine gun](https://en.wikipedia.org/wiki/McDonnell_Douglas_F-4_Phantom_II?useskin=vector) because we have guided missiles now, and _obviously_ those are better, right? But no, in fact it turns out that sometimes a gun was _actually_ preferable to a guided missile, because surprise! Different tools have different strengths and weaknesses.
Of course, if Microsoft had been in charge of the F-4 it would have [taken them 26 years](https://devblogs.microsoft.com/commandline/windows-command-line-introducing-the-windows-pseudo-console-conpty/) to finally add the machine gun, and when they did it would have fired at a 30-degree angle off from the heading of the jet, so I guess we can be thankful that we don't have to use our terminal emulators for air-to-air dogfights, at least.

View File

@ -1,32 +0,0 @@
---
title: Password Strength, Hackers, and You
date: 2023-10-21
draft: true
---
<script>
import Sidenote from '$lib/Sidenote.svelte';
</script>
Every once in a while, as my friends and family can attest, I go off on a random screed about passwords, password strength, password cracking, logins, etc. To which they listen with polite-if-increasingly-glassy-eyed expressions, followed by an equally polite change of conversational topic. To avoid falling into this conversational tarpit _quite_ so often, I've decided to write it all up here, so that instead of spewing it into an unsuspecting interlocutor's face I can simply link them here.<Sidenote>Maybe I can get business cards printed, or something.</Sidenote> Whereupon they can say "Thanks, that sounds interesting," and proceed to forget that it ever existed. So it's a win-win: I get to feel like I've Made A Difference, and they don't have to listen to a half-hour of only-marginally-interesting infosec jargon.
So.
## Password Strength
Everyone knows that the "best" password is at least 27 characters long and contains a mix of upper and lowercase letters, symbols, atomic symbols, and ancient Egyptian hieroglyphs. What may be slightly less known is exactly _why_ this is the recommended approach to picking passwords, and how the same goal might be accomplished by other, less eye-gougingly awful means.
So how do we measure the "strength" of a password? Ultimately, for the purposes of our discussion here, password strength comes down to one thing: How many tries<Sidenote>On average, that is. Obviously (especially with randomly-chosen passwords) the _exact_ number of tries is going to be somewhat random.</Sidenote> would it take for someone to guess this password? There are two ~~facets~~ to this question: 1) How many possible passwords are there (this is sometimes referred to as the "key space"), and 2) How likely is each of them to be the correct password?
The first of those questions is pretty easy to answer in the most basic sense: The number of possible passwords is the maximum password length, raised to the power of the number of possible characters. For instance, if the maximum password length is 16 characters, and the number of possible characters is 95<Sidenote>I.e. uppercase + lowercase + symbols.</Sidenote>, then the
So what makes a "strong" password? Most people have a pretty good intuition for this, I think: A strong password is one that can't be easily guessed. The absolute _worst_ password is something that might be guessed by someone who knows nothing at all about you, such as `password` or `123456`<Sidenote>This is, in fact, the most common password (or was last I checked), according to [Pwned Passwords](https://haveibeenpwned.com/passwords).</Sidenote> Only slightly stronger is a password that's obvious to anyone who knows the slightest bit about its circumstances, such as your first name or the name of the site/service/etc. to which it logs you in.
Ok, so it's pretty clear what makes a _really_ bad password. But what about an only-sort-of-bad password? This is where intuition starts to veer off the rails a little bit, I think. The "guessability" of a password might be quantified as "how long, on average, would it take to guess"? Unfortuantely, the intuitive situation of "guessing" a password is pretty divergent from the reality of what a password cracker is actually doing when they try to crack passwords. Most people, based on the conversations I've had, envision "password guessing" as someone sitting at a computer, typing in potential passwords one by one. Or, maybe slightly more sophisticatedly, they imagine a computer firing off attempted logins from a list of potential passwords, but critically, _against the live system that is under attack._ This is a problem, because most password cracking (at least, the kind you have to worry about) _doesn't_ take place against live login pages. Instead, it happens in what's known as an "offline" attack, when the password cracker has managed to obtain a copy of the password database and starts testing various candidates against it. To explain this, though, we have to take a little detour into...
## Password storage
Unless the system in question is hopelessly insecure (and there are such systems; we'll talk about that in a bit) it doesn't store a copy of your password in plain text. Instead it stores what's called a _hash_, which is what you get when you run the password through a particular type of data-munging process called a _hashing algorithm_. A good password hashing algorithm has two key properties that make it perfect for this use case: It's _non-reversible_, and it's _computationally expensive_.
### One-way hashing
Suppose your password is `password`, and its hash is something like `X03MO1qnZdYdgyfeuILPmQ`. The non-reversibility of the hashing algorithm means that given the second value, there isn't any direct way to derive the first again. The only way to figure it out is to, essentially, guess-and-check against a list of potential candidate inputs. If that sounds a little bit like black magic, don't worry - I felt the same way when I first encountered the concept. How can a hash be irreversible _even if you know the algorithm_?

View File

@ -2,11 +2,12 @@
title: Sidenotes
description: An entirely-too-detailed dive into how I implemented sidenotes for this blog.
date: 2023-08-14
draft: true
---
<script>
import Dropcap from '$lib/Dropcap.svelte';
import Sidenote from '$lib/Sidenote.svelte';
import UnstyledSidenote from '$lib/UnstyledSidenote.svelte';
import Frame from '$lib/projects/sidenotes/Frame.svelte';
</script>
<style>
@ -63,7 +64,7 @@ draft: true
}
</style>
One of my major goals when building this blog was to have sidenotes. I've always been a fan of sidenotes on the web, because the most comfortable reading width for a column of text is <em>far</em> less than the absurd amounts of screen width we tend to have available, and what else are we going to use it for?<Sidenote>Some sites use it for ads, of course, which is yet another example of how advertising ruins everything.</Sidenote>
<Dropcap word="One">of my major goals when building this blog was to have sidenotes. I've always been a fan of sidenotes on the web, because the most comfortable reading width for a column of text is <em>far</em> less than the absurd amounts of screen width we tend to have available, and what else are we going to use it for?<Sidenote>Some sites use it for ads, of course, which is yet another example of how advertising ruins everything.</Sidenote></Dropcap>
Footnotes don't really work on the web the way they do on paper, since the web doesn't have page breaks. You _can_ stick your footnotes in a floating box at the bottom of the page, so they're visible at the bottom of the text just like they would be on a printed page, but this sacrifices precious vertical space.<Sidenote>On mobile, it's _horizontal_ space that's at a premium, so I do use this approach there. Although I'm a pretty heavy user of sidenotes, so I have to make them toggleable as well or they'd fill up the entire screen.</Sidenote> Plus, you usually end up with the notes further away from the point of divergence than they would be as sidenotes anyway.

View File

@ -1,84 +0,0 @@
---
title: 'Converting ssh keys from old formats'
date: 2024-07-06
---
<script>
import Sidenote from '$lib/Sidenote.svelte';
</script>
Like a lot of people, my main experience with private keys has come from using them for SSH. I'm familiar with the theory, of course - I know generally what asymmetric encryption does,<Sidenote>Although exactly _how_ it does so is still a complete mystery to me. I've looked up descriptions of RSA several times, and even tried to work my way through a toy example, but it's never helped. And I couldn't even _begin_ to explain elliptic curve cryptography beyond "black math magic".</Sidenote> and I know that it means a compromised server can't reveal your private key, which is nice although if you only ever use a given private key to SSH into your server and the server is already compromised, is that really so helpful?<Sidenote>Yes, yes, I know that it means you can use the same private key for _multiple_ things without having to worry, but in practice a lot of people seem to use separate private keys for separate things, and even though I'm not entirely sure why I feel uncomfortable doing otherwise.</Sidenote>
What I was less aware of, however, was the various ways in which private keys can be _stored_, which rather suddenly became a more-than-purely-academic concern to me this past week. I had an old private key lying around which had originally been generated by AWS, and used a rather old format,<Sidenote>The oldest, I believe, that's in widespread use still.</Sidenote> and I needed it to be comprehensible by newer software which loftily refused to have anything to do with such outdated ways of expressing itself.<Sidenote>Who would write such obdurately high-handed software, you ask? Well, uh. Me, as it turns out. In my defense, though, I doubt it would have taken _less_ time to switch to a different SSH-key library than to figure out the particular magic incantation needed to get `ssh-keygen` to do it.</Sidenote> No problem, thought I, I'll just use `ssh-keygen` to convert the old format to a newer format! Unfortunately this was frustratingly<Sidenote>And needlessly, it seems to me?</Sidenote> difficult to figure out, so I'm writing it up here for posterity and so that I never have to look it up again.<Sidenote>You know how it works. Once you've taken the time to really describe process in detail, you have it locked in and never have to refer back to your notes.</Sidenote>
## Preamble: Fantastic Formats and Where to Find Them
I was aware, of course, that private keys are usually delivered as files containing big blobs of Base64-encoded text, prefaced by headers like `-----BEGIN OPENSSH PRIVATE KEY----`, and for whatever reason lacking file extensions.<Sidenote>Well, for the ones generated by `ssh-keygen`, at least. OpenSSL-generated ones often use `.key` or `.pem`, but those aren't typically used for SSH, so are less relevant here.</Sidenote> What I wasn't aware of is that there are actually several _different_ such formats, which although they look quite similar from the outside are internally pretty different. There are three you're likely to encounter in the wild:
1. OpenSSH-formatted keys, which start with `BEGIN OPENSSH KEY`<Sidenote>Plus the leading and trailing five dashes, but I'm tired of typing those out.</Sidenote> and are the preferred way of formatting private keys for use with SSH,
2. PCKS#8-formatted keys, which start with `BEGIN PRIVATE KEY` or `BEGIN ENCRYPTED PRIVATE KEY`, and
3. PEM or PKCS#1 format, which starts with `BEGIN RSA KEY`.
The oldest of these is PEM/PKCS#1 - the naming is a bit wishy-washy here. "PEM" when applied specifically to key files _for use with SSH_, i.e. the way that `ssh-keygen` uses it, seems to refer to this specific format. But "PEM" more generally is actually just a [generic container for binary data](https://en.wikipedia.org/w/index.php?title=Privacy-Enhanced_Mail&useskin=vector) that gets used a lot whenever it's helpful for binary data to be expressible as plaintext. In fact, _all_ of the private key formats I've ever seen used with OpenSSH<Sidenote>PuTTY does its own thing, which is why it's a major pain in the neck to convert between them, especially from the OpenSSH side. Fortunately [this is much less of a problem than it used to be.](https://github.com/PowerShell/Win32-OpenSSH?tab=readme-ov-file)</Sidenote> are some form of PEM file in the end.
Whatever you call it, however, this format has the major limitation that it can only handle RSA keys, which is why it fell out of favor when elliptic-curve cryptography started becoming more popular. The successor seems to have been PKCS#8, which is pretty similar but can hold other types of private keys. I haven't researched this one in quite as much detail, but I'm guessing that it also is a little nicer in how it handles encrypting private keys, since when you encrypt a PKCS#1 key it gets a couple of extra headers at the top, like this:
```
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,0B0A76ABB134DAFEB5C94C71760442EB
tOSEoYVcYcVEXl6TfBRjFRSihE3660NGRu692gAOqdYayozIvU9xpfeVCSlYO...
```
whereas when you encrypt a PKCS#8 private key the header just changes from `BEGIN PRIVATE KEY` to `BEGIN ENCRYPTED PRIVATE KEY` before starting in on the Base64 bit.
Both PKCS#1 and PKCS#8 use the same method for encoding the actual key data (which, when you get right down to it, is usually just a set of numbers with particular properties and relations between them): ASN.1, or Abstract Syntax Notation One. ASN.1 is... [complicated](https://en.wikipedia.org/wiki/ASN.1#Example). It seems very flexible, but it also seems like overkill unless you're trying to describe a very complex document, such as an X.509 certificate, or the legal code of the United States of America. But OpenSSH, it seems, longed for simpler days, when just reading a blasted private key didn't require pulling in a whole pile of parsing machinery, so it struck out on its own, and thereby was born the OpenSSH Private Key Format. This format does _not_ seem to be described in any RFCs, in fact the only detailed descriptions I can find are a [couple](https://coolaj86.com/articles/the-openssh-private-key-format/) of [blog posts](https://dnaeon.github.io/openssh-private-key-binary-format/) from people who figured it out for themselves from the OpenSSH source code.<Sidenote>Presumably this format is much simpler to parse and allows OpenSSH to do away with all the cumbersome drugery of dealing with ASN.1... or would, except that OpenSSH is still perfectly happy to read PKCS#1 and #8 keys. Maybe the eventual plan is to stop supporting the older formats? It's been 10 years, according to [another random blog post](https://www.thedigitalcatonline.com/blog/2021/06/03/public-key-cryptography-openssh-private-keys/), but maybe give it another 10 and we'll see some change?</Sidenote> I think this format can support every type of key that OpenSSH does, although I haven't personally confirmed that.<Sidenote>This is coming across as sarcastic, but I actually don't blame OpenSSH for coming up with its own private key format. If I had to deal with parsing ASN.1 I'd probably be looking for ways out too.</Sidenote>
## The `ssh-keygen` Manpage is a Tapestry of Lies
So, I thought, I can use `ssh-keygen` to convert between these various and sundry formats, right? It can do that, it _has_ to be able to do that, right?
Well, yes. It _can_, but good luck figuring out _how_. For starters, like many older CLI tools, `ssh-keygen` has an awful lot of flags and options, and it's hard to distinguish between which are _modifiers_ - "do the same thing, but differently" - and _modes of operation_ - "do a different thing entirely". The modern way to handle this distinction is with subcommands which take entirely different sets of arguments, but `ssh-keygen` dates back to a time before that was common.
It also dates back to a time when manpages were the primary way of communicating detailed documentation for CLI tools,<Sidenote>These days it seems more common to provide a reasonably-detailed `--help` output and then just link to web-based docs for more details.</Sidenote> which you'd _think_ would make it possible to figure out how to convert from one private key format to another, but oh-ho-ho! Not so fast, my friend. Here, feast your eyes on this:
```
-i This option will read an unencrypted private (or public) key file in the format specified by the -m option and print an
OpenSSH compatible private (or public) key to stdout. This option allows importing keys from other software, including
several commercial SSH implementations. The default import format is “RFC4716”.
```
Sounds great, right? Import private keys from other formats, i.e. convert them to our format, right? But it's _lying_. The `-i` mode doesn't accept private keys _at all_, that I've been able to tell, whatever their format. Giving it one will first prompt you for the passphrase, if any (so it's lying about needing an unencrypted input, although that's not a big deal) and then tell you bluntly that the your file is not in a valid format. The specific error message varies slightly with the particular format - attempting to give it a PEM file (with the appropriate option) returns `<file> is not a recognized public key format`, PKCS8 gets `unrecognised raw private key format`, and speicfy OpenSSH format just says `parse key: invalid format`. So really, the only thing this mode is useful for is reading a _public_ key in some PEM-ish format, and spitting out the line that you can used in `authorized_keys` - the one that starts with `ssh-rsa`, `ssh-ed25519`, etc.
## Enlightenment Ensues
But wait! The `-i` option mentions that the formats accepted are specified by the `-m` option, so let's take a look there:
```
-m key_format
Specify a key format for key generation, the -i (import), -e (export) conversion options, and the -p change passphrase
operation. The latter may be used to convert between OpenSSH private key and PEM private key formats. The supported
key formats are: “RFC4716” (RFC 4716/SSH2 public or private key), “PKCS8” (PKCS8 public or private key) or “PEM” (PEM
public key). By default OpenSSH will write newly-generated private keys in its own format, but when converting public
keys for export the default format is “RFC4716”. Setting a format of “PEM” when generating or updating a supported pri
vate key type will cause the key to be stored in the legacy PEM private key format.
```
Notice anything? I didn't, the first eleventy-seven times I read through this, because I was looking for a _list of known formats_, not a hint that you might be able to use _yet another option_ to do something _only marginally related to its core functionality_. So I missed that "The latter may be used to convert beteween OpenSSH private key and PEM private key<Sidenote>By PEM here OpenSSH apparently means both PKCS#1 _and_ PKCS#8, since it works perfectly well for both.</Sidenote> formats", and instead spent a while chasing my tail about RFC4716. This turns out to be not very helpful because it's _exclusively_ about a PEM-type encoding for _public_ keys, and doesn't mention private keys at all! OpenSSH seems to internally consider RFC4716 as the public-key counterpart to its home-grown private-key format, but this isn't explicitly laid out anywhere. Describing it as "RFC 4716/SSH2 public or private key" is confusing at best, because as we've established RFC 4716 doesn't mention private keys at all. "SSH2 private key" isn't obviously a thing: RFC 4716 public keys have the header `BEGIN SSH2 PUBLIC KEY`, but OpenSSH-formate private keys don't say anything about SSH2. The only way to figure out how it's being interpreted is to note that whenever `ssh-keygen` accepts the `-m` option _and_ happens to condescend to operate on private keys instead of public keys, giving it `-m RFC4716` produces and consumes private keys of the OpenSSH flavor.
_Anyway_. The documentation is so obtuse here that I didn't even discover this from the manpage at all, in the end. I had to get it from some random Github gist that I unfortunately can't find any more, but was probably written by someone just as frustrated as I am over the ridiculousness of this whole process. The _only_ way to change the format of a private key is to tell `ssh-keygen` that you want to _change its passphrase_? It's [git checkout](https://stevelosh.com/blog/2013/04/git-koans/#s2-one-thing-well) all over again.
At first I wondered whether maybe this is intentional, for security reasons, so that you don't accidentally remove the password from a private key while changing its format. But finally I don't think that makes sense, since if it's encrypted to start with then `ssh-keygen` is going to need its passphrase before it can make the conversion anyway, in which case there's no reason it can't just keep it hanging around and re-encrypt with the same passphrase after converting.
Most probably this is just a case of a tool evolving organically over time rather than being intentionally designed from the top down, and sure, that's understandable. Nobody sets out to create a tool that lies to you on its manpage<Sidenote>Maybe the `-i` option _did_ work with private keys at some point, although I have difficulty imagining why that functionality might have been removed.</Sidenote> and re-purposes modes of operation for other, only marginally-related operations, it just happens gradually over time because updates and new features are made in isolation, without consideration of the whole.
## Imagining a Brighter Tomorrow
But it doesn't have to be this way! Nothing (that I can see) prevents the `-i` option from being updated to accept private keys as well as public keys: it's clearly perfectly capable of telling when a file _isn't_ a valid _public_ key in the specified format, so it it seems like it could just parse it as a private key instead, and keep going if successful. Or an entirely new option could be added for converting private keys. `-c` is already taken for changing comments, but there are a few letters remaining. I don't see a `-j` on the manpage, for instance, or an `-x`.
I realize that an unforgiving reading of my travails in this endeavour might yield the conclusion that I'm an idiot with no reading comprehension, and that the manpage _clearly_ stated the solution to my problem _all along_, and if I had just RTFM<Sidenote>Noob.</Sidenote> then I could have avoided all this frustration, but that seems [a little unfair](https://xkcd.com/293/) to me.<Sidenote>Besides, I'm annoyed, and it's more satisfying to blame others than admit any fault of my own.</Sidenote> When you're writing the help message or manpage for your tool, you should _expect_ that people will be skimming it, looking to pick out the tidbits that are important to them right now, since for any tool of reasonable complexity 95% of the documentation is going to be irrelevant to any single user in any single situation.
How could the manpage be improved? Well, for starters, it could _not lie_ about the fact that the `-i` option doesn't do anything with private keys at all. If it were _really_ trying to be helpful it could even throw in a hint that if you want to work with private keys, you're barking up the wrong tree. Maybe it could say "Note: This option only accepts public keys. For private key conversions see -p and -m." or something like that. The `-p` option should probably also mention somewhere in its description that it happens to also be the preferred method for converting formats, since right now it only talks about changing passphrases.
Anyway, thanks for listening to my TED talk. At least now I'll never forget how to convert a private key again.

View File

@ -1,23 +0,0 @@
---
title: The Enduring Shell
date: 2023-11-26
draft: true
---
<script>
import Sidenote from '$lib/Sidenote.svelte';
</script>
Over twenty years ago, Neal Stephenson wrote an essay/pamphlet/novella/whatever-you-want-to-call-it titled [_In the beginning was the Command Line_](http://project.cyberpunk.ru/lib/in_the_beginning_was_the_command_line/). It's worth reading, and you should definitely do that at some point, but you should finish this first because it's quite long and Neal Stephenson is a much better writer than I am, so I worry you wouldn't come back.<Sidenote>I should probably also mention that it's Stephenson at his, ah, least restrained, so it's rather meandering. Don't get me wrong, it's _well-written_ meandering, but I don't think you can argue that an essay about command lines isn't meandering when it includes a thousand-word segment about Disney World.</Sidenote> As you might expect, Stephenson spends a lot of that material talking about the CLI versus the GUI, as though they were opposite poles of some abstract computational magnet. It's been a while since I read it, but I distinctly remember him describing the advent of the GUI as a sort of impending inevitability, an unfortunate but unstoppable end to which all things in time will eventually come. It's a little like watching [_Valkyrie_](https://www.imdb.com/title/tt0985699/), actually--you know whe whole time how it's going to turn out, but you can't keep yourself from watching it anyway.
The impending doom in this case is the ultimate triumph of the GUI over the CLI. Reading Stephenson's essay, you would be excused in coming away with the impression that the GUI is the way of the future, and that the CLI will eventually be relegated to the status of a quaint, old-timey practice and fall out of use except as a curiosity.<Sidenote>This isn't the only place I've run across this line of thought, either. David Edelman's [Jump 225 trilogy](https://www.goodreads.com/series/45075-jump-225) is set in a world where programming is no longer text-based but accomplished by manipulating a 3-dimensional model of the program; the programmer's tools are a set of physical instruments that he uses to maniuplate the program-model in various ways.</Sidenote>
He might have been surprised, then<Sidenote>He's still alive, I guess I could just ask him.</Sidenote> if he had known that today, in the far-distant future of 2023, many people (mostly technical people, it is to be admitted) use the command line every day, and that in some ways it's more alive and well than it ever has been. It's still not the dominant paradigm of computer interfaces for most people of course, and never will be again--that ship has most definitely sailed. But at the same time it's not going away any time soon, because there are aspects of the CLI that make it _better_ than a GUI for many uses.
A long time ago, the first time I needed to encode or transcode a video, I [downloaded Handbrake](https://handbrake.fr/downloads2.php).<Sidenote>I'm pretty sure the download page looked exactly the same then as it does now (except for the cookie warning, of course). It's nice that there are a few islands of stability in the sea of change that is the Internet.</Sidenote> I think I had read about it on Lifehacker, back when Lifehacker was good. I remember at the time being vaguely surprised that it came in both GUI and CLI flavors,<Sidenote>And you can thank Microsoft for that, as they have in their infinite wisdom decided that a given executable should function as either a CLI app or a GUI app, but never, ever be permitted to do both.</Sidenote> since it had never occurred to me for even the barest moment that I might want to use Handbrake via anything other than a GUI.
A lot of time has passed since then, and now I can easily imagine situations where I'd want the CLI version of Handbrake rather than the GUI. So what are those situations? What is it about the CLI that has kept it hanging around all these years, hanging on grimly by its fingertips in some cases, while generation after generation of graphical whizmos have come and gone? There are a number of reasons, I think.
## CLI apps are easier to write
blah blah words here

View File

@ -4,21 +4,22 @@ description: They're more similar than they are different, but they say the most
date: 2023-06-29
---
<script>
import Dropcap from '$lib/Dropcap.svelte';
import Sidenote from '$lib/Sidenote.svelte';
</script>
Recently I've had a chance to get to know Vue a bit. Since my frontend framework of choice has previously been Svelte (this blog is built in Svelte, for instance) I was naturally interested in how they compared.
<Dropcap word="Recently">I've had a chance to get to know Vue a bit. Since my frontend framework of choice has previously been Svelte (this blog is built in Svelte, for instance) I was naturally interested in how they compared.</Dropcap>
This is necessarily going to focus on a lot of small differences, because Vue and Svelte are really much more similar than they are different. Even among frontend frameworks, they share a lot of the same basic ideas and high-level concepts, which means that we get to dive right into the nitpicky details and have fun debating `bind:attr={value}` versus `:attr="value"`. In the meantime, a lot of the building blocks are basically the same or at least have equivalents, such as:
Of course, this is only possible because Vue and Svelte are really much more similar than they are different. Even among frontend frameworks, they share a lot of the same basic ideas and high-level concepts, which means that we get to dive right into the nitpicky details and have fun debating `bind:attr={value}` versus `:attr="value"`. In the meantime, a lot of the building blocks are basically the same or at least have equivalents, such as:
* Single-file components with separate sections for markup, style, and logic
* Automatically reactive data bindings
* Two-way data binding (a point of almost religious contention in certain circles)
* An "HTML-first" mindset, as compared to the "Javascript-first" mindset found in React and its ilk. The best way I can describe this is by saying that in Vue and Svelte, the template wraps the logic, whereas in React, the logic wraps the template.
* An "HTML-first" mindset, as compared to the "Javascript-first" mindset found in React and its ilk. The best way I can describe this is by saying that in Vue and Svelte, the template<Sidenote>Or single-file component, anyway.</Sidenote> embeds the logic, whereas in React, the logic embeds the template.
I should also note that everything I say about Vue applies to the Options API unless otherwise noted, because that's all I've used. I've only seen examples of the Composition API (which looks even more like Svelte, to my eyes), I've never used it myself.
With that said, there are plenty of differences between the two, and naturally I find myself in possession of immediate and vehement Preferences.<Sidenote>Completely arbitrary, of course, so feel free to disagree!</Sidenote> Starting with:
With that said, there are plenty of differences between the two, and naturally I find myself in possession of immediate and vehement Preferences.<Sidenote>I should also clarify that practically everything in this post is just that: a preference. While I obviously plan to explain my preferences and think it would be reasonable for other people to do the same, it's undeniably true that preferences can vary, and in a lot of cases are basically arbitrary. So if you find yourself disagreeing with all or most of what I say, consider it an opportunity to peer into the mindset of The Other Side.</Sidenote> Starting with:
## Template Syntax
@ -52,11 +53,11 @@ While Svelte takes the more common approach of wrapping bits of markup in its ow
</div>
```
While Vue's approach may be a tad unorthodox, I find that I actually prefer it in practice. It has the killer feature that, by embedding itself inside the existing HTML, it doesn't mess with my indentation - which is something that has always bugged me about Mustache, Liquid, Jinja, etc.<Sidenote>Maybe it's silly of me to spend time worrying about something so trivial, but hey, this whole post is one big bikeshed anyway.</Sidenote>
While Vue's approach may be a tad unorthodox, I find that I actually prefer it in practice. It has the killer feature that, by embedding itself inside the existing HTML, it doesn't mess with my indentation - which is something that has always bugged me about Mustache, Liquid, Jinja, etc.<Sidenote>Maybe it's silly of me to spend time worrying<Sidenote>Nested<Sidenote>Doubly-nested sidenote!</Sidenote> sidenote!</Sidenote> about something so trivial,<Sidenote>Second nested sidenote.</Sidenote> but hey, this whole post is one big bikeshed anyway.</Sidenote>
Additionally (and Vue cites this as the primary advantage of its style, I think) the fact that Vue's custom attributes are all syntactically valid HTML means that you can actually embed Vue templates directly into your page source. Then, when you mount your app to an element containing Vue code, it will automatically figure out what to do with it.<Sidenote>AlpineJS also works this way, but this is the *only* way that it works - it doesn't have an equivalent for Vue's full-fat "app mode" as it were.</Sidenote> This strikes me as a fantastic way to ease the transition between "oh I just need a tiny bit of interactivity on this page, so I'll just sprinkle in some inline components" and "whoops it got kind of complex, guess I have to factor this out into its own app with a build step and all now."
Detractors of this approach might point out that it's harder to spot things like `v-if` and `v-for` when they're hanging out inside of existing HTML tags, but that seems like a problem that's easily solved with a bit of syntax highlighting. However I do have to admit that it's a reversal of the typical order in which you read code: normally you see the control-flow constructs _first_, and only _after_ you've processed those do you start to worry about whatever they're controlling. So you end up with a sort of [garden-path-like](https://xkcd.com/2793/) problem where you have to mentally double back and re-read things in a different light. I still don't think it's a huge issue, though, because in every case I'm come across the control flow bits (so `v-if`, `v-for`, and `v-show`) are specified _immediately_ after the opening tag. So you don't really have to double back by an appreciable amount, and it doesn't take too long to get used to it.
Detractors of this approach might point out that it's harder to spot things like `v-if` and `v-for` when they're hanging out inside of existing HTML tags, but that seems like a problem that's easily solved with a bit of syntax highlighting.<Sidenote>I'm being unfair here. It's more than just a lack of syntax highlighting, it's a reversal of the typical order in which people are used to reading code, where the control flow is indicated before whatever it's controlling. So you end up with a sort of [garden-path-like](https://xkcd.com/2793/) problem where you have to mentally double back and re-read things in a different light. I still don't think it's a huge issue, though, because in every case I'm come across the control flow bits (so `v-if`, `v-for`, and `v-show`) are specified _immediately_ after the opening tag. So you don't really have to double back by an appreciable amount, and it doesn't take too long to get used to it.</Sidenote>
Continuing the exploration of template syntax, Vue has some cute shorthands for its most commonly-used directives, including `:` for `v-bind` and `@` for `v-on`. Svelte doesn't really have an equivalent for this, although it does allow you to shorten `attr={attr}` to `{attr}`, which can be convenient. Which might as well bring us to:
@ -66,11 +67,11 @@ I give this one to Svelte overall, although Vue has a few nice conveniences goin
Something that threw me a tiny bit when I first dug into Vue was that you need to use `v-bind` on any attribute that you want to have a dynamic value. So for instance, if you have a data property called `isDisabled` on your button component, you would do `<button v-bind:disabled="isDisabled">` (or the shorter `<button :disabled="isDisabled">`).
The reason this threw me is that Svelte makes the very intuitive decision that since we already have syntax for interpolating variables into the text contents of our markup, we can just reuse the same syntax for attributes. So the above would become `<button disabled={isDisabled}>`, which I find a lot more straightforward.<Sidenote>If your interpolation consists of a single expression you can even leave off the quote marks (as I did here), which is pleasant since you already have `{}` to act as visual delimiters.</Sidenote> I also find it simpler in cases where you want to compose a dynamic value out of some fixed and some variable parts, e.g. `<button title="Save {itemsCount} items">` vs. `<button :title="&#96;Save ${itemsCount} items&#96;">`.
The reason this threw me is that Svelte makes the very intuitive decision that since we already have syntax for interpolating variables into the text contents of our markup, we can just reuse the same syntax for attributes. So the above would become `<button disabled={isDisabled}>`, which I find a lot more straightforward.<Sidenote>If your interpolation consists of a single expression you can even leave off the quote marks (as I did here), which is pleasant since you already have `{}` to act as visual delimiters.</Sidenote> I also find it simpler in cases where you want to compose a dynamic value out of some fixed and some variable parts, e.g. `<button title="Save {{itemsCount}} items">` vs. `<button :title="&#96;Save ${itemsCount} items&#96;">`.
Two-way bindings in Svelte are similarly straightforward, for example: `<input type="checkbox" bind:checked={isChecked}>` In Vue this would be `<input type="checkbox" v-model="isChecked">`, which when you first see it doesn't exactly scream that the value of `isChecked` is going to apply to the `checked` property of the checkbox. On the other hand, this does give Vue the flexibility of doing special things for e.g. the values of `<select>` elements: `<select v-model="selectedOption">` is doing quite a bit of work, since it has to interact with not only the `<select>` but the child `<option>`s as well. Svelte just throws in the towel here and tells you to do `<select bind:value={selectedOption}>`, which looks great until you realize that `value` isn't technically a valid attribute for a `<select>`. So Svelte's vaunted principle of "using the platform" does get a _little_ bent out of shape here.
Oh, and two-way bindings in Vue get _really_ hairy if it's another Vue component whose attribute you want to bind, rather than a built-in form input. Vue enforces that props be immutable from the inside, i.e. a component isn't supposed to mutate its own props. So from the parent component it doesn't look too bad:
Oh, and two-way bindings in Vue get _really_ hairy if it's another Vue component whose attribute you want to bind, rather than a builtin form input. Vue enforces that props be immutable from the inside, i.e. a component isn't supposed to mutate its own props. So from the parent component it doesn't look too bad:
```markup
<ChildComponent v-model="childValue" />`
```
@ -87,9 +88,9 @@ export default {
}
```
In Svelte, you just `bind:` on a prop of a child component, and then if the child updates the prop it will be reflected in the parent as well. I don't think there's any denying that's a lot simpler.<Sidenote>I think this is where the "two-way data binding" holy wars start to get involved, but I actually really like the way Svelte does things here. I think most of the furor about two-way data binding refers to bindings that are _implicitly_ two-way, i.e. anyone with a reference to some value can mutate it in ways the original owner didn't expect or intend it to. (KnockoutJS observables work this way, I think?) In Svelte's case, though, this is only possible if you explicitly pass the state with `bind:`, which signifies that you _do_ want this state to be mutated by the child and that you have made provisions therefor. My understanding is that in React you'd just be emitting an event from the child component and handling that event up the tree somewhere, so in practice it's basically identical. That said, I haven't used React so perhaps I'm not giving the React Way™ a fair shake here.</Sidenote>
In Svelte, you just `bind:` on a prop of a child component, and then if the child updates the prop it will be reflected in the parent as well. I don't think there's any denying that's a lot simpler.<Sidenote>I think this is where the "two-way data binding" holy wars start to get involved, but I actually really like the way Svelte does things here. I think most of the furor about two-way data binding refers to bindings that are _implicitly_ two-way, i.e. the child can mutate state that the parent didn't expect or intend it to. In Svelte's case, though, this is only possible if you explicitly pass the state with `bind:`, which signifies that you _do_ want this state to be mutated by the child and that you have made provisions therefor. </Sidenote>
Vue does have some lovely convenience features for common cases, though. One of my favorites is binding an object to the `class` of an HTML element, for example: `<button :class="{btn: true, primary: false}">` Which doesn't look too useful on its own, but move that object into a data property and you can now toggle classes on the element extremely easily by just setting properties on the object. The closest Svelte comes is `<button class:btn={isBtn} class:primary={isPrimary}>`, which is a lot more verbose. Vue also lets you bind an array to `class` and the elements of the array will be treated as individual class names, which can be convenient in some cases if you have a big list of classes and you're toggling them all as a set. <Sidenote>Since I'm a fan of TailwindCSS, this tends to come up for me with some regularity.</Sidenote>
Vue does have some lovely convenience features for common cases, though. One of my favorites is binding an object to the `class` of an HTML element, for example: `<button :class="{btn: true, primary: false}">` Which doesn't look too useful on its own, but move that object into a data property and you can now toggle classes on the element extremely easily by just setting properties on the object. The closest Svelte comes is `<button class:btn={isBtn} class:primary={isPrimary}>`, which is a lot more verbose. Vue also lets you bind an array to `class` and the elements of the array will be treated as individual class names, which can be convenient in some cases if you have a big list of classes and you're toggling them all as a set.
The other area where I vastly prefer Vue's approach over Svelte's is in event handlers. Svelte requires that every event handler be a function, either named or inline, so with simple handlers you end up with a lot of `<button on:click={() => counter += 1}` situations. Vue takes the much more reasonable approach of letting you specify a plain statement as your event handler, e.g. `<button @click="counter += 1">`. For whatever reason this has always particularly annoyed me about Svelte, so Vue's take is very refreshing.
@ -106,7 +107,7 @@ You really only need to access the event when you're doing something more exotic
In Vue, reactive values (by which I mean "values that can automatically trigger a DOM update when they change") are either passed in as `props`, or declared in `data`. Or derived from either of those sources in `computed`. Then you reference them, either directly in your template or as properties of `this` in your logic. Which works fine, more or less, although you can run into problems if you're doing something fancy with nested objects or functions that get their own `this` scope.<Sidenote>It's worth noting that the Composition API avoids this, at the cost of having to call `ref()` on everything and reference `reactiveVar.value` rather than `reactiveVar` by itself.</Sidenote> The split between how you access something from the template and how you access it from logic was a touch surprising to me at first, though.
In Svelte, variables are just variables, you reference them the same way from everywhere, and if they need to be reactive it (mostly) just happens automagically.<Sidenote>And of course, after I first wrote this but just before I was finally ready to publish, Svelte went ahead and [changed this on me](https://svelte.dev/blog/runes). I'll leave my comments here as I originally wrote them, just keep in mind that if these changes stick then Svelte becomes even _more_ similar to Vue's composition API.</Sidenote> Svelte has a lot more freedom here because it's a compiler, rather than a library, so it can easily insert calls to its special `$$invalidate()` function after any update to a value that needs to be reactive.
In Svelte, variables are just variables, you reference them the same way from everywhere, and if they need to be reactive it (mostly) just happens automagically. Svelte has a lot more freedom here because it's a compiler, rather than a library, so it can easily insert calls to its special `$$invalidate()` function after any update to a value that needs to be reactive.
Both frameworks allow you to either derive reactive values from other values, or just execute arbitrary code in response to data updates. In Vue these are two different concepts - derived reactive values are declared in `computed`, and reactive statements via the `watch` option. In Svelte they're just the same thing: Prefix any statement with `$:` (which is actually valid JS, as it turns out) and it will automatically be re-run any time one of the reactive values that it references gets updated. So both of the following:
```js
@ -123,7 +124,7 @@ $: console.log(firstname, lastname);
I go back and forth on this one, but I _think_ I have a slight preference for Svelte (at least, at the moment.) The major difference is that Vue<Sidenote>If you're using the Options API, at least.</Sidenote> enforces a lot more structure than Svelte: Data is in `props`/`data`/`computed`, logic is in `methods`, reactive stuff is in `watch`, etc. Svelte, by contrast, just lets you do basically whatever you want. It does require that you have only one `<script>` tag, so all your logic ends up being co-located, but that's pretty much it. Everything else is just a convention, like declaring props at the top of your script.
The advantage of Vue's approach is that it can make it easier to find things when you're jumping from template to logic: you see `someFunction(whatever)`, you know it's going to be under `methods`. With Svelte, `someFunction` could be defined anywhere in the script section.<Sidenote>Code structure is actually one area that I think might be improved by the recently-announced Svelte 5 changes: Because you can now declare reactive state anywhere, rather than just at the top level of your script, you can take all the discrete bits of functionality within a single component and bundle each one up in its own function, or even factor them out into different files entirely. I can imagine this being helpful, but I haven't played with it yet so I don't know for sure how it will shake out.</Sidenote>
The advantage of Vue's approach is that it can make it easier to find things when you're jumping from template to logic: you see `someFunction(whatever)`, you know it's going to be under `methods`. With Svelte, `someFunction` could be defined anywhere in the script section.
On the other hand, this actually becomes a downside once your component gets a little bit complex. Separation of concerns is nice and all, but sometimes it just doesn't work very well to split a given component, and it ends up doing several unrelated or at least clearly distinct things. In Vue-land, the relevant bits of state, logic, etc. are all going to be scattered across `data`/`methods`/etc, meaning you can't really see "all the stuff that pertains to this one bit of functionality" in one place. It's also very clunky to split the logic for a single component across multiple JS files, which you might want to do as another way of managing the complexity of a large component. If you were to try, you'd end up with a big "skeleton" in your main component file, e.g.
@ -145,11 +146,11 @@ export default {
which doesn't seem very pleasant.
As a matter of fact, this was one of the primary [motivations](https://web.archive.org/web/20201109010309/https://composition-api.vuejs.org/#logic-reuse-code-organization)<Sidenote>Archive link, since that url now redirects to the [current Composition API FAQ](https://vuejs.org/guide/extras/composition-api-faq.html).</Sidenote> for the introduction of the Composition API in the first place. Unfortunately it also includes the downside that you have to call `ref()` on all your reactive values, and reference them by their `.value` property rather than just using the main variable. It's funny that this bothers me as much as it does, given that `this.someData` is hardly any more concise than `someData.value`, but there's no accounting for taste, I guess. Using `this` just feels more natural to me, although what feels most natural is Svelte's approach where you don't have to adjust how you reference reactive values at all.
As a matter of fact, this was one of the primary [motivations](https://web.archive.org/web/20201109010309/https://composition-api.vuejs.org/#logic-reuse-code-organization) for the introduction of the Composition API in the first place.<Sidenote>Archive link, since that url now redirects to the [current Composition API FAQ](https://vuejs.org/guide/extras/composition-api-faq.html).</Sidenote> Unfortunately it also includes the downside that you have to call `ref()` on all your reactive values, and reference them by their `.value` property rather than just using the main variable. It's funny that this bothers me as much as it does, given that `this.someData` is hardly any more concise than `someData.value`, but there's no accounting for taste, I guess. Using `this` just feels more natural to me, although what feels most natural is Svelte's approach where you don't have to adjust how you reference reactive values at all.
Also, as long as we're harping on minor annoyances: For some reason I cannot for the life of me remember to put commas after all my function definitions in `computed`, `methods` etc. in my Vue components. It's such a tiny thing, but it's repeatedly bitten me because my workflow involves Vue automatically rebuilding my app every time I save the file, and I'm not always watching the console output because my screen real estate is in use elsewhere.<Sidenote>E.g. text editor on one screen with two columns of text, web page on one half of the other screen and dev tools on the other half. Maybe I need a third monitor?</Sidenote> So I end up forgetting a comma, the rebuild fails but I don't notice, and then I spend five minutes trying to figure out why my change isn't taking effect before I think to check for syntax errors.
It would be remiss of me, however, not to point out that one thing the Vue Options API enables<Sidenote>Kind of its initial _raison d'être_, from what I understand.</Sidenote> which is more or less impossible<Sidenote>I mean, you could do it, but you'd have to ship the entire Svelte compiler with your page.</Sidenote> with Svelte is at-runtime or "inline" components, where you just stick a blob of JS onto your page that defines a Vue component and where it should go, and Vue does the rest on page load. Svelte can't do this because it's a compiler, so naturally it has to compile your components into a usable form. This has many advantages, but sometimes you don't want to or even _can't_ add a build step, and in those cases Vue can really shine.
It would be remiss of me, however, not to point out that one thing the Vue Options API enables<Sidenote>Kind of its initial _raison d'être_, from what I understand.</Sidenote> which is completely impossible with Svelte is at-runtime or "inline" components, where you just stick a blob of JS onto your page that defines a Vue component and where it should go, and Vue does the rest on page load. Svelte can't do this because it's a compiler, so naturally it has to compile your components into a usable form. This has many advantages, but sometimes you don't want to or even _can't_ add a build step, and in those cases Vue can really shine.
## Miscellany
@ -157,20 +158,8 @@ It would be remiss of me, however, not to point out that one thing the Vue Optio
Performance isn't really a major concern for me when it comes to JS frameworks, since I don't tend to build the kind of extremely-complex apps where the overhead of the framework starts to make a difference. For what it's worth, though, the [Big Benchmark List](https://krausest.github.io/js-framework-benchmark/current.html) has Vue slightly ahead of Svelte when it comes to speed.<Sidenote>Although [recent rumors](https://twitter.com/Rich_Harris/status/1688581184018583558) put the next major version of Svelte _very_ close to that of un-framework'd vanilla JS, so this might change in the future.</Sidenote> I don't know how representative this benchmark is of a real-world workload.
As far as bundle size goes, it's highly dependent on how many components you're shipping - since Svelte compiles everything down to standalone JS and there's no shared framework, the minimum functional bundle can be quite small indeed. The flipside is that it grows faster with each component than Vue, again because there's no shared framework to rely on. So a Svelte app with 10 components will probably be a lot smaller than the equivalent Vue app, but scale that up to 1000 components and the advantage will most likely have flipped. The Svelte people say that this problem doesn't tend to crop up a lot in practice, but I have yet to see real-world examples for the bundle size of a non-trivial<Sidenote>Probably because no one wants to bother implementing the exact same app in two different frameworks just to test a theory.</Sidenote> app implemented in Vue vs. Svelte.
As far as bundle size goes, it's highly dependent on how many components you're shipping - since Svelte compiles everything down to standalone JS and there's no shared framework, the minimum functional bundle can be quite small indeed. The flipside is that it grows faster with each component than Vue, again because there's no shared framework to rely on. So a Svelte app with 10 components will probably be a lot smaller than the equivalent Vue app, but scale that up to 1000 components and the advantage will most likely have flipped.
### Ecosystem
Vue has been around longer than Svelte, so it definitely has the advantage here. That said, Svelte has been growing pretty rapidly in recent years and there is a pretty decent ecosystem these days. This blog, for instance, uses [SvelteKit](https://kit.svelte.dev) and [mdsvex](https://mdsvex.pngwn.io/). But there are definitely gaps, e.g. I wasn't able to find an RSS feed generator when I went looking.<Sidenote>Arguably this is a lack in the SvelteKit ecosystem rather than the Svelte ecosystem, but I think it's fair to lump it together. SvelteKit is dependent on Svelte, so naturally it inherits all of Svelte's immaturity issues plus more of its own.</Sidenote> If I'd been using Vue/Nuxt it would have been available as a [first-party integration](https://content.nuxtjs.org/v1/community/integrations). All in all I'd say if a robust ecosystem is important to you then Vue is probably the better choice at this point.
### Stability
Not in terms of "will it crash while you're using it," but in terms of "will code that you write today still be usable in five years." This is always a bit of a big ask in the JS world, because everyone is always pivoting to chase the new shiny. As I write this now (and as I referenced above), Svelte has just announced a [change](https://svelte.dev/blog/runes) to how reactivity is done. The new style is opt-in for the moment, but that's never completely reassuring--there are plenty of examples of opt-in features that became required eventually. Vue had a similar moment with their 2-to-3 switch,<Sidenote>Just like Python, hmm. What is it about the 2-to-3 transition? Maybe we should call it Third System Effect?</Sidenote> but to be fair they have so far stuck to their promise to keep the Options API a first-class citizen.
I think that means I have to give Vue the edge on this one, because while both frameworks now have an "old style" vs. a "new style" Vue at least has proven their willingness to continue supporting the old style over the last few years.
## What's Next
I don't think we've reached the "end-game" when it comes to UI paradigms, either on the web or more generally. I _do_ think that eventually, _probably_ within my lifetime, we will see a stable and long-lasting consensus emerge, and the frenetic pace of "framework churn" in the frontend world will slow down somewhat. What exact form this will take is very much up in the air, of course, but I have a sneaking suspicion that WebAssembly will play a key part, if it can ever get support for directly communicating with the DOM (i.e. without needing to pass through the JS layer). _If_ and when that happens, it will unlock a huge new wave of frontend frameworks that don't have to involve on Javascript at all, and won't that be interesting?
But for now I'll stick with Svelte, although I think Vue is pretty good too. Just don't make me use React, please.

View File

@ -1,72 +0,0 @@
---
title: Why the Internet is Terrible
date: 2024-11-16
---
<script>import Sidenote from '$lib/Sidenote.svelte';</script>
I just got done deleting ~30 bogus user accounts from my [personal Gitea insteance](https://git.jfmonty2.com). They all had reasonable-ish-sounding names, one empty repository, and profiles that looked like [this](/bogus_user_profile.jpg). Note the exceedingly spammy link to a real site (still up as of writing) and the ad-copy bio.
Obviously this is just SEO spam. My Gitea instance got found by some automated system that noticed it had open registration,<Sidenote>The more fool I.</Sidenote> so it registered a bunch of bogus user accounts, added links to whatever sites it was trying to pump, added related text in the bio, and then sat back and waited for search engines to pick up on these new backlinks and improve the reputation of said sites, at least until the search engines catch on and downgrade the reputation of my Gitea instance.
This particular problem was easy enough to deal with: Just remove the offending users, and all their works, and all their empty promises. But it got me thinking about the general online dynamic that _everybody online is out to get you._
## The Internet is terrible, and everyone knows it
This isn't a news, of course. People go around [saying things like](https://www.stilldrinking.org/programming-sucks):
>Here are the secret rules of the internet: five minutes after you open a web browser for the first time, a kid in Russia has your social security number. Did you sign up for something? A computer at the NSA now automatically tracks your physical location for the rest of your life. Sent an email? Your email address just went up on a billboard in Nigeria.
and everyone just smiles and nods, because that's what they've experienced. I've encountered people who are highly reluctant to pay for anything online via credit card--they would much rather use the phone and give their credit card number to a real person who is presumably capable of stealing it, should they so desire--because the general terribleness of the internet has become so ingrained into their psyche that this feels like the better option, and you know what? I can't even blame them.
Anyone who works on web applications for a living (or a hobby) is _especially_ aware of this, because odds are that they've been burned by it already or at least are familiar with any number of existing examples. The very existence of sites like [Have I Been Pwned](https://haveibeenpwned.com) is predicated on the inescapable terribleness the permeates every nook and cranny of the Internet.
Of course, people trying to take advantage of the careless and clueless isn't a new phenomenon. The term "snake oil salesman" dates back to the 18th century and refers people who would go around selling _literal snake oil_<Sidenote>Probably not harvested from actual snakes, but they sure told people it was.</Sidenote> as a miracle cure, hair restorative, and whatever else. I'm fairly confident that as long as money has existed, there have been unscrupulous people making a living off of tricking it out of other people.
But something about the Internet makes it much more _present_, more in your face, than old-timey snake-oil salesmen. I've seen no hard numbers on this, and I don't know how you would even begin to estimate it, but but I would guess that the incidence rate of this sort of thing is vastly higher online than it's ever been in meatspace.
So what is it about the Internet that makes deception so much more prevalent? Ultimately, I think it boils down to three things: availability, automation, and anonymity. The Three A's of Awfulness, if you will.
## You're in the bad part of town
Have you ever wondered why physical locks are so easy to pick? It takes some know-how, but from what I can tell, most commonly-sold locks [can be bypassed within a minute](https://www.youtube.com/@lockpickinglawyer/videos). I'm just going to say it right here, and I don't think this is a controversial take: For a web application that would be an unacceptably low level of security. If it took an attacker less than a minute to, say, gain administrative access to a web application, I'd consider it just this side of "completely unsecured".
But! Meatspace is not the internet. The constraints are different. Over the lifetime of a given lock, the number of people who will ever be in a position to attempt to pick it is usually quite low, compared to the number of people who exist in the world. Of course, the circumstances matter a lot too: A lock in a big city is within striking distance of many more potential lock-pickers than the lock on a farm out in corn country somewhere, which is part of why people in cities are frequently much more concerned about keeping their doors locked than people in rural areas. And within a single city, people who live in the bad parts of town tend to worry more than people who don't, etc.
But on the Internet, everyone is in the bad part of town _all the time!_ That's right, there's nothing separating your podunk website from every aspiring journeyman member of Evil Inc. except a few keystrokes and a click or two. It doesn't take Sir Scams-A-Lot any longer to send an email to you than to your less-fortunate neighbors in the housing projects, and so on.<Sidenote>This is also my beef with [this xkcd comic](https://xkcd.com/1958/). The real danger isn't that people will do things to the _physical_ environment to mess with self-driving cars (like repainting lines on the road), but that they'll do something remotely from the other side of the world, and no one will know until their car drives off a bridge or whatever. And sure, most people aren't murderers. But even if there are only a few people in the world who are sufficiently unhinged as to set up fatal traffic accidents between total strangers, _if your self-driving car is Internet-connected then those people might have the opportunity._</Sidenote>
In other words, the size of the "target pool" for someone who has a) an Internet connection and b) no conscience is _literally everyone else with an internet connection._ At last count, that number was in the billions and rising. This alone would make "online scurrilousness" a far more attractive career choice than "cat thief", but don't worry, it gets even worse!
## Their strength is as the strength of ten
You might be tempted to think something like "Sure, being online gives the seamier sort of people immediate access to basically everyone in the world. But that shouldn't really change the overall incidence of these sorts of things, because after all, there are only so many hours in the day. A hard-working evildoer can still only affect a certain number of people per unit time, right? _right?_" But alas, even this limitation pales before the awesome might of modern communications infrastructure.
In meatspace, you can only be in one place at a time. If you're over on Maple Street burglarizing Mr. and Mrs. Holyoke's home, you can't also be selling fake stock certificates on Jefferson Ave, or running a crooked blackjack game in the abandoned warehouse off Stilton. But we aren't in meatspace any more, ~~Toto~~. We're _online_, where everything is done with computers. You know what computers really love doing? _Endlessly repeating the same boring repetitive task forever._ The Internet is a medium uniquely suited to automated consumption. In fact, approximately 30% of all internet traffic comes from automated systems, [according to Clouflare](https://radar.cloudflare.com/traffic#bot-vs-human), and they should know.
So what does a clever-but-unscrupulous technologist do? That's right, he goes looking for vulnerabilities in widely-used platforms like Wordpress, finds one, then sets up an automated system to identify and exploit vulnerable Wordpress installs. Or he uses an open-source large language model like [Llama](https://www.llama.com/) to send phishing emails to every email address he can get his hands on, and maybe even correspond with susceptible people across multiple messages,<Sidenote>This is something I'm sure we'll see more and more of as time goes on. I'm sure it's already happening, and it's only going to get worse.</Sidenote> or just tricks people into clicking on a link to a fake Log In With Google page where he snarfs up their usernames and passwords, or _whatever_. There are a million and one ways an unethical person can take advantage of others _without ever having to personally interact with them._ This acts as a force-multiplier for evil people, and I think it's a major contributor to the overwhelming frequency with which you encounter this sort of thing online.<Sidenote>Astute readers may realize that while you can't automate meatspace in exactly the same way as you can automate computers, you can still do the next-best thing: _get other people to do it for you._ This is the fundamental insight of the Mafia don, and organized crime more generally. Thing is, though, all of these subsidiary evildoers have to be just as willing to break the law as the kingpin string-puller, so it doesn't quite act as a force-multiplier for evil in the same way.</Sidenote>
Interestingly, the automate-ability of anything that happens over the Internet seems to have leaked back into the phone system as well. I don't think anybody would disagree that scam phone calls are far more common than they used to be.<Sidenote>Unless "Dealer Services" has developed a truly pathological level of concern for the vehicle warranty I didn't even know I had.</Sidenote> I suspect, although I don't have any hard evidence to back it up, that this is largely due to the ease with which you can automate phone calls these days via internet-to-phone bridge services like [Twilio](https://twilio.com). The hit rate for this sort of thing has to be incredibly low--especially as people start to catch on and stop answering calls from numbers they don't know--so it only makes sense for the scammer if it costs them _virtually nothing_ to attempt.
One might ask why this wasn't the case before the Internet, since auto-dialing phone systems certainly predate the widespread use of the Internet,<Sidenote> The [Telephone Consumer Protection Act](https://en.wikipedia.org/wiki/Telephone_Consumer_Protection_Act_of_1991) attempted to regulate them as far back as 1991!</Sidenote> so why didn't this happen then? I suspect that again, this comes down to ease of automation. In the 90s, you needed expensive dedicated equipment to set up a robocalling operation, but today you can just do it from your laptop.
## The scammer with no name
There's a third contrast with meatspace that makes life easier for people whose moral compass has been replaced by, say, an avocado: _Nobody knows who you are online._ In real life, being physically present at the scene of a crime exposes you to some degree of risk. There might be witnesses or security cameras, your coat might snag on a door and leave some fibers behind for the forensic team to examine, you might drop some sweat somewhere and leave DNA lying around, and of course there are always good ol' fingerprints.<Sidenote>Once again, the Mafia model demonstrates how you might insulate yourself from some of these risks, but again, it's not quite as complete because _somebody_ has to be there, and that somebody might talk. And yes, the Mafia [took steps](https://en.wikipedia.org/wiki/Omert%C3%A0) to remedy that problem as well, but that's why Witness Protection was invented.</Sidenote>
All of this is much less of an issue online. In fact, one of the loudest and most attention-seeking hacking groups literally just called themselves [Anonymous](https://en.wikipedia.org/wiki/Anonymous_(hacker_group)). Of course, [then a bunch of them got arrested](https://www.bbc.com/news/world-latin-america-17195893), so maybe they weren't _quite_ as anonymous as they seemed to think they were. Still, I think it's safe to say that it's a lot easier to stay anonymous when you're committing crimes online vs. in person. Or from another angle, it takes (on average) significantly more law-enforcement effort to de-anonymize a criminal online than in person.<Sidenote>I can't seem to find it any more, but I'm pretty sure I remember reading an article a while back that talked about how the NSA/FBI/etc. managed to identify people like [Silk Road](https://en.wikipedia.org/wiki/Silk_Road_(marketplace)) higher-ups. From what I recall, it was pretty resource-intensive and not really realistic except for high-priority targets.</Sidenote>
I'm pointing out the downsides here, of course, but it's worth noting that online anonymity is a coin with two faces. It's fundamental to the question of privacy, especially from governments who would love nothing better than to know every sordid detail of their citizens' lives forever.<Sidenote>Don't believe me? Just look at how hard any number of major governments have been trying to effectively outlaw things like end-to-end encrypted chat apps. Here's the [UK](https://www.wired.com/story/britain-admits-defeat-online-safety-bill-encryption/), [US](https://www.eff.org/deeplinks/2020/06/senates-new-anti-encryption-bill-even-worse-earn-it-and-thats-saying-something), [Australia](https://www.schneier.com/blog/archives/2024/09/australia-threatens-to-force-companies-to-break-encryption.html), etc. They don't give a crap about "safety" or "exploitative content". This is about surveillance. </Sidenote> In general, anything that improves privacy (such as end-to-end encryption, VPNs, proxies, etc.) also makes anonymity easier for people whose motives are less laudable than "I don't think the government should know everything bout me."
## The economics of evil
In the end, you can think of this all as a question of economics.<Sidenote>Seems like you can think of anything as a question of economics, if you try hard enough. [Even theology](https://en.wikipedia.org/wiki/Economy_of_Salvation).</Sidenote> The Internet is rife with scams, thievery, and general [scum and villainy](https://www.youtube.com/watch?v=Xcb4_QwP6fE) because it brings down the cost of doing such things to the point that it becomes worth it. There's no need to spend time or money moving from place to place, because you can do it all from the comfort of your own home. Instead of spending time on each individual operation you can put in the effort to automate it up-front and then sit back and reap the benefits (or keep finding more things to automate). The risk of doing all of this (which is a form of cost) is significantly lower than it would be to do something equivalent in real life. And all of this you get for the low, low price of your immortal soul! What's not to like?
## Will it ever change?
The Internet has often reminded me, alternately, of a) the Industrial Revolution and b) the Wild West. It reminds me of the Industrial Revolution because there are great examples of unscrupulous people taking advantage of a new set of economic realities to make tons of money at the expense of poor everyday folk who are just trying to live their lives. And not just straight-up criminals like we've been discussing, but also exploitative businesses and corporations (adtech, anybody?) that hearken back to the days of e.g. factory owners profiting from the slow destruction of their workers' lives. But the Internet also calls to mind the Wild West of the mid-to-late 1800s. Like the Wild West, it's a huge new swathe of unexplored territory rich with opportunity, if a little uncivilized.
But eventually, both the Industrial Revolution and the Wild West settled down and got a little more civilized. Eventually people developed things like labor unions and OSHA regulations,<Sidenote>Which I never thought I'd be holding up as a _good_ thing, because in my personal experience they've mostly been a source of frustration. But something tells me that if I were a worker in a 19th-century textile factory, I would have been very glad for some basic safety requirements.</Sidenote> and the world of heavy industry got a little more equitable. And eventually, the Wild West became civilized enough that you couldn't just walk into a saloon and shoot someone just because you felt like it.<Sidenote>Please note, I have no idea if this was ever _really_ possible, I'm basing it mostly on spaghetti Westerns and the like.</Sidenote>
Will the same thing happen to the Internet? I don't know. It might! Already you can start to see a sort of social "immune system" developing with regard to things like phishing emails and calls. For instance, I know plenty of people who have a policy of never answering their phone at all if the call is from a number they don't recognize.<Sidenote>Consumer Reports [claims](https://www.consumerreports.org/robocalls/mad-about-robocalls/) that this is actually 70% of US adults, which is a staggering number. Heaven help us if the scammers figure out how to reliably spoof numbers from people you know.</Sidenote> Unfortunateloy it's harder to make this work for something like poorly-secured web services, because it isn't easy to tell before you sign up for a service whether it's likely to get breached and leak your personal info in six months.
Ultimately the only workable solutions will have to a) increase the cost of carrying out these attacks, or b) reduce (on average) the reward. In the end it probably won't be _solved_ completely, much like crime isn't _solved_ today. But I'm hopeful that, much like today's Texans don't have to worry much about their stagecoach being waylaid by bandits, we'll see less and less of it as time goes on.

View File

@ -1,27 +0,0 @@
<script>
import '$styles/prose.scss';
</script>
<style>
.content {
max-width: var(--content-width);
margin: 0 auto;
}
</style>
<svelte:head>
<title>About Me | Joe's Blog</title>
</svelte:head>
<div class="prose content">
<h1>About Me</h1>
<p>(Joe's wife wrote this because Joe feels weird writing about himself.)</p>
<p>Joe is a quirky, techy Tolkienite with a beautiful singing voice, an uncanny ability to do mental math, a bony, un-cuddleable frame, and a big mushy heart. He enjoys bike riding, computers, watching TV, reading about computers, playing Breath of the Wild, building computers, talking about something called "programming languages", and spending time with his family (which often involves fixing their computers). He graduated with a Liberal Arts degree from Thomas Aquinas College, the school of his forebears. He often remarks that he has greatly benefitted from the critical thinking skills he acquired at his alma mater in his current line of work.</p>
<p>He has spent, at the current time, about 2 years working on this blog. Most of his posts are about all of the work it took and everything he learned making this blog. Unlike most "bloggers", he has started with many blog posts and no blog, rather than a blog without posts. "Someday", he says, "I will actually get that blog up". I always nod encouragingly.</p>
<p>If you are reading this, then that day has arrived. We hope you enjoy it, and maybe even learn something along the way.</p>
</div>

View File

@ -1,53 +0,0 @@
import { tag, text, serialize } from '$lib/xml.js';
import { postData } from '../_posts/all.js';
export const prerender = true;
export function GET() {
return new Response(renderFeed(), {
headers: {'Content-Type': 'application/atom+xml'}
});
}
function renderFeed() {
const feed = tag('feed', {xmlns: 'http://www.w3.org/2005/Atom'});
feed.addTag('id', {}, [text('https://blog.jfmonty2.com/')])
feed.addTag('title', {}, [text("Joe's Blog")]);
feed.addTag('link', {rel: 'alternate', href: 'https://blog.jfmonty2.com/'});
feed.addTag('link', {rel: 'self', href: 'https://blog.jfmonty2.com/feed/'});
const lastUpdate = iso(postData[0].updated || postData[0].date);
feed.addTag('updated', {}, [text(lastUpdate)]);
const author = feed.addTag('author');
author.addTag('name', {}, [text('Joseph Montanaro')]);
for (const post of postData) {
const url = `https://blog.jfmonty2.com/${post.slug}`
const entry = feed.addTag('entry');
entry.addTag('title', {}, [text(post.title)]);
entry.addTag('link', {rel: 'alternate', href: url});
entry.addTag('id', {}, [text(url)]);
const publishedDate = iso(post.date);
entry.addTag('published', {}, [text(publishedDate)])
const updatedDate = iso(post.updated || post.date);
entry.addTag('updated', {}, [text(updatedDate)]);
entry.addTag('content', {type: 'html'}, [text(renderDescription(post))]);
}
return serialize(feed);
}
function renderDescription(post) {
return `<p>${post.description} <a href="https://blog.jfmonty2.com/${post.slug}">Read more</a></p>`;
}
function iso(datetimeStr) {
return new Date(datetimeStr).toISOString();
}

18
src/routes/index.svelte Normal file
View File

@ -0,0 +1,18 @@
<script context="module">
export async function load({ fetch }) {
const resp = await fetch('/latest.json');
const postMeta = await resp.json();
const post = await import(`./_posts/${postMeta.slug}.svx`);
return {
props: {
post: post.default,
}
}
}
</script>
<script>
export let post;
</script>
<svelte:component this={post} />

View File

@ -0,0 +1,5 @@
import { postData } from './posts.js';
export async function get() {
return {body: postData[0]};
}

View File

@ -1,26 +1,28 @@
import { dev } from '$app/environment';
const posts = import.meta.globEager('./*.svx');
import { dev } from '$app/env';
const posts = import.meta.globEager('./_posts/*.svx');
export let postData = [];
let postData = [];
for (const path in posts) {
// skip draft posts in production mode
if (!dev && posts[path].metadata.draft) {
continue;
}
// slice off the ./ and the .svx
const slug = path.slice(2, -4);
const slug = path.slice(9, -4)
posts[path].metadata.slug = slug;
postData.push(posts[path].metadata);
}
postData.sort((a, b) => {
// sorting in reverse, so we flip the intuitive order
if (a.date > b.date) return -1;
if (a.date < b.date) return 1;
return 0;
});
})
export async function get() {
return {
body: {postData}
};
}
export { postData };

73
src/routes/posts.svelte Normal file
View File

@ -0,0 +1,73 @@
<script>
import { formatDate } from '$lib/datefmt.js';
export let postData;
</script>
<style lang="scss">
#posts {
/*text-align: center;*/
max-width: 24rem;
// margin-top: 1.25rem;
margin-left: auto;
margin-right: auto;
}
.post {
border-bottom: 2px solid #eee;
margin-top: 1rem;
}
/* .post-title {
font-weight: bold;
font-size: 1.2rem;
}*/
.post-date {
color: #808080;
}
.draft-notice {
vertical-align: 0.3rem;
font-size: 0.6rem;
padding: 0 0.3rem;
color: #e00;
background-color: #ffd9d9;
border: 1px solid red;
border-radius: 20%/50%;
margin: 0 0.2rem;
}
.post-link {
text-decoration: none;
}
.post-link:hover {
text-decoration: underline;
}
h3 {
display: inline;
margin: 0;
}
</style>
<svelte:head>
<title>Posts</title>
</svelte:head>
<div id="posts">
<h1 style:text-align="center">All Posts</h1>
{#each postData as post}
<div class="post">
<div class="post-date">{new Date(post.date).toISOString().split('T')[0]}</div>
<div>
<a sveltekit:prefetch class="post-link" href="/{post.slug}">
<h3>{post.title}<h3>
</a>
{#if post.draft}
<span class="draft-notice">Draft</span>
{/if}
</div>
<p>{post.description}</p>
</div>
{/each}
</div>

View File

@ -1,86 +0,0 @@
<script>
import '$styles/prose.scss';
import { formatDate } from '$lib/datefmt.js';
import { postData } from '../_posts/all.js';
</script>
<style lang="scss">
.wrapper {
padding: 0 var(--content-padding);
}
.posts {
max-width: var(--content-width);
margin: 0 auto;
}
hr {
margin: 2.5rem 0;
border-color: #eee;
}
.post-date {
color: var(--content-color-faded);
}
.draft-notice {
vertical-align: middle;
font-size: 0.75rem;
padding: 0 0.3rem;
color: #e00;
background-color: #ffd9d9;
border: 1px solid red;
border-radius: 20% / 50%;
}
.post-link {
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
h2 {
display: flex;
align-items: center;
gap: 0.75rem;
margin-top: 0.5rem;
margin-bottom: 0.75rem;
font-size: 1.5rem;
& a {
color: currentcolor;
}
}
</style>
<svelte:head>
<title>Posts</title>
</svelte:head>
<div class="wrapper">
<div class="posts prose">
<h1 style:text-align="center">All Posts</h1>
{#each postData as post, idx}
<div class="post">
<div class="post-date">{new Date(post.date).toISOString().split('T')[0]}</div>
<h2 class="prose">
<a data-sveltekit-preload-data="hover" class="post-link" href="/{post.slug}">
{post.title}
</a>
{#if post.draft}
<span class="draft-notice">Draft</span>
{/if}
</h2>
<p>{post.description}</p>
</div>
{#if idx < postData.length - 1}
<hr>
{/if}
{/each}
</div>
</div>

View File

@ -1,27 +0,0 @@
@import 'prism-dracula';
@font-face {
font-family: 'Hack';
font-style: normal;
font-weight: 400;
src: url(/Hack-Regular.woff2) format('woff2');
font-display: block;
}
code {
padding: 0.05rem 0.2rem 0.1rem;
background: #eee;
border-radius: 0.2rem;
font-size: 0.75em;
font-family: 'Hack', monospace;
}
pre[class*="language-"] {
line-height: 1.25;
}
pre > code[class*="language-"] {
font-size: 0.75em;
font-family: 'Hack', monospace;
}

View File

@ -1,39 +0,0 @@
@import 'reset';
@font-face {
font-family: 'Tajawal';
font-style: normal;
font-weight: 400;
src: url(/Tajawal-Regular.woff2) format('woff2');
font-display: block;
}
:root {
--content-size: 1.25rem;
--content-size-sm: 1rem;
--content-line-height: 1.4;
--content-width: 52.5rem;
--content-padding: 0.65rem;
--content-color: #1e1e1e;
--content-color-faded: #555;
--primary-color: hsl(202deg 72% 28%);
--primary-color-faded: hsl(202deg 14% 36%);
--accent-color: hsl(0deg, 92%, 29%);
--accent-color-faded: hsl(0deg, 25%, 55%);
@media(max-width: 640px) {
--content-line-height: 1.25;
--content-size: 1.15rem;
--content-size-sm: 0.9rem;
}
}
body {
font-family: 'Tajawal', sans-serif;
font-size: var(--content-size);
line-height: var(--content-line-height);
letter-spacing: -0.005em;
color: var(--content-color);
}

View File

@ -1,51 +0,0 @@
.prose {
h1, h2, h3, h4, h5, h6 {
font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, Arial, sans-serif;
font-weight: 600;
color: #464646;
}
h1 {
margin-top: 0.5em;
font-size: 2em;
font-variant: petite-caps;
}
h2 {
font-size: 1.5em;
}
h3 {
font-size: 1.2em;
}
h4 {
font-size: 1.1em;
}
h1, h2, h3, h4 {
margin-bottom: 0.5em;
}
p, ul, ol {
margin-bottom: 0.8em;
}
ul, ol, blockquote {
padding: 0;
margin-left: 2em;
}
blockquote {
position: relative;
font-style: italic;
}
blockquote::before {
content: '';
position: absolute;
left: -01em;
height: 100%;
border-right: 3px solid var(--accent-color);
}
}

View File

@ -1,22 +0,0 @@
// This reset lifted largely from Josh Comeau's "CSS for JS Devs" course
// Use a more-intuitive box-sizing model.
*, *::before, *::after {
box-sizing: border-box;
}
// Remove default margin
* {
margin: 0;
}
// Allow percentage-based heights in the application
html, body {
min-height: 100%;
}
// Improve media defaults
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

91
static/style.css Normal file
View File

@ -0,0 +1,91 @@
/* ### TYPOGRAPHY ### */
@font-face {
font-family: 'Tajawal';
font-style: normal;
font-weight: 400;
src: url(/Tajawal-Regular.woff2) format('woff2');
font-display: block;
}
@font-face {
font-family: 'Baskerville';
font-style: normal;
font-weight: 400;
src: url(/Baskerville-Regular.woff2) format('woff2');
font-display: block;
}
:root {
--content-size: 1.25rem;
--content-line-height: 1.3;
--content-color: #1e1e1e;
--content-color-faded: #555;
--accent-color: #8c0606;
}
html {
font-family: 'Tajawal', sans-serif;
font-size: var(--content-size);
line-height: var(--content-line-height);
letter-spacing: -0.005em;
color: var(--content-color);
}
body {
margin: 0;
}
h1, h2, h3, h4, h5, h6 {
font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, Arial, sans-serif;;
font-weight: 600;
color: #464646;
}
h1 {
font-variant: petite-caps;
margin-top: 0.75rem;
}
h1, h2 {
margin-bottom: 0.75rem;
}
h3 {
font-size: 1.2rem;
}
h4 {
font-size: 1.1rem;
}
h3, h4 {
margin-bottom: 0.5rem;
}
h5, h6 {
font-size: 1rem;
margin-bottom: 0;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
/*ul, ol {
margin: 0.5rem 0;
}*/
code {
background: #eee;
border-radius: 0.2rem;
font-family: Consolas, monospace;
font-size: 0.8rem;
padding: 0.05rem 0.2rem 0.1rem;
}
pre > code {
font-size: 0.8rem;
}
/* TESTING */

View File

@ -1,32 +1,24 @@
import { resolve } from 'node:path';
import staticAdapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import { mdsvex } from 'mdsvex';
import staticAdapter from '@sveltejs/adapter-static';
import svp from 'svelte-preprocess';
import slug from './src/lib/slug.js';
import { localRemark } from './src/plugins/remark.js';
import { localRehype } from './src/plugins/rehype.js';
/** @type {import('@sveltejs/kit').Config} */
const config = {
extensions: ['.svelte', '.svx'],
preprocess: [
mdsvex({
layout: './src/lib/Post.svelte',
remarkPlugins: [localRemark],
rehypePlugins: [localRehype],
rehypePlugins: [slug],
}),
vitePreprocess(),
svp.scss(),
],
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
// hydrate the <div id="svelte"> element in src/app.html
adapter: staticAdapter(),
alias: {
'$styles': 'src/styles',
'$projects': 'src/projects',
}
prerender: {
default: true,
},
}
};

281
tmp/crane.svg Normal file
View File

@ -0,0 +1,281 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<!-- Crane image created by macrovector on Freepik: https://www.freepik.com/free-vector/construction-icons-set_1537228.htm#query=crane&position=3&from_view=keyword -->
<svg
width="28.305676mm"
height="28.174238mm"
viewBox="0 0 28.305676 28.174238"
version="1.1"
id="svg1392"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
sodipodi:docname="crane.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1394"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="2.1089995"
inkscape:cx="-5.6899017"
inkscape:cy="78.710306"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1389" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-139.84716,-103.71933)">
<path
d="m 166.79605,124.82179 h 0.18627 v -20.83188 h -0.18627 v 20.83188"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path324" />
<path
d="m 166.25101,125.97184 h 1.27635 v -0.51893 c 0,-0.1323 -0.10724,-0.23919 -0.23918,-0.23919 h -0.79763 c -0.13229,0 -0.23954,0.10689 -0.23954,0.23919 v 0.51893"
style="fill:#e4731a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path326" />
<path
d="m 166.6934,125.21372 h 0.39193 v -0.64981 h -0.39193 v 0.64981"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path328" />
<path
d="m 165.62554,126.85626 c 0,0.69814 0.56585,1.26365 1.26365,1.26365 0.69779,0 1.26365,-0.56551 1.26365,-1.26365 0,-0.6978 -0.56586,-1.26365 -1.26365,-1.26365 -0.6978,0 -1.26365,0.56585 -1.26365,1.26365"
style="fill:#f9a727;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path330" />
<path
d="m 143.76637,124.8673 19.84057,-20.49675 -0.0935,-0.0903 -19.84093,20.49639 0.0938,0.0907"
style="fill:#100f0d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path332" />
<path
d="m 149.82885,128.1506 2.90478,-6.56802 10.82675,-17.25718 1.80869,1.13488 -10.81899,17.2466 -4.61751,5.50863 z m 13.67261,-24.08167 -10.93188,17.42652 -2.97638,6.72711 0.37782,0.23707 4.72899,-5.64338 10.92553,-17.41488 -2.12408,-1.33244"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path334" />
<path
d="m 154.67073,122.68783 -0.0988,0.1577 -1.96638,-1.23367 0.0988,-0.15769 1.96638,1.23366"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path336" />
<path
d="m 154.712,122.74604 -0.18132,0.0413 -0.71791,-3.13443 0.18168,-0.0416 0.71755,3.13478"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path338" />
<path
d="m 155.85042,120.93135 -3.13443,0.71755 -0.0416,-0.18132 3.13478,-0.71791 0.0413,0.18168"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path340" />
<path
d="m 155.87935,120.76167 -0.0991,0.15769 -1.96639,-1.23366 0.0991,-0.1577 1.96639,1.23367"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path342" />
<path
d="m 155.92062,120.81952 -0.18168,0.0416 -0.71755,-3.13443 0.18133,-0.0416 0.7179,3.13443"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path344" />
<path
d="m 157.05903,119.00483 -3.13478,0.71791 -0.0413,-0.18133 3.13443,-0.7179 0.0416,0.18132"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path346" />
<path
d="m 157.08761,118.8355 -0.0988,0.15769 -1.96638,-1.23366 0.0988,-0.15769 1.96638,1.23366"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path348" />
<path
d="m 157.12888,118.89336 -0.18132,0.0416 -0.71791,-3.13478 0.18168,-0.0413 0.71755,3.13443"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path350" />
<path
d="m 158.2673,117.07867 -3.13443,0.7179 -0.0416,-0.18168 3.13478,-0.71755 0.0413,0.18133"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path352" />
<path
d="m 158.29623,116.90898 -0.0991,0.15769 -1.96639,-1.23331 0.0991,-0.15804 1.96639,1.23366"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path354" />
<path
d="m 158.3375,116.96719 -0.18168,0.0416 -0.71755,-3.13479 0.18133,-0.0413 0.7179,3.13443"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path356" />
<path
d="m 159.47592,115.1525 -3.13479,0.7179 -0.0413,-0.18168 3.13443,-0.71755 0.0416,0.18133"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path358" />
<path
d="m 159.50449,114.98282 -0.0988,0.15804 -1.96638,-1.23366 0.0988,-0.15805 1.96638,1.23367"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path360" />
<path
d="m 159.54577,115.04102 -0.18133,0.0416 -0.71791,-3.13478 0.18169,-0.0413 0.71755,3.13443"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path362" />
<path
d="m 160.68418,113.22633 -3.13443,0.71791 -0.0416,-0.18168 3.13478,-0.71755 0.0413,0.18132"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path364" />
<path
d="m 160.71311,113.05665 -0.0991,0.15769 -1.96639,-1.23331 0.0991,-0.15805 1.96639,1.23367"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path366" />
<path
d="m 160.75438,113.11486 -0.18168,0.0416 -0.7179,-3.13478 0.18168,-0.0413 0.7179,3.13443"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path368" />
<path
d="m 161.8928,111.30017 -3.13479,0.7179 -0.0413,-0.18168 3.13443,-0.71755 0.0416,0.18133"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path370" />
<path
d="m 161.92137,111.13048 -0.0991,0.15805 -1.96603,-1.23367 0.0988,-0.15804 1.96638,1.23366"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path372" />
<path
d="m 161.96265,111.18869 -0.18133,0.0416 -0.71791,-3.13479 0.18169,-0.0416 0.71755,3.13478"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path374" />
<path
d="m 163.10106,109.374 -3.13443,0.7179 -0.0416,-0.18168 3.13478,-0.7179 0.0413,0.18168"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path376" />
<path
d="m 163.12999,109.20432 -0.0991,0.15769 -1.96639,-1.23367 0.0991,-0.15769 1.96639,1.23367"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path378" />
<path
d="m 163.17126,109.26252 -0.18168,0.0413 -0.71755,-3.13443 0.18133,-0.0416 0.7179,3.13478"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path380" />
<path
d="m 164.30968,107.44783 -3.13479,0.71755 -0.0413,-0.18132 3.13443,-0.71791 0.0416,0.18168"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path382" />
<path
d="m 164.33825,107.27815 -0.0988,0.15769 -1.96638,-1.23366 0.0988,-0.1577 1.96638,1.23367"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path384" />
<path
d="m 164.37953,107.33636 -0.18133,0.0413 -0.7179,-3.13443 0.18168,-0.0416 0.71755,3.13479"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path386" />
<path
d="m 165.51794,105.52167 -3.13443,0.71755 -0.0416,-0.18133 3.13443,-0.7179 0.0416,0.18168"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path388" />
<path
d="m 153.38944,124.32366 -0.18133,0.0416 -0.64382,-2.81128 0.18133,-0.0416 0.64382,2.81128"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path390" />
<path
d="m 153.34817,124.26581 -0.0988,0.15769 -1.48908,-0.93415 0.0991,-0.1577 1.48873,0.93416"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path392" />
<path
d="m 154.64215,122.85752 -2.81163,0.64382 -0.0413,-0.18168 2.81129,-0.64382 0.0416,0.18168"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path394" />
<path
d="m 152.06476,125.91399 -0.18556,0.012 -0.16228,-2.50931 0.18591,-0.0123 0.16193,2.50966"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path396" />
<path
d="m 152.02137,125.84096 -0.0988,0.15769 -0.98954,-0.62053 0.0991,-0.15769 0.98919,0.62053"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path398" />
<path
d="m 153.33405,124.43056 -2.31598,0.95461 -0.0709,-0.17215 2.31599,-0.95462 0.0709,0.17216"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path400" />
<path
d="m 167.06769,103.88196 c -0.20602,-0.12912 -0.44944,-0.1838 -0.69074,-0.15523 l -2.87549,0.3422 -0.0935,0.16122 2.09691,1.31551 1.58573,-1.32151 c 0.0522,-0.0434 0.0801,-0.10936 0.0759,-0.17709 -0.005,-0.0677 -0.0416,-0.12912 -0.0988,-0.1651"
style="fill:#e4731a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path402" />
<path
d="m 163.77733,104.24179 2.71922,-0.33902 c 0.13687,-0.0169 0.27551,0.0138 0.39264,0.0871 l 0.0395,0.0247 c 0.0162,0.0102 0.0265,0.0275 0.0279,0.0466 0.001,0.019 -0.007,0.0378 -0.0215,0.0501 l -1.44533,1.20474 -1.71239,-1.07421"
style="fill:#f9a727;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path404" />
<path
d="m 145.63997,128.61168 h 3.81459 v 0.62547 h -3.81459 v -0.62547"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path406" />
<path
d="m 140.21425,126.26888 c 0.0783,-0.35489 0.34149,-0.63994 0.68897,-0.74683 l 6.68832,-2.05246 -0.3549,5.14209 h -6.77756 c -0.18556,0 -0.3609,-0.084 -0.47696,-0.2286 -0.11606,-0.14429 -0.16051,-0.33373 -0.12065,-0.5147 l 0.35278,-1.5995"
style="fill:#f9a727;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path408" />
<path
d="m 151.90037,131.89357 c 0.75917,0 1.37689,-0.61771 1.37689,-1.37724 0,-0.75918 -0.61772,-1.3769 -1.37689,-1.3769 -0.0243,-0.002 -3.12667,-0.22013 -4.15961,-0.22013 -1.03293,0 -4.13526,0.21802 -4.1663,0.22049 h -7.1e-4 c -0.75212,0 -1.36948,0.61736 -1.36948,1.37654 0,0.75953 0.61771,1.37724 1.37689,1.37724 h 8.31921"
style="fill:#252529;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path410" />
<path
d="m 143.58116,131.69778 c -0.65158,0 -1.18145,-0.52987 -1.18145,-1.18145 0,-0.65123 0.52669,-1.1811 1.17475,-1.1811 l 0.0141,-7.1e-4 c 0.03,-0.002 3.12667,-0.21943 4.15219,-0.21943 1.03082,0 4.11551,0.21696 4.14338,0.21908 l 0.008,7e-4 h 0.008 c 0.65158,0 1.18145,0.53023 1.18145,1.18146 0,0.65158 -0.52987,1.18145 -1.18145,1.18145 h -8.31921"
style="fill:#e4731a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path412" />
<path
d="m 150.71891,130.51633 c 0,-0.65229 0.52882,-1.18146 1.18146,-1.18146 0.65263,0 1.18145,0.52917 1.18145,1.18146 0,0.65263 -0.52882,1.18145 -1.18145,1.18145 -0.65264,0 -1.18146,-0.52882 -1.18146,-1.18145"
style="fill:#b8bbb6;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path414" />
<path
d="m 151.90037,131.19507 c 0.37429,0 0.67874,-0.30445 0.67874,-0.67874 0,-0.3743 -0.30445,-0.67875 -0.67874,-0.67875 -0.3743,0 -0.67875,0.30445 -0.67875,0.67875 0,0.37429 0.30445,0.67874 0.67875,0.67874"
style="fill:#252529;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path416" />
<path
d="m 145.53555,129.58146 c 0,-0.19014 0.15416,-0.34431 0.34431,-0.34431 0.19015,0 0.34431,0.15417 0.34431,0.34431 0,0.19015 -0.15416,0.34432 -0.34431,0.34432 -0.19015,0 -0.34431,-0.15417 -0.34431,-0.34432"
style="fill:#252529;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path418" />
<path
d="m 145.54331,131.35311 c 0,-0.19014 0.15416,-0.34431 0.34431,-0.34431 0.19015,0 0.34396,0.15417 0.34396,0.34431 0,0.19015 -0.15381,0.34432 -0.34396,0.34432 -0.19015,0 -0.34431,-0.15417 -0.34431,-0.34432"
style="fill:#252529;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path420" />
<path
d="m 149.25735,131.35311 c 0,-0.19014 0.15417,-0.34431 0.34432,-0.34431 0.19014,0 0.34431,0.15417 0.34431,0.34431 0,0.19015 -0.15417,0.34432 -0.34431,0.34432 -0.19015,0 -0.34432,-0.15417 -0.34432,-0.34432"
style="fill:#252529;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path422" />
<path
d="m 149.94598,129.58146 c 0,-0.19014 -0.15417,-0.34431 -0.34431,-0.34431 -0.19015,0 -0.34432,0.15417 -0.34432,0.34431 0,0.19015 0.15417,0.34432 0.34432,0.34432 0.19014,0 0.34431,-0.15417 0.34431,-0.34432"
style="fill:#252529;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path424" />
<path
d="m 144.76261,130.51633 c 0,-0.65229 -0.52881,-1.18146 -1.18145,-1.18146 -0.65264,0 -1.18145,0.52917 -1.18145,1.18146 0,0.65263 0.52881,1.18145 1.18145,1.18145 0.65264,0 1.18145,-0.52882 1.18145,-1.18145"
style="fill:#b8bbb6;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path426" />
<path
d="m 143.58116,131.19507 c 0.3743,0 0.67874,-0.30445 0.67874,-0.67874 0,-0.3743 -0.30444,-0.67875 -0.67874,-0.67875 -0.3743,0 -0.67874,0.30445 -0.67874,0.67875 0,0.37429 0.30444,0.67874 0.67874,0.67874"
style="fill:#252529;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path428" />
<path
d="m 143.43652,131.0088 h 8.60848 v -0.98495 h -8.60848 v 0.98495"
style="fill:#f9a727;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path430" />
<path
d="m 147.23664,128.61168 h 3.55812 c 0.33761,0 0.61172,-0.27376 0.61172,-0.61172 v -0.73236 c 0,-0.72073 0.11465,-2.63737 -0.76377,-3.69429 -0.0744,-0.0896 -0.18485,-0.14147 -0.30092,-0.14147 h -2.60173 c -0.16158,0 -0.29704,0.12277 -0.31256,0.28364 l -0.19086,1.97943 v 2.91677"
style="fill:#e4731a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path432" />
<path
d="m 150.76301,128.2649 c 0.16969,0 0.30797,-0.13794 0.30797,-0.30762 v -0.7165 c 0,-0.066 7.1e-4,-0.14217 0.002,-0.22648 0.01,-0.76165 0.03,-2.34562 -0.68192,-3.20216 -0.0173,-0.0212 -0.0437,-0.0335 -0.0709,-0.0335 h -2.54529 c -0.008,0 -0.0155,0.006 -0.0162,0.0148 l -0.16404,1.69827 c -0.0141,0.14464 0.025,0.28928 0.10971,0.40711 l 1.52153,2.11702 c 0.11219,0.15628 0.29316,0.24906 0.48578,0.24906 h 1.05163"
style="fill:#4c5462;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path434" />
<path
d="m 141.47331,125.91928 h 2.36891 c 0.0723,0 0.13052,0.0924 0.13052,0.20637 h -0.13052 -0.13053 -2.10785 -0.13053 -0.13053 c 0,-0.11394 0.0582,-0.20637 0.13053,-0.20637"
style="fill:#e4731a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path436" />
<path
d="m 141.47331,126.34649 h 2.36891 c 0.0723,0 0.13052,0.0924 0.13052,0.20673 h -0.13052 -0.13053 -2.10785 -0.13053 -0.13053 c 0,-0.1143 0.0582,-0.20673 0.13053,-0.20673"
style="fill:#e4731a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path438" />
<path
d="m 141.47331,126.77406 h 2.36891 c 0.0723,0 0.13052,0.0924 0.13052,0.20637 h -0.13052 -0.13053 -2.10785 -0.13053 -0.13053 c 0,-0.11394 0.0582,-0.20637 0.13053,-0.20637"
style="fill:#e4731a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0352778"
id="path440" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

99
tmp/index.html Normal file
View File

@ -0,0 +1,99 @@
<!DOCTYPE html>
<html>
<head>
<title>Under Construction</title>
<style>
body {
margin: 0;
}
main {
background-color: #f2f2f2;
padding: 1rem;
height: 100vh;
width: 100vw;
justify-content: center;
align-content: center;
display: grid;
}
#hero {
padding: 4rem;
background-color: white;
border-radius: 100%;
}
#hero img {
width: 16rem;
}
p {
font-family: sans-serif;
margin-bottom: 2rem;
margin-top: 2rem;
font-size: 1.5rem;
text-align: center;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/luxon/3.0.4/luxon.min.js" integrity="sha512-XdACFfCJeqqfVU8mvvXReyFR130qjFvfv/PZOFGwVyBz0HC+57fNkSacMPF2Dyek5jqi4D7ykFrx/T7N6F2hwQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>
<body>
<main>
<p style="font-size:2.5rem;color:#505050">Coming Soon&trade;</p>
<div id="hero">
<img src="/crane.svg">
</div>
<p>
Under Construction for <br />
<span id="counter" style="margin-top:0.5rem"></span>
</p>
</main>
</body>
<script>
function u(v, unit) {
if (v === 1) {
return `${v} ${unit}`;
}
else {
return `${v} ${unit}s`;
}
}
function f(n) {
let s = n.toString();
if (s.length == 1) {
return '0' + s;
}
return s;
}
const start = luxon.DateTime.fromSeconds(1634529923);
function setDuration() {
var diff = luxon.DateTime.now().diff(start);
const years = Math.floor(diff.as('years'));
diff = diff.minus(luxon.Duration.fromObject({years}));
const months = Math.floor(diff.as('months'));
diff = diff.minus(luxon.Duration.fromObject({months}));
const days = Math.floor(diff.as('days'));
diff = diff.minus(luxon.Duration.fromObject({days}));
const hours = Math.floor(diff.as('hours'))
diff = diff.minus(luxon.Duration.fromObject({hours}));
const minutes = Math.floor(diff.as('minutes'));
diff = diff.minus(luxon.Duration.fromObject({minutes}));
const seconds = Math.floor(diff.as('seconds'));
diff = diff.minus(luxon.Duration.fromObject({seconds}));
const millis = diff.as('milliseconds');
const timeString = `${u(years, "year")}, ${u(months, "month")}, ${u(days, "day")}, ${f(hours)}:${f(minutes)}:${f(seconds)}.${Math.floor(millis / 100)}`;
document.getElementById('counter').innerHTML = timeString;
window.setTimeout(setDuration, 10);
}
setDuration();
</script>
</html>

View File

@ -1,6 +0,0 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});