HN user

slightknack

311 karma

A software architect who likes language design, RL, and impressionist paintings.

[ my public key: https://keybase.io/slightknack; my proof: https://keybase.io/slightknack/sigs/WcaeY2T7Yk1zxtpKL0lKKQ_8EfoCF5pReRxzOEDSUcI ]

Posts15
Comments52
View on HN

The minimaxir/gemimg repo is pretty cool, fwiw.

Going further, one thing you can do is give Gemini 2.5 a system prompt like the following:

https://goto.isaac.sh/image-prompt

And then pass Gemini 2.5's output directly to Nano-Banana. Doing this yields very high-quality images. This is also good for style transfer and image combination. For example, if you then give Gemini 2.5 a user prompt that looks something like this:

    I would like to perform style transfer. I will provide the image generation model a photograph alongside your generated prompt. Please write a prompt to transfer the following style: {{ brief style description here }}.
You can get aesthetic consistently-styled images, like these:

https://goto.isaac.sh/image-style-transfer

He identified the four forces which act on a heavier-than-air flying vehicle: weight, lift, drag and thrust. [...] He also designed the first glider reliably reported to carry a human aloft. He correctly predicted that sustained flight would not occur until a lightweight engine was developed to provide adequate thrust and lift. The Wright brothers acknowledged his importance to the development of aviation.

https://en.wikipedia.org/wiki/George_Cayley

I think by the time the Wright brothers came around, the general theory for powered flight was in place. The Wright Flyer was precision engineered, compared to most 'kites' that came before it. It's not every day you see an internal combustion engine on a kite.

I had a little too much fun...

    x>0.2-abs(sin(t*2))*0.05&&x<0.8+abs(sin(t*2))*0.05?(i%2?(min((x-sin(t%1.57)*0.3-0.5)*tan(-t%1.57)+cos(t%1.57)*0.3+0.5,(x-sin(t%1.57-1.57)*0.3-0.5)*tan(-t%1.57-1.57)+cos(t%1.57-1.57)*0.3+0.5)*0.4+0.5):(1-(min((-x-sin(t%1.57)*0.3+0.5)*tan(-t%1.57)+cos(t%1.57)*0.3+0.5,(-x-sin(t%1.57-1.57)*0.3+0.5)*tan(-t%1.57-1.57)+cos(t%1.57-1.57)*0.3+0.5)*0.4+0.5))):0.5
https://sliderland.blinry.org/#x%3E0.2-abs%28sin%28t*2%29%29...

I love Actual, one of the best local-first apps I've used. I'm excited that it's been open sourced, but I also understand that this release is a bit bittersweet. Awesome work James, best of luck in your future endeavors :)

Don't forget the built-in chat in google docs that nobody uses! (Yes, the one other than the commenting system).

We shouldn't have to live in a culture when being called out for making a 'choice with tenuous political relevancy' is socially expected, and something someone is 'guilty of'.

Like you, I am not a huge fan of JS, but I'd like to say that the ideas behind TermKit — namely having a shell with richer graphical interaction (e.g. cat works for images too), semantic awareness and tokenization of commands, rich autocomplete, etc. — were ahead of their time.

Maybe it's expected that TermKit-the-project died, but I don't think that TermKit-the-idea should: whether it be bringing features from the project as extensions to existing terminals (cat for images would be nice), or building a new TermKit built on modern graphics libraries powered by WASM/Lua/bring-your-own scripting language.

This is a neat idea. Would love to run some queries against this with a larger index. Here's one instance of the question just going over the transformer's head, I guess:

My question:

how does google work

The response:

A decentralized autonomous organization (DAO) is a software running on a distributed ledger that offers users a built-in model for the collective management of its code. To become a member of a DAO, users need to first join the DAO by buying its currency. Unlike traditional organizations, there is no hierarchy in DAOs. One of the key features of DAOS is the internal capital that is used to incentivize these actors and ensure that the organization runs smoothly.

A snapshot is a the entire state of a program at a single moment in time. Continuations are basically exposed snapshots, i.e. taking a snapshot, storing it in a variable, doing some work, and then 'calling' the snapshot to return to an earlier point. Continuations allow you to implement a naive version of single-shot delimited continuations - coroutines! This can be very useful for modeling concurrency.

Aside from coroutines and continuations, snapshots are neat for distributed computing: spin up a vm, take a snapshot, and replicate it over the network. You could also send snapshots of different tasks to other computers to execute. In the context of edge computing, you could snapshot the program once it's 'warm' to cut back on VM startup time.

Snapshots allow you to peek into your program. Imagine a debugger that takes snapshots on breakpoints, lets you to inspect the stack and heap, and replay the program forward from a given point in a deterministic manner. You could also send a snapshot to a friend so they can run an application from a given point on their machine. If you do snapshots + live reloading there are tons of other things you can do (e.g. live patching and replaying of functions while debugging).

Tossing my hat in for Passerine [1]. Gorgeous ML inspired syntax.

I work on Passerine, cool to see the language mentioned in the wild, glad you like it!

Written in Rust, and I suspect scripting Rust libraries is in the cards.

We have a rudimentary FFI to Rust already in place. We're also working on a proc macro lib for creating high-level bindings to Rust types/functions/crates (currently unreleased). Also working on an algebraic-effect-based system-injection engine, which will replace the FFI in the 0.10.0 or 0.11.0 release :)

Hey HN,

Isaac Clayton again[0]! This summer I worked as an intern at tonari[1] where I developed algorithms for real-time 3D scene construction on the GPU. As a part of my research, I developed a library / tool for prototyping shader-based graphical pipelines on the GPU. This tool, Shadergarden[2], uses GLSL[3] for shaders, but introduces a lisp-like configuration language[4] for chaining shaders (and other transformations) into directed acyclic graphical pipelines. We've been using it for both art and research, kinda like a local Shadertoy.

Hope you enjoy the write up, have a nice day!

PS — If your browser automatically pauses videos, you might need to 'show video controls' to play some of the visualizations.

[0] https://news.ycombinator.com/item?id=25347140 / https://news.ycombinator.com/item?id=26212491

[1] https://tonari.no

[2] https://github.com/tonarino/shadergarden

[3] https://thebookofshaders.com/

[4] https://github.com/tonarino/shadergarden/blob/master/LISP.md (When all you have is a compiler, everything's a DSL)

As someone who lives in Rome, it's about as quick to order a fresh pizza to go from a local restaurant as it is to get one from this vending machine. Alice Pizza is a popular pizza-to-go chain, but the place with what I think has the best pizza is Sementi la Pizza.

Just my own 2¢, I digress.

Because Passerine is pass-by-value, it only allows each reference to be owned by one object.

So if some code somewhere hands me a list that contains references to some heap data, how do you track this?

Passerine is a high-level language, so it does not give the programmer control over whether something is a list of references to data on the heap or a list of values. Assuming `a`, `b`, and `c` have been defined, If I write this:

    list = [a, b, c]
`a`, `b`, and `c` will be moved into the list upon construction. If I do something like:
    list = [a, b, c]
    print (foo a c)
`b` will be moved into the list, and copies of `a` and `c` will be moved into the list.

So, to now answer your question:

    list = [a, b, c]
    list = drop_first list
> We lost/dropped/gave up a. How do we know how a was tied to the stack?

`a` was tied to the stack because it's in `[a, b, c]`, which is tied to the stack via `list`.

How many other references might point at the object previously referenced by a? Can we collect that object now?

By definition, none - `a` is a unique value. Now that we dropped `a`, there are guaranteed to be no other references of `a`, so we can collect that object now.

Is this the case where reference counting kicks in?

No, the reference counting is a small optimization for sharing immutable values in closures. If a closure captures a value, say a large list, we don't want to go around copying this willy-nilly:

    big_list = [...]
    c = () -> {
        -- big list is copied when the closure is made
        -- this happens because the reference in the closure can outlive local stack frame
        a -> first big_list
    }

    -- these calls to c do not copy big_list again
    a_outer = c ()
    a_another = c ()
Closures need to copy values immutably to prevent the construction of cycles. I wrote a long explanation on why this is the case, but I've decided to move it to the blog post when I publish it. I thought of a way to efficiently count references acyclically, but I need to work it out before I made any more claims.

Hope this helps!

Thanks for your excellent questions. They're really helping me fuzz through my approach - I wish I had people I could do this with more often, haha. :P

Anyway, something I realized after I posted that question is that assignment doesn't have to force a copy; for instance, this could just take a CoW slice reference to the original list:

    [head & tail] = &rem'
The only requirement is that the value returned from a function is an owned value, not CoW. To avoid talking myself into circles, I'll show that `append` still works under this rule. It just moves the copy to the point where we construct the new list, rather that at the point where we de-structure it:
    loop [head' & acc'] tail'
So in `[head' & acc']`.

For the code sample you provided, a reference would be passed down, rather than a copy of the entire list, so just the first two elements would be copied. Do you have any further questions regarding this? I'd like to be able to prove the soundness of this system, but I'm not sure going back and forth on example after example is the best strategy in the long run.

In a functional language, insertion into a binary tree only copies nodes along one path in the tree. Would your language copy the entire tree instead, at potentially exponentially higher cost?

Most functional languages use persistent data-types, because all data in those languages are immutable. I haven't thought about how they'd work into Passerine, but if you mutate data that will be used later, a copy of that data will be made. So yes, mutating a tree makes a copy of it if the original tree is used later.

This is less of a problem then you'd think. It's common to write programs as series of transformations on data, so note that if you do something like this on a big million-element list children .map { child -> (child.name, child.age, child.hobby) } .filter { (_, age, _) -> age > 10 } .map { (name, _, hobby) -> (first_name name, hobby) } .filter { (_, hobby) -> is_fun hobby } .fold "" { (string, (name, hobby)) -> string + name + " enjoys " + hobby + "\n" } .print

No copies will be made, because each step is the last usage of the data-structure produced in the previous step.

(I'm also slightly confused by your definition of append -- the accumulator seems to be a list, but then the initial call of loop would need to use [value] instead of value, no?)

Once again, correct.

Regarding "tying references to the stack" I had another question here: https://news.ycombinator.com/item?id=26228961

I wrote a long response, to this, but it ended up being a couple thousand words, so I'm trying to trim it down. (I went off the deep end, with stack diagrams, explaining how cycles get created, etc, etc.) TL;DR for now is that you can't have a list of references, each object is only mutably 'owned' by one other. Once I trim down the response, I'll post it - should be sometime later today.

It's loosely based on Proust ASAP, but without the explicit quadratic-time construction of paths (in a sense, this is done 'dynamically' at runtime with the CoW strategy we've discussed). When you think about it, Vaporization is really just an extension to escape analysis. It's not a perfect system though, namely, It required immutable closures. But if you're coming from a functional programming language, that's a small sacrifice to make, given that mutability is generally frowned upon.

I think I'll have at least one more question about how the "tying of references to the stack" works, but I need to think things through a bit more.

I tried to answer your sibling comment on appending a CoW xs. I hope that clears some things up so you can ask any questions you have!

Under the hood I'm going to use Rust `Vec<T>`s to represent lists, so it'd be trivial to provide a FFI operation to append something to the end in constant time. However, we can treat the list like a linked list, and, using recursion, here's a tail-call-optimized implementation of append in linear time:

    append = list value -> {
        loop = acc rem -> match rem {
            [] -> acc,
            [head & tail] -> loop [head & acc] tail
        }
        loop value (List::reverse list)
    }
I'm thinking of using something other than the `[a & b]` syntax, but that's besides the point. Note that `loop` does not close over any values other than itself, so it doesn't need to be inside `append`:
    loop = acc rem -> ...
    append = list value -> ...
The function really starts when we reach this line:
    loop value' (List::reverse list')

