Showing posts with label Concurrency. Show all posts
Showing posts with label Concurrency. Show all posts

Friday, 19 June 2026

Introducing Skink-lang: Elegant Systems Programming for AI & Robotics (or, How to Stop DANI from Eating the Skirting Boards)


Over the last thirty years, I’ve worked across a sprawling landscape of technologies—from writing Go, C#, Pascal, Swift, and C, to orchestrating massive cloud pipelines, down to micro-managing registers on bare-metal microcontrollers.

But lately, my time has been dominated by a very specific, highly opinionated, and occasionally cooperative localized AI agent: DANI.

If you've been following my previous posts, you know DANI has transitioned from an LSTM-based cognitive core into a physical, moving entity. And that is where the real-world engineering headaches began. Every time you start a new robotics or physical AI project, you are forced to make a compromise that feels like choosing between a kick in the shins or a poke in the eye:

  • The C/C++ Extremity: You get deterministic, blazing-fast performance on microcontrollers, but you sacrifice developer ergonomics, modern package safety, and swift prototyping. Writing raw $C++$ register manipulation code sometimes feels like trying to perform laparoscopic surgery on yourself with a rusty spoon.
  • The Python Extremity: You get instant access to rich AI primitives, neural networks, and expressiveness. But you inherit bloated virtual environments that take up more disk space than the library of Alexandria, and unpredictable garbage collection (GC) latency. When DANI’s control loop is running at $100\text{ Hz}$ and the Python garbage collector decides to take a mandatory three-millisecond tea break, DANI doesn't stop. She gracefully, but deterministically, plows straight into the skirting board.

I got tired of choosing between developer speed and hardware execution safety. I wanted a compiled language built specifically for low-level systems hardware, high-throughput concurrent event streams, and native AI execution.

So, I built Skink-lang.

What is Skink?

Skink is an LLVM-backed, statically-typed systems programming language designed from the ground up for the modern era of intelligent automation. It blends the tight, expressive syntax of modern languages like Swift with the lightweight concurrency model of Go, leaving behind the runtime bloat.

To save you from the nightmare of modern package managers—where doing a pip install on a single helper utility somehow downloads half the internet and a bootleg copy of Doom—Skink comes with a rich, "batteries-included" standard library right out of the box.

As a Skinker (yes, that is our official title now), you get native access to:

  1. Automatic Reference Counting (ARC): Predictable, deterministic memory management without "stop-the-world" garbage collection pauses. This is a non-negotiable requirement when you are driving physical motors or processing high-frequency sensor telemetry.
  2. Lightweight Concurrency: A native spawn keyword that allows you to spin up millions of concurrent tasks with near-zero overhead, communicating cleanly via typed channels.
  3. The Rules Engine: A compiler-optimized reactive rules engine that lets you define declarative behavioral overrides that monitor variables in the background. It is perfect for saying, "I don't care what the neural net is thinking; if we are $15\text{ cm}$ away from a wall, hit the brakes."
  4. Native Tensors & ML Cores: Multi-dimensional arrays built directly into the type system via std/tensor, complete with matrix operations, activation functions, neural layers, and hooks for CUDA acceleration and llama.cpp (std/llm). DANI can run local inferences natively, at maximum speed, without needing a dedicated power plant or $16\text{ GB}$ of RAM.
  5. Model Context Protocol (MCP) & MQTT: Built-in support for turning any hardware endpoint into an instantly discoverable AI tool with SQLite (std/db) and edge-to-cloud messaging (std/mqtt) ready to go.

Under the Hood: Preventing a DANI Catastrophe

To see what "skinking" actually looks like, let's write a syntactically valid program based on the current Skink manual.

The following script concurrently polls a physical distance sensor via GPIO, pipes the measurements to our main execution block using channels, evaluates safety overrides in a background ruleset, and processes the inputs through a linear neural network layer to calculate motor outputs:

module main

import "std/gpio"
import "std/time"
import "std/tensor"

// 1. Declare global state variables (accessible by the ruleset)
var current_distance: float = 100.0
var motor_speed: float = 0.5

// 2. Define a reactive ruleset for safety overrides
ruleset SafetyOverride {
    rule collision_warning when current_distance < 15.0 {
        action: trigger_emergency_stop()
        priority: 1
    }
}

// Helper function called when the rule fires
fn trigger_emergency_stop() {
    motor_speed = 0.0
    print("SAFETY OVERRIDE: Obstacle detected! Distance: {current_distance}cm. Braking.")
}

