Code

Programming projects that typically have one or more blog posts assoicated with them.

build-info-dir.py

A simple Python script for indexing info manuals, see Indexing Info Manuals for details.

build-info-dir.py

"""build-info-dir.py <directory> <output>

Index all info manuals in a given directory and produce a ``dir`` index file.
"""
import argparse
import gzip
import pathlib
import sys

DIR_SECTION = "INFO-DIR-SECTION "

def extract_dir_nodes(filename: pathlib.Path) -> dict[str, list[str]]:
    nodes: dict[str, list[str]] = {}

    section: str = ""
    entry: list[str] | None = None

    with open_info_file(filename) as f:
        for line in f.readlines():
            text = line.decode(errors="ignore").rstrip()
            if text.startswith(DIR_SECTION):
                section = text.replace(DIR_SECTION, "")
                continue

            elif text == "START-INFO-DIR-ENTRY":
                entry = []
                continue

            elif text == "END-INFO-DIR-ENTRY":
                nodes.setdefault(section, []).extend(entry)
                entry = None

            elif isinstance(entry, list):
                entry.append(text)

    return nodes

def open_info_file(filename: pathlib.Path):
    if ".gz" in filename.suffix:
        return gzip.open(filename)

    return filename.open()

def index_info_dir(info_dir: pathlib.Path) -> str:
    nodes: dict[str, list[str]] = {}

    for filename in info_dir.glob("*.info*"):
        # Some manuals are split into mutliple files, we only need to index the top-level file
        if "info-" in filename.name:
            continue

        print(f"Indexing {filename.name}...", file=sys.stderr)
        index = extract_dir_nodes(filename)
        for section, items in index.items():
            nodes.setdefault(section, []).extend(items)

    lines = [
        chr(0x1f),
        "File: dir\tNode: Top\tThis is the top of the INFO tree",
        "",
        "* Menu:",
        ""
    ]

    for section, items in nodes.items():
        lines.append(section)
        lines.extend(items)

    return "\n".join(lines)

def main():
    cli = argparse.ArgumentParser()
    cli.add_argument("infodir", type=pathlib.Path)
    cli.add_argument("-o", "--output", type=argparse.FileType("w"), default="-")

    args = cli.parse_args()
    content = index_info_dir(args.infodir)
    print(content, file=args.output)


if __name__ == "__main__":
    main()

Click & Drag with Vanilla JS

An experiment in implementing clicking and dragging SVG elements using vanilla JavaScript, see Implementing Click & Drag with Vanilla JS for details.

click-drag.js

const svgns = "http://www.w3.org/2000/svg"
const main = document.getElementById("main")

const canvas = document.createElementNS(svgns, "svg")
canvas.setAttribute("width", "100%")
canvas.setAttribute("height", "100%")
canvas.style.border = "solid 2px #242930"

main.appendChild(canvas)

let bbox = canvas.getBoundingClientRect()

const aspectRatio = bbox.width / bbox.height

const height = 100
const width = height * aspectRatio

const viewBox = {minX: 0, minY: 0, width: width, height: height}

const viewBoxStr = [
  viewBox.minX, viewBox.minY, viewBox.width, viewBox.height
].join(" ")

canvas.setAttribute("viewBox", viewBoxStr)

const circle = document.createElementNS(svgns, "circle")
circle.setAttribute("cx", viewBox.width / 2)
circle.setAttribute("cy", viewBox.height / 2)
circle.setAttribute("r", 15)
circle.setAttribute("fill", "#57cc8a")

canvas.appendChild(circle)

let clicked = false

canvas.addEventListener("mousemove", (event) => {

   if (!clicked) {
      return
   }

   bbox = canvas.getBoundingClientRect()

   const x = (event.clientX - bbox.left) / bbox.width
   const y = (event.clientY - bbox.top) / bbox.height

   circle.setAttribute("cx", x * viewBox.width)
   circle.setAttribute("cy", y * viewBox.height)
})

circle.addEventListener("mousedown",  (_) => { clicked = true })
circle.addEventListener("mouseup", (_) => { clicked = false })

canvas.addEventListener("mouseleave", (_) => { clicked = false })

.config/

Some of my dotfiles

bash
00-options

shopt -s autocd         # If no command found, but matches a directory, cd into it
shopt -s checkjobs      # Warn about background jobs before exiting
shopt -s checkwinsize   # Update the COLUMNS and LINES environment variables between each command
shopt -s extglob        # Enable extended pattern matching features
shopt -s globstar       # Enable recursive globbing i.e `./**/*.py`