I added `'` to indicate that this is the last occurrence of both `list` and `value` in this scope, which means that they're passed to `loop` without making a CoW reference. Let's look at `loop`:
    loop = acc rem -> match rem {
        [] -> acc,
        [head & tail] -> loop [head & acc] tail
    }
This is where things get complicated, depending on the definition of match. If it were defined as macro, it'd be laborious to write out how it all works by hand. But if we treat `loop` as a function with multiple heads, it's a bit easier.

Here, we have two arms. If the remaining list is empty, we return `acc`. note that each arm is independent, so this usage of `acc` is the last in the scope.

If the list is not empty, we match `rem` against `[head & tail]`. This is equivalent to, roughly:

    [head & tail] = rem'
Note how this is the last usage of rem in this scope, meaning it's passed by actual value. This list is then split in-place, with head taking on the value of the first element of the list and tail taking on the value of the rest. Once passed to loop, note that all the values used are the last occurrence of the value in the scope:
    loop [head' & acc'] tail'
This means that the actual value is passed down to the next level. Looking at this as a whole, we can see that nothing is ever passed by CoW reference, which means that this operation is performed in-place. And because it's done in a tail-recursive manner, `append` only uses constant space.

So to answer your question specifically: what if append is called with `xs` being a CoW reference?

    append &xs y