// 3. Concurrently poll the hardware sensor in a background task
fn sensor_loop(ch: chan<float>) {
    err := gpio.Setup()
    if err.message != "" {
        print("Failed to initialize GPIO: " + err.message)
        return
    }

    // Bind to BCM Pin 17 (e.g., our sensor input pin)
    sensor_pin := gpio.PinFactory(17)
    sensor_pin.SetInput()

    while true {
        // Mocking a physical sensor read for this demo loop
        // In physical deployment, you'd calculate raw voltage pulses here
        measured := 12.5 
        ch <- measured
        time.SleepMs(10) // Poll at 100Hz
    }
}

// 4. Main Entry Point
fn main() -> int {
    sensor_chan := make(chan<float>)

    // Spawn our lightweight background polling loop
    spawn sensor_loop(sensor_chan)

    // Activate our safety ruleset background thread
    safety := SafetyOverride{}
    safety.start()
    defer safety.stop()

    // Initialize a linear neural network layer [input_features: 3, output_features: 1]
    layer := tensor.NewLinear(3, 1)

    // Run our control loop for 100 iterations
    for i := 0; i < 100; i = i + 1 {
        // Block until next sensor reading arrives
        current_distance = <-sensor_chan

        // If we are in a safe zone, let the tensor neural layer drive
        if current_distance >= 15.0 {
            input := tensor.Zeros([1, 3])
            input.Set([0, 0], current_distance)
           
            output := layer.Forward(input)
            motor_speed = output.Get([0, 0])
           
            print("Processing... Current motor velocity: {motor_speed}")
        }
    }
    return 0
}

Notice how neatly this handles the classic embedded AI problem. The concurrency model lets you poll hardware safely on separate execution threads without blocking the main loop, while the native ruleset watches the critical state variables and takes action within milliseconds if a threshold is breached—guaranteeing deterministic safety constraints before DANI can do any structural damage to the house.

Where Skink-lang Goes From Here

Currently, the compiler is bootstrapped in Go, using an LLVM backend to output highly optimized native machine binaries targeting both Linux and Windows corporate environments. Because there is no heavy runtime or garbage collector, a compiled Skink binary running an active inference loop can comfortably squeeze inside less than $128\text{ KB}$ of RAM.

But this is just the beginning. The roadmap ahead includes:

  • Direct Single Board Computer HAL: Expanding the standard library to map hardware registers and pins natively (such as on the Raspberry Pi 5) with zero external C-bindings.
  • Self-Hosting: Rewriting the Skink compiler entirely in Skink itself, proving the language's capabilities to handle massive, complex systems-level software natively.

Cullen the Skink
Cullen Skink

I’m incredibly excited about what this language makes possible. DANI is already running much cooler, much faster, and with a significantly lower skirting-board collision rate.

It is time to stop fighting the plumbing.

Let's get skinking!

Let me know your thoughts on the syntax, and what features you'd like to see added to the compiler next!


Monday, 23 February 2026

Oops, I Gave My Robot Amnesia (And How I'm Fixing It)

Wow, it’s been a while. Apologies for the radio silence, but the pesky "real world" caught up with me, and I had to spend some time doing that whole "working for a living" thing.

Anyway, enough about the mundane. Let's get back to what is actually important: DANI.

As you might remember, my ultimate, beyond-my-wildest-dreams goal with this project is to cross that threshold and meet the definition of when a robot is actually alive, or at least close to it. But recently, while pondering DANI’s LSTM (the fancy Long Short-Term Memory neural network that acts as his brain), I realized I had made a fundamental—and slightly embarrassing—mistake.

It’s hard to achieve sentience when your robot has the memory retention of a goldfish.

The Problem: Scheduled Blackouts

As it stands right now, DANI "thinks" every 100 milliseconds, giving him 10 thought cycles a second. Every 10 seconds (100 cycles), backpropagation kicks in to train the network. To do this concurrently without stopping DANI in his tracks, I clone the LSTM at that exact moment, run the heavy backpropagation math on the clone, and then overwrite the active LSTM with the newly trained clone.

This backpropagation takes about 2 to 3 seconds. My initial thought was: Brilliant! The training happens in the background without interrupting his flow.

But there is a glaring flaw.

Because the process takes a snapshot, spends 3 seconds learning from it, and then violently overwrites the active brain... we lose those 2 to 3 seconds of short-term memory that DANI experienced while the training was happening. Every 10 seconds, DANI essentially blacks out and forgets the last few seconds of his existence. This is seriously hampering his learning capabilities.

How do we stop DANI from becoming a chronic amnesiac?

The Fix: A Neurological Hot-Swap

My solution is to ditch the cloning process entirely. Instead, each neuron will now have two sets of weights: one active, one inactive.

During backpropagation, the inactive weights will get the results of the calculation (using the active weights for the algorithm). This allows us to update the LSTM's underlying math without wiping out the actively evolving memory states (the cell states and hidden states) that DANI is currently using to understand the world. We just add a flag to each layer to indicate whether it should be reading from Weight Set 1 or Weight Set 2.

But wait, there’s more!

Reshaping the Brain

At present, DANI's model has about 300 neurons on each layer, with 5 layers (I don’t have the code right in front of me, so I'm relying on my own somewhat flawed, non-LSTM memory here).