[ -d "$HOME/Projects" ] && export CDPATH=".:~/Projects"

paths=(
    "$HOME/go/bin"
    "$HOME/.cargo/bin"
    "$HOME/.npm-packages/bin"
)

for p in ${paths[@]}
do
    [ -d $p ] && export PATH="$p:$PATH"
done

shopt -s histappend

HISTCONTROL=erasedups
HISTFILESIZE=100000
HISTIGNORE='cd:ls'
HISTSIZE=10000
10-aliases

alias ls='ls -CFhX --color=auto --group-directories-first'
alias pypath='echo $PYTHONPATH | tr '\'':'\'' '\''\n'\'''
20-prompt

__venv_py_version()
{
    if [ -z "${VIRTUAL_ENV}" ]; then
        echo ""
    else
        echo " 🐍 v$(python --version | sed 's/Python //')"
    fi
}

__is_toolbox()
{
    if [ -f /run/.containerenv ] && [ -f /run/.toolboxenv ]; then
        name=$(grep name /run/.containerenv | sed 's/.*"\(.*\)"/\1/')

    else
        name="\h"
    fi

    echo "\[\e[32m\]${name}\[\e[0m\]"
}

if [ -f "/usr/share/git-core/contrib/completion/git-prompt.sh" ]; then
    source "/usr/share/git-core/contrib/completion/git-prompt.sh"

    export GIT_PS1_SHOWDIRTYSTATE=1       # (*) unstaged changes, (+) staged changes
    export GIT_PS1_SHOWSTASHSTATE=1       # ($) stashed
    export GIT_PS1_SHOWUNTRACKEDFILES=1   # (%) untracked files
    export GIT_PS1_SHOWUPSTREAM=verbose
    export GIT_PS1_SHOWCOLORHINTS=1

    export PROMPT_COMMAND='__git_ps1 "\n\w " "$(__venv_py_version)\n$(__is_toolbox) > " "[ %s]"'
else
    export PS1="\W\n> "
fi
jj
config.toml

"$schema" = "https://jj-vcs.github.io/jj/latest/config-schema.json"

user.name = "Alex Carney"
user.email = "alcarneyme@gmail.com"

ui.editor = "emacsclient"

ui.default-command = "status"

ui.conflict-marker-style = "git"
ui.diff-formatter = ":git"

[aliases]
push-down = ["new", "-B", "@", "--no-edit"]
nvim
lua
alc
lsp
esbonio.lua

function setup(opts)
   opts = opts or {}

   if not opts.capabilities then
     opts.capabilities = vim.lsp.protocol.make_client_capabilities()
   end

   require('lspconfig').esbonio.setup {
     cmd = get_esbonio_cmd(opts),
     capabilities = capabilities,
     handlers = {
       ["sphinx/clientCreated"] = client_created,
       ["sphinx/appCreated"] = app_created,
       ["sphinx/clientErrored"] = client_errored,
       ["sphinx/clientDestroyed"] = client_destroyed,
     },
     commands = {
       EsbonioPreviewFile = {
         preview_file,
         description = 'Preview Current File',
       },
     },
     settings = {
       esbonio = {
         logging = {
           level = 'debug',
           window = opts.devtools or false,
         }
       }
     }
   }
end

function get_esbonio_cmd(opts)
  local cmd = {}

  -- TODO: Devcontainer support.
  --
  -- local workspace = require('alc.devcontainer').workspace()
  -- if workspace then
  --   table.insert(cmd, 'devcontainer')
  --   table.insert(cmd, 'exec')
  --   table.insert(cmd, '--workspace-folder')
  --   table.insert(cmd, workspace)
  -- end

  if opts.devtools then
    table.insert(cmd, 'lsp-devtools')
    table.insert(cmd, 'agent')
    table.insert(cmd, '--')
  end

  table.insert(cmd, 'esbonio')
  return cmd
end

function preview_file()
  local params = {
    command = 'esbonio.server.previewFile',
    arguments = {
      { uri = vim.uri_from_bufnr(0), show = true },
    },
  }

  local clients = require('lspconfig.util').get_lsp_clients {
    bufnr = vim.api.nvim_get_current_buf(),
    name = 'esbonio',
  }
  for _, client in ipairs(clients) do
    client.request('workspace/executeCommand', params, nil, 0)
  end

  local augroup = vim.api.nvim_create_augroup("EsbonioSyncScroll", { clear = true })
  vim.api.nvim_create_autocmd({"WinScrolled"}, {
    callback = scroll_view, group = augroup, buffer = 0
  })