When we reach the line:
    [head & tail] = &rem'
`rem` is mutated, i.e. split in two, so a copy of it is made. This copy is it's own value, so head no tail are no longer CoW. we then call:
    loop [head & acc] tail
Note that head and tail are now passed by value and not CoW - same goes for acc. Because nothing is CoW anymore, this tail-recursive implementation operates on the new list, mutating it in place.

In short: the first iteration in `append` makes a copy to the list, but in subsequent iterations the copy is used, and the list is mutated in place.

That was a big one, I'm practically writing the blog post out here, haha! Does this help clear things up?

Right, so there would have to be built in functions that mutate and return an object. So, for instance, one might be:

    List::replace list index value -> list
List would just write to the `list` it is given. If `list` is a CoW reference, it would make a copy first before mutation (hence copy-on-write), if it was not, it would just mutate it in place.

To mutate `xs` with append as you suggested, you'd have to reassign `xs` with the result of append. Because this would make the `xs` in `append xs y` the last occurence of the value, `xs` would be mutated in place:

    xs = append xs y
If the function were of the form `append y xs`, you could use dot-notation like so:
    xs = xs.append y
and if you really hate writing out `xs =`, you could always make a macro:
    syntax 'mut value function {
        value = function value
    }

    mut xs (append y)
I've got to go, but I'll get back to you on tail recursion.