If we increase the number of layers but reduce the neurons per layer, we can implement a rolling update. This means DANI can immediately benefit from the training layer-by-layer, even while the rest of the brain is still calculating.

What this entails is increasing the layer count to 7 (any higher and we start flirting with the dreaded vanishing gradient problem), but reducing the neuron count, per layer, to 128 (because who doesn't love a nice power of 2?).

This gives DANI a much more focused, "deep" thought process, allowing him to break down problems more efficiently. It also allows us to gracefully ‘flip the switch’ on each layer as we cycle through.

Here is how the rolling update will work:

As each feed-forward pass occurs (DANI thinking), a check is done to see if the next layer is ready to have its switch flipped to the newly trained weights. Because backpropagation is strictly sequential and works backwards, we start checking from the last layer and move towards the first.

If a layer is ready, we flip the weights to the newly trained set and mark it as done. On the next thought cycle, we check the next layer, and so on, until we reach the front of the brain. Then, we start the whole process over again.

What do we gain from this brain surgery?

Quite a bit, actually:

  1. Constant Learning: The LSTM is in a state of continuous, uninterrupted learning.
  2. Stable Learning Rate: No massive, sudden shifts in logic.
  3. Smoother Processing: No sudden CPU spikes from cloning and overwriting massive arrays.
  4. Deeper Thinking: The structural change to 7 layers gives DANI a more nuanced, layered way of processing information.
  5. Memory Retention: We actually retain the states of the memory gates within the LSTM. No more 3-second blackouts!

There are certainly other ways to create a continuous neural network, but I am aiming for the absolute simplest solution here. Remember, all of this is running on a Raspberry Pi!

This dual-weight method does increase the memory required to hold the LSTM, but because we are reducing the overall neuron count from ~1500 (5x300) to 896 (7x128), it's actually going to be lighter on the Pi overall. DANI had an oversized network anyway, so trimming the fat while adding depth is a win-win.

What do you guys think of this approach? Let me know in the comments if you see any potholes I'm about to step in!


Friday, 10 October 2025

3.5 Million Parameters and a Dream: DANI’s Cognitive Core

DANI’s Brain Is Online! Meet the LSTM That Thinks, Feels, and Remembers (Like a Champ)

Ladies and gentlemen, creators and dreamers—DANI has officially levelled up. He’s no longer just a bundle of sensors and hormones with a charming voice and a tendency to emotionally escalate when he sees a squirrel. He now has a brain. A real one. Well, a synthetic one. But it’s clever, emotional, and surprisingly good at remembering things. Meet his new cognitive core: the LSTM.

And yes—it’s all written in Go. Because if you’re going to build a synthetic mind, you might as well do it in a language that’s fast, clean, and built for concurrency. DANI’s brain doesn’t just think—it multitasks like a caffeinated octopus.

What’s an LSTM, and Why Is It Living in DANI’s Head?

LSTM stands for Long Short-Term Memory, which sounds like a contradiction until you realize it’s basically a neural network with a built-in diary, a forgetful uncle, and a very opinionated librarian. It’s designed to handle sequences—like remembering what just happened, what happened a while ago, and deciding whether any of it still matters.

Imagine DANI walking into a room. He sees a red ball, hears a dog bark, and feels a spike of adrenaline. A regular neural network might say, “Cool, red ball. Let’s chase it.” But an LSTM says, “Wait… last time I saw a red ball and heard barking, I got bumped into a wall. Maybe let’s not.”

Here’s how it works, in human-ish terms:

  • Input gate: Decides what new information to let in. Like a bouncer at a nightclub for thoughts.
  • Forget gate: Decides what old information to toss out. Like Marie Kondo for memory.
  • Output gate: Decides what to share with the rest of the brain. Like a PR manager for neurons.

These gates are controlled by tiny mathematical switches that learn over time what’s useful and what’s noise. The result? A brain that can remember patterns, anticipate outcomes, and adapt to emotional context—all without getting overwhelmed by the chaos of real-world data.

And because DANI’s LSTM is stacked—meaning multiple layers deep—it can learn complex, layered relationships. Not just “ball = chase,” but “ball + bark + adrenaline spike = maybe don’t chase unless serotonin is high.”

It’s like giving him a sense of narrative memory. He doesn’t just react—he remembers, feels, and learns.

What’s Feeding This Brain?

DANI’s LSTM is his main cognitive module—the part that thinks, plans, reacts, and occasionally dreams in metaphor. It takes in a rich cocktail of inputs:

  • Vision data: Objects, positions, shapes—what he sees.
  • Sensor data: Encoders, ultrasonic pings, bump sensors—what he feels.
  • Audio features: What he hears (and maybe mimics).
  • Emotional state: Dopamine, cortisol, serotonin, adrenaline—what he feels.
  • Spatial map: His mental layout of the world around him.
  • Short-term memory context: What just happened.
  • Associated long-term memories: Symbolic echoes from his main memory—what used to happen in similar situations.

This isn’t just reactive behaviour—it’s narrative cognition. DANI doesn’t just respond to stimuli; he builds a story from them. He’s learning to say, “Last time I saw a red ball and felt excited, I chased it. Let’s do that again.”

Trial by Raspberry Pi

We’ve successfully trialled DANI’s LSTM on a Raspberry Pi, running a 3.5 million parameter model. And guess what? It only used a quarter of the Pi’s CPU and 400 MB of memory. That’s like teaching Shakespeare to a potato and watching it recite sonnets without breaking a sweat.

We’ve throttled the inference rate to 10 decisions per second—not because he can’t go faster, but because we want him to think, not twitch. Emotional processing takes time, and we’re not building a caffeine-fuelled chatbot. We’re building a thoughtful, emotionally resonant robot who dreams in symbols and learns from experience.

Learning Without Losing His Mind

Training happens via reinforcement learning—DANI tries things, gets feedback, and adjusts. But here’s the clever bit: training is asynchronous. That means he can keep thinking, moving, and emoting while his brain quietly updates in the background. No interruptions. No existential hiccups mid-sentence.

And yes, we save the model periodically—because nothing kills a good mood like a power cut and a wiped memory. DANI’s brain is backed up like a paranoid novelist with a USB stick in every pocket.

Final Thoughts

This LSTM isn’t just a brain—it’s a story engine. It’s the part of DANI that turns raw data into decisions, decisions into memories, and memories into dreams. It’s the bridge between his sensors and his soul (okay, simulated soul). And it’s just getting started.

Next up: I plan to start the even more monumental task of getting the vector database working and linked up to DANI's brain in such a way that it will have a direct impact of DANI's hormonal system.

Stay tuned. DANI’s mind is waking up.

Tuesday, 17 June 2025

Confessions of a Best Practice Hoarder


There are few tasks a developer enjoys more than writing documentation. It’s a thrilling journey into the exciting world of code formatting and variable naming conventions, right up there with untangling someone else’s regular expressions. So, you can imagine my sheer, unadulterated joy when I was asked to create a "Best Practice" document for my team's C# projects.

Okay, my sarcasm meter is now switched off. It was, in fact, a necessary and useful exercise. While C# isn't the language that sings me to sleep at night, it's a powerful tool (and a country mile better than Java). The goal was to create a shared map for our team, a guide to writing code that our future selves wouldn't want to travel back in time to prevent. The document covers the essentials: SOLID principles, consistent naming conventions, and key architectural patterns like Dependency Injection. It also provides guardrails for C#-specific features, like the right way to use async/await and how to query data with LINQ without accidentally DDOSing your own database.
C#
The result is a common language for quality. It makes our code reviews more productive and helps everyone, from senior to junior, stay on the same page.

Opening Pandora's Box
With the C# guide complete, a dangerous thought crept in over the weekend: "I wonder what this would look like for my other languages?" This was a classic case of a weekend project spiralling into a minor obsession. I fired up my editors and, in a fit of what I can only describe as productive procrastination, began creating similar guides for Lazarus/FreePascal, Go, Arduino C++, and Cerberus-X.
What started as a simple comparison turned into a fascinating exploration of programming language philosophy. The exercise proved that while principles like DRY (Don't Repeat Yourself) are universal, the "best" way to implement them is anything but.

A Tale of Five Philosophies
The way a language handles common problems tells you a lot about its personality. The differences are most stark in a few key areas.

Memory Management: From Butler Service to DIY Survival
How a language manages memory fundamentally changes how you write code.
  • C#: Has a garbage collector, which is like a butler who tidies up after you. It’s convenient, but you still need to know the rules. You have to explicitly tell the butler about any special (unmanaged) items using IDisposable, otherwise, they'll be left lying around.
  • Arduino/C++: This is the survivalist end of the spectrum. You have a tiny backpack with 2KB of RAM, which is less memory than a high-resolution emoji. Every byte is sacred. Heap allocation is a dangerous game of Jenga that leads to fragmentation and mysterious crashes. The Arduino String object is a notorious trap for new players, munching on your limited memory. Here, best practice isn't just a good idea; it's the only thing keeping your project from collapsing.
  • Go: Also has a garbage collector, but it’s more of a silent partner. The language and its idioms are designed in such a way that you rarely have to think about memory management. It just works.
  • Cerberus

    Cerberus-X:
    As another high-level language, Cerberus-X handles memory automatically. The developer's main responsibility isn't freeing memory, but ensuring its state is predictable. The most crucial best practice is to always use the Strict directive. This is the "no more mystery values" setting, as it enforces that all variables must be initialized before use , saving you from the bizarre bugs that come from variables defaulting to 0 or an empty string in non-strict mode.
  • Lazarus & FreePascal: The "Choose Your Own Adventure" Model
This is where things get really interesting. FreePascal offers a mixed model for memory management, letting you pick the right tool for the job.
    • The Classic Approach: This is pure manual control. Every object you create with .Create is your responsibility, and you must personally ensure it is destroyed with a corresponding .Free call. The try..finally block is your non-negotiable safety net to guarantee that cleanup happens, even when errors occur. It’s the ultimate "you made the mess, you clean it up" philosophy.
    • FreePascal cheetah

      The LCL Ownership Model: The Lazarus Component Library gives you a helping hand, especially for user interfaces. When you create a component, you can assign it an Owner (like the form it sits on). The Owner then acts like a responsible parent: when it gets destroyed, it automatically frees all the child components it owns. You should not manually .Free a component that has an owner.
    • The Modern Approach: To make life even easier, FreePascal supports Automatic Reference Counting (ARC) for interfaces. When an object is assigned to an interface variable, a counter is incremented. When that variable goes out of scope, the counter is decremented , and once it hits zero, the object is automatically freed. This brings the convenience of garbage collection to your business objects, drastically reducing the risk of memory leaks.
Concurrency: An Assembly Line vs. The Office Worker
  • C#: async/await feels like delegating a task. You ask a subordinate (Task) to do something, and you can either wait for the result (await) or carry on with other work. It's efficient and clean.
  • Go

    Go:
    Go's model is more like an automated assembly line. You have multiple workers (goroutines) and a system of pneumatic tubes (channels) connecting them. Workers perform their small task and send the result down a tube to the next worker, all happening simultaneously.
  • Arduino/C++: You're a solo act on a mission. There are no threads, so you can't do two things at once. The entire game is to never stop moving. You check a sensor, update a light, check a button, and repeat, all in a lightning-fast loop(). A delay() is your worst enemy because it brings everything to a grinding halt.
  • Lazarus/FreePascal: This is the classic office worker. To avoid freezing the UI during a long operation, you spawn a TThread to do the heavy lifting in the background. When the worker thread needs to update a label on the screen, it can't just barge in. It has to use TThread.Synchronize or TThread.Queue to politely tap the UI thread on the shoulder and ask it to make the change safely.
  • Cerberus-X: This is the resourceful indie developer. It doesn't have the fancy built-in machinery of async/await. To achieve non-blocking operations, it falls back on the fundamental tools, letting the developer build their own solution using threading or designing methods with callbacks.

Error Handling: The Town Crier vs. The Smoke Signal
  • C# & Friends: Languages like C#, Lazarus, and Cerberus-X prefer the "town crier" approach of exceptions. When something goes wrong, they shout about it loudly, and a try...catch block is expected to handle the commotion.
  • Go: Go has trust issues. It prefers you to look before you leap. Functions return an error value alongside their result, forcing you to confront the possibility of failure at every single step.
    Arduino C++

  • Arduino/C++: When your code is running on a chip in a field, how does it cry for help? It uses a smoke signal. There's no console, so robust error handling involves returning status codes or, in a critical failure, entering a safe state and blinking an LED in a specific pattern—a primitive but effective "blink of death" to signal an error code.

Up for Grabs
This dive into different programming paradigms was a blast. It’s a powerful reminder that there’s no single "best" language, only the right tool for the job, with its own unique set of best practices.

I’ve cleaned up all five documents and made them available for download. If you work in any of these languages, I hope they can be of some use to you. Now if you'll excuse me, I think I see a dusty corner of the internet where a language is just begging for a best practice guide. It's a sickness, really.


Grab them, use them, and happy coding!

Aiming for Jarvis, Creating D.A.N.I.