end

function scroll_view(event)
  local esbonio = vim.lsp.get_active_clients({ bufnr = 0, name = 'esbonio' })[1]
  local view = vim.fn.winsaveview()

  local params = { uri = vim.uri_from_bufnr(0),  line = view.topline }
  esbonio.notify('view/scroll', params)
end

function client_created(err, result, ctx, config)
  vim.notify("Sphinx client created in " .. result.scope, vim.log.levels.INFO)
end

function app_created(err, result, ctx, config)
  vim.notify("Application created", vim.log.levels.INFO)
end

function client_errored(err, result, ctx, config)
  vim.notify("Client error: " .. result.error, vim.log.levels.ERROR)
end

function client_destroyed(err, result, ctx, config)
  vim.notify("Sphinx client destroyed", vim.log.levels.INFO)
end

return {
  setup = setup
}
init.lua

function lsp_attach(event)
  vim.keymap.set('n', 'gd', require('telescope.builtin').lsp_definitions, { buffer = event.buf })
  vim.keymap.set('n', 'gr', require('telescope.builtin').lsp_references, { buffer = event.buf })
  vim.keymap.set('n', 'gI', require('telescope.builtin').lsp_implementations, { buffer = event.buf })
  vim.keymap.set('n', '<leader>D', require('telescope.builtin').lsp_type_definitions, { buffer = event.buf })
  vim.keymap.set('n', '<leader>ds', require('telescope.builtin').lsp_document_symbols, { buffer = event.buf })
  vim.keymap.set('n', '<leader>ws', require('telescope.builtin').lsp_dynamic_workspace_symbols, { buffer = event.buf })
  vim.keymap.set('n', '<leader>rn', vim.lsp.buf.rename, { buffer = event.buf })
  vim.keymap.set('n', '<leader>ca', vim.lsp.buf.code_action, { buffer = event.buf })
  vim.keymap.set('n', 'gD', vim.lsp.buf.declaration, { buffer = event.buf })
end

return {
   'neovim/nvim-lspconfig',
   dependencies = {
     'hrsh7th/cmp-nvim-lsp',
     { 'j-hui/fidget.nvim', opts = {
         notification = {
           override_vim_notify = true,
         }
       }
     },
   },
   config = function()
     vim.api.nvim_create_autocmd('LspAttach', {
       group = vim.api.nvim_create_augroup('alc-lsp-attach', { clear = true }),
       callback = lsp_attach
     })

     -- Ensure the additional capabilities provided by nvim-cmp are provided to the server
     local capabilities = vim.lsp.protocol.make_client_capabilities()
     capabilities = vim.tbl_deep_extend('force', capabilities, require('cmp_nvim_lsp').default_capabilities())

     -- Setup each server
     local use_devtools = os.getenv('LSP_DEVTOOLS') ~= nil
     require('alc.lsp.esbonio').setup { capabilities = capabilities, devtools = use_devtools }
     require('alc.lsp.python').setup { capabilities = capabilities }
   end
}
python.lua

function setup(opts)
   opts = opts or {}

   if not opts.capabilities then
     opts.capabilities = vim.lsp.protocol.make_client_capabilities()
   end

   require('lspconfig').pyright.setup {
     capabilities = capabilities,
     settings = {
       python = {
       }
     }
   }
end

return {
  setup = setup
}
completion.lua

function setup()
  local cmp = require('cmp')

  cmp.setup {
    completion = { completeopt = 'menu,menuone,noinsert' },
    mapping = cmp.mapping.preset.insert {
       ['<C-n>'] = cmp.mapping.select_next_item(),
       ['<C-p>'] = cmp.mapping.select_prev_item(),
       ['<C-b>'] = cmp.mapping.scroll_docs(-4),
       ['<C-f>'] = cmp.mapping.scroll_docs(4),
       ['<C-y>'] = cmp.mapping.confirm { select = true },
     },
    sources = {
       { name = 'nvim_lsp' },
     },
  }
end

return {
  'hrsh7th/nvim-cmp',
  event = 'InsertEnter',
  dependencies = {
    'hrsh7th/cmp-nvim-lsp',
  },
  config = setup
}
devcontainer.lua