So Passerine has a record type, and is row-polymorphic. Names keyword arguments in passerine would look like:

    draw_arc {
        x:      3,
        y:      5,
        radius: 10,
        start:  0,
        end:    2 * 3.142,
    }
As for defaults, this is something a bit more difficult. In a dynamic language, you can just try getting keys and replacing missing ones with the default ones.

However, in a static language, this isn't quite possible. a missing field on a record is a compile-time error, not a runtime one. Even row-polymorphic type systems, which are generally more lenient with respect to records aren't a solution.

Rust has `Default::default()` which allows one to splice in default values when a struct is constructed. This relies on the `..` splicing syntax; I've been considering something similar for Passerine, but I haven't come to any conclusions yet. I like this because it makes the fact that you're using defaults explicit, but at the same time I'm against it because it feels like unneeded boilerplate:

    draw_arc {
        x: 3,
        y: 5,
        ...Arc::default(),
    }
Another option is to just defer field checking to runtime, and allow these errors to be handled. Given that I want to be able to compile Passerine to machine code at some point, I'm not sure whether this is a good idea:
    -- if the expression errs, replace it with default
    syntax expr 'default def {
        match (try expr) {
            Ok v -> v,
            Err _ -> def,
        }
    }

    draw_arc {
        x: 3,
        y: 5,
    }

    draw_arc = arc -> {
        full_arc = { 
            x: (arc.x) default 0,
            y: ... 
        }
    }
But I'm not sure which avenue I should take at this point in time.

I'd love to be pointed to some information on how this "school" of languages handles problems where this is a less well-defined order of partial application.

I know Ocaml has labels, which are basically public function names. so like:

    let f ~banana = a + 1;
means that the type signature is something like:
    val f : banana:int -> int = <fun>