function workspace()
  local cwd = vim.uv.cwd()
  if not cwd then
    return nil
  end

  local repo = require('lspconfig.util').find_git_ancestor(cwd)
  if not repo then
    return nil
  end

  local devcontainer = vim.fs.joinpath(repo, '.devcontainer')
  if not vim.uv.fs_stat(devcontainer) then
    return nil
  end

  return repo
end

return {
  workspace = workspace
}
telescope.lua

return {
  'nvim-telescope/telescope.nvim',
  event = 'VimEnter',
  branch = '0.1.x',
  dependencies = {
    'nvim-lua/plenary.nvim',
    'nvim-telescope/telescope-ui-select.nvim',
  },
  config = function()
    local themes = require('telescope.themes')

    require('telescope').setup {
      defaults = themes.get_ivy(opts)
    }

    pcall(require('telescope').load_extension, 'ui-select')

    local builtin = require 'telescope.builtin'
    vim.keymap.set('n', '<leader>ff', builtin.find_files)
  end
}
init.lua

vim.opt.termguicolors = false

vim.opt.breakindent = true

vim.opt.inccommand = 'split'
vim.opt.incsearch = true
vim.opt.ignorecase = false

vim.opt.number = true

vim.opt.signcolumn = 'number'

vim.opt.list = true
vim.opt.listchars = { tab = '».', trail = '·', extends = '→', precedes= '←' }

vim.g.mapleader = ' '
vim.g.maplocalleader = ' '

vim.keymap.set('n', '<Esc>', '<cmd>nohlsearch<CR>')

vim.keymap.set('n', '<C-h>', '<C-w><C-h>')
vim.keymap.set('n', '<C-l>', '<C-w><C-l>')
vim.keymap.set('n', '<C-j>', '<C-w><C-j>')
vim.keymap.set('n', '<C-k>', '<C-w><C-k>')

vim.keymap.set('n', 'n', 'nzz')
vim.keymap.set('n', 'N', 'Nzz')
vim.keymap.set('n', 'G', 'Gzz')
vim.keymap.set('n', '<c-i>', '<c-i>zz')
vim.keymap.set('n', '<c-o>', '<c-o>zz')

local lazypath = vim.fn.stdpath('data') .. '/lazy/lazy.nvim'
if not vim.uv.fs_stat(lazypath) then
  local lazyrepo = 'https://github.com/folke/lazy.nvim.git'
  local out = vim.fn.system({ 'git', 'clone', '--filter=blob:none', '--branch=stable', lazyrepo, lazypath})

  if vim.v.shell_error ~= 0 then
    error('Error cloning lazy.nvim:\n' .. out)
  end
end

vim.opt.rtp:prepend(lazypath)

require('lazy').setup({
  'tpope/vim-sleuth',
  require 'alc.completion',
  require 'alc.lsp',
  require 'alc.telescope',
})
vscode
settings.json

{

"window.autoDetectColorScheme": true,
"workbench.preferredLightColorTheme": "GitHub Light Default",
"workbench.preferredDarkColorTheme": "GitHub Dark Dimmed",

"workbench.iconTheme": "material-icon-theme",

"editor.fontFamily": "'UbuntuMono NF', 'monospace', monospace",
"editor.fontSize": 12,
"editor.fontLigatures": true,
"terminal.integrated.fontFamily": "'Ubuntu Mono NF', 'monospace', monospace",
"terminal.integrated.fontSize": 12,
"terminal.integrated.lineHeight": 1,

"editor.cursorStyle": "block",
"editor.cursorBlinking": "solid",
"editor.cursorSmoothCaretAnimation": "on",
"editor.minimap.enabled": false,
"editor.smoothScrolling": true,
"debug.toolBarLocation": "commandCenter",
"window.commandCenter": true,
"window.titleBarStyle": "custom",
"workbench.activityBar.location": "top",
"workbench.editor.showTabs": "single",
"workbench.layoutControl.enabled": true,
"workbench.list.smoothScrolling": true,

"files.autoSave": "off",
"files.enableTrash": false,
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true,
"files.trimFinalNewlines": true,

"files.exclude": {
   "**/.hg": true,
   "**/.git": true,
   "**/.svn": true,
   "**/*.pyc": true,
   "**/.mypy_cache": true,
   "**/__pycache__": true,
},

"[python]": {
  "editor.rulers": [ 89 ]
},

"[restructuredtext]": {
  "editor.tabSize": 3,
  "editor.wordBasedSuggestions": "off",
},

}
wezterm
commands
screenshot.lua

local wezterm = require 'wezterm'
local io = require 'io'
local os = require 'os'
local act = wezterm.action

local M = {}

local function append_tables(...)
 local result = {}
 for _, tbl in ipairs({...}) do
     for i = 1, #tbl do
         result[#result + 1] = tbl[i]
     end
 end
 return result
end

function M.setup()

  wezterm.on('augment-command-palette', function(window, pane)
    return {
      {
        brief = 'Take screenshot',
        icon = 'cod_device_camera',

        action = act.PromptInputLine {
          description = 'Screenshot name',
          -- initial_value = os.date('%Y%m%dT%H%M%S--terminal-screenshot'), -- apparently this requires a nightly build
          action = wezterm.action_callback(function(window, pane, line)
            if line then
              local dimensions = pane:get_dimensions()
              local theme = wezterm.get_builtin_color_schemes()[window:effective_config().color_scheme]
              local body = {0, "o", pane:get_lines_as_escapes()}

              local header = {
                version = 3,
                term = {
                  cols = dimensions.cols,
                  rows = dimensions.viewport_rows,
                  theme = {
                    fg = theme.foreground,
                    bg = theme.background,
                    palette = table.concat(append_tables(theme.ansi, theme.brights), ':'),
                  },
                },
              }

              local f = io.open(line .. '.cast', 'w+')
              f:write(wezterm.json_encode(header) .. '\n')
              f:write(wezterm.json_encode(body))
              f:flush()
              f:close()
            end
          end),
        },
      },
    }
  end)
end

return M
wezterm.lua

local wezterm = require 'wezterm'
local config = wezterm.config_builder()
config.keys = {}

config.font = wezterm.font('UbuntuMono Nerd Font')

if wezterm.gui.get_appearance():find("Dark") then
  config.color_scheme = 'Modus-Vivendi'
else
  config.color_scheme = 'Modus-Operandi'
end

config.use_fancy_tab_bar = true

config.default_prog = { '/usr/bin/fish', '-l' }

table.insert(config.keys, {
  key = 'Enter', mods = 'ALT', action = wezterm.action.DisableDefaultAssignment,
})

local screenshot_command = require 'commands.screenshot'
screenshot_command.setup()

return config
Makefile

.PHONY: wezterm
wezterm:
     test -L $(HOME)/.config/wezterm || ln -s $(shell pwd)/wezterm $(HOME)/.config/wezterm

.PHONY: bash
bash:
     test -L $(HOME)/.bash_profile || ln -s $(shell pwd)/bash_profile $(HOME)/.bash_profile
     test -L $(HOME)/.bashrc || ln -s $(shell pwd)/bashrc $(HOME)/.bashrc
     test -L $(HOME)/.bashrc.d || ln -s $(shell pwd)/bash $(HOME)/.bashrc.d

.PHONY: git
git:
     test -L $(HOME)/.gitconfig || ln -s $(shell pwd)/gitconfig $(HOME)/.gitconfig

.PHONY: nvim
nvim:
     test -L $(HOME)/.config/nvim || ln -s $(shell pwd)/nvim $(HOME)/.config/nvim

.PHONY: vscode
vscode:
     test -L $(HOME)/.config/Code/User || ln -s $(shell pwd)/vscode/ $(HOME)/.config/Code/User
     -code --install-extension charliermarsh.ruff
     -code --install-extension eamodio.gitlens
     -code --install-extension github.github-vscode-theme
     -code --install-extension ms-python.python
     -code --install-extension pkief.material-icon-theme
     -code --install-extension tamasfe.even-better-toml
bash_profile

# Get the aliases and functions
if [ -f ~/.bashrc ]; then
        . ~/.bashrc
fi
bashrc

if [ -f /etc/bashrc ]; then
        . /etc/bashrc
fi

if ! [[ "$PATH" =~ "$HOME/.local/bin:$HOME/bin:" ]]
then
    PATH="$HOME/.local/bin:$HOME/bin:$PATH"
fi
export PATH

if [ -d ~/.bashrc.d ]; then
        for rc in ~/.bashrc.d/*; do
                if [ -f "$rc" ]; then
                        . "$rc"
                fi
        done
fi

unset rc
gitconfig

[user]
name  = Alex Carney
email = alcarneyme@gmail.com

[alias]
co = checkout
amend = commit --amend --no-edit  # Squash changes into the previous commit

hist = log --branches --remotes --tags --graph --oneline --decorate

s = !git status -sb && git --no-pager diff --shortstat

[credential "https://github.com"]
helper =
helper = !gh auth git-credential

[diff]
algorithm = histogram
colorMoved = default

[merge]
conflictstyle = diff3

[rebase]
autosquash = true
autostash = true

[commit]
verbose = true

[core]
editor = nvim

[pull]
rebase = true

[rerere]
enabled = true

[github]
user = alcarney
starship.toml

"$schema" = 'https://starship.rs/config-schema.json'

add_newline = true

[custom.jj]
ignore_timeout = true
description = "The current jj status"
when = "jj root --ignore-working-copy"
symbol = "jj "
command = '''
jj log --revisions @ --no-graph --ignore-working-copy --color always --limit 1 --template '
  separate(" ",
    change_id.shortest(8),
    bookmarks,
    "|",
    concat(
      if(conflict, "✘"),
      if(divergent, ""),
      if(hidden, ""),
      if(immutable, "◆"),
    ),
    raw_escape_sequence("\x1b[1;32m") ++ if(empty, "(empty)") ++ raw_escape_sequence("\x1b[0m"),
    raw_escape_sequence("\x1b[1;32m") ++ coalesce(
      truncate_end(29, description.first_line(), "…"),
      "(no description set)",
    ) ++ raw_escape_sequence("\x1b[0m"),
  )
'
'''

[git_state]
disabled = true

[git_commit]
disabled = true

[git_metrics]
disabled = true

[git_branch]
disabled = true

[custom.git_branch]
when = true
command = "jj root --ignore-working-copy >/dev/null 2>&1 || starship module git_branch"

Simple AST

An experiment in representing and evaluating a toy abstract syntax tree using C code, see Evaluating a Simple Abstract Syntax Tree for details.

simple-ast.c

#include <stdio.h>

typedef enum {
    AST_LITERAL,
    AST_PLUS,
    AST_MULTIPLY,
} AstNodeType;

typedef struct _ast {
    /* The type of node this represents e.g.
       a literal, an operator etc. */
    AstNodeType type;

    /* In the case of a literal value, this field
       holds the actual value */
    float value;

    /* In the case of an operator, this pointer
       refers to the left child node */
    struct _ast *left;

    /* In the case of an operator, this pointer
       refers to the right child node */
    struct _ast *right;
} AstNode;