Notice how `banana` is a part of the type signature. There are also optional named parameters:
    let f ?(banana = "ripe") = ...;
Which when used have to be explicitly listed:
    f ~banana:"rotten" ();
That's about it. What are your thoughts?

Something interesting is partial application. Say you have a function that takes two arguments:

    add = a b -> a + b
This is really equivalent to:
    add = a -> b -> a + b
Which in turn is equivalent to:
    add = a -> {
        b -> { a + b }
    }
(for good measure). This means that the first call to add, when you pass a value `x`, returns a function (the `b -> { a + b }) that closes over x:
    add_5 = add 5
    add_5 7  -- is 12
    add_5 -1 -- is 4
This also has a unique property: most function calls are just chains of identifiers and values:
    foo bar "Hello" baz
Which means, if we consider them as chains, we can match against those chains and apply transformations to them:
   syntax 'foo 'bar name 'baz { print name }
Which is the basis of Passerine's macro system! (And don't worry, if the form matches multiple macros of if you use an identifier defined in a macro, it'll let you know).

Hope this helps!

Thanks for discussing this with me and helping me work through it. To answer your question:

Let's annotate the variable lifetimes. `'` indicates last use, `_n` indicates that a variable holds the same value. I'll only be annotating the values in question:

    foo = (list1_0 list2_0') -> {
        mutate_last list1_0'
    }

    big_list_0 = [ ... ]
    foo big_list_0 big_list_0
    print big_list_0'
Let's focus on this line. Because these both aren't the last reference to `big_list`, they're passed as a CoW reference. I'll use `&` to denote this:
    foo &big_list_0 &big_list_0
In `foo`, `list1_0 = &big_list_0` and `list2_0' = &big_list_0`. `list2_0` is never used, so this reference is immediately dropped (not the value itself, rather the reference to that value). Let's now look at the body of foo:
    mutate_last list1_0'
Because list1_0 is a final reference, it's passed the value of the CoW reference, which is, well, the CoW reference. mutate list mutates this, and because the reference is copy-on-write, a copy of `&big_list_0` is returned. We'll call this `big_list_1`.

So the CoW reference of a value is a CoW<Value>. a CoW reference of a CoW<Value> stays the same, it's still a CoW<Value>.

So how do you tell inside foo whether the call is the last use of the object referenced by list1? Heroic interprocedural pointer analysis? A uniqueness type system? Something else?

Something else. Vaporization is not all compile-time magic, it depends on the runtime as well. You can basically sum it up with two operations:

    reference: Value or CoW<Value>            -> CoW<Value>
    mutate:    Value or CoW<Value> + mutation -> Value
So when `mutate_list` mutates the list, depending on whether `list1` is a value or a CoW reference, either a copy will be made or it'll be mutated in place. I'm not claiming that Vaporization is the best approach to compile-time memory management, rather, it's more of a set of compiletime+runtime strategies that allow the interpretation of programs without garbage collection (in the traditional sense)

So, the question becomes: how do we indicate whether something is a value or a CoW reference? Something along the lines of a tagged pointer will do. Passerine already uses NaN-tagging, so this fits right in.

What about compiling to low-level targets, like LLVM IR, which don't really have a 'runtime' and rather specify the manual layout of structs and references?

I'm not quite sure, but here's one possibility. All values that can escape a local scope exist in some region of memory allocated for local variables. When a function is called, we can pass a bitfield indicating which of the locals variables on the stack are CoW references, and which are not. When the runtime goes to mutate a reference, it quickly checks the bitfield (which can be stored in a register or in the call frame) whether it needs to make a copy before mutating. In practice, the check is just a quick bit-shift and `&`.

Note that we can go a bit further: the bit-fields of all calls are known at compile time, so instead of copying and passing the bitfield itself, we can pass the index of the bitfield (which is a constant) to the function to use. In fact, I imagine you could somewhat 'monomorphize' this, so to speak, and generate different functions for different bitfields... but I'd have to think some more about this.

Anyway, that's about it. I hope I've helped clear some things up. If you notice anything else slightly off, or if you'd like to discuss this further, please let me know :)