void
ast_print(AstNode *ast, int level)
{
    for (int i = 0; i < level; i++) {
        printf("  ");
    }

switch(ast->type) {
case AST_LITERAL:
    printf("%.2f\n", ast->value);
    break;

case AST_PLUS:
    printf("+\n");
    ast_print(ast->left, level + 1);
    ast_print(ast->right, level + 1);
    break;

    case AST_MULTIPLY:
        printf("*\n");
        ast_print(ast->left, level + 1);
        ast_print(ast->right, level + 1);
        break;
    }
}

float
ast_evaluate(AstNode *ast)
{
    switch(ast->type) {
    case AST_LITERAL:
        return ast->value;

case AST_PLUS: {
    float a = ast_evaluate(ast->left);
    float b = ast_evaluate(ast->right);

    return a + b;
}

    case AST_MULTIPLY: {
        float a = ast_evaluate(ast->left);
        float b = ast_evaluate(ast->right);

        return a * b;
    }
    }
}

int
main()
{
    AstNode one = { .type = AST_LITERAL, .value = 1.0f };
    AstNode two = { .type = AST_LITERAL, .value = 2.0f };
    AstNode three = { .type = AST_LITERAL, .value = 3.0f };

    AstNode plus1 = { .type = AST_PLUS, .left = &one, .right = &two};
    AstNode example1 = { .type = AST_MULTIPLY, .left = &plus1, .right = &three};

    AstNode mul1 = { .type = AST_MULTIPLY, .left = &two, .right = &three};
    AstNode example2 = { .type = AST_PLUS, .left = &one, .right = &mul1};

    float result1 = ast_evaluate(&example1);
    float result2 = ast_evaluate(&example2);

    ast_print(&example1, 0);
    printf("Example 1: %.2f\n", result1);

    ast_print(&example2, 0);
    printf("Example 2: %.2f\n", result2);

    return 0;
}